From d6e9ea2e954ffa269ef5f02f81475db6960ea24c Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Fri, 12 Apr 2019 16:10:23 +0200 Subject: [PATCH] Initial import --- LICENSE | 339 +++ README.md | 9 + application/clicommands/ScheduleCommand.php | 24 + application/controllers/ConfigController.php | 29 + .../controllers/PlugController.php.disabled | 72 + application/controllers/ReportController.php | 202 ++ .../controllers/ReportController.php.modal | 126 + application/controllers/ReportsController.php | 94 + application/controllers/TestController.php | 47 + application/forms/SelectBackendForm.php | 28 + .../views/scripts/config/backend.phtml | 6 + composer.json | 7 + composer.lock | 177 ++ config/systemd/icinga-reporting.service | 10 + configuration.php | 27 + doc/02-Installation.md | 68 + library/Reporting/Actions/SendMail.php | 116 + library/Reporting/Cli/Command.php | 25 + library/Reporting/Database.php | 43 + library/Reporting/Dimensions.php | 21 + library/Reporting/DivDecorator.php | 121 + library/Reporting/Hook/ActionHook.php | 38 + library/Reporting/Hook/ReportHook.php | 117 + library/Reporting/Mail.php | 177 ++ library/Reporting/ProvidedActions.php | 20 + library/Reporting/ProvidedReports.php | 20 + library/Reporting/Report.php | 322 +++ library/Reporting/ReportData.php | 71 + library/Reporting/ReportRow.php | 10 + library/Reporting/Reportlet.php | 86 + library/Reporting/Reports/SystemReport.php | 39 + library/Reporting/RetryConnection.php | 66 + library/Reporting/Schedule.php | 160 ++ library/Reporting/Scheduler.php | 176 ++ library/Reporting/Str.php | 37 + library/Reporting/Timeframe.php | 168 ++ library/Reporting/Timerange.php | 35 + library/Reporting/Values.php | 21 + library/Reporting/Web/Controller.php | 20 + library/Reporting/Web/Forms/ReportForm.php | 157 ++ library/Reporting/Web/Forms/ScheduleForm.php | 167 ++ library/Reporting/Web/Forms/SendForm.php | 63 + .../vendor/ipl/web/src/Common/BaseTarget.php | 30 + .../ipl/web/src/Compat/CompatController.php | 77 + .../ipl/web/src/Compat/ViewRenderer.php | 66 + library/vendor/ipl/web/src/Url.php | 15 + .../vendor/ipl/web/src/Widget/ActionBar.php | 44 + .../vendor/ipl/web/src/Widget/ActionLink.php | 29 + .../vendor/ipl/web/src/Widget/ButtonLink.php | 9 + library/vendor/ipl/web/src/Widget/Content.php | 15 + .../vendor/ipl/web/src/Widget/Controls.php | 47 + .../ipl/web/src/Widget/DropdownLink.php | 70 + library/vendor/ipl/web/src/Widget/Icon.php | 29 + library/vendor/ipl/web/src/Widget/Link.php | 81 + library/vendor/ipl/web/src/Widget/Tabs.php | 13 + module.info | 6 + public/css/forms.less | 203 ++ public/css/module.less | 99 + public/css/system-report.css | 81 + public/css/vendor/flatpickr.css | 741 ++++++ public/css/vendor/flatpickr.min.css | 13 + public/img/select-icon-2x.png | Bin 0 -> 190 bytes public/img/select-icon.png | Bin 0 -> 155 bytes public/img/textarea-corner-2x.png | Bin 0 -> 238 bytes public/img/textarea-corner.png | Bin 0 -> 181 bytes public/js/module.js | 29 + public/js/vendor/flatpickr.js | 2174 +++++++++++++++++ public/js/vendor/flatpickr.min.js | 2 + run.php | 17 + schema/mysql.sql | 84 + vendor/autoload.php | 7 + vendor/composer/ClassLoader.php | 445 ++++ vendor/composer/LICENSE | 21 + vendor/composer/autoload_classmap.php | 9 + vendor/composer/autoload_files.php | 11 + vendor/composer/autoload_namespaces.php | 9 + vendor/composer/autoload_psr4.php | 11 + vendor/composer/autoload_real.php | 70 + vendor/composer/autoload_static.php | 44 + vendor/composer/installed.json | 165 ++ vendor/guzzlehttp/psr7/CHANGELOG.md | 225 ++ vendor/guzzlehttp/psr7/LICENSE | 19 + vendor/guzzlehttp/psr7/README.md | 745 ++++++ vendor/guzzlehttp/psr7/composer.json | 45 + vendor/guzzlehttp/psr7/src/AppendStream.php | 241 ++ vendor/guzzlehttp/psr7/src/BufferStream.php | 137 ++ vendor/guzzlehttp/psr7/src/CachingStream.php | 138 ++ vendor/guzzlehttp/psr7/src/DroppingStream.php | 42 + vendor/guzzlehttp/psr7/src/FnStream.php | 158 ++ vendor/guzzlehttp/psr7/src/InflateStream.php | 52 + vendor/guzzlehttp/psr7/src/LazyOpenStream.php | 39 + vendor/guzzlehttp/psr7/src/LimitStream.php | 155 ++ vendor/guzzlehttp/psr7/src/MessageTrait.php | 183 ++ .../guzzlehttp/psr7/src/MultipartStream.php | 153 ++ vendor/guzzlehttp/psr7/src/NoSeekStream.php | 22 + vendor/guzzlehttp/psr7/src/PumpStream.php | 165 ++ vendor/guzzlehttp/psr7/src/Request.php | 142 ++ vendor/guzzlehttp/psr7/src/Response.php | 136 ++ vendor/guzzlehttp/psr7/src/Rfc7230.php | 18 + vendor/guzzlehttp/psr7/src/ServerRequest.php | 376 +++ vendor/guzzlehttp/psr7/src/Stream.php | 270 ++ .../psr7/src/StreamDecoratorTrait.php | 149 ++ vendor/guzzlehttp/psr7/src/StreamWrapper.php | 161 ++ vendor/guzzlehttp/psr7/src/UploadedFile.php | 316 +++ vendor/guzzlehttp/psr7/src/Uri.php | 738 ++++++ vendor/guzzlehttp/psr7/src/UriNormalizer.php | 216 ++ vendor/guzzlehttp/psr7/src/UriResolver.php | 219 ++ vendor/guzzlehttp/psr7/src/functions.php | 898 +++++++ .../guzzlehttp/psr7/src/functions_include.php | 6 + vendor/psr/http-message/CHANGELOG.md | 36 + vendor/psr/http-message/LICENSE | 19 + vendor/psr/http-message/README.md | 13 + vendor/psr/http-message/composer.json | 26 + .../psr/http-message/src/MessageInterface.php | 187 ++ .../psr/http-message/src/RequestInterface.php | 129 + .../http-message/src/ResponseInterface.php | 68 + .../src/ServerRequestInterface.php | 261 ++ .../psr/http-message/src/StreamInterface.php | 158 ++ .../src/UploadedFileInterface.php | 123 + vendor/psr/http-message/src/UriInterface.php | 323 +++ vendor/ralouphie/getallheaders/.gitignore | 5 + vendor/ralouphie/getallheaders/LICENSE | 21 + vendor/ralouphie/getallheaders/README.md | 19 + vendor/ralouphie/getallheaders/composer.json | 21 + vendor/ralouphie/getallheaders/phpunit.xml | 22 + .../getallheaders/src/getallheaders.php | 46 + .../getallheaders/tests/GetAllHeadersTest.php | 121 + 127 files changed, 16089 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 application/clicommands/ScheduleCommand.php create mode 100644 application/controllers/ConfigController.php create mode 100644 application/controllers/PlugController.php.disabled create mode 100644 application/controllers/ReportController.php create mode 100644 application/controllers/ReportController.php.modal create mode 100644 application/controllers/ReportsController.php create mode 100644 application/controllers/TestController.php create mode 100644 application/forms/SelectBackendForm.php create mode 100644 application/views/scripts/config/backend.phtml create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 config/systemd/icinga-reporting.service create mode 100644 configuration.php create mode 100644 doc/02-Installation.md create mode 100644 library/Reporting/Actions/SendMail.php create mode 100644 library/Reporting/Cli/Command.php create mode 100644 library/Reporting/Database.php create mode 100644 library/Reporting/Dimensions.php create mode 100644 library/Reporting/DivDecorator.php create mode 100644 library/Reporting/Hook/ActionHook.php create mode 100644 library/Reporting/Hook/ReportHook.php create mode 100644 library/Reporting/Mail.php create mode 100644 library/Reporting/ProvidedActions.php create mode 100644 library/Reporting/ProvidedReports.php create mode 100644 library/Reporting/Report.php create mode 100644 library/Reporting/ReportData.php create mode 100644 library/Reporting/ReportRow.php create mode 100644 library/Reporting/Reportlet.php create mode 100644 library/Reporting/Reports/SystemReport.php create mode 100644 library/Reporting/RetryConnection.php create mode 100644 library/Reporting/Schedule.php create mode 100644 library/Reporting/Scheduler.php create mode 100644 library/Reporting/Str.php create mode 100644 library/Reporting/Timeframe.php create mode 100644 library/Reporting/Timerange.php create mode 100644 library/Reporting/Values.php create mode 100644 library/Reporting/Web/Controller.php create mode 100644 library/Reporting/Web/Forms/ReportForm.php create mode 100644 library/Reporting/Web/Forms/ScheduleForm.php create mode 100644 library/Reporting/Web/Forms/SendForm.php create mode 100644 library/vendor/ipl/web/src/Common/BaseTarget.php create mode 100644 library/vendor/ipl/web/src/Compat/CompatController.php create mode 100644 library/vendor/ipl/web/src/Compat/ViewRenderer.php create mode 100644 library/vendor/ipl/web/src/Url.php create mode 100644 library/vendor/ipl/web/src/Widget/ActionBar.php create mode 100644 library/vendor/ipl/web/src/Widget/ActionLink.php create mode 100644 library/vendor/ipl/web/src/Widget/ButtonLink.php create mode 100644 library/vendor/ipl/web/src/Widget/Content.php create mode 100644 library/vendor/ipl/web/src/Widget/Controls.php create mode 100644 library/vendor/ipl/web/src/Widget/DropdownLink.php create mode 100644 library/vendor/ipl/web/src/Widget/Icon.php create mode 100644 library/vendor/ipl/web/src/Widget/Link.php create mode 100644 library/vendor/ipl/web/src/Widget/Tabs.php create mode 100644 module.info create mode 100644 public/css/forms.less create mode 100644 public/css/module.less create mode 100644 public/css/system-report.css create mode 100644 public/css/vendor/flatpickr.css create mode 100644 public/css/vendor/flatpickr.min.css create mode 100644 public/img/select-icon-2x.png create mode 100644 public/img/select-icon.png create mode 100644 public/img/textarea-corner-2x.png create mode 100644 public/img/textarea-corner.png create mode 100644 public/js/module.js create mode 100644 public/js/vendor/flatpickr.js create mode 100644 public/js/vendor/flatpickr.min.js create mode 100644 run.php create mode 100644 schema/mysql.sql create mode 100644 vendor/autoload.php create mode 100644 vendor/composer/ClassLoader.php create mode 100644 vendor/composer/LICENSE create mode 100644 vendor/composer/autoload_classmap.php create mode 100644 vendor/composer/autoload_files.php create mode 100644 vendor/composer/autoload_namespaces.php create mode 100644 vendor/composer/autoload_psr4.php create mode 100644 vendor/composer/autoload_real.php create mode 100644 vendor/composer/autoload_static.php create mode 100644 vendor/composer/installed.json create mode 100644 vendor/guzzlehttp/psr7/CHANGELOG.md create mode 100644 vendor/guzzlehttp/psr7/LICENSE create mode 100644 vendor/guzzlehttp/psr7/README.md create mode 100644 vendor/guzzlehttp/psr7/composer.json create mode 100644 vendor/guzzlehttp/psr7/src/AppendStream.php create mode 100644 vendor/guzzlehttp/psr7/src/BufferStream.php create mode 100644 vendor/guzzlehttp/psr7/src/CachingStream.php create mode 100644 vendor/guzzlehttp/psr7/src/DroppingStream.php create mode 100644 vendor/guzzlehttp/psr7/src/FnStream.php create mode 100644 vendor/guzzlehttp/psr7/src/InflateStream.php create mode 100644 vendor/guzzlehttp/psr7/src/LazyOpenStream.php create mode 100644 vendor/guzzlehttp/psr7/src/LimitStream.php create mode 100644 vendor/guzzlehttp/psr7/src/MessageTrait.php create mode 100644 vendor/guzzlehttp/psr7/src/MultipartStream.php create mode 100644 vendor/guzzlehttp/psr7/src/NoSeekStream.php create mode 100644 vendor/guzzlehttp/psr7/src/PumpStream.php create mode 100644 vendor/guzzlehttp/psr7/src/Request.php create mode 100644 vendor/guzzlehttp/psr7/src/Response.php create mode 100644 vendor/guzzlehttp/psr7/src/Rfc7230.php create mode 100644 vendor/guzzlehttp/psr7/src/ServerRequest.php create mode 100644 vendor/guzzlehttp/psr7/src/Stream.php create mode 100644 vendor/guzzlehttp/psr7/src/StreamDecoratorTrait.php create mode 100644 vendor/guzzlehttp/psr7/src/StreamWrapper.php create mode 100644 vendor/guzzlehttp/psr7/src/UploadedFile.php create mode 100644 vendor/guzzlehttp/psr7/src/Uri.php create mode 100644 vendor/guzzlehttp/psr7/src/UriNormalizer.php create mode 100644 vendor/guzzlehttp/psr7/src/UriResolver.php create mode 100644 vendor/guzzlehttp/psr7/src/functions.php create mode 100644 vendor/guzzlehttp/psr7/src/functions_include.php create mode 100644 vendor/psr/http-message/CHANGELOG.md create mode 100644 vendor/psr/http-message/LICENSE create mode 100644 vendor/psr/http-message/README.md create mode 100644 vendor/psr/http-message/composer.json create mode 100644 vendor/psr/http-message/src/MessageInterface.php create mode 100644 vendor/psr/http-message/src/RequestInterface.php create mode 100644 vendor/psr/http-message/src/ResponseInterface.php create mode 100644 vendor/psr/http-message/src/ServerRequestInterface.php create mode 100644 vendor/psr/http-message/src/StreamInterface.php create mode 100644 vendor/psr/http-message/src/UploadedFileInterface.php create mode 100644 vendor/psr/http-message/src/UriInterface.php create mode 100644 vendor/ralouphie/getallheaders/.gitignore create mode 100644 vendor/ralouphie/getallheaders/LICENSE create mode 100644 vendor/ralouphie/getallheaders/README.md create mode 100644 vendor/ralouphie/getallheaders/composer.json create mode 100644 vendor/ralouphie/getallheaders/phpunit.xml create mode 100644 vendor/ralouphie/getallheaders/src/getallheaders.php create mode 100644 vendor/ralouphie/getallheaders/tests/GetAllHeadersTest.php diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..ecbc0593 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..e675e00c --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Icinga Reporting + +Icinga Reporting is the central component for reporting related functionality in the monitoring web frontend and +framework Icinga Web 2. The engine allows you to create reports over a specified time period for ad-hoc and scheduled +generation of reports. Other modules use the provided functionality in order to provide concrete reports. + +## Documentation + +* [Installation](doc/02-Installation.md) diff --git a/application/clicommands/ScheduleCommand.php b/application/clicommands/ScheduleCommand.php new file mode 100644 index 00000000..e5541389 --- /dev/null +++ b/application/clicommands/ScheduleCommand.php @@ -0,0 +1,24 @@ +getDb()); + + $scheduler->run(); + } +} diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php new file mode 100644 index 00000000..5e8e29e1 --- /dev/null +++ b/application/controllers/ConfigController.php @@ -0,0 +1,29 @@ +assertPermission('config/modules'); + + parent::init(); + } + + public function backendAction() + { + $form = (new SelectBackendForm()) + ->setIniConfig(Config::module('reporting')); + + $form->handleRequest(); + + $this->view->tabs = $this->Module()->getConfigTabs()->activate('backend'); + $this->view->form = $form; + } +} diff --git a/application/controllers/PlugController.php.disabled b/application/controllers/PlugController.php.disabled new file mode 100644 index 00000000..a2c64537 --- /dev/null +++ b/application/controllers/PlugController.php.disabled @@ -0,0 +1,72 @@ +params->get('module', 'reporting')); + + $reportsByModule = []; + + foreach (ReportHook::getReports() as $class => $report) { + $moduleName = $report->getModuleName(); + + if (! isset($reportsByModule[$moduleName])) { + $reportsByModule[$moduleName] = []; + } + + $reportsByModule[$moduleName][$class] = $report; + } + + $editor = Html::tag('div', ['class' => 'editor']); + + $nav = []; + + $cards = []; + + foreach ($reportsByModule as $moduleName => $reports) { + $link = Html::tag('a', ['href' => Url::fromRequest(['module' => $moduleName])], $moduleName); + + $nav[] = $link; + + if ($moduleName !== $moduleToShow) { + continue; + } + + $link->getAttributes()->add('class', 'active'); + + foreach ($reports as $report) { + $cards[] = Html::tag( + 'div', + ['class' => 'card'], + [ + Html::tag('div', ['class' => 'card-top'], $report->getPreview()), + Html::tag( + 'div', + ['class' => 'card-content'], + Html::tag('h5', ['class' => 'card-title'], $report->getName()), + Html::tag('p', ['class' => 'card-text'], $report->getDescription()) + ) + ] + ); + } + } + + $editor->add(Html::tag('div', ['class' => 'editor-nav'], $nav)); + $editor->add(Html::tag('div', ['class' => 'editor-content'], $cards)); + + $this->addContent($editor); + + $this->addContent(Html::tag('a', ['href' => 'plug', 'class' => 'modal-toggle', 'data-base-target' => 'modal-container'], 'Modal')); + } +} diff --git a/application/controllers/ReportController.php b/application/controllers/ReportController.php new file mode 100644 index 00000000..23de754e --- /dev/null +++ b/application/controllers/ReportController.php @@ -0,0 +1,202 @@ +report = Report::fromDb($this->params->getRequired('id')); + } + + public function indexAction() + { + $this->setTitle($this->report->getName()); + + $this->addControl($this->assembleActions()); + + $this->addContent($this->report->toHtml()); + } + + public function editAction() + { + $this->setTitle('Edit Report'); + + $values = [ + 'name' => $this->report->getName(), + // TODO(el): Must cast to string here because ipl/html does not support integer return values for attribute callbacks + 'timeframe' => (string) $this->report->getTimeframe()->getId(), + ]; + + $reportlet = $this->report->getReportlets()[0]; + + $values['reportlet'] = $reportlet->getClass(); + + foreach ($reportlet->getConfig() as $name => $value) { + $values[$name] = $value; + } + + $form = new ReportForm(); + $form->setId($this->report->getId()); + $form->populate($values); + $form->handleRequest(ServerRequest::fromGlobals()); + + $this->redirectForm($form, 'reporting/reports'); + + $this->addContent($form); + } + + public function sendAction() + { + $this->setTitle('Send Report'); + + $form = new SendForm(); + $form + ->setReport($this->report) + ->handleRequest(ServerRequest::fromGlobals()); + + $this->redirectForm($form, "reporting/report?id={$this->report->getId()}"); + + $this->addContent($form); + } + + public function scheduleAction() + { + $this->setTitle('Schedule'); + + $form = new ScheduleForm(); + $form + ->setReport($this->report) + ->handleRequest(ServerRequest::fromGlobals()); + + $this->redirectForm($form, "reporting/report?id={$this->report->getId()}"); + + $this->addContent($form); + } + + public function downloadAction() + { + $type = $this->params->getRequired('type'); + + $name = sprintf( + '%s (%s) %s', + $this->report->getName(), + $this->report->getTimeframe()->getName(), + date('Y-m-d H:i') + ); + + switch ($type) { + case 'pdf': + $pdfexport = null; + + if (Hook::has('Pdfexport')) { + $pdfexport = Hook::first('Pdfexport'); + + if (! $pdfexport->isSupported()) { + throw new \Exception("Can't export"); + } + } + + if (! $pdfexport) { + throw new \Exception("Can't export"); + } + + $html = Html::tag( + 'html', + null, + [ + Html::tag( + 'head', + null, + Html::tag( + 'style', + null, + new HtmlString(StyleSheet::forPdf()) + ) + ), + Html::tag( + 'body', + null, + Html::tag( + 'div', + ['class' => 'icinga-module module-reporting'], + new HtmlString($this->report->toHtml()) + ) + ) + ] + ); + + /** @var Hook\PdfexportHook */ + $pdfexport->streamPdfFromHtml((string) $html, $name); + exit; + case 'csv': + $response = $this->getResponse(); + $response + ->setHeader('Content-Type', 'text/csv') + ->setHeader('Cache-Control', 'no-store') + ->setHeader( + 'Content-Disposition', + 'attachment; filename=' . $name . '.csv' + ) + ->appendBody($this->report->toCsv()) + ->sendResponse(); + exit; + case 'json': + $response = $this->getResponse(); + $response + ->setHeader('Content-Type', 'application/json') + ->setHeader('Cache-Control', 'no-store') + ->setHeader( + 'Content-Disposition', + 'inline; filename=' . $name . '.json' + ) + ->appendBody($this->report->toJson()) + ->sendResponse(); + exit; + } + } + + protected function assembleActions() + { + $reportId = $this->report->getId(); + + $download = (new DropdownLink('Download')) + ->addLink('PDF', Url::fromPath('reporting/report/download?type=pdf', ['id' => $reportId])); + + if ($this->report->providesData()) { + $download->addLink('CSV', Url::fromPath('reporting/report/download?type=csv', ['id' => $reportId])); + $download->addLink('JSON', Url::fromPath('reporting/report/download?type=json', ['id' => $reportId])); + } + + $actions = new ActionBar(); + + $actions + ->addLink('Modify', Url::fromPath('reporting/report/edit', ['id' => $reportId]), 'edit') + ->addLink('Schedule', Url::fromPath('reporting/report/schedule', ['id' => $reportId]), 'calendar-empty') + ->add($download) + ->addLink('Send', Url::fromPath('reporting/report/send', ['id' => $reportId]), 'forward'); + + return $actions; + } +} diff --git a/application/controllers/ReportController.php.modal b/application/controllers/ReportController.php.modal new file mode 100644 index 00000000..915bf2bf --- /dev/null +++ b/application/controllers/ReportController.php.modal @@ -0,0 +1,126 @@ +report = Report::fromDb($this->params->getRequired('id')); + } + + public function indexAction() + { + $this->setTitle($this->report->getName()); + + $this->addControl($this->assembleActions()); + + $this->addContent($this->report->toHtml()); + } + + public function editAction() + { + $this->setTitle('Edit Report'); + + $values = [ + 'name' => $this->report->getName(), + 'timeframe' => $this->report->getTimeframe()->getId(), + ]; + + $reportlet = $this->report->getReportlets()[0]; + + $values['reportlet'] = $reportlet->getClass(); + + foreach ($reportlet->getConfig() as $name => $value) { + $values[$name] = $value; + } + + $form = new ReportForm(); + $form->setId($this->report->getId()); + $form->populate($values); + $form->handleRequest(ServerRequest::fromGlobals()); + + $this->redirectForm($form, 'reporting/reports'); + + $this->addContent($form); + } + + public function sendAction() + { + $this->setTitle('Send Report'); + + $form = new SendForm(); + $form + ->setReport($this->report) + ->handleRequest(ServerRequest::fromGlobals()); + + $this->redirectForm($form, "reporting/report?id={$this->report->getId()}"); + + $this->addContent(new Modal($form)); + } + + public function scheduleAction() + { + $this->setTitle('Schedule'); + + $form = new ScheduleForm(); + $form + ->setReport($this->report) + ->handleRequest(ServerRequest::fromGlobals()); + + $this->redirectForm($form, "reporting/report?id={$this->report->getId()}"); + + $this->addContent(new Modal($form)); + + $this->getResponse()->setHeader('X-Icinga-History', 'no', true); + } + + protected function assembleActions() + { + $reportId = $this->report->getId(); + + $download = (new DropdownLink('Download')) + ->addLink('PDF', Url::fromPath('reporting/report/download?type=pdf', ['id' => $reportId])); + + $send = (new DropdownLink('Send', 'forward')) + ->addLink('PDF', Url::fromPath('reporting/report/send?type=pdf', ['id' => $reportId])); + + if ($this->report->providesCsv()) { + $download->addLink('CSV', Url::fromPath('reporting/report/download?type=csv', ['id' => $reportId])); + $send->addLink('CSV', Url::fromPath('reporting/report/send?type=csv', ['id' => $reportId])); + } + + if ($this->report->providesJson()) { + $download->addLink('JSON', Url::fromPath('reporting/report/download?type=json', ['id' => $reportId])); + $send->addLink('JSON', Url::fromPath('reporting/report/send?type=json', ['id' => $reportId])); + } + + $actions = new ActionBar(); + + $actions + ->addLink('Modify', Url::fromPath('reporting/report/edit', ['id' => $reportId]), 'edit') + ->add(new ModalToggle('Schedule', Url::fromPath('reporting/report/schedule', ['id' => $reportId]), 'calendar-empty')) + ->add($download) + ->addLink('Send', Url::fromPath('reporting/report/send', ['id' => $reportId]), 'forward'); + + return $actions; + } +} diff --git a/application/controllers/ReportsController.php b/application/controllers/ReportsController.php new file mode 100644 index 00000000..b65b5d30 --- /dev/null +++ b/application/controllers/ReportsController.php @@ -0,0 +1,94 @@ +setTitle($this->translate('Reports')); + + $newReport = new ButtonLink( + $this->translate('New Report'), + Url::fromPath('reporting/reports/new')->getAbsoluteUrl('&'), + 'plus' + ); + + $this->addControl($newReport); + + $tableRows = []; + + $select = (new Select()) + ->from('report r') + ->columns(['r.*', 'timeframe' => 't.name']) + ->join('timeframe t', 'r.timeframe_id = t.id') + ->orderBy('r.mtime', SORT_DESC); + + foreach ($this->getDb()->select($select) as $report) { + $url = Url::fromPath('reporting/report', ['id' => $report->id])->getAbsoluteUrl('&'); + + $tableRows[] = Html::tag('tr', ['href' => $url], [ + Html::tag('td', null, $report->name), + Html::tag('td', null, $report->author), + Html::tag('td', null, $report->timeframe), + Html::tag('td', null, date('Y-m-d H:i', $report->ctime / 1000)), + Html::tag('td', null, date('Y-m-d H:i', $report->mtime / 1000)) + ]); + } + + if (! empty($tableRows)) { + $table = Html::tag( + 'table', + ['class' => 'common-table table-row-selectable', 'data-base-target' => '_next'], + [ + Html::tag( + 'thead', + null, + Html::tag( + 'tr', + null, + [ + Html::tag('th', null, 'Name'), + Html::tag('th', null, 'Author'), + Html::tag('th', null, 'Timeframe'), + Html::tag('th', null, 'Date Created'), + Html::tag('th', null, 'Date Modified') + ] + ) + ), + Html::tag('tbody', null, $tableRows) + ] + ); + + $this->addContent($table); + } else { + $this->addContent(Html::tag('p', null, 'No reports created yet.')); + } + } + + public function newAction() + { + $this->setTitle($this->translate('New Report')); + + $form = new ReportForm(); + $form->handleRequest(ServerRequest::fromGlobals()); + + $this->redirectForm($form, 'reporting/reports'); + + $this->addContent($form); + } +} diff --git a/application/controllers/TestController.php b/application/controllers/TestController.php new file mode 100644 index 00000000..f92a2acd --- /dev/null +++ b/application/controllers/TestController.php @@ -0,0 +1,47 @@ +from('timeframe') + ->columns('*'); + + $table = new Table(); + + $table->getAttributes()->add('class', 'common-table'); + + $table->getHeader()->add(Table::row(['Name', 'Title', 'Start', 'End'], null, 'th')); + + foreach ($this->getDb()->select($select) as $row) { + $timeframe = (new Timeframe()) + ->setName($row->name) + ->setTitle($row->title) + ->setStart($row->start) + ->setEnd($row->end); + + $table->getBody()->add(Table::row([ + $timeframe->getName(), + $timeframe->getTitle(), + $timeframe->getTimerange()->getStart()->format('Y-m-d H:i:s'), + $timeframe->getTimerange()->getEnd()->format('Y-m-d H:i:s') + ])); + } + + $this->setTitle('Timeframes'); + + $this->addContent($table); + } +} diff --git a/application/forms/SelectBackendForm.php b/application/forms/SelectBackendForm.php new file mode 100644 index 00000000..c0a0e0d1 --- /dev/null +++ b/application/forms/SelectBackendForm.php @@ -0,0 +1,28 @@ +setName('reporting_backend'); + $this->setSubmitLabel($this->translate('Save Changes')); + } + + public function createElements(array $formData) + { + $dbResources = ResourceFactory::getResourceConfigs('db')->keys(); + + $this->addElement('select', 'backend_resource', [ + 'label' => $this->translate('Database'), + 'description' => $this->translate('Database resource'), + 'multiOptions' => array_combine($dbResources, $dbResources), + 'required' => true + ]); + } +} diff --git a/application/views/scripts/config/backend.phtml b/application/views/scripts/config/backend.phtml new file mode 100644 index 00000000..25744020 --- /dev/null +++ b/application/views/scripts/config/backend.phtml @@ -0,0 +1,6 @@ +
+ +
+
+ +
diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..78037176 --- /dev/null +++ b/composer.json @@ -0,0 +1,7 @@ +{ + "name": "icinga/icingaweb2-module-reporting", + "require": { + "guzzlehttp/psr7": "^1.5", + "ext-json": "*" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..bd2288a1 --- /dev/null +++ b/composer.lock @@ -0,0 +1,177 @@ +{ + "_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#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "6b6c5cd3b1f8ba4a8d1c169cd8a9ba00", + "packages": [ + { + "name": "guzzlehttp/psr7", + "version": "1.5.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "9f83dded91781a01c63574e387eaa769be769115" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", + "reference": "9f83dded91781a01c63574e387eaa769be769115", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2018-12-04T20:46:45+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~3.7.0", + "satooshi/php-coveralls": ">=1.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2016-02-11T07:05:27+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "ext-json": "*" + }, + "platform-dev": [] +} diff --git a/config/systemd/icinga-reporting.service b/config/systemd/icinga-reporting.service new file mode 100644 index 00000000..51cc1557 --- /dev/null +++ b/config/systemd/icinga-reporting.service @@ -0,0 +1,10 @@ +[Unit] +Description=Icinga Reporting Scheduler + +[Service] +Type=simple +ExecStart=/usr/bin/icingacli reporting schedule run +Restart=on-success + +[Install] +WantedBy=multi-user.target diff --git a/configuration.php b/configuration.php new file mode 100644 index 00000000..450a03c9 --- /dev/null +++ b/configuration.php @@ -0,0 +1,27 @@ +provideCssFile('forms.less'); + $this->provideCssFile('system-report.css'); + + if (version_compare(Version::VERSION, '2.7.0', '<')) { + $this->provideJsFile('vendor/flatpickr.min.js'); + $this->provideCssFile('vendor/flatpickr.min.css'); + } + + $this->menuSection(N_('Reporting'))->add(N_('Reports'), array( + 'url' => 'reporting/reports', + )); + + $this->provideConfigTab('backend', array( + 'title' => $this->translate('Configure the database backend'), + 'label' => $this->translate('Backend'), + 'url' => 'config/backend' + )); +} diff --git a/doc/02-Installation.md b/doc/02-Installation.md new file mode 100644 index 00000000..227cb1fe --- /dev/null +++ b/doc/02-Installation.md @@ -0,0 +1,68 @@ +# Installation + +## Requirements + +* Icinga Web 2 (>= 2.6) +* PHP (>= 5.6, preferably 7.x) +* MySQL or MariaDB +* Icinga Web 2 modules: + * [reactbundle](https://github.com/Icinga/icingaweb2-module-reactbundle) (>= 0.4) + * [Icinga PHP Library (ipl)](https://github.com/Icinga/icingaweb2-module-ipl) (>= 0.1) + * [pdfexport](https://github.com/Icinga/icingaweb2-module-pdfexport) (>= 0.9) + +## Database Setup + +The module needs a MySQL/MariaDB database with the schema that's provided in the `etc/schema/mysql.sql` file. + +Note that if you're using a version of MySQL < 5.7 or MariaDB < 10.2, the following server options must be set: + +``` +innodb_file_format=barracuda +innodb_file_per_table=1 +innodb_large_prefix=1 +``` + +Example command for creating the MySQL/MariaDB database. Please change the password: + +``` +CREATE DATABASE reporting; +GRANT SELECT, INSERT, UPDATE, DELETE, DROP, CREATE VIEW, INDEX, EXECUTE ON reporting.* TO reporting@localhost IDENTIFIED BY 'secret'; +``` + +After, you can import the schema using the following command: + +``` +mysql -p -u root reporting < schema/mysql.sql +``` + +## Installation + +1. Just drop this module to a `reporting` subfolder in your Icinga Web 2 module path. + +2. Log in with a privileged user in Icinga Web 2 and enable the module in `Configuration -> Modules -> reporting`. +Or use the `icingacli` and run `icingacli module enable reporting`. + +3. Once you've set up the database, create a new Icinga Web 2 resource for it using the +`Configuration -> Application -> Resources` menu. + +4. The next step involves telling the Reporting module which database resource to use. This can be done in +`Configuration -> Modules -> reporting -> Backend`. + +This concludes the installation. You should now be able create reports. + +## Scheduler Daemon + +There is a daemon for generating and distributing reports on a schedule if configured: + +``` +icingacli reporting schedule run +``` + +This command schedules the execution of all applicable reports. + +You may configure this command as `systemd` service. Just copy the example service definition from +`config/systemd/icinga-reporting.service` to `/etc/systemd/system/icinga-reporting.service` and enable it afterwards: + +``` +systemctl enable icinga-reporting.service +``` diff --git a/library/Reporting/Actions/SendMail.php b/library/Reporting/Actions/SendMail.php new file mode 100644 index 00000000..9f75218e --- /dev/null +++ b/library/Reporting/Actions/SendMail.php @@ -0,0 +1,116 @@ +getName(), + $report->getTimeframe()->getName(), + date('Y-m-d H:i') + ); + + $mail = new Mail(); + + switch ($config['type']) { + case 'pdf': + $pdfexport = null; + + if (Hook::has('Pdfexport')) { + $pdfexport = Hook::first('Pdfexport'); + + if (! $pdfexport->isSupported()) { + throw new \Exception("Can't export"); + } + } + + if (! $pdfexport) { + throw new \Exception("Can't export"); + } + + $html = Html::tag( + 'html', + null, + [ + Html::tag( + 'head', + null, + Html::tag( + 'style', + null, + new HtmlString(StyleSheet::forPdf()) + ) + ), + Html::tag( + 'body', + null, + Html::tag( + 'div', + ['class' => 'icinga-module module-reporting'], + new HtmlString($report->toHtml()) + ) + ) + ] + ); + + $mail->attachPdf($pdfexport->htmlToPdf((string) $html), $name); + + break; + case 'csv': + $mail->attachCsv($report->toCsv(), $name); + + break; + case 'json': + $mail->attachJson($report->toJson(), $name); + + break; + default: + throw new \InvalidArgumentException(); + + } + + $recipients = StringHelper::trimSplit($config['recipients']); + + $mail->send(null, $recipients); + } + + public function initConfigForm(Form $form, Report $report) + { + $types = ['pdf' => 'PDF']; + + if ($report->providesData()) { + $types['csv'] = 'CSV'; + $types['json'] = 'JSON'; + } + + $form->addElement('select', 'type', [ + 'required' => true, + 'label' => 'Type', + 'options' => $types + ]); + + $form->addElement('textarea', 'recipients', [ + 'required' => true, + 'label' => 'Recipients' + ]); + } +} diff --git a/library/Reporting/Cli/Command.php b/library/Reporting/Cli/Command.php new file mode 100644 index 00000000..2629b35d --- /dev/null +++ b/library/Reporting/Cli/Command.php @@ -0,0 +1,25 @@ +getModuleManager(); + foreach ($mm->getModule($this->getModuleName())->getDependencies() as $module => $_) { + if ($mm->hasInstalled($module)) { + $mm->loadModule($module); + } + } + } +} diff --git a/library/Reporting/Database.php b/library/Reporting/Database.php new file mode 100644 index 00000000..9fe4a184 --- /dev/null +++ b/library/Reporting/Database.php @@ -0,0 +1,43 @@ +get('backend', 'resource') + )); + + $config->options = [ + \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_OBJ, + \PDO::MYSQL_ATTR_INIT_COMMAND => "SET SESSION SQL_MODE='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE" + . ",ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'" + ]; + + $conn = new RetryConnection($config); + + return $conn; + } + + protected function listTimeframes() + { + $select = (new Sql\Select()) + ->from('timeframe') + ->columns(['id', 'name']); + + $timeframes = []; + + foreach ($this->getDb()->select($select) as $row) { + $timeframes[$row->id] = $row->name; + } + + return $timeframes; + } +} diff --git a/library/Reporting/Dimensions.php b/library/Reporting/Dimensions.php new file mode 100644 index 00000000..dfedbc8f --- /dev/null +++ b/library/Reporting/Dimensions.php @@ -0,0 +1,21 @@ +dimensions; + } + + public function setDimensions(array $dimensions) + { + $this->dimensions = $dimensions; + + return $this; + } +} diff --git a/library/Reporting/DivDecorator.php b/library/Reporting/DivDecorator.php new file mode 100644 index 00000000..0ecbcdca --- /dev/null +++ b/library/Reporting/DivDecorator.php @@ -0,0 +1,121 @@ +formElement = $formElement; + + // TODO(el): Should be SubmitElementInterface + if ($formElement instanceof SubmitElement) { + $class = 'form-control'; + } else { + $class = 'form-element'; + } + + $decorator->getAttributes()->add('class', $class); + + $formElement->prependWrapper($decorator); + + return $decorator; + } + + public function add($content) + { + if ($content === $this->formElement) { + if ($this->formElementAdded) { + return $this; + } + + $this->formElementAdded = true; + } + + parent::add($content); + + return $this; + } + + protected function assemble() + { + if ($this->formElement->hasBeenValidatedAndIsNotValid()) { + $this->getAttributes()->add('class', 'has-error'); + } + + $this->add([ + $this->assembleLabel(), + $this->formElement, + $this->assembleDescription(), + $this->assembleErrors() + ]); + } + + protected function assembleLabel() + { + $label = $this->formElement->getLabel(); + + if ($label !== null) { + $attributes = null; + + if ($this->formElement->getAttributes()->has('id')) { + $attributes = new Attributes(['for' => $this->formElement->getAttributes()->get('id')]); + } + + return Html::tag('label', $attributes, $label); + } + + return null; + } + + protected function assembleDescription() + { + $description = $this->formElement->getDescription(); + + if ($description !== null) { + return Html::tag('p', ['class' => 'form-element-description'], $description); + } + + return null; + } + + protected function assembleErrors() + { + $errors = []; + + foreach ($this->formElement->getMessages() as $message) { + $errors[] = Html::tag('p', ['class' => 'form-element-error'], $message); + } + + if (! empty($errors)) { + return $errors; + } + + return null; + } +} diff --git a/library/Reporting/Hook/ActionHook.php b/library/Reporting/Hook/ActionHook.php new file mode 100644 index 00000000..ae3364ed --- /dev/null +++ b/library/Reporting/Hook/ActionHook.php @@ -0,0 +1,38 @@ +getDeclaringClass()->getName() !== self::class; + } + + /** + * Get whether the report provides HTML + * + * @return bool + */ + public function providesHtml() + { + try { + $method = new \ReflectionMethod($this, 'getHtml'); + } catch (\ReflectionException $e) { + return false; + } + + return $method->getDeclaringClass()->getName() !== self::class; + } + + /** + * Get the module name of the report + * + * @return string + */ + final public function getModuleName() + { + return ClassLoader::extractModuleName(get_class($this)); + } + + /** + * Get all provided reports + * + * @return ReportHook[] + */ + final public static function getReports() + { + return Hook::all('reporting/Report'); + } +} diff --git a/library/Reporting/Mail.php b/library/Reporting/Mail.php new file mode 100644 index 00000000..10de6c0f --- /dev/null +++ b/library/Reporting/Mail.php @@ -0,0 +1,177 @@ +from)) { + return $this->from; + } + + if (isset($_SERVER['SERVER_ADMIN'])) { + $this->from = $_SERVER['SERVER_ADMIN']; + + return $this->from; + } + + foreach (['HTTP_HOST', 'SERVER_NAME', 'HOSTNAME'] as $key) { + if (isset($_SEVER[$key])) { + $this->from = 'icinga-reporting@' . $_SERVER[$key]; + + return $this->from; + } + } + + $this->from = 'icinga-reporting@localhost'; + + return $this->from; + } + + /** + * Set the from part + * + * @param string $from + * + * @return $this + */ + public function setFrom($from) + { + $this->from = $from; + + return $this; + } + + /** + * Get the subject + * + * @return string + */ + public function getSubject() + { + return $this->subject; + } + + /** + * Set the subject + * + * @param string $subject + * + * @return $this + */ + public function setSubject($subject) + { + $this->subject = $subject; + + return $this; + } + + /** + * Get the mail transport + * + * @return Zend_Mail_Transport_Sendmail + */ + public function getTransport() + { + if (! isset($this->transport)) { + $this->transport = new Zend_Mail_Transport_Sendmail('-f ' . escapeshellarg($this->getFrom())); + } + + return $this->transport; + } + + public function attachCsv($csv, $filename) + { + if (is_array($csv)) { + $csv = Str::putcsv($csv); + } + + $attachment = new Zend_Mime_Part($csv); + + $attachment->type = 'text/csv'; + $attachment->disposition = Zend_Mime::DISPOSITION_ATTACHMENT; + $attachment->encoding = Zend_Mime::ENCODING_BASE64; + $attachment->filename = basename($filename, '.csv') . '.csv'; + + $this->attachments[] = $attachment; + + return $this; + } + + public function attachJson($json, $filename) + { + if (is_array($json)) { + $json = json_encode($json); + } + + $attachment = new Zend_Mime_Part($json); + + $attachment->type = 'application/json'; + $attachment->disposition = Zend_Mime::DISPOSITION_ATTACHMENT; + $attachment->encoding = Zend_Mime::ENCODING_BASE64; + $attachment->filename = basename($filename, '.json') . '.json'; + + $this->attachments[] = $attachment; + + return $this; + } + + public function attachPdf($pdf, $filename) + { + $attachment = new Zend_Mime_Part($pdf); + + $attachment->type = 'application/pdf'; + $attachment->disposition = Zend_Mime::DISPOSITION_ATTACHMENT; + $attachment->encoding = Zend_Mime::ENCODING_BASE64; + $attachment->filename = basename($filename, '.pdf') . '.pdf'; + + $this->attachments[] = $attachment; + + return $this; + } + + public function send($body, $recipient) + { + $mail = new Zend_Mail('UTF-8'); + + $mail->setFrom($this->getFrom()); + $mail->addTo($recipient); + $mail->setSubject($this->getSubject()); + + if (strlen($body) !== strlen(strip_tags($body))) { + $mail->setBodyHtml($body); + } else { + $mail->setBodyText($body); + } + + foreach ($this->attachments as $attachment) { + $mail->addAttachment($attachment); + } + + $mail->send($this->getTransport()); + } +} diff --git a/library/Reporting/ProvidedActions.php b/library/Reporting/ProvidedActions.php new file mode 100644 index 00000000..2590d1f4 --- /dev/null +++ b/library/Reporting/ProvidedActions.php @@ -0,0 +1,20 @@ + $action) { + $actions[$class] = $action->getName(); + } + + return $actions; + } +} diff --git a/library/Reporting/ProvidedReports.php b/library/Reporting/ProvidedReports.php new file mode 100644 index 00000000..edfc2ce0 --- /dev/null +++ b/library/Reporting/ProvidedReports.php @@ -0,0 +1,20 @@ + $report) { + $reports[$class] = $report->getName(); + } + + return $reports; + } +} diff --git a/library/Reporting/Report.php b/library/Reporting/Report.php new file mode 100644 index 00000000..3bdd7cb5 --- /dev/null +++ b/library/Reporting/Report.php @@ -0,0 +1,322 @@ +getDb(); + + $select = (new Sql\Select()) + ->from('report') + ->columns('*') + ->where(['id = ?' => $id]); + + $row = $db->select($select)->fetch(); + + if ($row === false) { + throw new \Exception('Report not found'); + } + + $report + ->setId($row->id) + ->setName($row->name) + ->setAuthor($row->author) + ->setTimeframe(Timeframe::fromDb($row->timeframe_id)); + + $select = (new Sql\Select()) + ->from('reportlet') + ->columns('*') + ->where(['report_id = ?' => $id]); + + $row = $db->select($select)->fetch(); + + if ($row === false) { + throw new \Exception('No reportlets configured.'); + } + + $reportlet = new Reportlet(); + + $reportlet + ->setId($row->id) + ->setClass($row->class); + + $select = (new Sql\Select()) + ->from('config') + ->columns('*') + ->where(['reportlet_id = ?' => $row->id]); + + $rows = $db->select($select)->fetchAll(); + + $config = []; + + foreach ($rows as $row) { + $config[$row->name] = $row->value; + } + + $reportlet->setConfig($config); + + $report->setReportlets([$reportlet]); + + $select = (new Sql\Select()) + ->from('schedule') + ->columns('*') + ->where(['report_id = ?' => $id]); + + $row = $db->select($select)->fetch(); + + if ($row !== false) { + $schedule = new Schedule(); + + $schedule + ->setId($row->id) + ->setStart((new \DateTime())->setTimestamp((int) $row->start / 1000)) + ->setFrequency($row->frequency) + ->setAction($row->action) + ->setConfig(json_decode($row->config, true)); + + $report->setSchedule($schedule); + } + + return $report; + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @param int $id + * + * @return $this + */ + public function setId($id) + { + $this->id = $id; + + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getAuthor() + { + return $this->author; + } + + /** + * @param string $author + * + * @return $this + */ + public function setAuthor($author) + { + $this->author = $author; + + return $this; + } + + /** + * @return Timeframe + */ + public function getTimeframe() + { + return $this->timeframe; + } + + /** + * @param Timeframe $timeframe + * + * @return $this + */ + public function setTimeframe(Timeframe $timeframe) + { + $this->timeframe = $timeframe; + + return $this; + } + + /** + * @return Reportlet[] + */ + public function getReportlets() + { + return $this->reportlets; + } + + /** + * @param Reportlet[] $reportlets + * + * @return $this + */ + public function setReportlets(array $reportlets) + { + $this->reportlets = $reportlets; + + return $this; + } + + /** + * @return Schedule + */ + public function getSchedule() + { + return $this->schedule; + } + + /** + * @param Schedule $schedule + * + * @return $this + */ + public function setSchedule(Schedule $schedule) + { + $this->schedule = $schedule; + + return $this; + } + + public function providesData() + { + foreach ($this->getReportlets() as $reportlet) { + $implementation = $reportlet->getImplementation(); + + if ($implementation->providesData()) { + return true; + } + } + + return false; + } + + /** + * @return HtmlDocument + */ + public function toHtml() + { + $timerange = $this->getTimeframe()->getTimerange(); + + $html = new HtmlDocument(); + + foreach ($this->getReportlets() as $reportlet) { + $implementation = $reportlet->getImplementation(); + + $html->add($implementation->getHtml($timerange, $reportlet->getConfig())); + } + + return $html; + } + + /** + * @return string + */ + public function toCsv() + { + $timerange = $this->getTimeframe()->getTimerange(); + + $csv = []; + + foreach ($this->getReportlets() as $reportlet) { + $implementation = $reportlet->getImplementation(); + + if ($implementation->providesData()) { + $data = $implementation->getData($timerange, $reportlet->getConfig()); + $csv[] = array_merge($data->getDimensions(), $data->getValues()); + foreach ($data->getRows() as $row) { + $csv[] = array_merge($row->getDimensions(), $row->getValues()); + } + + break; + } + } + + return Str::putcsv($csv); + } + + /** + * @return string + */ + public function toJson() + { + $timerange = $this->getTimeframe()->getTimerange(); + + $json = []; + + foreach ($this->getReportlets() as $reportlet) { + $implementation = $reportlet->getImplementation(); + + if ($implementation->providesData()) { + $data = $implementation->getData($timerange, $reportlet->getConfig()); + $dimensions = $data->getDimensions(); + $values = $data->getValues(); + foreach ($data->getRows() as $row) { + $json[] = \array_combine($dimensions, $row->getDimensions()) + + \array_combine($values, $row->getValues()); + } + + break; + } + } + + return json_encode($json); + } +} diff --git a/library/Reporting/ReportData.php b/library/Reporting/ReportData.php new file mode 100644 index 00000000..1ef9ec5e --- /dev/null +++ b/library/Reporting/ReportData.php @@ -0,0 +1,71 @@ +rows; + } + + public function setRows(array $rows) + { + $this->rows = $rows; + + return $this; + } + + public function getAverages() + { + $totals = $this->getTotals(); + $averages = []; + $count = \count($this); + + foreach ($totals as $total) { + $averages[] = $total / $count; + } + + return $averages; + } + +// public function getMaximums() +// { +// } + +// public function getMinimums() +// { +// } + + public function getTotals() + { + $totals = []; + + foreach ((array) $this->getRows() as $row) { + $i = 0; + foreach ((array) $row->getValues() as $value) { + if (! isset($totals[$i])) { + $totals[$i] = $value; + } else { + $totals[$i] += $value; + } + + ++$i; + } + } + + return $totals; + } + + public function count() + { + return count((array) $this->getRows()); + } +} diff --git a/library/Reporting/ReportRow.php b/library/Reporting/ReportRow.php new file mode 100644 index 00000000..1536488c --- /dev/null +++ b/library/Reporting/ReportRow.php @@ -0,0 +1,10 @@ +id; + } + + /** + * @param int $id + * + * @return $this + */ + public function setId($id) + { + $this->id = $id; + + return $this; + } + + /** + * @return string + */ + public function getClass() + { + return $this->class; + } + + /** + * @param string $class + * + * @return $this + */ + public function setClass($class) + { + $this->class = $class; + + return $this; + } + + /** + * @return array + */ + public function getConfig() + { + return $this->config; + } + + /** + * @param array $config + * + * @return $this + */ + public function setConfig($config) + { + $this->config = $config; + + return $this; + } + + /** + * @return \Icinga\Module\Reporting\Hook\ReportHook + */ + public function getImplementation() + { + $class = $this->getClass(); + + return new $class; + } +} diff --git a/library/Reporting/Reports/SystemReport.php b/library/Reporting/Reports/SystemReport.php new file mode 100644 index 00000000..8a3d8dd1 --- /dev/null +++ b/library/Reporting/Reports/SystemReport.php @@ -0,0 +1,39 @@ +loadHTML($html); + + $style = $doc->getElementsByTagName('style')->item(0); + $style->parentNode->removeChild($style); + + $title = $doc->getElementsByTagName('title')->item(0); + $title->parentNode->removeChild($title); + + $meta = $doc->getElementsByTagName('meta')->item(0); + $meta->parentNode->removeChild($meta); + + $doc->getElementsByTagName('div')->item(0)->setAttribute('class', 'system-report'); + + return new HtmlString($doc->saveHTML()); + } +} diff --git a/library/Reporting/RetryConnection.php b/library/Reporting/RetryConnection.php new file mode 100644 index 00000000..ebadfd2a --- /dev/null +++ b/library/Reporting/RetryConnection.php @@ -0,0 +1,66 @@ +getMessage(), [ + 'server has gone away', + 'no connection to the server', + 'Lost connection', + 'Error while sending', + 'is dead or not enabled', + 'decryption failed or bad record mac', + 'server closed the connection unexpectedly', + 'SSL connection has been closed unexpectedly', + 'Error writing data to the connection', + 'Resource deadlock avoided', + 'Transaction() on null', + 'child connection forced to terminate due to client_idle_limit', + 'query_wait_timeout', + 'reset by peer', + 'Physical connection is not usable', + 'TCP Provider: Error code 0x68', + 'ORA-03114', + 'Packets out of order. Expected', + 'Adaptive Server connection failed', + 'Communication link failure', + ]); + + if (! $lostConnection) { + throw $e; + } + + $this->disconnect(); + + try { + $this->connect(); + } catch (\Exception $e) { + $noConnection = Str::contains($e->getMessage(), [ + 'No such file or directory', + 'Connection refused' + ]); + + if (! $noConnection) { + throw $e; + } + + \sleep(10); + + $this->connect(); + } + + $sth = parent::prepexec($stmt, $values); + } + + return $sth; + } +} diff --git a/library/Reporting/Schedule.php b/library/Reporting/Schedule.php new file mode 100644 index 00000000..e0ffa9f1 --- /dev/null +++ b/library/Reporting/Schedule.php @@ -0,0 +1,160 @@ +id; + } + + /** + * @param int $id + * + * @return $this + */ + public function setId($id) + { + $this->id = $id; + + return $this; + } + + /** + * @return int + */ + public function getReportId() + { + return $this->reportId; + } + + /** + * @param int $id + * + * @return $this + */ + public function setReportId($id) + { + $this->reportId = $id; + + return $this; + } + + /** + * @return \DateTime + */ + public function getStart() + { + return $this->start; + } + + /** + * @param \DateTime $start + * + * @return $this + */ + public function setStart(\DateTime $start) + { + $this->start = $start; + + return $this; + } + + /** + * @return string + */ + public function getFrequency() + { + return $this->frequency; + } + + /** + * @param string $frequency + * + * @return $this + */ + public function setFrequency($frequency) + { + $this->frequency = $frequency; + + return $this; + } + + /** + * @return string + */ + public function getAction() + { + return $this->action; + } + + /** + * @param string $action + * + * @return $this + */ + public function setAction($action) + { + $this->action = $action; + + return $this; + } + + /** + * @return array + */ + public function getConfig() + { + return $this->config; + } + + /** + * @param array $config + * + * @return $this + */ + public function setConfig(array $config) + { + $this->config = $config; + + return $this; + } + + /** + * @return string + */ + public function getChecksum() + { + return \md5( + $this->getId() + . $this->getReportId() + . $this->getStart()->format('Y-m-d H:i:s') + . $this->getAction() + . $this->getFrequency() + . \json_encode($this->getConfig()) + ); + } +} diff --git a/library/Reporting/Scheduler.php b/library/Reporting/Scheduler.php new file mode 100644 index 00000000..1b8d9f6d --- /dev/null +++ b/library/Reporting/Scheduler.php @@ -0,0 +1,176 @@ +modify('midnight'); + + $diff = $midnight->diff($dateTime); + + return $diff->h * 60 * 60 + $diff->i * 60 + $diff->s; +} + +class Scheduler +{ + protected $db; + + protected $loop; + + /** @var array */ + protected $schedules = []; + + /** @var array */ + protected $timers = []; + + public function __construct(Connection $db) + { + $this->db = $db; + $this->loop = Loop::create(); + } + + public function run() + { + $updateTimers = function () use (&$updateTimers) { + $this->updateTimers(); + + $this->loop->addTimer(60, $updateTimers); + }; + + $this->loop->futureTick($updateTimers); + + $this->loop->run(); + } + + protected function fetchSchedules() + { + $schedules = []; + + $select = (new Select()) + ->from('schedule') + ->columns('*'); + + foreach ($this->db->select($select) as $row) { + $schedule = (new Schedule()) + ->setId((int) $row->id) + ->setReportId((int) $row->report_id) + ->setAction($row->action) + ->setConfig(\json_decode($row->config, true)) + ->setStart((new \DateTime())->setTimestamp((int) $row->start / 1000)) + ->setFrequency($row->frequency); + + $schedules[$schedule->getChecksum()] = $schedule; + } + + return $schedules; + } + + protected function updateTimers() + { + $schedules = $this->fetchSchedules(); + + $remove = \array_diff_key($this->schedules, $schedules); + + foreach ($remove as $schedule) { + printf("Removing job %s.\n", "Schedule {$schedule->getId()}"); + + $checksum = $schedule->getChecksum(); + + if (isset($this->timers[$checksum])) { + $this->loop->cancelTimer($this->timers[$checksum]); + unset($this->timers[$checksum]); + } else { + printf("Can't find timer for job %s.\n", $checksum); + } + } + + $add = \array_diff_key($schedules, $this->schedules); + + foreach ($add as $schedule) { + $this->add($schedule); + } + + $this->schedules = $schedules; + } + + + protected function add(Schedule $schedule) + { + $name = "Schedule {$schedule->getId()}"; + $frequency = $schedule->getFrequency(); + $start = clone $schedule->getStart(); + $callback = function () use ($schedule) { + $actionClass = $schedule->getAction(); + /** @var ActionHook $action */ + $action = new $actionClass; + + $action->execute( + Report::fromDb($schedule->getReportId()), + $schedule->getConfig() + ); + }; + + switch ($frequency) { + case 'minutely': + $modify = '+1 minute'; + break; + case 'hourly': + $modify = '+1 hour'; + break; + case 'daily': + $modify = '+1 day'; + break; + case 'weekly': + $modify = '+1 week'; + break; + case 'monthly': + $modify = '+1 month'; + break; + default: + throw new \InvalidArgumentException('Invalid frequency.'); + } + + $now = new \DateTime(); + + if ($start < $now) { +// printf("Scheduling job %s to run immediately.\n", $name); +// $this->loop->futureTick($callback); + + while ($start < $now) { + $start->modify($modify); + } + } + + $next = clone $start; + $next->modify($modify); + $interval = $next->getTimestamp() - $start->getTimestamp(); + + $current = $start->getTimestamp() - $now->getTimestamp(); + + printf("Scheduling job %s to run at %s.\n", $name, $start->format('Y-m-d H:i:s')); + + $loop = function () use (&$loop, $name, $callback, $interval, $schedule) { + $callback(); + + $nextRun = (new \DateTime()) + ->add(new \DateInterval("PT{$interval}S")); + + printf("Scheduling job %s to run at %s.\n", $name, $nextRun->format('Y-m-d H:i:s')); + + $timer = $this->loop->addTimer($interval, $loop); + + $this->timers[$schedule->getChecksum()] = $timer; + }; + + $timer = $this->loop->addTimer($current, $loop); + + $this->timers[$schedule->getChecksum()] = $timer; + } +} diff --git a/library/Reporting/Str.php b/library/Reporting/Str.php new file mode 100644 index 00000000..d4c73554 --- /dev/null +++ b/library/Reporting/Str.php @@ -0,0 +1,37 @@ +getDb(); + + $select = (new Select()) + ->from('timeframe') + ->columns('*') + ->where(['id = ?' => $id]); + + $row = $db->select($select)->fetch(); + + if ($row === false) { + throw new \Exception('Timeframe not found'); + } + + $timeframe + ->setId($row->id) + ->setName($row->name) + ->setTitle($row->title) + ->setStart($row->start) + ->setEnd($row->end); + + return $timeframe; + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @param int $id + * + * @return $this + */ + public function setId($id) + { + $this->id = $id; + + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * @param string $title + * + * @return $this + */ + public function setTitle($title) + { + $this->title = $title; + + return $this; + } + + /** + * @return string + */ + public function getStart() + { + return $this->start; + } + + /** + * @param string $start + * + * @return $this + */ + public function setStart($start) + { + $this->start = $start; + + return $this; + } + + /** + * @return string + */ + public function getEnd() + { + return $this->end; + } + + /** + * @param string $end + * + * @return $this + */ + public function setEnd($end) + { + $this->end = $end; + + return $this; + } + + public function getTimerange() + { + $start = new \DateTime($this->getStart()); + $end = new \DateTime($this->getEnd()); + + return new Timerange($start, $end); + } +} diff --git a/library/Reporting/Timerange.php b/library/Reporting/Timerange.php new file mode 100644 index 00000000..086bfb83 --- /dev/null +++ b/library/Reporting/Timerange.php @@ -0,0 +1,35 @@ +start = $start; + $this->end = $end; + } + + /** + * @return \DateTime + */ + public function getStart() + { + return $this->start; + } + + /** + * @return \DateTime + */ + public function getEnd() + { + return $this->end; + } +} diff --git a/library/Reporting/Values.php b/library/Reporting/Values.php new file mode 100644 index 00000000..3aa9b24a --- /dev/null +++ b/library/Reporting/Values.php @@ -0,0 +1,21 @@ +values; + } + + public function setValues(array $values) + { + $this->values = $values; + + return $this; + } +} diff --git a/library/Reporting/Web/Controller.php b/library/Reporting/Web/Controller.php new file mode 100644 index 00000000..98e26946 --- /dev/null +++ b/library/Reporting/Web/Controller.php @@ -0,0 +1,20 @@ +hasBeenSubmitted() + && ((isset($form->valid) && $form->valid === true) + || $form->isValid()) + ) { + $this->redirectNow($url); + } + } +} diff --git a/library/Reporting/Web/Forms/ReportForm.php b/library/Reporting/Web/Forms/ReportForm.php new file mode 100644 index 00000000..a15accad --- /dev/null +++ b/library/Reporting/Web/Forms/ReportForm.php @@ -0,0 +1,157 @@ +id = $id; + + return $this; + } + + protected function assemble() + { + $this->setDefaultElementDecorator(new DivDecorator()); + + $this->addElement('text', 'name', [ + 'required' => true, + 'label' => 'Name' + ]); + + $this->addElement('select', 'timeframe', [ + 'required' => true, + 'label' => 'Timeframe', + 'options' => [null => 'Please choose'] + $this->listTimeframes() + ]); + + $this->addElement('select', 'reportlet', [ + 'required' => true, + 'label' => 'Report', + 'options' => [null => 'Please choose'] + $this->listReports(), + 'class' => 'autosubmit' + ]); + + $values = $this->getValues(); + + if (isset($values['reportlet'])) { + $config = new Form(); +// $config->populate($this->getValues()); + + /** @var \Icinga\Module\Reporting\Hook\ReportHook $reportlet */ + $reportlet = new $values['reportlet']; + + $reportlet->initConfigForm($config); + + foreach ($config->getElements() as $element) { + $this->addElement($element); + } + } + + $this->addElement('submit', 'submit', [ + 'label' => $this->id === null ? 'Create Report' : 'Update Report' + ]); + + if ($this->id !== null) { + $this->addElement('submit', 'remove', [ + 'label' => 'Remove Report', + 'class' => 'remove-button', + 'formnovalidate' => true + ]); + + /** @var SubmitElementInterface $remove */ + $remove = $this->getElement('remove'); + if ($remove->hasBeenPressed()) { + $this->getDb()->delete('report', ['id = ?' => $this->id]); + + // Stupid cheat because ipl/html is not capable of multiple submit buttons + $this->getSubmitButton()->setValue($this->getSubmitButton()->getButtonLabel()); + $this->valid = true; + + return; + } + } + + // TODO(el): Remove once ipl/html's TextareaElement sets the value as content + foreach ($this->getElements() as $element) { + if ($element instanceof TextareaElement && $element->hasValue()) { + $element->setContent($element->getValue()); + } + } + } + + public function onSuccess() + { + $db = $this->getDb(); + + $values = $this->getValues(); + + $now = time() * 1000; + + $db->beginTransaction(); + + if ($this->id === null) { + $db->insert('report', [ + 'name' => $values['name'], + 'author' => Auth::getInstance()->getUser()->getUsername(), + 'timeframe_id' => $values['timeframe'], + 'ctime' => $now, + 'mtime' => $now + ]); + + $reportId = $db->lastInsertId(); + } else { + $db->update('report', [ + 'name' => $values['name'], + 'timeframe_id' => $values['timeframe'], + 'mtime' => $now + ], ['id = ?' => $this->id]); + + $reportId = $this->id; + } + + unset($values['name']); + unset($values['timeframe']); + + if ($this->id !== null) { + $db->delete('reportlet', ['report_id = ?' => $reportId]); + } + + $db->insert('reportlet', [ + 'report_id' => $reportId, + 'class' => $values['reportlet'], + 'ctime' => $now, + 'mtime' => $now + ]); + + $reportletId = $db->lastInsertId(); + + unset($values['reportlet']); + + foreach ($values as $name => $value) { + $db->insert('config', [ + 'reportlet_id' => $reportletId, + 'name' => $name, + 'value' => $value, + 'ctime' => $now, + 'mtime' => $now + ]); + } + + $db->commitTransaction(); + } +} diff --git a/library/Reporting/Web/Forms/ScheduleForm.php b/library/Reporting/Web/Forms/ScheduleForm.php new file mode 100644 index 00000000..5ef23464 --- /dev/null +++ b/library/Reporting/Web/Forms/ScheduleForm.php @@ -0,0 +1,167 @@ +report = $report; + + $schedule = $report->getSchedule(); + + if ($schedule !== null) { + $this->setId($schedule->getId()); + + $values = [ + 'start' => $schedule->getStart()->format('Y-m-d H:i'), + 'frequency' => $schedule->getFrequency(), + 'action' => $schedule->getAction() + ] + $schedule->getConfig(); + + $this->populate($values); + } + + return $this; + } + + public function setId($id) + { + $this->id = $id; + + return $this; + } + + protected function assemble() + { + $this->setDefaultElementDecorator(new DivDecorator()); + + $frequency = [ + 'minutely' => 'Minutely', + 'hourly' => 'Hourly', + 'daily' => 'Daily', + 'weekly' => 'Weekly', + 'monthly' => 'Monthly' + ]; + + $this->addElement('text', 'start', [ + 'required' => true, + 'label' => 'Start', + 'class' => 'flatpickr' + ]); + + $this->addElement('select', 'frequency', [ + 'required' => true, + 'label' => 'Frequency', + 'options' => [null => 'Please choose'] + $frequency, + ]); + + $this->addElement('select', 'action', [ + 'required' => true, + 'label' => 'Action', + 'options' => [null => 'Please choose'] + $this->listActions(), + 'class' => 'autosubmit' + ]); + + $values = $this->getValues(); + + if (isset($values['action'])) { + $config = new Form(); +// $config->populate($this->getValues()); + + /** @var \Icinga\Module\Reporting\Hook\ActionHook $action */ + $action = new $values['action']; + + $action->initConfigForm($config, $this->report); + + foreach ($config->getElements() as $element) { + $this->addElement($element); + } + } + + $this->addElement('submit', 'submit', [ + 'label' => $this->id === null ? 'Create Schedule' : 'Update Schedule' + ]); + + if ($this->id !== null) { + $this->addElement('submit', 'remove', [ + 'label' => 'Remove Schedule', + 'class' => 'remove-button', + 'formnovalidate' => true + ]); + + /** @var SubmitElementInterface $remove */ + $remove = $this->getElement('remove'); + if ($remove->hasBeenPressed()) { + $this->getDb()->delete('schedule', ['id = ?' => $this->id]); + + // Stupid cheat because ipl/html is not capable of multiple submit buttons + $this->getSubmitButton()->setValue($this->getSubmitButton()->getButtonLabel()); + $this->valid = true; + + return; + } + } + + // TODO(el): Remove once ipl/html's TextareaElement sets the value as content + foreach ($this->getElements() as $element) { + if ($element instanceof TextareaElement && $element->hasValue()) { + $element->setContent($element->getValue()); + } + } + } + + public function onSuccess() + { + $db = $this->getDb(); + + $values = $this->getValues(); + + $now = time() * 1000; + + $data = [ + 'start' => \DateTime::createFromFormat('Y-m-d H:i', $values['start'])->getTimestamp() * 1000, + 'frequency' => $values['frequency'], + 'action' => $values['action'], + 'mtime' => $now + ]; + + unset($values['start']); + unset($values['frequency']); + unset($values['action']); + + $data['config'] = json_encode($values); + + $db->beginTransaction(); + + if ($this->id === null) { + $db->insert('schedule', $data + [ + 'author' => Auth::getInstance()->getUser()->getUsername(), + 'report_id' => $this->report->getId(), + 'ctime' => $now + ]); + } else { + $db->update('schedule', $data, ['id = ?' => $this->id]); + } + + $db->commitTransaction(); + } +} diff --git a/library/Reporting/Web/Forms/SendForm.php b/library/Reporting/Web/Forms/SendForm.php new file mode 100644 index 00000000..0add2a17 --- /dev/null +++ b/library/Reporting/Web/Forms/SendForm.php @@ -0,0 +1,63 @@ +report = $report; + + return $this; + } + + protected function assemble() + { + $this->setDefaultElementDecorator(new DivDecorator()); + + $types = ['pdf' => 'PDF']; + + if ($this->report->providesData()) { + $types['csv'] = 'CSV'; + $types['json'] = 'JSON'; + } + + $this->addElement('select', 'type', [ + 'required' => true, + 'label' => 'Type', + 'options' => [null => 'Please choose'] + $types + ]); + + $this->addElement('textarea', 'recipients', [ + 'required' => true, + 'label' => 'Recipients' + ]); + + $this->addElement('submit', 'submit', [ + 'label' => 'Send Report' + ]); + } + + public function onSuccess() + { + $values = $this->getValues(); + + $sendMail = new SendMail(); + + $sendMail->execute($this->report, $values); + } +} diff --git a/library/vendor/ipl/web/src/Common/BaseTarget.php b/library/vendor/ipl/web/src/Common/BaseTarget.php new file mode 100644 index 00000000..0d92f424 --- /dev/null +++ b/library/vendor/ipl/web/src/Common/BaseTarget.php @@ -0,0 +1,30 @@ +getAttributes()->get('data-base-target')->getValue(); + } + + /** + * @param string $target + * + * @return $this + */ + public function setBaseTarget($target) + { + $this->getAttributes()->set('data-base-target', $target); + + return $this; + } +} diff --git a/library/vendor/ipl/web/src/Compat/CompatController.php b/library/vendor/ipl/web/src/Compat/CompatController.php new file mode 100644 index 00000000..4d562daf --- /dev/null +++ b/library/vendor/ipl/web/src/Compat/CompatController.php @@ -0,0 +1,77 @@ +view->tabs); + + $this->controls = new Controls(); + $this->content = new Content(); + $this->tabs = new Tabs(); + + $this->controls->setTabs($this->tabs); + + ViewRenderer::inject(); + + $this->view->controls = $this->controls; + $this->view->content = $this->content; + } + + /** + * Get the tabs + * + * @return Tabs + */ + public function getTabs() + { + return $this->tabs; + } + + protected function addControl(ValidHtml $control) + { + $this->controls->add($control); + + return $this; + } + + protected function addContent(ValidHtml $content) + { + $this->content->add($content); + + return $this; + } + + protected function setTitle($title, ...$args) + { + $title = vsprintf($title, $args); + + $this->view->title = $title; + + $this->getTabs()->add(uniqid(), [ + 'active' => true, + 'label' => $title, + 'url' => $this->getRequest()->getUrl() + ]); + } +} diff --git a/library/vendor/ipl/web/src/Compat/ViewRenderer.php b/library/vendor/ipl/web/src/Compat/ViewRenderer.php new file mode 100644 index 00000000..16427a27 --- /dev/null +++ b/library/vendor/ipl/web/src/Compat/ViewRenderer.php @@ -0,0 +1,66 @@ + $value) { + if ($property === '_inflector') { + continue; + } + + $inject->$property = $value; + } + + Zf1HelperBroker::removeHelper('ViewRenderer'); + Zf1HelperBroker::addHelper($inject); + } + + /** + * Render the view w/o using a view script + * + * {@inheritdoc} + */ + public function render($action = null, $name = null, $noController = null) + { + $view = $this->view; + + if (($view->controls->isEmpty() && $view->content->isEmpty()) + || $this->getRequest()->getParam('error_handler') !== null + ) { + parent::render($action, $name, $noController); + + return; + } + + if ($name === null) { + $name = $this->getResponseSegment(); + } + + $this->getResponse()->appendBody( + $view->controls->render() . "\n" . $view->content->render(), + $name + ); + + $this->setNoRender(); + } + + public function getName() + { + return 'ViewRenderer'; + } +} diff --git a/library/vendor/ipl/web/src/Url.php b/library/vendor/ipl/web/src/Url.php new file mode 100644 index 00000000..abf87470 --- /dev/null +++ b/library/vendor/ipl/web/src/Url.php @@ -0,0 +1,15 @@ +getAbsoluteUrl('&'); + } +} diff --git a/library/vendor/ipl/web/src/Widget/ActionBar.php b/library/vendor/ipl/web/src/Widget/ActionBar.php new file mode 100644 index 00000000..2d658b6e --- /dev/null +++ b/library/vendor/ipl/web/src/Widget/ActionBar.php @@ -0,0 +1,44 @@ + 'action-bar', 'data-base-target' => '_self']; + + /** + * Create a action bar + * + * @param Attributes|array $attributes + */ + public function __construct($attributes = null) + { + $this->getAttributes()->add(Attributes::wantAttributes($attributes)); + } + + /** + * @param mixed $content + * @param Url|string $url + * @param string $icon + * + * @return $this + */ + public function addLink($content, $url, $icon = null) + { + $this->add(new ActionLink($content, $url, $icon)); + + return $this; + } +} diff --git a/library/vendor/ipl/web/src/Widget/ActionLink.php b/library/vendor/ipl/web/src/Widget/ActionLink.php new file mode 100644 index 00000000..584e7380 --- /dev/null +++ b/library/vendor/ipl/web/src/Widget/ActionLink.php @@ -0,0 +1,29 @@ + 'action-link']; + + /** + * Create a action link + * + * @param mixed $content + * @param Url|string $url + * @param string $icon + * @param Attributes|array $attributes + */ + public function __construct($content, $url, $icon = null, $attributes = null) + { + parent::__construct($content, $url, $attributes); + + if ($icon !== null) { + $this->prepend(new Icon($icon)); + } + } +} diff --git a/library/vendor/ipl/web/src/Widget/ButtonLink.php b/library/vendor/ipl/web/src/Widget/ButtonLink.php new file mode 100644 index 00000000..b217071e --- /dev/null +++ b/library/vendor/ipl/web/src/Widget/ButtonLink.php @@ -0,0 +1,9 @@ + 'button-link', 'data-base-target' => '_main']; +} diff --git a/library/vendor/ipl/web/src/Widget/Content.php b/library/vendor/ipl/web/src/Widget/Content.php new file mode 100644 index 00000000..23c72d75 --- /dev/null +++ b/library/vendor/ipl/web/src/Widget/Content.php @@ -0,0 +1,15 @@ + 'content']; +} diff --git a/library/vendor/ipl/web/src/Widget/Controls.php b/library/vendor/ipl/web/src/Widget/Controls.php new file mode 100644 index 00000000..b19e45b5 --- /dev/null +++ b/library/vendor/ipl/web/src/Widget/Controls.php @@ -0,0 +1,47 @@ + 'controls']; + + /** @var Tabs */ + protected $tabs; + + /** + * Get the tabs + * + * @return Tabs + */ + public function getTabs() + { + return $this->tabs; + } + + /** + * Set the tabs + * + * @param Tabs $tabs + * + * @return $this + */ + public function setTabs(Tabs $tabs) + { + $this->tabs = $tabs; + + return $this; + } + + protected function assemble() + { + $this->prepend($this->getTabs()); + } +} diff --git a/library/vendor/ipl/web/src/Widget/DropdownLink.php b/library/vendor/ipl/web/src/Widget/DropdownLink.php new file mode 100644 index 00000000..906dcf91 --- /dev/null +++ b/library/vendor/ipl/web/src/Widget/DropdownLink.php @@ -0,0 +1,70 @@ + 'dropdown']; + + /** @var array */ + protected $links = []; + + /** + * Create a dropdown link + * + * @param mixed $content + * @param \ipl\Html\Attributes|array $attributes + */ + public function __construct($content, $attributes = null) + { + $toggle = new ActionLink($content, '#'); + + $toggle->getAttributes()->add([ + 'class' => 'dropdown-toggle', + 'role' => 'button', + 'aria-haspopup' => true, + 'aria-expanded' => false + ]); + + $this->hasBeenAssembled = true; + + $this + ->setContent($toggle) + ->getAttributes() + ->add(Attributes::wantAttributes($attributes)); + + $this->hasBeenAssembled = false; + } + + /** + * @param mixed $content + * @param Url|string $url + * @param string $icon + * + * @return $this + */ + public function addLink($content, $url, $icon = null) + { + $link = new ActionLink($content, $url, $icon); + + $link->getAttributes()->add('class', 'dropdown-item'); + $link->getAttributes()->add('target', '_blank'); + + $this->links[] = $link; + + return $this; + } + + protected function assemble() + { + $this->add(Html::tag('div', ['class' => 'dropdown-menu'], $this->links)); + } +} diff --git a/library/vendor/ipl/web/src/Widget/Icon.php b/library/vendor/ipl/web/src/Widget/Icon.php new file mode 100644 index 00000000..0a03d181 --- /dev/null +++ b/library/vendor/ipl/web/src/Widget/Icon.php @@ -0,0 +1,29 @@ +. The given + * name will be used as automatically added CSS class for the icon element in the format 'icon-$name'. In addition, + * the CSS class 'icon' will be automatically added too. + * + * @param string $name The name of the icon + * @param \ipl\Html\Attributes|array $attributes The HTML attributes for the element + */ + public function __construct($name, $attributes = null) + { + $this->getAttributes() + ->add('class', ['icon', "icon-$name"]) + ->add(Attributes::wantAttributes($attributes)); + } +} diff --git a/library/vendor/ipl/web/src/Widget/Link.php b/library/vendor/ipl/web/src/Widget/Link.php new file mode 100644 index 00000000..8c7cac88 --- /dev/null +++ b/library/vendor/ipl/web/src/Widget/Link.php @@ -0,0 +1,81 @@ +hasBeenAssembled = true; + + $this + ->setContent($content) + ->setUrl($url) + ->getAttributes() + ->add(Attributes::wantAttributes($attributes)) + ->registerAttributeCallback('href', [$this, 'getHrefAttribute']); + + $this->hasBeenAssembled = false; + } + + /** + * Get the URL of the link + * + * @return Url + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set the URL of the link + * + * @param Url|string $url + * + * @return $this + */ + public function setUrl($url) + { + if (! $url instanceof \Icinga\Web\Url) { + try { + $url = Url::fromPath($url); + } catch (\Exception $e) { + $url = "Invalid: {$e->getMessage()}"; + } + } + + $this->url = $url; + + return $this; + } + + /** + * @return Attribute + */ + public function getHrefAttribute() + { + return new Attribute('href', (string) $this->getUrl()); + } +} diff --git a/library/vendor/ipl/web/src/Widget/Tabs.php b/library/vendor/ipl/web/src/Widget/Tabs.php new file mode 100644 index 00000000..2e8c8655 --- /dev/null +++ b/library/vendor/ipl/web/src/Widget/Tabs.php @@ -0,0 +1,13 @@ + tbody::before { + content: "\200C"; + display: block; + line-height: 0.5em; +} + +/** + * Mixin to add subtile box-shadow to make element seem to hover above the background + */ +.elevate() { + box-shadow: 0 0 2em 0 rgba(0,0,0,.2); +} + +.dropdown { + display: inline-block; + position: relative; +} + +.dropdown-toggle::after { + content: ""; + display: inline-block; + width: 0; + height: 0; + margin-left: .255em; + vertical-align: .255em; + border-top: .3em solid; + border-right: .3em solid transparent; + border-bottom: 0; + border-left: .3em solid transparent; +} + +.dropdown-menu { + display: none; + min-width: 10em; + border: 1px solid #ddd; + background: white; + margin: -.25em; + border-radius: .25em; + padding: .25em; + + position: absolute; +} + +.dropdown:hover > .dropdown-menu { + display: block; + .elevate(); +} + +.dropdown-item { + display: block; + padding: .5em; + margin: -.25em; + + &.action-link:hover { + padding: .5em; + .rounded-corners(0) + } +} + +.action-bar .dropdown:first-child:hover .dropdown-menu { + left: .25em; +} + +.action-bar .dropdown:last-child:hover .dropdown-menu { + right: .25em; +} + +.action-bar > *:not(:last-child) { + margin-right: .5em; +} diff --git a/public/css/system-report.css b/public/css/system-report.css new file mode 100644 index 00000000..cf42ae6d --- /dev/null +++ b/public/css/system-report.css @@ -0,0 +1,81 @@ +/* Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 */ + +.system-report { + background-color: #fff; + color: #222; + font-family: sans-serif; + + width: 100%; +} +.system-report pre { + margin: 0; + font-family: monospace; +} +.system-report a:link { + color: #009; + text-decoration: none; + background-color: #fff; +} +.system-report a:hover { + text-decoration: underline; +} +.system-report table { + border-collapse: collapse; + border: 0; + width: 934px; + box-shadow: 1px 2px 3px #ccc; +} +.system-report .center { + text-align: center; +} +.system-report .center table { + margin: 1em auto; + text-align: left; +} +.system-report .center th { + text-align: center !important; +} +.system-report td, +.system-report th { + border: 1px solid #666; + font-size: 75%; + vertical-align: baseline; + padding: 4px 5px; +} +.system-report h1 { + font-size: 150%; +} +.system-report h2 { + font-size: 125%; +} +.system-report .p { + text-align: left; +} +.system-report .e { + background-color: #ccf; + width: 300px; + font-weight: bold; +} +.system-report .h { + background-color: #99c; + font-weight: bold; +} +.system-report .v { + background-color: #ddd; + max-width: 300px; + overflow-x: auto; + word-wrap: break-word; +} +.system-report .v i { + color: #999; +} +.system-report img { + float: right; + border: 0; +} +.system-report hr { + width: 934px; + background-color: #ccc; + border: 0; + height: 1px; +} diff --git a/public/css/vendor/flatpickr.css b/public/css/vendor/flatpickr.css new file mode 100644 index 00000000..6467ff22 --- /dev/null +++ b/public/css/vendor/flatpickr.css @@ -0,0 +1,741 @@ +.flatpickr-calendar { + background: transparent; + opacity: 0; + display: none; + text-align: center; + visibility: hidden; + padding: 0; + -webkit-animation: none; + animation: none; + direction: ltr; + border: 0; + font-size: 14px; + line-height: 24px; + border-radius: 5px; + position: absolute; + width: 307.875px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + -ms-touch-action: manipulation; + touch-action: manipulation; + background: #fff; + -webkit-box-shadow: 1px 0 0 #e6e6e6, -1px 0 0 #e6e6e6, 0 1px 0 #e6e6e6, 0 -1px 0 #e6e6e6, 0 3px 13px rgba(0,0,0,0.08); + box-shadow: 1px 0 0 #e6e6e6, -1px 0 0 #e6e6e6, 0 1px 0 #e6e6e6, 0 -1px 0 #e6e6e6, 0 3px 13px rgba(0,0,0,0.08); +} +.flatpickr-calendar.open, +.flatpickr-calendar.inline { + opacity: 1; + max-height: 640px; + visibility: visible; +} +.flatpickr-calendar.open { + display: inline-block; + z-index: 99999; +} +.flatpickr-calendar.animate.open { + -webkit-animation: fpFadeInDown 300ms cubic-bezier(0.23, 1, 0.32, 1); + animation: fpFadeInDown 300ms cubic-bezier(0.23, 1, 0.32, 1); +} +.flatpickr-calendar.inline { + display: block; + position: relative; + top: 2px; +} +.flatpickr-calendar.static { + position: absolute; + top: calc(100% + 2px); +} +.flatpickr-calendar.static.open { + z-index: 999; + display: block; +} +.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7) { + -webkit-box-shadow: none !important; + box-shadow: none !important; +} +.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1) { + -webkit-box-shadow: -2px 0 0 #e6e6e6, 5px 0 0 #e6e6e6; + box-shadow: -2px 0 0 #e6e6e6, 5px 0 0 #e6e6e6; +} +.flatpickr-calendar .hasWeeks .dayContainer, +.flatpickr-calendar .hasTime .dayContainer { + border-bottom: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.flatpickr-calendar .hasWeeks .dayContainer { + border-left: 0; +} +.flatpickr-calendar.showTimeInput.hasTime .flatpickr-time { + height: 40px; + border-top: 1px solid #e6e6e6; +} +.flatpickr-calendar.noCalendar.hasTime .flatpickr-time { + height: auto; +} +.flatpickr-calendar:before, +.flatpickr-calendar:after { + position: absolute; + display: block; + pointer-events: none; + border: solid transparent; + content: ''; + height: 0; + width: 0; + left: 22px; +} +.flatpickr-calendar.rightMost:before, +.flatpickr-calendar.rightMost:after { + left: auto; + right: 22px; +} +.flatpickr-calendar:before { + border-width: 5px; + margin: 0 -5px; +} +.flatpickr-calendar:after { + border-width: 4px; + margin: 0 -4px; +} +.flatpickr-calendar.arrowTop:before, +.flatpickr-calendar.arrowTop:after { + bottom: 100%; +} +.flatpickr-calendar.arrowTop:before { + border-bottom-color: #e6e6e6; +} +.flatpickr-calendar.arrowTop:after { + border-bottom-color: #fff; +} +.flatpickr-calendar.arrowBottom:before, +.flatpickr-calendar.arrowBottom:after { + top: 100%; +} +.flatpickr-calendar.arrowBottom:before { + border-top-color: #e6e6e6; +} +.flatpickr-calendar.arrowBottom:after { + border-top-color: #fff; +} +.flatpickr-calendar:focus { + outline: 0; +} +.flatpickr-wrapper { + position: relative; + display: inline-block; +} +.flatpickr-months { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} +.flatpickr-months .flatpickr-month { + background: transparent; + color: rgba(0,0,0,0.9); + fill: rgba(0,0,0,0.9); + height: 28px; + line-height: 1; + text-align: center; + position: relative; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + overflow: hidden; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} +.flatpickr-months .flatpickr-prev-month, +.flatpickr-months .flatpickr-next-month { + text-decoration: none; + cursor: pointer; + position: absolute; + top: 0px; + line-height: 16px; + height: 28px; + padding: 10px; + z-index: 3; + color: rgba(0,0,0,0.9); + fill: rgba(0,0,0,0.9); +} +.flatpickr-months .flatpickr-prev-month.disabled, +.flatpickr-months .flatpickr-next-month.disabled { + display: none; +} +.flatpickr-months .flatpickr-prev-month i, +.flatpickr-months .flatpickr-next-month i { + position: relative; +} +.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month, +.flatpickr-months .flatpickr-next-month.flatpickr-prev-month { +/* + /*rtl:begin:ignore*/ +/* + */ + left: 0; +/* + /*rtl:end:ignore*/ +/* + */ +} +/* + /*rtl:begin:ignore*/ +/* + /*rtl:end:ignore*/ +.flatpickr-months .flatpickr-prev-month.flatpickr-next-month, +.flatpickr-months .flatpickr-next-month.flatpickr-next-month { +/* + /*rtl:begin:ignore*/ +/* + */ + right: 0; +/* + /*rtl:end:ignore*/ +/* + */ +} +/* + /*rtl:begin:ignore*/ +/* + /*rtl:end:ignore*/ +.flatpickr-months .flatpickr-prev-month:hover, +.flatpickr-months .flatpickr-next-month:hover { + color: #959ea9; +} +.flatpickr-months .flatpickr-prev-month:hover svg, +.flatpickr-months .flatpickr-next-month:hover svg { + fill: #f64747; +} +.flatpickr-months .flatpickr-prev-month svg, +.flatpickr-months .flatpickr-next-month svg { + width: 14px; + height: 14px; +} +.flatpickr-months .flatpickr-prev-month svg path, +.flatpickr-months .flatpickr-next-month svg path { + -webkit-transition: fill 0.1s; + transition: fill 0.1s; + fill: inherit; +} +.numInputWrapper { + position: relative; + height: auto; +} +.numInputWrapper input, +.numInputWrapper span { + display: inline-block; +} +.numInputWrapper input { + width: 100%; +} +.numInputWrapper input::-ms-clear { + display: none; +} +.numInputWrapper span { + position: absolute; + right: 0; + width: 14px; + padding: 0 4px 0 2px; + height: 50%; + line-height: 50%; + opacity: 0; + cursor: pointer; + border: 1px solid rgba(57,57,57,0.15); + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.numInputWrapper span:hover { + background: rgba(0,0,0,0.1); +} +.numInputWrapper span:active { + background: rgba(0,0,0,0.2); +} +.numInputWrapper span:after { + display: block; + content: ""; + position: absolute; +} +.numInputWrapper span.arrowUp { + top: 0; + border-bottom: 0; +} +.numInputWrapper span.arrowUp:after { + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-bottom: 4px solid rgba(57,57,57,0.6); + top: 26%; +} +.numInputWrapper span.arrowDown { + top: 50%; +} +.numInputWrapper span.arrowDown:after { + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid rgba(57,57,57,0.6); + top: 40%; +} +.numInputWrapper span svg { + width: inherit; + height: auto; +} +.numInputWrapper span svg path { + fill: rgba(0,0,0,0.5); +} +.numInputWrapper:hover { + background: rgba(0,0,0,0.05); +} +.numInputWrapper:hover span { + opacity: 1; +} +.flatpickr-current-month { + font-size: 135%; + line-height: inherit; + font-weight: 300; + color: inherit; + position: absolute; + width: 75%; + left: 12.5%; + padding: 6.16px 0 0 0; + line-height: 1; + height: 28px; + display: inline-block; + text-align: center; + -webkit-transform: translate3d(0px, 0px, 0px); + transform: translate3d(0px, 0px, 0px); +} +.flatpickr-current-month span.cur-month { + font-family: inherit; + font-weight: 700; + color: inherit; + display: inline-block; + margin-left: 0.5ch; + padding: 0; +} +.flatpickr-current-month span.cur-month:hover { + background: rgba(0,0,0,0.05); +} +.flatpickr-current-month .numInputWrapper { + width: 6ch; + width: 7ch\0; + display: inline-block; +} +.flatpickr-current-month .numInputWrapper span.arrowUp:after { + border-bottom-color: rgba(0,0,0,0.9); +} +.flatpickr-current-month .numInputWrapper span.arrowDown:after { + border-top-color: rgba(0,0,0,0.9); +} +.flatpickr-current-month input.cur-year { + background: transparent; + -webkit-box-sizing: border-box; + box-sizing: border-box; + color: inherit; + cursor: text; + padding: 0 0 0 0.5ch; + margin: 0; + display: inline-block; + font-size: inherit; + font-family: inherit; + font-weight: 300; + line-height: inherit; + height: auto; + border: 0; + border-radius: 0; + vertical-align: initial; +} +.flatpickr-current-month input.cur-year:focus { + outline: 0; +} +.flatpickr-current-month input.cur-year[disabled], +.flatpickr-current-month input.cur-year[disabled]:hover { + font-size: 100%; + color: rgba(0,0,0,0.5); + background: transparent; + pointer-events: none; +} +.flatpickr-weekdays { + background: transparent; + text-align: center; + overflow: hidden; + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + height: 28px; +} +.flatpickr-weekdays .flatpickr-weekdaycontainer { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} +span.flatpickr-weekday { + cursor: default; + font-size: 90%; + background: transparent; + color: rgba(0,0,0,0.54); + line-height: 1; + margin: 0; + text-align: center; + display: block; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + font-weight: bolder; +} +.dayContainer, +.flatpickr-weeks { + padding: 1px 0 0 0; +} +.flatpickr-days { + position: relative; + overflow: hidden; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: start; + -webkit-align-items: flex-start; + -ms-flex-align: start; + align-items: flex-start; + width: 307.875px; +} +.flatpickr-days:focus { + outline: 0; +} +.dayContainer { + padding: 0; + outline: 0; + text-align: left; + width: 307.875px; + min-width: 307.875px; + max-width: 307.875px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + display: inline-block; + display: -ms-flexbox; + display: -webkit-box; + display: -webkit-flex; + display: flex; + -webkit-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-wrap: wrap; + -ms-flex-pack: justify; + -webkit-justify-content: space-around; + justify-content: space-around; + -webkit-transform: translate3d(0px, 0px, 0px); + transform: translate3d(0px, 0px, 0px); + opacity: 1; +} +.dayContainer + .dayContainer { + -webkit-box-shadow: -1px 0 0 #e6e6e6; + box-shadow: -1px 0 0 #e6e6e6; +} +.flatpickr-day { + background: none; + border: 1px solid transparent; + border-radius: 150px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + color: #393939; + cursor: pointer; + font-weight: 400; + width: 14.2857143%; + -webkit-flex-basis: 14.2857143%; + -ms-flex-preferred-size: 14.2857143%; + flex-basis: 14.2857143%; + max-width: 39px; + height: 39px; + line-height: 39px; + margin: 0; + display: inline-block; + position: relative; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + text-align: center; +} +.flatpickr-day.inRange, +.flatpickr-day.prevMonthDay.inRange, +.flatpickr-day.nextMonthDay.inRange, +.flatpickr-day.today.inRange, +.flatpickr-day.prevMonthDay.today.inRange, +.flatpickr-day.nextMonthDay.today.inRange, +.flatpickr-day:hover, +.flatpickr-day.prevMonthDay:hover, +.flatpickr-day.nextMonthDay:hover, +.flatpickr-day:focus, +.flatpickr-day.prevMonthDay:focus, +.flatpickr-day.nextMonthDay:focus { + cursor: pointer; + outline: 0; + background: #e6e6e6; + border-color: #e6e6e6; +} +.flatpickr-day.today { + border-color: #959ea9; +} +.flatpickr-day.today:hover, +.flatpickr-day.today:focus { + border-color: #959ea9; + background: #959ea9; + color: #fff; +} +.flatpickr-day.selected, +.flatpickr-day.startRange, +.flatpickr-day.endRange, +.flatpickr-day.selected.inRange, +.flatpickr-day.startRange.inRange, +.flatpickr-day.endRange.inRange, +.flatpickr-day.selected:focus, +.flatpickr-day.startRange:focus, +.flatpickr-day.endRange:focus, +.flatpickr-day.selected:hover, +.flatpickr-day.startRange:hover, +.flatpickr-day.endRange:hover, +.flatpickr-day.selected.prevMonthDay, +.flatpickr-day.startRange.prevMonthDay, +.flatpickr-day.endRange.prevMonthDay, +.flatpickr-day.selected.nextMonthDay, +.flatpickr-day.startRange.nextMonthDay, +.flatpickr-day.endRange.nextMonthDay { + background: #569ff7; + -webkit-box-shadow: none; + box-shadow: none; + color: #fff; + border-color: #569ff7; +} +.flatpickr-day.selected.startRange, +.flatpickr-day.startRange.startRange, +.flatpickr-day.endRange.startRange { + border-radius: 50px 0 0 50px; +} +.flatpickr-day.selected.endRange, +.flatpickr-day.startRange.endRange, +.flatpickr-day.endRange.endRange { + border-radius: 0 50px 50px 0; +} +.flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n+1)), +.flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n+1)), +.flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n+1)) { + -webkit-box-shadow: -10px 0 0 #569ff7; + box-shadow: -10px 0 0 #569ff7; +} +.flatpickr-day.selected.startRange.endRange, +.flatpickr-day.startRange.startRange.endRange, +.flatpickr-day.endRange.startRange.endRange { + border-radius: 50px; +} +.flatpickr-day.inRange { + border-radius: 0; + -webkit-box-shadow: -5px 0 0 #e6e6e6, 5px 0 0 #e6e6e6; + box-shadow: -5px 0 0 #e6e6e6, 5px 0 0 #e6e6e6; +} +.flatpickr-day.disabled, +.flatpickr-day.disabled:hover, +.flatpickr-day.prevMonthDay, +.flatpickr-day.nextMonthDay, +.flatpickr-day.notAllowed, +.flatpickr-day.notAllowed.prevMonthDay, +.flatpickr-day.notAllowed.nextMonthDay { + color: rgba(57,57,57,0.3); + background: transparent; + border-color: transparent; + cursor: default; +} +.flatpickr-day.disabled, +.flatpickr-day.disabled:hover { + cursor: not-allowed; + color: rgba(57,57,57,0.1); +} +.flatpickr-day.week.selected { + border-radius: 0; + -webkit-box-shadow: -5px 0 0 #569ff7, 5px 0 0 #569ff7; + box-shadow: -5px 0 0 #569ff7, 5px 0 0 #569ff7; +} +.flatpickr-day.hidden { + visibility: hidden; +} +.rangeMode .flatpickr-day { + margin-top: 1px; +} +.flatpickr-weekwrapper { + display: inline-block; + float: left; +} +.flatpickr-weekwrapper .flatpickr-weeks { + padding: 0 12px; + -webkit-box-shadow: 1px 0 0 #e6e6e6; + box-shadow: 1px 0 0 #e6e6e6; +} +.flatpickr-weekwrapper .flatpickr-weekday { + float: none; + width: 100%; + line-height: 28px; +} +.flatpickr-weekwrapper span.flatpickr-day, +.flatpickr-weekwrapper span.flatpickr-day:hover { + display: block; + width: 100%; + max-width: none; + color: rgba(57,57,57,0.3); + background: transparent; + cursor: default; + border: none; +} +.flatpickr-innerContainer { + display: block; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-sizing: border-box; + box-sizing: border-box; + overflow: hidden; +} +.flatpickr-rContainer { + display: inline-block; + padding: 0; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.flatpickr-time { + text-align: center; + outline: 0; + display: block; + height: 0; + line-height: 40px; + max-height: 40px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + overflow: hidden; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} +.flatpickr-time:after { + content: ""; + display: table; + clear: both; +} +.flatpickr-time .numInputWrapper { + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + width: 40%; + height: 40px; + float: left; +} +.flatpickr-time .numInputWrapper span.arrowUp:after { + border-bottom-color: #393939; +} +.flatpickr-time .numInputWrapper span.arrowDown:after { + border-top-color: #393939; +} +.flatpickr-time.hasSeconds .numInputWrapper { + width: 26%; +} +.flatpickr-time.time24hr .numInputWrapper { + width: 49%; +} +.flatpickr-time input { + background: transparent; + -webkit-box-shadow: none; + box-shadow: none; + border: 0; + border-radius: 0; + text-align: center; + margin: 0; + padding: 0; + height: inherit; + line-height: inherit; + color: #393939; + font-size: 14px; + position: relative; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.flatpickr-time input.flatpickr-hour { + font-weight: bold; +} +.flatpickr-time input.flatpickr-minute, +.flatpickr-time input.flatpickr-second { + font-weight: 400; +} +.flatpickr-time input:focus { + outline: 0; + border: 0; +} +.flatpickr-time .flatpickr-time-separator, +.flatpickr-time .flatpickr-am-pm { + height: inherit; + display: inline-block; + float: left; + line-height: inherit; + color: #393939; + font-weight: bold; + width: 2%; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; +} +.flatpickr-time .flatpickr-am-pm { + outline: 0; + width: 18%; + cursor: pointer; + text-align: center; + font-weight: 400; +} +.flatpickr-time input:hover, +.flatpickr-time .flatpickr-am-pm:hover, +.flatpickr-time input:focus, +.flatpickr-time .flatpickr-am-pm:focus { + background: #f3f3f3; +} +.flatpickr-input[readonly] { + cursor: pointer; +} +@-webkit-keyframes fpFadeInDown { + from { + opacity: 0; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +@keyframes fpFadeInDown { + from { + opacity: 0; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} diff --git a/public/css/vendor/flatpickr.min.css b/public/css/vendor/flatpickr.min.css new file mode 100644 index 00000000..5bcd1ff8 --- /dev/null +++ b/public/css/vendor/flatpickr.min.css @@ -0,0 +1,13 @@ +.flatpickr-calendar{background:transparent;opacity:0;display:none;text-align:center;visibility:hidden;padding:0;-webkit-animation:none;animation:none;direction:ltr;border:0;font-size:14px;line-height:24px;border-radius:5px;position:absolute;width:307.875px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-touch-action:manipulation;touch-action:manipulation;background:#fff;-webkit-box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08);box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08);}.flatpickr-calendar.open,.flatpickr-calendar.inline{opacity:1;max-height:640px;visibility:visible}.flatpickr-calendar.open{display:inline-block;z-index:99999}.flatpickr-calendar.animate.open{-webkit-animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1);animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.inline{display:block;position:relative;top:2px}.flatpickr-calendar.static{position:absolute;top:calc(100% + 2px);}.flatpickr-calendar.static.open{z-index:999;display:block}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7){-webkit-box-shadow:none !important;box-shadow:none !important}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1){-webkit-box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6}.flatpickr-calendar .hasWeeks .dayContainer,.flatpickr-calendar .hasTime .dayContainer{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.flatpickr-calendar .hasWeeks .dayContainer{border-left:0}.flatpickr-calendar.showTimeInput.hasTime .flatpickr-time{height:40px;border-top:1px solid #e6e6e6}.flatpickr-calendar.noCalendar.hasTime .flatpickr-time{height:auto}.flatpickr-calendar:before,.flatpickr-calendar:after{position:absolute;display:block;pointer-events:none;border:solid transparent;content:'';height:0;width:0;left:22px}.flatpickr-calendar.rightMost:before,.flatpickr-calendar.rightMost:after{left:auto;right:22px}.flatpickr-calendar:before{border-width:5px;margin:0 -5px}.flatpickr-calendar:after{border-width:4px;margin:0 -4px}.flatpickr-calendar.arrowTop:before,.flatpickr-calendar.arrowTop:after{bottom:100%}.flatpickr-calendar.arrowTop:before{border-bottom-color:#e6e6e6}.flatpickr-calendar.arrowTop:after{border-bottom-color:#fff}.flatpickr-calendar.arrowBottom:before,.flatpickr-calendar.arrowBottom:after{top:100%}.flatpickr-calendar.arrowBottom:before{border-top-color:#e6e6e6}.flatpickr-calendar.arrowBottom:after{border-top-color:#fff}.flatpickr-calendar:focus{outline:0}.flatpickr-wrapper{position:relative;display:inline-block}.flatpickr-months{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}.flatpickr-months .flatpickr-month{background:transparent;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9);height:28px;line-height:1;text-align:center;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:hidden;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.flatpickr-months .flatpickr-prev-month,.flatpickr-months .flatpickr-next-month{text-decoration:none;cursor:pointer;position:absolute;top:0;line-height:16px;height:28px;padding:10px;z-index:3;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9);}.flatpickr-months .flatpickr-prev-month.disabled,.flatpickr-months .flatpickr-next-month.disabled{display:none}.flatpickr-months .flatpickr-prev-month i,.flatpickr-months .flatpickr-next-month i{position:relative}.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,.flatpickr-months .flatpickr-next-month.flatpickr-prev-month{/* + /*rtl:begin:ignore*/left:0;/* + /*rtl:end:ignore*/}/* + /*rtl:begin:ignore*/ +/* + /*rtl:end:ignore*/ +.flatpickr-months .flatpickr-prev-month.flatpickr-next-month,.flatpickr-months .flatpickr-next-month.flatpickr-next-month{/* + /*rtl:begin:ignore*/right:0;/* + /*rtl:end:ignore*/}/* + /*rtl:begin:ignore*/ +/* + /*rtl:end:ignore*/ +.flatpickr-months .flatpickr-prev-month:hover,.flatpickr-months .flatpickr-next-month:hover{color:#959ea9;}.flatpickr-months .flatpickr-prev-month:hover svg,.flatpickr-months .flatpickr-next-month:hover svg{fill:#f64747}.flatpickr-months .flatpickr-prev-month svg,.flatpickr-months .flatpickr-next-month svg{width:14px;height:14px;}.flatpickr-months .flatpickr-prev-month svg path,.flatpickr-months .flatpickr-next-month svg path{-webkit-transition:fill .1s;transition:fill .1s;fill:inherit}.numInputWrapper{position:relative;height:auto;}.numInputWrapper input,.numInputWrapper span{display:inline-block}.numInputWrapper input{width:100%;}.numInputWrapper input::-ms-clear{display:none}.numInputWrapper span{position:absolute;right:0;width:14px;padding:0 4px 0 2px;height:50%;line-height:50%;opacity:0;cursor:pointer;border:1px solid rgba(57,57,57,0.15);-webkit-box-sizing:border-box;box-sizing:border-box;}.numInputWrapper span:hover{background:rgba(0,0,0,0.1)}.numInputWrapper span:active{background:rgba(0,0,0,0.2)}.numInputWrapper span:after{display:block;content:"";position:absolute}.numInputWrapper span.arrowUp{top:0;border-bottom:0;}.numInputWrapper span.arrowUp:after{border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:4px solid rgba(57,57,57,0.6);top:26%}.numInputWrapper span.arrowDown{top:50%;}.numInputWrapper span.arrowDown:after{border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid rgba(57,57,57,0.6);top:40%}.numInputWrapper span svg{width:inherit;height:auto;}.numInputWrapper span svg path{fill:rgba(0,0,0,0.5)}.numInputWrapper:hover{background:rgba(0,0,0,0.05);}.numInputWrapper:hover span{opacity:1}.flatpickr-current-month{font-size:135%;line-height:inherit;font-weight:300;color:inherit;position:absolute;width:75%;left:12.5%;padding:6.16px 0 0 0;line-height:1;height:28px;display:inline-block;text-align:center;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);}.flatpickr-current-month span.cur-month{font-family:inherit;font-weight:700;color:inherit;display:inline-block;margin-left:.5ch;padding:0;}.flatpickr-current-month span.cur-month:hover{background:rgba(0,0,0,0.05)}.flatpickr-current-month .numInputWrapper{width:6ch;width:7ch\0;display:inline-block;}.flatpickr-current-month .numInputWrapper span.arrowUp:after{border-bottom-color:rgba(0,0,0,0.9)}.flatpickr-current-month .numInputWrapper span.arrowDown:after{border-top-color:rgba(0,0,0,0.9)}.flatpickr-current-month input.cur-year{background:transparent;-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;cursor:text;padding:0 0 0 .5ch;margin:0;display:inline-block;font-size:inherit;font-family:inherit;font-weight:300;line-height:inherit;height:auto;border:0;border-radius:0;vertical-align:initial;}.flatpickr-current-month input.cur-year:focus{outline:0}.flatpickr-current-month input.cur-year[disabled],.flatpickr-current-month input.cur-year[disabled]:hover{font-size:100%;color:rgba(0,0,0,0.5);background:transparent;pointer-events:none}.flatpickr-weekdays{background:transparent;text-align:center;overflow:hidden;width:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:28px;}.flatpickr-weekdays .flatpickr-weekdaycontainer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}span.flatpickr-weekday{cursor:default;font-size:90%;background:transparent;color:rgba(0,0,0,0.54);line-height:1;margin:0;text-align:center;display:block;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;font-weight:bolder}.dayContainer,.flatpickr-weeks{padding:1px 0 0 0}.flatpickr-days{position:relative;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;width:307.875px;}.flatpickr-days:focus{outline:0}.dayContainer{padding:0;outline:0;text-align:left;width:307.875px;min-width:307.875px;max-width:307.875px;-webkit-box-sizing:border-box;box-sizing:border-box;display:inline-block;display:-ms-flexbox;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-wrap:wrap;-ms-flex-pack:justify;-webkit-justify-content:space-around;justify-content:space-around;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1;}.dayContainer + .dayContainer{-webkit-box-shadow:-1px 0 0 #e6e6e6;box-shadow:-1px 0 0 #e6e6e6}.flatpickr-day{background:none;border:1px solid transparent;border-radius:150px;-webkit-box-sizing:border-box;box-sizing:border-box;color:#393939;cursor:pointer;font-weight:400;width:14.2857143%;-webkit-flex-basis:14.2857143%;-ms-flex-preferred-size:14.2857143%;flex-basis:14.2857143%;max-width:39px;height:39px;line-height:39px;margin:0;display:inline-block;position:relative;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;text-align:center;}.flatpickr-day.inRange,.flatpickr-day.prevMonthDay.inRange,.flatpickr-day.nextMonthDay.inRange,.flatpickr-day.today.inRange,.flatpickr-day.prevMonthDay.today.inRange,.flatpickr-day.nextMonthDay.today.inRange,.flatpickr-day:hover,.flatpickr-day.prevMonthDay:hover,.flatpickr-day.nextMonthDay:hover,.flatpickr-day:focus,.flatpickr-day.prevMonthDay:focus,.flatpickr-day.nextMonthDay:focus{cursor:pointer;outline:0;background:#e6e6e6;border-color:#e6e6e6}.flatpickr-day.today{border-color:#959ea9;}.flatpickr-day.today:hover,.flatpickr-day.today:focus{border-color:#959ea9;background:#959ea9;color:#fff}.flatpickr-day.selected,.flatpickr-day.startRange,.flatpickr-day.endRange,.flatpickr-day.selected.inRange,.flatpickr-day.startRange.inRange,.flatpickr-day.endRange.inRange,.flatpickr-day.selected:focus,.flatpickr-day.startRange:focus,.flatpickr-day.endRange:focus,.flatpickr-day.selected:hover,.flatpickr-day.startRange:hover,.flatpickr-day.endRange:hover,.flatpickr-day.selected.prevMonthDay,.flatpickr-day.startRange.prevMonthDay,.flatpickr-day.endRange.prevMonthDay,.flatpickr-day.selected.nextMonthDay,.flatpickr-day.startRange.nextMonthDay,.flatpickr-day.endRange.nextMonthDay{background:#569ff7;-webkit-box-shadow:none;box-shadow:none;color:#fff;border-color:#569ff7}.flatpickr-day.selected.startRange,.flatpickr-day.startRange.startRange,.flatpickr-day.endRange.startRange{border-radius:50px 0 0 50px}.flatpickr-day.selected.endRange,.flatpickr-day.startRange.endRange,.flatpickr-day.endRange.endRange{border-radius:0 50px 50px 0}.flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n+1)),.flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n+1)),.flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n+1)){-webkit-box-shadow:-10px 0 0 #569ff7;box-shadow:-10px 0 0 #569ff7}.flatpickr-day.selected.startRange.endRange,.flatpickr-day.startRange.startRange.endRange,.flatpickr-day.endRange.startRange.endRange{border-radius:50px}.flatpickr-day.inRange{border-radius:0;-webkit-box-shadow:-5px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-5px 0 0 #e6e6e6,5px 0 0 #e6e6e6}.flatpickr-day.disabled,.flatpickr-day.disabled:hover,.flatpickr-day.prevMonthDay,.flatpickr-day.nextMonthDay,.flatpickr-day.notAllowed,.flatpickr-day.notAllowed.prevMonthDay,.flatpickr-day.notAllowed.nextMonthDay{color:rgba(57,57,57,0.3);background:transparent;border-color:transparent;cursor:default}.flatpickr-day.disabled,.flatpickr-day.disabled:hover{cursor:not-allowed;color:rgba(57,57,57,0.1)}.flatpickr-day.week.selected{border-radius:0;-webkit-box-shadow:-5px 0 0 #569ff7,5px 0 0 #569ff7;box-shadow:-5px 0 0 #569ff7,5px 0 0 #569ff7}.flatpickr-day.hidden{visibility:hidden}.rangeMode .flatpickr-day{margin-top:1px}.flatpickr-weekwrapper{display:inline-block;float:left;}.flatpickr-weekwrapper .flatpickr-weeks{padding:0 12px;-webkit-box-shadow:1px 0 0 #e6e6e6;box-shadow:1px 0 0 #e6e6e6}.flatpickr-weekwrapper .flatpickr-weekday{float:none;width:100%;line-height:28px}.flatpickr-weekwrapper span.flatpickr-day,.flatpickr-weekwrapper span.flatpickr-day:hover{display:block;width:100%;max-width:none;color:rgba(57,57,57,0.3);background:transparent;cursor:default;border:none}.flatpickr-innerContainer{display:block;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;}.flatpickr-rContainer{display:inline-block;padding:0;-webkit-box-sizing:border-box;box-sizing:border-box}.flatpickr-time{text-align:center;outline:0;display:block;height:0;line-height:40px;max-height:40px;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}.flatpickr-time:after{content:"";display:table;clear:both}.flatpickr-time .numInputWrapper{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;width:40%;height:40px;float:left;}.flatpickr-time .numInputWrapper span.arrowUp:after{border-bottom-color:#393939}.flatpickr-time .numInputWrapper span.arrowDown:after{border-top-color:#393939}.flatpickr-time.hasSeconds .numInputWrapper{width:26%}.flatpickr-time.time24hr .numInputWrapper{width:49%}.flatpickr-time input{background:transparent;-webkit-box-shadow:none;box-shadow:none;border:0;border-radius:0;text-align:center;margin:0;padding:0;height:inherit;line-height:inherit;color:#393939;font-size:14px;position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;}.flatpickr-time input.flatpickr-hour{font-weight:bold}.flatpickr-time input.flatpickr-minute,.flatpickr-time input.flatpickr-second{font-weight:400}.flatpickr-time input:focus{outline:0;border:0}.flatpickr-time .flatpickr-time-separator,.flatpickr-time .flatpickr-am-pm{height:inherit;display:inline-block;float:left;line-height:inherit;color:#393939;font-weight:bold;width:2%;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.flatpickr-time .flatpickr-am-pm{outline:0;width:18%;cursor:pointer;text-align:center;font-weight:400}.flatpickr-time input:hover,.flatpickr-time .flatpickr-am-pm:hover,.flatpickr-time input:focus,.flatpickr-time .flatpickr-am-pm:focus{background:#f3f3f3}.flatpickr-input[readonly]{cursor:pointer}@-webkit-keyframes fpFadeInDown{from{opacity:0;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:1;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}@keyframes fpFadeInDown{from{opacity:0;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:1;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}} \ No newline at end of file diff --git a/public/img/select-icon-2x.png b/public/img/select-icon-2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8d24b106d9929ab3d350d9295fcec20c12aab835 GIT binary patch literal 190 zcmeAS@N?(olHy`uVBq!ia0vp^20-kdZ4;S!wgGKp@O1TaS?83{ F1OP$tI9mV! literal 0 HcmV?d00001 diff --git a/public/img/textarea-corner-2x.png b/public/img/textarea-corner-2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ee9cb50bd3cd12a6347dc4c62231e8a34431ff1a GIT binary patch literal 238 zcmV45_G+*8a?=O*zI&Fdf?`khWUynjxq}I^wk`bO zJ_bL|6@c;ob|D1JD1$dQexW`y4l?xrCR z@}^SO8j2y33#qkE_`cVW8A&~^++9ORi on9r-W6Y!l4x@NL7EnN%(04+TCyX`0iqW}N^07*qoM6N<$g5QE)cmMzZ literal 0 HcmV?d00001 diff --git a/public/img/textarea-corner.png b/public/img/textarea-corner.png new file mode 100644 index 0000000000000000000000000000000000000000..3a2242c56d7043c3623eebaa81c57f3fa72ba2c7 GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^oFL4>1|%O$WD@{VHJ&bxAr*|t1^OQjc71<;|NO>^ zt6I7O(#xA#wZn6x_9U%k?dT6aIH~mYpXuu&7jp?6UYT&Q>*{K;^>Mk}LdSueRiT2X zudfehIQcMSL&nintPYdsIDXn8x+Y>O)8gvN{>!L(iLuNF* f2@`M>U}m^q*RlD>?s@lt?qKkA^>bP0l+XkKNmWU^ literal 0 HcmV?d00001 diff --git a/public/js/module.js b/public/js/module.js new file mode 100644 index 00000000..f6cc549e --- /dev/null +++ b/public/js/module.js @@ -0,0 +1,29 @@ +// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 + +;(function (Icinga) { + + 'use strict'; + + var Reporting = function(module) { + this.module = module; + + this.initialize(); + }; + + Reporting.prototype.initialize = function () { + if (typeof $().flatpickr === 'function') { + this.module.on('rendered', function (event) { + var $container = $('
'); + event.target.insertAdjacentElement('beforeend', $container[0]); + $(".flatpickr").flatpickr({ + enableTime: true, + dateFormat: 'Y-m-d H:i', + appendTo: $container[0] + }); + }); + } + }; + + Icinga.availableModules.reporting = Reporting; + +}(Icinga)); diff --git a/public/js/vendor/flatpickr.js b/public/js/vendor/flatpickr.js new file mode 100644 index 00000000..dbc16c00 --- /dev/null +++ b/public/js/vendor/flatpickr.js @@ -0,0 +1,2174 @@ +/* flatpickr v4.5.2, @license MIT */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.flatpickr = factory()); +}(this, (function () { 'use strict'; + + var pad = function pad(number) { + return ("0" + number).slice(-2); + }; + var int = function int(bool) { + return bool === true ? 1 : 0; + }; + function debounce(func, wait, immediate) { + if (immediate === void 0) { + immediate = false; + } + + var timeout; + return function () { + var context = this, + args = arguments; + timeout !== null && clearTimeout(timeout); + timeout = window.setTimeout(function () { + timeout = null; + if (!immediate) func.apply(context, args); + }, wait); + if (immediate && !timeout) func.apply(context, args); + }; + } + var arrayify = function arrayify(obj) { + return obj instanceof Array ? obj : [obj]; + }; + + var do_nothing = function do_nothing() { + return undefined; + }; + + var monthToStr = function monthToStr(monthNumber, shorthand, locale) { + return locale.months[shorthand ? "shorthand" : "longhand"][monthNumber]; + }; + var revFormat = { + D: do_nothing, + F: function F(dateObj, monthName, locale) { + dateObj.setMonth(locale.months.longhand.indexOf(monthName)); + }, + G: function G(dateObj, hour) { + dateObj.setHours(parseFloat(hour)); + }, + H: function H(dateObj, hour) { + dateObj.setHours(parseFloat(hour)); + }, + J: function J(dateObj, day) { + dateObj.setDate(parseFloat(day)); + }, + K: function K(dateObj, amPM, locale) { + dateObj.setHours(dateObj.getHours() % 12 + 12 * int(new RegExp(locale.amPM[1], "i").test(amPM))); + }, + M: function M(dateObj, shortMonth, locale) { + dateObj.setMonth(locale.months.shorthand.indexOf(shortMonth)); + }, + S: function S(dateObj, seconds) { + dateObj.setSeconds(parseFloat(seconds)); + }, + U: function U(_, unixSeconds) { + return new Date(parseFloat(unixSeconds) * 1000); + }, + W: function W(dateObj, weekNum) { + var weekNumber = parseInt(weekNum); + return new Date(dateObj.getFullYear(), 0, 2 + (weekNumber - 1) * 7, 0, 0, 0, 0); + }, + Y: function Y(dateObj, year) { + dateObj.setFullYear(parseFloat(year)); + }, + Z: function Z(_, ISODate) { + return new Date(ISODate); + }, + d: function d(dateObj, day) { + dateObj.setDate(parseFloat(day)); + }, + h: function h(dateObj, hour) { + dateObj.setHours(parseFloat(hour)); + }, + i: function i(dateObj, minutes) { + dateObj.setMinutes(parseFloat(minutes)); + }, + j: function j(dateObj, day) { + dateObj.setDate(parseFloat(day)); + }, + l: do_nothing, + m: function m(dateObj, month) { + dateObj.setMonth(parseFloat(month) - 1); + }, + n: function n(dateObj, month) { + dateObj.setMonth(parseFloat(month) - 1); + }, + s: function s(dateObj, seconds) { + dateObj.setSeconds(parseFloat(seconds)); + }, + w: do_nothing, + y: function y(dateObj, year) { + dateObj.setFullYear(2000 + parseFloat(year)); + } + }; + var tokenRegex = { + D: "(\\w+)", + F: "(\\w+)", + G: "(\\d\\d|\\d)", + H: "(\\d\\d|\\d)", + J: "(\\d\\d|\\d)\\w+", + K: "", + M: "(\\w+)", + S: "(\\d\\d|\\d)", + U: "(.+)", + W: "(\\d\\d|\\d)", + Y: "(\\d{4})", + Z: "(.+)", + d: "(\\d\\d|\\d)", + h: "(\\d\\d|\\d)", + i: "(\\d\\d|\\d)", + j: "(\\d\\d|\\d)", + l: "(\\w+)", + m: "(\\d\\d|\\d)", + n: "(\\d\\d|\\d)", + s: "(\\d\\d|\\d)", + w: "(\\d\\d|\\d)", + y: "(\\d{2})" + }; + var formats = { + Z: function Z(date) { + return date.toISOString(); + }, + D: function D(date, locale, options) { + return locale.weekdays.shorthand[formats.w(date, locale, options)]; + }, + F: function F(date, locale, options) { + return monthToStr(formats.n(date, locale, options) - 1, false, locale); + }, + G: function G(date, locale, options) { + return pad(formats.h(date, locale, options)); + }, + H: function H(date) { + return pad(date.getHours()); + }, + J: function J(date, locale) { + return locale.ordinal !== undefined ? date.getDate() + locale.ordinal(date.getDate()) : date.getDate(); + }, + K: function K(date, locale) { + return locale.amPM[int(date.getHours() > 11)]; + }, + M: function M(date, locale) { + return monthToStr(date.getMonth(), true, locale); + }, + S: function S(date) { + return pad(date.getSeconds()); + }, + U: function U(date) { + return date.getTime() / 1000; + }, + W: function W(date, _, options) { + return options.getWeek(date); + }, + Y: function Y(date) { + return date.getFullYear(); + }, + d: function d(date) { + return pad(date.getDate()); + }, + h: function h(date) { + return date.getHours() % 12 ? date.getHours() % 12 : 12; + }, + i: function i(date) { + return pad(date.getMinutes()); + }, + j: function j(date) { + return date.getDate(); + }, + l: function l(date, locale) { + return locale.weekdays.longhand[date.getDay()]; + }, + m: function m(date) { + return pad(date.getMonth() + 1); + }, + n: function n(date) { + return date.getMonth() + 1; + }, + s: function s(date) { + return date.getSeconds(); + }, + w: function w(date) { + return date.getDay(); + }, + y: function y(date) { + return String(date.getFullYear()).substring(2); + } + }; + + var english = { + weekdays: { + shorthand: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], + longhand: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + }, + months: { + shorthand: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + longhand: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] + }, + daysInMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], + firstDayOfWeek: 0, + ordinal: function ordinal(nth) { + var s = nth % 100; + if (s > 3 && s < 21) return "th"; + + switch (s % 10) { + case 1: + return "st"; + + case 2: + return "nd"; + + case 3: + return "rd"; + + default: + return "th"; + } + }, + rangeSeparator: " to ", + weekAbbreviation: "Wk", + scrollTitle: "Scroll to increment", + toggleTitle: "Click to toggle", + amPM: ["AM", "PM"], + yearAriaLabel: "Year" + }; + + var createDateFormatter = function createDateFormatter(_ref) { + var _ref$config = _ref.config, + config = _ref$config === void 0 ? defaults : _ref$config, + _ref$l10n = _ref.l10n, + l10n = _ref$l10n === void 0 ? english : _ref$l10n; + return function (dateObj, frmt, overrideLocale) { + var locale = overrideLocale || l10n; + + if (config.formatDate !== undefined) { + return config.formatDate(dateObj, frmt, locale); + } + + return frmt.split("").map(function (c, i, arr) { + return formats[c] && arr[i - 1] !== "\\" ? formats[c](dateObj, locale, config) : c !== "\\" ? c : ""; + }).join(""); + }; + }; + var createDateParser = function createDateParser(_ref2) { + var _ref2$config = _ref2.config, + config = _ref2$config === void 0 ? defaults : _ref2$config, + _ref2$l10n = _ref2.l10n, + l10n = _ref2$l10n === void 0 ? english : _ref2$l10n; + return function (date, givenFormat, timeless, customLocale) { + if (date !== 0 && !date) return undefined; + var locale = customLocale || l10n; + var parsedDate; + var date_orig = date; + if (date instanceof Date) parsedDate = new Date(date.getTime());else if (typeof date !== "string" && date.toFixed !== undefined) parsedDate = new Date(date);else if (typeof date === "string") { + var format = givenFormat || (config || defaults).dateFormat; + var datestr = String(date).trim(); + + if (datestr === "today") { + parsedDate = new Date(); + timeless = true; + } else if (/Z$/.test(datestr) || /GMT$/.test(datestr)) parsedDate = new Date(date);else if (config && config.parseDate) parsedDate = config.parseDate(date, format);else { + parsedDate = !config || !config.noCalendar ? new Date(new Date().getFullYear(), 0, 1, 0, 0, 0, 0) : new Date(new Date().setHours(0, 0, 0, 0)); + var matched, + ops = []; + + for (var i = 0, matchIndex = 0, regexStr = ""; i < format.length; i++) { + var token = format[i]; + var isBackSlash = token === "\\"; + var escaped = format[i - 1] === "\\" || isBackSlash; + + if (tokenRegex[token] && !escaped) { + regexStr += tokenRegex[token]; + var match = new RegExp(regexStr).exec(date); + + if (match && (matched = true)) { + ops[token !== "Y" ? "push" : "unshift"]({ + fn: revFormat[token], + val: match[++matchIndex] + }); + } + } else if (!isBackSlash) regexStr += "."; + + ops.forEach(function (_ref3) { + var fn = _ref3.fn, + val = _ref3.val; + return parsedDate = fn(parsedDate, val, locale) || parsedDate; + }); + } + + parsedDate = matched ? parsedDate : undefined; + } + } + + if (!(parsedDate instanceof Date && !isNaN(parsedDate.getTime()))) { + config.errorHandler(new Error("Invalid date provided: " + date_orig)); + return undefined; + } + + if (timeless === true) parsedDate.setHours(0, 0, 0, 0); + return parsedDate; + }; + }; + function compareDates(date1, date2, timeless) { + if (timeless === void 0) { + timeless = true; + } + + if (timeless !== false) { + return new Date(date1.getTime()).setHours(0, 0, 0, 0) - new Date(date2.getTime()).setHours(0, 0, 0, 0); + } + + return date1.getTime() - date2.getTime(); + } + var getWeek = function getWeek(givenDate) { + var date = new Date(givenDate.getTime()); + date.setHours(0, 0, 0, 0); + date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); + var week1 = new Date(date.getFullYear(), 0, 4); + return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7); + }; + var isBetween = function isBetween(ts, ts1, ts2) { + return ts > Math.min(ts1, ts2) && ts < Math.max(ts1, ts2); + }; + var duration = { + DAY: 86400000 + }; + + var HOOKS = ["onChange", "onClose", "onDayCreate", "onDestroy", "onKeyDown", "onMonthChange", "onOpen", "onParseConfig", "onReady", "onValueUpdate", "onYearChange", "onPreCalendarPosition"]; + var defaults = { + _disable: [], + _enable: [], + allowInput: false, + altFormat: "F j, Y", + altInput: false, + altInputClass: "form-control input", + animate: typeof window === "object" && window.navigator.userAgent.indexOf("MSIE") === -1, + ariaDateFormat: "F j, Y", + clickOpens: true, + closeOnSelect: true, + conjunction: ", ", + dateFormat: "Y-m-d", + defaultHour: 12, + defaultMinute: 0, + defaultSeconds: 0, + disable: [], + disableMobile: false, + enable: [], + enableSeconds: false, + enableTime: false, + errorHandler: function errorHandler(err) { + return typeof console !== "undefined" && console.warn(err); + }, + getWeek: getWeek, + hourIncrement: 1, + ignoredFocusElements: [], + inline: false, + locale: "default", + minuteIncrement: 5, + mode: "single", + nextArrow: "", + noCalendar: false, + now: new Date(), + onChange: [], + onClose: [], + onDayCreate: [], + onDestroy: [], + onKeyDown: [], + onMonthChange: [], + onOpen: [], + onParseConfig: [], + onReady: [], + onValueUpdate: [], + onYearChange: [], + onPreCalendarPosition: [], + plugins: [], + position: "auto", + positionElement: undefined, + prevArrow: "", + shorthandCurrentMonth: false, + showMonths: 1, + static: false, + time_24hr: false, + weekNumbers: false, + wrap: false + }; + + function toggleClass(elem, className, bool) { + if (bool === true) return elem.classList.add(className); + elem.classList.remove(className); + } + function createElement(tag, className, content) { + var e = window.document.createElement(tag); + className = className || ""; + content = content || ""; + e.className = className; + if (content !== undefined) e.textContent = content; + return e; + } + function clearNode(node) { + while (node.firstChild) { + node.removeChild(node.firstChild); + } + } + function findParent(node, condition) { + if (condition(node)) return node;else if (node.parentNode) return findParent(node.parentNode, condition); + return undefined; + } + function createNumberInput(inputClassName, opts) { + var wrapper = createElement("div", "numInputWrapper"), + numInput = createElement("input", "numInput " + inputClassName), + arrowUp = createElement("span", "arrowUp"), + arrowDown = createElement("span", "arrowDown"); + numInput.type = "text"; + numInput.pattern = "\\d*"; + if (opts !== undefined) for (var key in opts) { + numInput.setAttribute(key, opts[key]); + } + wrapper.appendChild(numInput); + wrapper.appendChild(arrowUp); + wrapper.appendChild(arrowDown); + return wrapper; + } + + if (typeof Object.assign !== "function") { + Object.assign = function (target) { + if (!target) { + throw TypeError("Cannot convert undefined or null to object"); + } + + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + var _loop = function _loop() { + var source = args[_i]; + + if (source) { + Object.keys(source).forEach(function (key) { + return target[key] = source[key]; + }); + } + }; + + for (var _i = 0; _i < args.length; _i++) { + _loop(); + } + + return target; + }; + } + + var DEBOUNCED_CHANGE_MS = 300; + + function FlatpickrInstance(element, instanceConfig) { + var self = { + config: Object.assign({}, flatpickr.defaultConfig), + l10n: english + }; + self.parseDate = createDateParser({ + config: self.config, + l10n: self.l10n + }); + self._handlers = []; + self._bind = bind; + self._setHoursFromDate = setHoursFromDate; + self._positionCalendar = positionCalendar; + self.changeMonth = changeMonth; + self.changeYear = changeYear; + self.clear = clear; + self.close = close; + self._createElement = createElement; + self.destroy = destroy; + self.isEnabled = isEnabled; + self.jumpToDate = jumpToDate; + self.open = open; + self.redraw = redraw; + self.set = set; + self.setDate = setDate; + self.toggle = toggle; + + function setupHelperFunctions() { + self.utils = { + getDaysInMonth: function getDaysInMonth(month, yr) { + if (month === void 0) { + month = self.currentMonth; + } + + if (yr === void 0) { + yr = self.currentYear; + } + + if (month === 1 && (yr % 4 === 0 && yr % 100 !== 0 || yr % 400 === 0)) return 29; + return self.l10n.daysInMonth[month]; + } + }; + } + + function init() { + self.element = self.input = element; + self.isOpen = false; + parseConfig(); + setupLocale(); + setupInputs(); + setupDates(); + setupHelperFunctions(); + if (!self.isMobile) build(); + bindEvents(); + + if (self.selectedDates.length || self.config.noCalendar) { + if (self.config.enableTime) { + setHoursFromDate(self.config.noCalendar ? self.latestSelectedDateObj || self.config.minDate : undefined); + } + + updateValue(false); + } + + setCalendarWidth(); + self.showTimeInput = self.selectedDates.length > 0 || self.config.noCalendar; + var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + + if (!self.isMobile && isSafari) { + positionCalendar(); + } + + triggerEvent("onReady"); + } + + function bindToInstance(fn) { + return fn.bind(self); + } + + function setCalendarWidth() { + var config = self.config; + if (config.weekNumbers === false && config.showMonths === 1) return;else if (config.noCalendar !== true) { + window.requestAnimationFrame(function () { + self.calendarContainer.style.visibility = "hidden"; + self.calendarContainer.style.display = "block"; + + if (self.daysContainer !== undefined) { + var daysWidth = (self.days.offsetWidth + 1) * config.showMonths; + self.daysContainer.style.width = daysWidth + "px"; + self.calendarContainer.style.width = daysWidth + (self.weekWrapper !== undefined ? self.weekWrapper.offsetWidth : 0) + "px"; + self.calendarContainer.style.removeProperty("visibility"); + self.calendarContainer.style.removeProperty("display"); + } + }); + } + } + + function updateTime(e) { + if (self.selectedDates.length === 0) return; + + if (e !== undefined && e.type !== "blur") { + timeWrapper(e); + } + + var prevValue = self._input.value; + setHoursFromInputs(); + updateValue(); + + if (self._input.value !== prevValue) { + self._debouncedChange(); + } + } + + function ampm2military(hour, amPM) { + return hour % 12 + 12 * int(amPM === self.l10n.amPM[1]); + } + + function military2ampm(hour) { + switch (hour % 24) { + case 0: + case 12: + return 12; + + default: + return hour % 12; + } + } + + function setHoursFromInputs() { + if (self.hourElement === undefined || self.minuteElement === undefined) return; + var hours = (parseInt(self.hourElement.value.slice(-2), 10) || 0) % 24, + minutes = (parseInt(self.minuteElement.value, 10) || 0) % 60, + seconds = self.secondElement !== undefined ? (parseInt(self.secondElement.value, 10) || 0) % 60 : 0; + + if (self.amPM !== undefined) { + hours = ampm2military(hours, self.amPM.textContent); + } + + var limitMinHours = self.config.minTime !== undefined || self.config.minDate && self.minDateHasTime && self.latestSelectedDateObj && compareDates(self.latestSelectedDateObj, self.config.minDate, true) === 0; + var limitMaxHours = self.config.maxTime !== undefined || self.config.maxDate && self.maxDateHasTime && self.latestSelectedDateObj && compareDates(self.latestSelectedDateObj, self.config.maxDate, true) === 0; + + if (limitMaxHours) { + var maxTime = self.config.maxTime !== undefined ? self.config.maxTime : self.config.maxDate; + hours = Math.min(hours, maxTime.getHours()); + if (hours === maxTime.getHours()) minutes = Math.min(minutes, maxTime.getMinutes()); + if (minutes === maxTime.getMinutes()) seconds = Math.min(seconds, maxTime.getSeconds()); + } + + if (limitMinHours) { + var minTime = self.config.minTime !== undefined ? self.config.minTime : self.config.minDate; + hours = Math.max(hours, minTime.getHours()); + if (hours === minTime.getHours()) minutes = Math.max(minutes, minTime.getMinutes()); + if (minutes === minTime.getMinutes()) seconds = Math.max(seconds, minTime.getSeconds()); + } + + setHours(hours, minutes, seconds); + } + + function setHoursFromDate(dateObj) { + var date = dateObj || self.latestSelectedDateObj; + if (date) setHours(date.getHours(), date.getMinutes(), date.getSeconds()); + } + + function setDefaultHours() { + var hours = self.config.defaultHour; + var minutes = self.config.defaultMinute; + var seconds = self.config.defaultSeconds; + + if (self.config.minDate !== undefined) { + var min_hr = self.config.minDate.getHours(); + var min_minutes = self.config.minDate.getMinutes(); + hours = Math.max(hours, min_hr); + if (hours === min_hr) minutes = Math.max(min_minutes, minutes); + if (hours === min_hr && minutes === min_minutes) seconds = self.config.minDate.getSeconds(); + } + + if (self.config.maxDate !== undefined) { + var max_hr = self.config.maxDate.getHours(); + var max_minutes = self.config.maxDate.getMinutes(); + hours = Math.min(hours, max_hr); + if (hours === max_hr) minutes = Math.min(max_minutes, minutes); + if (hours === max_hr && minutes === max_minutes) seconds = self.config.maxDate.getSeconds(); + } + + setHours(hours, minutes, seconds); + } + + function setHours(hours, minutes, seconds) { + if (self.latestSelectedDateObj !== undefined) { + self.latestSelectedDateObj.setHours(hours % 24, minutes, seconds || 0, 0); + } + + if (!self.hourElement || !self.minuteElement || self.isMobile) return; + self.hourElement.value = pad(!self.config.time_24hr ? (12 + hours) % 12 + 12 * int(hours % 12 === 0) : hours); + self.minuteElement.value = pad(minutes); + if (self.amPM !== undefined) self.amPM.textContent = self.l10n.amPM[int(hours >= 12)]; + if (self.secondElement !== undefined) self.secondElement.value = pad(seconds); + } + + function onYearInput(event) { + var year = parseInt(event.target.value) + (event.delta || 0); + + if (year / 1000 > 1 || event.key === "Enter" && !/[^\d]/.test(year.toString())) { + changeYear(year); + } + } + + function bind(element, event, handler, options) { + if (event instanceof Array) return event.forEach(function (ev) { + return bind(element, ev, handler, options); + }); + if (element instanceof Array) return element.forEach(function (el) { + return bind(el, event, handler, options); + }); + element.addEventListener(event, handler, options); + + self._handlers.push({ + element: element, + event: event, + handler: handler, + options: options + }); + } + + function onClick(handler) { + return function (evt) { + evt.which === 1 && handler(evt); + }; + } + + function triggerChange() { + triggerEvent("onChange"); + } + + function bindEvents() { + if (self.config.wrap) { + ["open", "close", "toggle", "clear"].forEach(function (evt) { + Array.prototype.forEach.call(self.element.querySelectorAll("[data-" + evt + "]"), function (el) { + return bind(el, "click", self[evt]); + }); + }); + } + + if (self.isMobile) { + setupMobile(); + return; + } + + var debouncedResize = debounce(onResize, 50); + self._debouncedChange = debounce(triggerChange, DEBOUNCED_CHANGE_MS); + if (self.daysContainer && !/iPhone|iPad|iPod/i.test(navigator.userAgent)) bind(self.daysContainer, "mouseover", function (e) { + if (self.config.mode === "range") onMouseOver(e.target); + }); + bind(window.document.body, "keydown", onKeyDown); + if (!self.config.static) bind(self._input, "keydown", onKeyDown); + if (!self.config.inline && !self.config.static) bind(window, "resize", debouncedResize); + if (window.ontouchstart !== undefined) bind(window.document, "click", documentClick);else bind(window.document, "mousedown", onClick(documentClick)); + bind(window.document, "focus", documentClick, { + capture: true + }); + + if (self.config.clickOpens === true) { + bind(self._input, "focus", self.open); + bind(self._input, "mousedown", onClick(self.open)); + } + + if (self.daysContainer !== undefined) { + bind(self.monthNav, "mousedown", onClick(onMonthNavClick)); + bind(self.monthNav, ["keyup", "increment"], onYearInput); + bind(self.daysContainer, "mousedown", onClick(selectDate)); + } + + if (self.timeContainer !== undefined && self.minuteElement !== undefined && self.hourElement !== undefined) { + var selText = function selText(e) { + return e.target.select(); + }; + + bind(self.timeContainer, ["increment"], updateTime); + bind(self.timeContainer, "blur", updateTime, { + capture: true + }); + bind(self.timeContainer, "mousedown", onClick(timeIncrement)); + bind([self.hourElement, self.minuteElement], ["focus", "click"], selText); + if (self.secondElement !== undefined) bind(self.secondElement, "focus", function () { + return self.secondElement && self.secondElement.select(); + }); + + if (self.amPM !== undefined) { + bind(self.amPM, "mousedown", onClick(function (e) { + updateTime(e); + triggerChange(); + })); + } + } + } + + function jumpToDate(jumpDate) { + var jumpTo = jumpDate !== undefined ? self.parseDate(jumpDate) : self.latestSelectedDateObj || (self.config.minDate && self.config.minDate > self.now ? self.config.minDate : self.config.maxDate && self.config.maxDate < self.now ? self.config.maxDate : self.now); + + try { + if (jumpTo !== undefined) { + self.currentYear = jumpTo.getFullYear(); + self.currentMonth = jumpTo.getMonth(); + } + } catch (e) { + e.message = "Invalid date supplied: " + jumpTo; + self.config.errorHandler(e); + } + + self.redraw(); + } + + function timeIncrement(e) { + if (~e.target.className.indexOf("arrow")) incrementNumInput(e, e.target.classList.contains("arrowUp") ? 1 : -1); + } + + function incrementNumInput(e, delta, inputElem) { + var target = e && e.target; + var input = inputElem || target && target.parentNode && target.parentNode.firstChild; + var event = createEvent("increment"); + event.delta = delta; + input && input.dispatchEvent(event); + } + + function build() { + var fragment = window.document.createDocumentFragment(); + self.calendarContainer = createElement("div", "flatpickr-calendar"); + self.calendarContainer.tabIndex = -1; + + if (!self.config.noCalendar) { + fragment.appendChild(buildMonthNav()); + self.innerContainer = createElement("div", "flatpickr-innerContainer"); + + if (self.config.weekNumbers) { + var _buildWeeks = buildWeeks(), + weekWrapper = _buildWeeks.weekWrapper, + weekNumbers = _buildWeeks.weekNumbers; + + self.innerContainer.appendChild(weekWrapper); + self.weekNumbers = weekNumbers; + self.weekWrapper = weekWrapper; + } + + self.rContainer = createElement("div", "flatpickr-rContainer"); + self.rContainer.appendChild(buildWeekdays()); + + if (!self.daysContainer) { + self.daysContainer = createElement("div", "flatpickr-days"); + self.daysContainer.tabIndex = -1; + } + + buildDays(); + self.rContainer.appendChild(self.daysContainer); + self.innerContainer.appendChild(self.rContainer); + fragment.appendChild(self.innerContainer); + } + + if (self.config.enableTime) { + fragment.appendChild(buildTime()); + } + + toggleClass(self.calendarContainer, "rangeMode", self.config.mode === "range"); + toggleClass(self.calendarContainer, "animate", self.config.animate === true); + toggleClass(self.calendarContainer, "multiMonth", self.config.showMonths > 1); + self.calendarContainer.appendChild(fragment); + var customAppend = self.config.appendTo !== undefined && self.config.appendTo.nodeType !== undefined; + + if (self.config.inline || self.config.static) { + self.calendarContainer.classList.add(self.config.inline ? "inline" : "static"); + + if (self.config.inline) { + if (!customAppend && self.element.parentNode) self.element.parentNode.insertBefore(self.calendarContainer, self._input.nextSibling);else if (self.config.appendTo !== undefined) self.config.appendTo.appendChild(self.calendarContainer); + } + + if (self.config.static) { + var wrapper = createElement("div", "flatpickr-wrapper"); + if (self.element.parentNode) self.element.parentNode.insertBefore(wrapper, self.element); + wrapper.appendChild(self.element); + if (self.altInput) wrapper.appendChild(self.altInput); + wrapper.appendChild(self.calendarContainer); + } + } + + if (!self.config.static && !self.config.inline) (self.config.appendTo !== undefined ? self.config.appendTo : window.document.body).appendChild(self.calendarContainer); + } + + function createDay(className, date, dayNumber, i) { + var dateIsEnabled = isEnabled(date, true), + dayElement = createElement("span", "flatpickr-day " + className, date.getDate().toString()); + dayElement.dateObj = date; + dayElement.$i = i; + dayElement.setAttribute("aria-label", self.formatDate(date, self.config.ariaDateFormat)); + + if (className.indexOf("hidden") === -1 && compareDates(date, self.now) === 0) { + self.todayDateElem = dayElement; + dayElement.classList.add("today"); + dayElement.setAttribute("aria-current", "date"); + } + + if (dateIsEnabled) { + dayElement.tabIndex = -1; + + if (isDateSelected(date)) { + dayElement.classList.add("selected"); + self.selectedDateElem = dayElement; + + if (self.config.mode === "range") { + toggleClass(dayElement, "startRange", self.selectedDates[0] && compareDates(date, self.selectedDates[0], true) === 0); + toggleClass(dayElement, "endRange", self.selectedDates[1] && compareDates(date, self.selectedDates[1], true) === 0); + if (className === "nextMonthDay") dayElement.classList.add("inRange"); + } + } + } else { + dayElement.classList.add("disabled"); + } + + if (self.config.mode === "range") { + if (isDateInRange(date) && !isDateSelected(date)) dayElement.classList.add("inRange"); + } + + if (self.weekNumbers && self.config.showMonths === 1 && className !== "prevMonthDay" && dayNumber % 7 === 1) { + self.weekNumbers.insertAdjacentHTML("beforeend", "" + self.config.getWeek(date) + ""); + } + + triggerEvent("onDayCreate", dayElement); + return dayElement; + } + + function focusOnDayElem(targetNode) { + targetNode.focus(); + if (self.config.mode === "range") onMouseOver(targetNode); + } + + function getFirstAvailableDay(delta) { + var startMonth = delta > 0 ? 0 : self.config.showMonths - 1; + var endMonth = delta > 0 ? self.config.showMonths : -1; + + for (var m = startMonth; m != endMonth; m += delta) { + var month = self.daysContainer.children[m]; + var startIndex = delta > 0 ? 0 : month.children.length - 1; + var endIndex = delta > 0 ? month.children.length : -1; + + for (var i = startIndex; i != endIndex; i += delta) { + var c = month.children[i]; + if (c.className.indexOf("hidden") === -1 && isEnabled(c.dateObj)) return c; + } + } + + return undefined; + } + + function getNextAvailableDay(current, delta) { + var givenMonth = current.className.indexOf("Month") === -1 ? current.dateObj.getMonth() : self.currentMonth; + var endMonth = delta > 0 ? self.config.showMonths : -1; + var loopDelta = delta > 0 ? 1 : -1; + + for (var m = givenMonth - self.currentMonth; m != endMonth; m += loopDelta) { + var month = self.daysContainer.children[m]; + var startIndex = givenMonth - self.currentMonth === m ? current.$i + delta : delta < 0 ? month.children.length - 1 : 0; + var numMonthDays = month.children.length; + + for (var i = startIndex; i >= 0 && i < numMonthDays && i != (delta > 0 ? numMonthDays : -1); i += loopDelta) { + var c = month.children[i]; + if (c.className.indexOf("hidden") === -1 && isEnabled(c.dateObj) && Math.abs(current.$i - i) >= Math.abs(delta)) return focusOnDayElem(c); + } + } + + self.changeMonth(loopDelta); + focusOnDay(getFirstAvailableDay(loopDelta), 0); + return undefined; + } + + function focusOnDay(current, offset) { + var dayFocused = isInView(document.activeElement || document.body); + var startElem = current !== undefined ? current : dayFocused ? document.activeElement : self.selectedDateElem !== undefined && isInView(self.selectedDateElem) ? self.selectedDateElem : self.todayDateElem !== undefined && isInView(self.todayDateElem) ? self.todayDateElem : getFirstAvailableDay(offset > 0 ? 1 : -1); + if (startElem === undefined) return self._input.focus(); + if (!dayFocused) return focusOnDayElem(startElem); + getNextAvailableDay(startElem, offset); + } + + function buildMonthDays(year, month) { + var firstOfMonth = (new Date(year, month, 1).getDay() - self.l10n.firstDayOfWeek + 7) % 7; + var prevMonthDays = self.utils.getDaysInMonth((month - 1 + 12) % 12); + var daysInMonth = self.utils.getDaysInMonth(month), + days = window.document.createDocumentFragment(), + isMultiMonth = self.config.showMonths > 1, + prevMonthDayClass = isMultiMonth ? "prevMonthDay hidden" : "prevMonthDay", + nextMonthDayClass = isMultiMonth ? "nextMonthDay hidden" : "nextMonthDay"; + var dayNumber = prevMonthDays + 1 - firstOfMonth, + dayIndex = 0; + + for (; dayNumber <= prevMonthDays; dayNumber++, dayIndex++) { + days.appendChild(createDay(prevMonthDayClass, new Date(year, month - 1, dayNumber), dayNumber, dayIndex)); + } + + for (dayNumber = 1; dayNumber <= daysInMonth; dayNumber++, dayIndex++) { + days.appendChild(createDay("", new Date(year, month, dayNumber), dayNumber, dayIndex)); + } + + for (var dayNum = daysInMonth + 1; dayNum <= 42 - firstOfMonth && (self.config.showMonths === 1 || dayIndex % 7 !== 0); dayNum++, dayIndex++) { + days.appendChild(createDay(nextMonthDayClass, new Date(year, month + 1, dayNum % daysInMonth), dayNum, dayIndex)); + } + + var dayContainer = createElement("div", "dayContainer"); + dayContainer.appendChild(days); + return dayContainer; + } + + function buildDays() { + if (self.daysContainer === undefined) { + return; + } + + clearNode(self.daysContainer); + if (self.weekNumbers) clearNode(self.weekNumbers); + var frag = document.createDocumentFragment(); + + for (var i = 0; i < self.config.showMonths; i++) { + var d = new Date(self.currentYear, self.currentMonth, 1); + d.setMonth(self.currentMonth + i); + frag.appendChild(buildMonthDays(d.getFullYear(), d.getMonth())); + } + + self.daysContainer.appendChild(frag); + self.days = self.daysContainer.firstChild; + + if (self.config.mode === "range" && self.selectedDates.length === 1) { + onMouseOver(); + } + } + + function buildMonth() { + var container = createElement("div", "flatpickr-month"); + var monthNavFragment = window.document.createDocumentFragment(); + var monthElement = createElement("span", "cur-month"); + var yearInput = createNumberInput("cur-year", { + tabindex: "-1" + }); + var yearElement = yearInput.getElementsByTagName("input")[0]; + yearElement.setAttribute("aria-label", self.l10n.yearAriaLabel); + if (self.config.minDate) yearElement.setAttribute("data-min", self.config.minDate.getFullYear().toString()); + + if (self.config.maxDate) { + yearElement.setAttribute("data-max", self.config.maxDate.getFullYear().toString()); + yearElement.disabled = !!self.config.minDate && self.config.minDate.getFullYear() === self.config.maxDate.getFullYear(); + } + + var currentMonth = createElement("div", "flatpickr-current-month"); + currentMonth.appendChild(monthElement); + currentMonth.appendChild(yearInput); + monthNavFragment.appendChild(currentMonth); + container.appendChild(monthNavFragment); + return { + container: container, + yearElement: yearElement, + monthElement: monthElement + }; + } + + function buildMonths() { + clearNode(self.monthNav); + self.monthNav.appendChild(self.prevMonthNav); + + for (var m = self.config.showMonths; m--;) { + var month = buildMonth(); + self.yearElements.push(month.yearElement); + self.monthElements.push(month.monthElement); + self.monthNav.appendChild(month.container); + } + + self.monthNav.appendChild(self.nextMonthNav); + } + + function buildMonthNav() { + self.monthNav = createElement("div", "flatpickr-months"); + self.yearElements = []; + self.monthElements = []; + self.prevMonthNav = createElement("span", "flatpickr-prev-month"); + self.prevMonthNav.innerHTML = self.config.prevArrow; + self.nextMonthNav = createElement("span", "flatpickr-next-month"); + self.nextMonthNav.innerHTML = self.config.nextArrow; + buildMonths(); + Object.defineProperty(self, "_hidePrevMonthArrow", { + get: function get() { + return self.__hidePrevMonthArrow; + }, + set: function set(bool) { + if (self.__hidePrevMonthArrow !== bool) { + toggleClass(self.prevMonthNav, "disabled", bool); + self.__hidePrevMonthArrow = bool; + } + } + }); + Object.defineProperty(self, "_hideNextMonthArrow", { + get: function get() { + return self.__hideNextMonthArrow; + }, + set: function set(bool) { + if (self.__hideNextMonthArrow !== bool) { + toggleClass(self.nextMonthNav, "disabled", bool); + self.__hideNextMonthArrow = bool; + } + } + }); + self.currentYearElement = self.yearElements[0]; + updateNavigationCurrentMonth(); + return self.monthNav; + } + + function buildTime() { + self.calendarContainer.classList.add("hasTime"); + if (self.config.noCalendar) self.calendarContainer.classList.add("noCalendar"); + self.timeContainer = createElement("div", "flatpickr-time"); + self.timeContainer.tabIndex = -1; + var separator = createElement("span", "flatpickr-time-separator", ":"); + var hourInput = createNumberInput("flatpickr-hour"); + self.hourElement = hourInput.getElementsByTagName("input")[0]; + var minuteInput = createNumberInput("flatpickr-minute"); + self.minuteElement = minuteInput.getElementsByTagName("input")[0]; + self.hourElement.tabIndex = self.minuteElement.tabIndex = -1; + self.hourElement.value = pad(self.latestSelectedDateObj ? self.latestSelectedDateObj.getHours() : self.config.time_24hr ? self.config.defaultHour : military2ampm(self.config.defaultHour)); + self.minuteElement.value = pad(self.latestSelectedDateObj ? self.latestSelectedDateObj.getMinutes() : self.config.defaultMinute); + self.hourElement.setAttribute("data-step", self.config.hourIncrement.toString()); + self.minuteElement.setAttribute("data-step", self.config.minuteIncrement.toString()); + self.hourElement.setAttribute("data-min", self.config.time_24hr ? "0" : "1"); + self.hourElement.setAttribute("data-max", self.config.time_24hr ? "23" : "12"); + self.minuteElement.setAttribute("data-min", "0"); + self.minuteElement.setAttribute("data-max", "59"); + self.timeContainer.appendChild(hourInput); + self.timeContainer.appendChild(separator); + self.timeContainer.appendChild(minuteInput); + if (self.config.time_24hr) self.timeContainer.classList.add("time24hr"); + + if (self.config.enableSeconds) { + self.timeContainer.classList.add("hasSeconds"); + var secondInput = createNumberInput("flatpickr-second"); + self.secondElement = secondInput.getElementsByTagName("input")[0]; + self.secondElement.value = pad(self.latestSelectedDateObj ? self.latestSelectedDateObj.getSeconds() : self.config.defaultSeconds); + self.secondElement.setAttribute("data-step", self.minuteElement.getAttribute("data-step")); + self.secondElement.setAttribute("data-min", self.minuteElement.getAttribute("data-min")); + self.secondElement.setAttribute("data-max", self.minuteElement.getAttribute("data-max")); + self.timeContainer.appendChild(createElement("span", "flatpickr-time-separator", ":")); + self.timeContainer.appendChild(secondInput); + } + + if (!self.config.time_24hr) { + self.amPM = createElement("span", "flatpickr-am-pm", self.l10n.amPM[int((self.latestSelectedDateObj ? self.hourElement.value : self.config.defaultHour) > 11)]); + self.amPM.title = self.l10n.toggleTitle; + self.amPM.tabIndex = -1; + self.timeContainer.appendChild(self.amPM); + } + + return self.timeContainer; + } + + function buildWeekdays() { + if (!self.weekdayContainer) self.weekdayContainer = createElement("div", "flatpickr-weekdays");else clearNode(self.weekdayContainer); + + for (var i = self.config.showMonths; i--;) { + var container = createElement("div", "flatpickr-weekdaycontainer"); + self.weekdayContainer.appendChild(container); + } + + updateWeekdays(); + return self.weekdayContainer; + } + + function updateWeekdays() { + var firstDayOfWeek = self.l10n.firstDayOfWeek; + var weekdays = self.l10n.weekdays.shorthand.concat(); + + if (firstDayOfWeek > 0 && firstDayOfWeek < weekdays.length) { + weekdays = weekdays.splice(firstDayOfWeek, weekdays.length).concat(weekdays.splice(0, firstDayOfWeek)); + } + + for (var i = self.config.showMonths; i--;) { + self.weekdayContainer.children[i].innerHTML = "\n \n " + weekdays.join("") + "\n \n "; + } + } + + function buildWeeks() { + self.calendarContainer.classList.add("hasWeeks"); + var weekWrapper = createElement("div", "flatpickr-weekwrapper"); + weekWrapper.appendChild(createElement("span", "flatpickr-weekday", self.l10n.weekAbbreviation)); + var weekNumbers = createElement("div", "flatpickr-weeks"); + weekWrapper.appendChild(weekNumbers); + return { + weekWrapper: weekWrapper, + weekNumbers: weekNumbers + }; + } + + function changeMonth(value, is_offset) { + if (is_offset === void 0) { + is_offset = true; + } + + var delta = is_offset ? value : value - self.currentMonth; + if (delta < 0 && self._hidePrevMonthArrow === true || delta > 0 && self._hideNextMonthArrow === true) return; + self.currentMonth += delta; + + if (self.currentMonth < 0 || self.currentMonth > 11) { + self.currentYear += self.currentMonth > 11 ? 1 : -1; + self.currentMonth = (self.currentMonth + 12) % 12; + triggerEvent("onYearChange"); + } + + buildDays(); + triggerEvent("onMonthChange"); + updateNavigationCurrentMonth(); + } + + function clear(triggerChangeEvent) { + if (triggerChangeEvent === void 0) { + triggerChangeEvent = true; + } + + self.input.value = ""; + if (self.altInput !== undefined) self.altInput.value = ""; + if (self.mobileInput !== undefined) self.mobileInput.value = ""; + self.selectedDates = []; + self.latestSelectedDateObj = undefined; + self.showTimeInput = false; + + if (self.config.enableTime === true) { + setDefaultHours(); + } + + self.redraw(); + if (triggerChangeEvent) triggerEvent("onChange"); + } + + function close() { + self.isOpen = false; + + if (!self.isMobile) { + self.calendarContainer.classList.remove("open"); + + self._input.classList.remove("active"); + } + + triggerEvent("onClose"); + } + + function destroy() { + if (self.config !== undefined) triggerEvent("onDestroy"); + + for (var i = self._handlers.length; i--;) { + var h = self._handlers[i]; + h.element.removeEventListener(h.event, h.handler, h.options); + } + + self._handlers = []; + + if (self.mobileInput) { + if (self.mobileInput.parentNode) self.mobileInput.parentNode.removeChild(self.mobileInput); + self.mobileInput = undefined; + } else if (self.calendarContainer && self.calendarContainer.parentNode) { + if (self.config.static && self.calendarContainer.parentNode) { + var wrapper = self.calendarContainer.parentNode; + wrapper.lastChild && wrapper.removeChild(wrapper.lastChild); + + if (wrapper.parentNode) { + while (wrapper.firstChild) { + wrapper.parentNode.insertBefore(wrapper.firstChild, wrapper); + } + + wrapper.parentNode.removeChild(wrapper); + } + } else self.calendarContainer.parentNode.removeChild(self.calendarContainer); + } + + if (self.altInput) { + self.input.type = "text"; + if (self.altInput.parentNode) self.altInput.parentNode.removeChild(self.altInput); + delete self.altInput; + } + + if (self.input) { + self.input.type = self.input._type; + self.input.classList.remove("flatpickr-input"); + self.input.removeAttribute("readonly"); + self.input.value = ""; + } + + ["_showTimeInput", "latestSelectedDateObj", "_hideNextMonthArrow", "_hidePrevMonthArrow", "__hideNextMonthArrow", "__hidePrevMonthArrow", "isMobile", "isOpen", "selectedDateElem", "minDateHasTime", "maxDateHasTime", "days", "daysContainer", "_input", "_positionElement", "innerContainer", "rContainer", "monthNav", "todayDateElem", "calendarContainer", "weekdayContainer", "prevMonthNav", "nextMonthNav", "currentMonthElement", "currentYearElement", "navigationCurrentMonth", "selectedDateElem", "config"].forEach(function (k) { + try { + delete self[k]; + } catch (_) {} + }); + } + + function isCalendarElem(elem) { + if (self.config.appendTo && self.config.appendTo.contains(elem)) return true; + return self.calendarContainer.contains(elem); + } + + function documentClick(e) { + if (self.isOpen && !self.config.inline) { + var isCalendarElement = isCalendarElem(e.target); + var isInput = e.target === self.input || e.target === self.altInput || self.element.contains(e.target) || e.path && e.path.indexOf && (~e.path.indexOf(self.input) || ~e.path.indexOf(self.altInput)); + var lostFocus = e.type === "blur" ? isInput && e.relatedTarget && !isCalendarElem(e.relatedTarget) : !isInput && !isCalendarElement; + var isIgnored = !self.config.ignoredFocusElements.some(function (elem) { + return elem.contains(e.target); + }); + + if (lostFocus && isIgnored) { + self.close(); + + if (self.config.mode === "range" && self.selectedDates.length === 1) { + self.clear(false); + self.redraw(); + } + } + } + } + + function changeYear(newYear) { + if (!newYear || self.config.minDate && newYear < self.config.minDate.getFullYear() || self.config.maxDate && newYear > self.config.maxDate.getFullYear()) return; + var newYearNum = newYear, + isNewYear = self.currentYear !== newYearNum; + self.currentYear = newYearNum || self.currentYear; + + if (self.config.maxDate && self.currentYear === self.config.maxDate.getFullYear()) { + self.currentMonth = Math.min(self.config.maxDate.getMonth(), self.currentMonth); + } else if (self.config.minDate && self.currentYear === self.config.minDate.getFullYear()) { + self.currentMonth = Math.max(self.config.minDate.getMonth(), self.currentMonth); + } + + if (isNewYear) { + self.redraw(); + triggerEvent("onYearChange"); + } + } + + function isEnabled(date, timeless) { + if (timeless === void 0) { + timeless = true; + } + + var dateToCheck = self.parseDate(date, undefined, timeless); + if (self.config.minDate && dateToCheck && compareDates(dateToCheck, self.config.minDate, timeless !== undefined ? timeless : !self.minDateHasTime) < 0 || self.config.maxDate && dateToCheck && compareDates(dateToCheck, self.config.maxDate, timeless !== undefined ? timeless : !self.maxDateHasTime) > 0) return false; + if (self.config.enable.length === 0 && self.config.disable.length === 0) return true; + if (dateToCheck === undefined) return false; + var bool = self.config.enable.length > 0, + array = bool ? self.config.enable : self.config.disable; + + for (var i = 0, d; i < array.length; i++) { + d = array[i]; + if (typeof d === "function" && d(dateToCheck)) return bool;else if (d instanceof Date && dateToCheck !== undefined && d.getTime() === dateToCheck.getTime()) return bool;else if (typeof d === "string" && dateToCheck !== undefined) { + var parsed = self.parseDate(d, undefined, true); + return parsed && parsed.getTime() === dateToCheck.getTime() ? bool : !bool; + } else if (typeof d === "object" && dateToCheck !== undefined && d.from && d.to && dateToCheck.getTime() >= d.from.getTime() && dateToCheck.getTime() <= d.to.getTime()) return bool; + } + + return !bool; + } + + function isInView(elem) { + if (self.daysContainer !== undefined) return elem.className.indexOf("hidden") === -1 && self.daysContainer.contains(elem); + return false; + } + + function onKeyDown(e) { + var isInput = e.target === self._input; + var allowInput = self.config.allowInput; + var allowKeydown = self.isOpen && (!allowInput || !isInput); + var allowInlineKeydown = self.config.inline && isInput && !allowInput; + + if (e.keyCode === 13 && isInput) { + if (allowInput) { + self.setDate(self._input.value, true, e.target === self.altInput ? self.config.altFormat : self.config.dateFormat); + return e.target.blur(); + } else self.open(); + } else if (isCalendarElem(e.target) || allowKeydown || allowInlineKeydown) { + var isTimeObj = !!self.timeContainer && self.timeContainer.contains(e.target); + + switch (e.keyCode) { + case 13: + if (isTimeObj) updateTime();else selectDate(e); + break; + + case 27: + e.preventDefault(); + focusAndClose(); + break; + + case 8: + case 46: + if (isInput && !self.config.allowInput) { + e.preventDefault(); + self.clear(); + } + + break; + + case 37: + case 39: + if (!isTimeObj) { + e.preventDefault(); + + if (self.daysContainer !== undefined && (allowInput === false || isInView(document.activeElement))) { + var _delta = e.keyCode === 39 ? 1 : -1; + + if (!e.ctrlKey) focusOnDay(undefined, _delta);else { + changeMonth(_delta); + focusOnDay(getFirstAvailableDay(1), 0); + } + } + } else if (self.hourElement) self.hourElement.focus(); + + break; + + case 38: + case 40: + e.preventDefault(); + var delta = e.keyCode === 40 ? 1 : -1; + + if (self.daysContainer && e.target.$i !== undefined) { + if (e.ctrlKey) { + changeYear(self.currentYear - delta); + focusOnDay(getFirstAvailableDay(1), 0); + } else if (!isTimeObj) focusOnDay(undefined, delta * 7); + } else if (self.config.enableTime) { + if (!isTimeObj && self.hourElement) self.hourElement.focus(); + updateTime(e); + + self._debouncedChange(); + } + + break; + + case 9: + if (!isTimeObj) { + self.element.focus(); + break; + } + + var elems = [self.hourElement, self.minuteElement, self.secondElement, self.amPM].filter(function (x) { + return x; + }); + var i = elems.indexOf(e.target); + + if (i !== -1) { + var target = elems[i + (e.shiftKey ? -1 : 1)]; + + if (target !== undefined) { + e.preventDefault(); + target.focus(); + } else { + self.element.focus(); + } + } + + break; + + default: + break; + } + } + + if (self.amPM !== undefined && e.target === self.amPM) { + switch (e.key) { + case self.l10n.amPM[0].charAt(0): + case self.l10n.amPM[0].charAt(0).toLowerCase(): + self.amPM.textContent = self.l10n.amPM[0]; + setHoursFromInputs(); + updateValue(); + break; + + case self.l10n.amPM[1].charAt(0): + case self.l10n.amPM[1].charAt(0).toLowerCase(): + self.amPM.textContent = self.l10n.amPM[1]; + setHoursFromInputs(); + updateValue(); + break; + } + } + + triggerEvent("onKeyDown", e); + } + + function onMouseOver(elem) { + if (self.selectedDates.length !== 1 || elem && (!elem.classList.contains("flatpickr-day") || elem.classList.contains("disabled"))) return; + var hoverDate = elem ? elem.dateObj.getTime() : self.days.firstElementChild.dateObj.getTime(), + initialDate = self.parseDate(self.selectedDates[0], undefined, true).getTime(), + rangeStartDate = Math.min(hoverDate, self.selectedDates[0].getTime()), + rangeEndDate = Math.max(hoverDate, self.selectedDates[0].getTime()), + lastDate = self.daysContainer.lastChild.lastChild.dateObj.getTime(); + var containsDisabled = false; + var minRange = 0, + maxRange = 0; + + for (var t = rangeStartDate; t < lastDate; t += duration.DAY) { + if (!isEnabled(new Date(t), true)) { + containsDisabled = containsDisabled || t > rangeStartDate && t < rangeEndDate; + if (t < initialDate && (!minRange || t > minRange)) minRange = t;else if (t > initialDate && (!maxRange || t < maxRange)) maxRange = t; + } + } + + for (var m = 0; m < self.config.showMonths; m++) { + var month = self.daysContainer.children[m]; + var prevMonth = self.daysContainer.children[m - 1]; + + var _loop = function _loop(i, l) { + var dayElem = month.children[i], + date = dayElem.dateObj; + var timestamp = date.getTime(); + var outOfRange = minRange > 0 && timestamp < minRange || maxRange > 0 && timestamp > maxRange; + + if (outOfRange) { + dayElem.classList.add("notAllowed"); + ["inRange", "startRange", "endRange"].forEach(function (c) { + dayElem.classList.remove(c); + }); + return "continue"; + } else if (containsDisabled && !outOfRange) return "continue"; + + ["startRange", "inRange", "endRange", "notAllowed"].forEach(function (c) { + dayElem.classList.remove(c); + }); + + if (elem !== undefined) { + elem.classList.add(hoverDate < self.selectedDates[0].getTime() ? "startRange" : "endRange"); + + if (month.contains(elem) || !(m > 0 && prevMonth && prevMonth.lastChild.dateObj.getTime() >= timestamp)) { + if (initialDate < hoverDate && timestamp === initialDate) dayElem.classList.add("startRange");else if (initialDate > hoverDate && timestamp === initialDate) dayElem.classList.add("endRange"); + if (timestamp >= minRange && (maxRange === 0 || timestamp <= maxRange) && isBetween(timestamp, initialDate, hoverDate)) dayElem.classList.add("inRange"); + } + } + }; + + for (var i = 0, l = month.children.length; i < l; i++) { + var _ret = _loop(i, l); + + if (_ret === "continue") continue; + } + } + } + + function onResize() { + if (self.isOpen && !self.config.static && !self.config.inline) positionCalendar(); + } + + function open(e, positionElement) { + if (positionElement === void 0) { + positionElement = self._positionElement; + } + + if (self.isMobile === true) { + if (e) { + e.preventDefault(); + e.target && e.target.blur(); + } + + if (self.mobileInput !== undefined) { + self.mobileInput.focus(); + self.mobileInput.click(); + } + + triggerEvent("onOpen"); + return; + } + + if (self._input.disabled || self.config.inline) return; + var wasOpen = self.isOpen; + self.isOpen = true; + + if (!wasOpen) { + self.calendarContainer.classList.add("open"); + + self._input.classList.add("active"); + + triggerEvent("onOpen"); + positionCalendar(positionElement); + } + + if (self.config.enableTime === true && self.config.noCalendar === true) { + if (self.selectedDates.length === 0) { + self.setDate(self.config.minDate !== undefined ? new Date(self.config.minDate.getTime()) : new Date(), false); + setDefaultHours(); + updateValue(); + } + + if (self.config.allowInput === false && (e === undefined || !self.timeContainer.contains(e.relatedTarget))) { + setTimeout(function () { + return self.hourElement.select(); + }, 50); + } + } + } + + function minMaxDateSetter(type) { + return function (date) { + var dateObj = self.config["_" + type + "Date"] = self.parseDate(date, self.config.dateFormat); + var inverseDateObj = self.config["_" + (type === "min" ? "max" : "min") + "Date"]; + + if (dateObj !== undefined) { + self[type === "min" ? "minDateHasTime" : "maxDateHasTime"] = dateObj.getHours() > 0 || dateObj.getMinutes() > 0 || dateObj.getSeconds() > 0; + } + + if (self.selectedDates) { + self.selectedDates = self.selectedDates.filter(function (d) { + return isEnabled(d); + }); + if (!self.selectedDates.length && type === "min") setHoursFromDate(dateObj); + updateValue(); + } + + if (self.daysContainer) { + redraw(); + if (dateObj !== undefined) self.currentYearElement[type] = dateObj.getFullYear().toString();else self.currentYearElement.removeAttribute(type); + self.currentYearElement.disabled = !!inverseDateObj && dateObj !== undefined && inverseDateObj.getFullYear() === dateObj.getFullYear(); + } + }; + } + + function parseConfig() { + var boolOpts = ["wrap", "weekNumbers", "allowInput", "clickOpens", "time_24hr", "enableTime", "noCalendar", "altInput", "shorthandCurrentMonth", "inline", "static", "enableSeconds", "disableMobile"]; + var userConfig = Object.assign({}, instanceConfig, JSON.parse(JSON.stringify(element.dataset || {}))); + var formats$$1 = {}; + self.config.parseDate = userConfig.parseDate; + self.config.formatDate = userConfig.formatDate; + Object.defineProperty(self.config, "enable", { + get: function get() { + return self.config._enable; + }, + set: function set(dates) { + self.config._enable = parseDateRules(dates); + } + }); + Object.defineProperty(self.config, "disable", { + get: function get() { + return self.config._disable; + }, + set: function set(dates) { + self.config._disable = parseDateRules(dates); + } + }); + var timeMode = userConfig.mode === "time"; + + if (!userConfig.dateFormat && (userConfig.enableTime || timeMode)) { + formats$$1.dateFormat = userConfig.noCalendar || timeMode ? "H:i" + (userConfig.enableSeconds ? ":S" : "") : flatpickr.defaultConfig.dateFormat + " H:i" + (userConfig.enableSeconds ? ":S" : ""); + } + + if (userConfig.altInput && (userConfig.enableTime || timeMode) && !userConfig.altFormat) { + formats$$1.altFormat = userConfig.noCalendar || timeMode ? "h:i" + (userConfig.enableSeconds ? ":S K" : " K") : flatpickr.defaultConfig.altFormat + (" h:i" + (userConfig.enableSeconds ? ":S" : "") + " K"); + } + + Object.defineProperty(self.config, "minDate", { + get: function get() { + return self.config._minDate; + }, + set: minMaxDateSetter("min") + }); + Object.defineProperty(self.config, "maxDate", { + get: function get() { + return self.config._maxDate; + }, + set: minMaxDateSetter("max") + }); + + var minMaxTimeSetter = function minMaxTimeSetter(type) { + return function (val) { + self.config[type === "min" ? "_minTime" : "_maxTime"] = self.parseDate(val, "H:i"); + }; + }; + + Object.defineProperty(self.config, "minTime", { + get: function get() { + return self.config._minTime; + }, + set: minMaxTimeSetter("min") + }); + Object.defineProperty(self.config, "maxTime", { + get: function get() { + return self.config._maxTime; + }, + set: minMaxTimeSetter("max") + }); + + if (userConfig.mode === "time") { + self.config.noCalendar = true; + self.config.enableTime = true; + } + + Object.assign(self.config, formats$$1, userConfig); + + for (var i = 0; i < boolOpts.length; i++) { + self.config[boolOpts[i]] = self.config[boolOpts[i]] === true || self.config[boolOpts[i]] === "true"; + } + + HOOKS.filter(function (hook) { + return self.config[hook] !== undefined; + }).forEach(function (hook) { + self.config[hook] = arrayify(self.config[hook] || []).map(bindToInstance); + }); + self.isMobile = !self.config.disableMobile && !self.config.inline && self.config.mode === "single" && !self.config.disable.length && !self.config.enable.length && !self.config.weekNumbers && /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + + for (var _i = 0; _i < self.config.plugins.length; _i++) { + var pluginConf = self.config.plugins[_i](self) || {}; + + for (var key in pluginConf) { + if (HOOKS.indexOf(key) > -1) { + self.config[key] = arrayify(pluginConf[key]).map(bindToInstance).concat(self.config[key]); + } else if (typeof userConfig[key] === "undefined") self.config[key] = pluginConf[key]; + } + } + + triggerEvent("onParseConfig"); + } + + function setupLocale() { + if (typeof self.config.locale !== "object" && typeof flatpickr.l10ns[self.config.locale] === "undefined") self.config.errorHandler(new Error("flatpickr: invalid locale " + self.config.locale)); + self.l10n = Object.assign({}, flatpickr.l10ns.default, typeof self.config.locale === "object" ? self.config.locale : self.config.locale !== "default" ? flatpickr.l10ns[self.config.locale] : undefined); + tokenRegex.K = "(" + self.l10n.amPM[0] + "|" + self.l10n.amPM[1] + "|" + self.l10n.amPM[0].toLowerCase() + "|" + self.l10n.amPM[1].toLowerCase() + ")"; + self.formatDate = createDateFormatter(self); + self.parseDate = createDateParser({ + config: self.config, + l10n: self.l10n + }); + } + + function positionCalendar(customPositionElement) { + if (self.calendarContainer === undefined) return; + triggerEvent("onPreCalendarPosition"); + var positionElement = customPositionElement || self._positionElement; + var calendarHeight = Array.prototype.reduce.call(self.calendarContainer.children, function (acc, child) { + return acc + child.offsetHeight; + }, 0), + calendarWidth = self.calendarContainer.offsetWidth, + configPos = self.config.position.split(" "), + configPosVertical = configPos[0], + configPosHorizontal = configPos.length > 1 ? configPos[1] : null, + inputBounds = positionElement.getBoundingClientRect(), + distanceFromBottom = window.innerHeight - inputBounds.bottom, + showOnTop = configPosVertical === "above" || configPosVertical !== "below" && distanceFromBottom < calendarHeight && inputBounds.top > calendarHeight; + var top = window.pageYOffset + inputBounds.top + (!showOnTop ? positionElement.offsetHeight + 2 : -calendarHeight - 2); + toggleClass(self.calendarContainer, "arrowTop", !showOnTop); + toggleClass(self.calendarContainer, "arrowBottom", showOnTop); + if (self.config.inline) return; + var left = window.pageXOffset + inputBounds.left - (configPosHorizontal != null && configPosHorizontal === "center" ? (calendarWidth - inputBounds.width) / 2 : 0); + var right = window.document.body.offsetWidth - inputBounds.right; + var rightMost = left + calendarWidth > window.document.body.offsetWidth; + toggleClass(self.calendarContainer, "rightMost", rightMost); + if (self.config.static) return; + self.calendarContainer.style.top = top + "px"; + + if (!rightMost) { + self.calendarContainer.style.left = left + "px"; + self.calendarContainer.style.right = "auto"; + } else { + self.calendarContainer.style.left = "auto"; + self.calendarContainer.style.right = right + "px"; + } + } + + function redraw() { + if (self.config.noCalendar || self.isMobile) return; + updateNavigationCurrentMonth(); + buildDays(); + } + + function focusAndClose() { + self._input.focus(); + + if (window.navigator.userAgent.indexOf("MSIE") !== -1 || navigator.msMaxTouchPoints !== undefined) { + setTimeout(self.close, 0); + } else { + self.close(); + } + } + + function selectDate(e) { + e.preventDefault(); + e.stopPropagation(); + + var isSelectable = function isSelectable(day) { + return day.classList && day.classList.contains("flatpickr-day") && !day.classList.contains("disabled") && !day.classList.contains("notAllowed"); + }; + + var t = findParent(e.target, isSelectable); + if (t === undefined) return; + var target = t; + var selectedDate = self.latestSelectedDateObj = new Date(target.dateObj.getTime()); + var shouldChangeMonth = (selectedDate.getMonth() < self.currentMonth || selectedDate.getMonth() > self.currentMonth + self.config.showMonths - 1) && self.config.mode !== "range"; + self.selectedDateElem = target; + if (self.config.mode === "single") self.selectedDates = [selectedDate];else if (self.config.mode === "multiple") { + var selectedIndex = isDateSelected(selectedDate); + if (selectedIndex) self.selectedDates.splice(parseInt(selectedIndex), 1);else self.selectedDates.push(selectedDate); + } else if (self.config.mode === "range") { + if (self.selectedDates.length === 2) self.clear(false); + self.selectedDates.push(selectedDate); + if (compareDates(selectedDate, self.selectedDates[0], true) !== 0) self.selectedDates.sort(function (a, b) { + return a.getTime() - b.getTime(); + }); + } + setHoursFromInputs(); + + if (shouldChangeMonth) { + var isNewYear = self.currentYear !== selectedDate.getFullYear(); + self.currentYear = selectedDate.getFullYear(); + self.currentMonth = selectedDate.getMonth(); + if (isNewYear) triggerEvent("onYearChange"); + triggerEvent("onMonthChange"); + } + + updateNavigationCurrentMonth(); + buildDays(); + updateValue(); + if (self.config.enableTime) setTimeout(function () { + return self.showTimeInput = true; + }, 50); + if (!shouldChangeMonth && self.config.mode !== "range" && self.config.showMonths === 1) focusOnDayElem(target);else self.selectedDateElem && self.selectedDateElem.focus(); + if (self.hourElement !== undefined) setTimeout(function () { + return self.hourElement !== undefined && self.hourElement.select(); + }, 451); + + if (self.config.closeOnSelect) { + var single = self.config.mode === "single" && !self.config.enableTime; + var range = self.config.mode === "range" && self.selectedDates.length === 2 && !self.config.enableTime; + + if (single || range) { + focusAndClose(); + } + } + + triggerChange(); + } + + var CALLBACKS = { + locale: [setupLocale, updateWeekdays], + showMonths: [buildMonths, setCalendarWidth, buildWeekdays] + }; + + function set(option, value) { + if (option !== null && typeof option === "object") Object.assign(self.config, option);else { + self.config[option] = value; + if (CALLBACKS[option] !== undefined) CALLBACKS[option].forEach(function (x) { + return x(); + });else if (HOOKS.indexOf(option) > -1) self.config[option] = arrayify(value); + } + self.redraw(); + jumpToDate(); + updateValue(false); + } + + function setSelectedDate(inputDate, format) { + var dates = []; + if (inputDate instanceof Array) dates = inputDate.map(function (d) { + return self.parseDate(d, format); + });else if (inputDate instanceof Date || typeof inputDate === "number") dates = [self.parseDate(inputDate, format)];else if (typeof inputDate === "string") { + switch (self.config.mode) { + case "single": + case "time": + dates = [self.parseDate(inputDate, format)]; + break; + + case "multiple": + dates = inputDate.split(self.config.conjunction).map(function (date) { + return self.parseDate(date, format); + }); + break; + + case "range": + dates = inputDate.split(self.l10n.rangeSeparator).map(function (date) { + return self.parseDate(date, format); + }); + break; + + default: + break; + } + } else self.config.errorHandler(new Error("Invalid date supplied: " + JSON.stringify(inputDate))); + self.selectedDates = dates.filter(function (d) { + return d instanceof Date && isEnabled(d, false); + }); + if (self.config.mode === "range") self.selectedDates.sort(function (a, b) { + return a.getTime() - b.getTime(); + }); + } + + function setDate(date, triggerChange, format) { + if (triggerChange === void 0) { + triggerChange = false; + } + + if (format === void 0) { + format = self.config.dateFormat; + } + + if (date !== 0 && !date || date instanceof Array && date.length === 0) return self.clear(triggerChange); + setSelectedDate(date, format); + self.showTimeInput = self.selectedDates.length > 0; + self.latestSelectedDateObj = self.selectedDates[0]; + self.redraw(); + jumpToDate(); + setHoursFromDate(); + updateValue(triggerChange); + if (triggerChange) triggerEvent("onChange"); + } + + function parseDateRules(arr) { + return arr.slice().map(function (rule) { + if (typeof rule === "string" || typeof rule === "number" || rule instanceof Date) { + return self.parseDate(rule, undefined, true); + } else if (rule && typeof rule === "object" && rule.from && rule.to) return { + from: self.parseDate(rule.from, undefined), + to: self.parseDate(rule.to, undefined) + }; + + return rule; + }).filter(function (x) { + return x; + }); + } + + function setupDates() { + self.selectedDates = []; + self.now = self.parseDate(self.config.now) || new Date(); + var preloadedDate = self.config.defaultDate || ((self.input.nodeName === "INPUT" || self.input.nodeName === "TEXTAREA") && self.input.placeholder && self.input.value === self.input.placeholder ? null : self.input.value); + if (preloadedDate) setSelectedDate(preloadedDate, self.config.dateFormat); + var initialDate = self.selectedDates.length > 0 ? self.selectedDates[0] : self.config.minDate && self.config.minDate.getTime() > self.now.getTime() ? self.config.minDate : self.config.maxDate && self.config.maxDate.getTime() < self.now.getTime() ? self.config.maxDate : self.now; + self.currentYear = initialDate.getFullYear(); + self.currentMonth = initialDate.getMonth(); + if (self.selectedDates.length > 0) self.latestSelectedDateObj = self.selectedDates[0]; + if (self.config.minTime !== undefined) self.config.minTime = self.parseDate(self.config.minTime, "H:i"); + if (self.config.maxTime !== undefined) self.config.maxTime = self.parseDate(self.config.maxTime, "H:i"); + self.minDateHasTime = !!self.config.minDate && (self.config.minDate.getHours() > 0 || self.config.minDate.getMinutes() > 0 || self.config.minDate.getSeconds() > 0); + self.maxDateHasTime = !!self.config.maxDate && (self.config.maxDate.getHours() > 0 || self.config.maxDate.getMinutes() > 0 || self.config.maxDate.getSeconds() > 0); + Object.defineProperty(self, "showTimeInput", { + get: function get() { + return self._showTimeInput; + }, + set: function set(bool) { + self._showTimeInput = bool; + if (self.calendarContainer) toggleClass(self.calendarContainer, "showTimeInput", bool); + self.isOpen && positionCalendar(); + } + }); + } + + function setupInputs() { + self.input = self.config.wrap ? element.querySelector("[data-input]") : element; + + if (!self.input) { + self.config.errorHandler(new Error("Invalid input element specified")); + return; + } + + self.input._type = self.input.type; + self.input.type = "text"; + self.input.classList.add("flatpickr-input"); + self._input = self.input; + + if (self.config.altInput) { + self.altInput = createElement(self.input.nodeName, self.input.className + " " + self.config.altInputClass); + self._input = self.altInput; + self.altInput.placeholder = self.input.placeholder; + self.altInput.disabled = self.input.disabled; + self.altInput.required = self.input.required; + self.altInput.tabIndex = self.input.tabIndex; + self.altInput.type = "text"; + self.input.setAttribute("type", "hidden"); + if (!self.config.static && self.input.parentNode) self.input.parentNode.insertBefore(self.altInput, self.input.nextSibling); + } + + if (!self.config.allowInput) self._input.setAttribute("readonly", "readonly"); + self._positionElement = self.config.positionElement || self._input; + } + + function setupMobile() { + var inputType = self.config.enableTime ? self.config.noCalendar ? "time" : "datetime-local" : "date"; + self.mobileInput = createElement("input", self.input.className + " flatpickr-mobile"); + self.mobileInput.step = self.input.getAttribute("step") || "any"; + self.mobileInput.tabIndex = 1; + self.mobileInput.type = inputType; + self.mobileInput.disabled = self.input.disabled; + self.mobileInput.required = self.input.required; + self.mobileInput.placeholder = self.input.placeholder; + self.mobileFormatStr = inputType === "datetime-local" ? "Y-m-d\\TH:i:S" : inputType === "date" ? "Y-m-d" : "H:i:S"; + + if (self.selectedDates.length > 0) { + self.mobileInput.defaultValue = self.mobileInput.value = self.formatDate(self.selectedDates[0], self.mobileFormatStr); + } + + if (self.config.minDate) self.mobileInput.min = self.formatDate(self.config.minDate, "Y-m-d"); + if (self.config.maxDate) self.mobileInput.max = self.formatDate(self.config.maxDate, "Y-m-d"); + self.input.type = "hidden"; + if (self.altInput !== undefined) self.altInput.type = "hidden"; + + try { + if (self.input.parentNode) self.input.parentNode.insertBefore(self.mobileInput, self.input.nextSibling); + } catch (_a) {} + + bind(self.mobileInput, "change", function (e) { + self.setDate(e.target.value, false, self.mobileFormatStr); + triggerEvent("onChange"); + triggerEvent("onClose"); + }); + } + + function toggle(e) { + if (self.isOpen === true) return self.close(); + self.open(e); + } + + function triggerEvent(event, data) { + if (self.config === undefined) return; + var hooks = self.config[event]; + + if (hooks !== undefined && hooks.length > 0) { + for (var i = 0; hooks[i] && i < hooks.length; i++) { + hooks[i](self.selectedDates, self.input.value, self, data); + } + } + + if (event === "onChange") { + self.input.dispatchEvent(createEvent("change")); + self.input.dispatchEvent(createEvent("input")); + } + } + + function createEvent(name) { + var e = document.createEvent("Event"); + e.initEvent(name, true, true); + return e; + } + + function isDateSelected(date) { + for (var i = 0; i < self.selectedDates.length; i++) { + if (compareDates(self.selectedDates[i], date) === 0) return "" + i; + } + + return false; + } + + function isDateInRange(date) { + if (self.config.mode !== "range" || self.selectedDates.length < 2) return false; + return compareDates(date, self.selectedDates[0]) >= 0 && compareDates(date, self.selectedDates[1]) <= 0; + } + + function updateNavigationCurrentMonth() { + if (self.config.noCalendar || self.isMobile || !self.monthNav) return; + self.yearElements.forEach(function (yearElement, i) { + var d = new Date(self.currentYear, self.currentMonth, 1); + d.setMonth(self.currentMonth + i); + self.monthElements[i].textContent = monthToStr(d.getMonth(), self.config.shorthandCurrentMonth, self.l10n) + " "; + yearElement.value = d.getFullYear().toString(); + }); + self._hidePrevMonthArrow = self.config.minDate !== undefined && (self.currentYear === self.config.minDate.getFullYear() ? self.currentMonth <= self.config.minDate.getMonth() : self.currentYear < self.config.minDate.getFullYear()); + self._hideNextMonthArrow = self.config.maxDate !== undefined && (self.currentYear === self.config.maxDate.getFullYear() ? self.currentMonth + 1 > self.config.maxDate.getMonth() : self.currentYear > self.config.maxDate.getFullYear()); + } + + function getDateStr(format) { + return self.selectedDates.map(function (dObj) { + return self.formatDate(dObj, format); + }).filter(function (d, i, arr) { + return self.config.mode !== "range" || self.config.enableTime || arr.indexOf(d) === i; + }).join(self.config.mode !== "range" ? self.config.conjunction : self.l10n.rangeSeparator); + } + + function updateValue(triggerChange) { + if (triggerChange === void 0) { + triggerChange = true; + } + + if (self.selectedDates.length === 0) return self.clear(triggerChange); + + if (self.mobileInput !== undefined && self.mobileFormatStr) { + self.mobileInput.value = self.latestSelectedDateObj !== undefined ? self.formatDate(self.latestSelectedDateObj, self.mobileFormatStr) : ""; + } + + self.input.value = getDateStr(self.config.dateFormat); + + if (self.altInput !== undefined) { + self.altInput.value = getDateStr(self.config.altFormat); + } + + if (triggerChange !== false) triggerEvent("onValueUpdate"); + } + + function onMonthNavClick(e) { + e.preventDefault(); + var isPrevMonth = self.prevMonthNav.contains(e.target); + var isNextMonth = self.nextMonthNav.contains(e.target); + + if (isPrevMonth || isNextMonth) { + changeMonth(isPrevMonth ? -1 : 1); + } else if (self.yearElements.indexOf(e.target) >= 0) { + e.target.select(); + } else if (e.target.classList.contains("arrowUp")) { + self.changeYear(self.currentYear + 1); + } else if (e.target.classList.contains("arrowDown")) { + self.changeYear(self.currentYear - 1); + } + } + + function timeWrapper(e) { + e.preventDefault(); + var isKeyDown = e.type === "keydown", + input = e.target; + + if (self.amPM !== undefined && e.target === self.amPM) { + self.amPM.textContent = self.l10n.amPM[int(self.amPM.textContent === self.l10n.amPM[0])]; + } + + var min = parseFloat(input.getAttribute("data-min")), + max = parseFloat(input.getAttribute("data-max")), + step = parseFloat(input.getAttribute("data-step")), + curValue = parseInt(input.value, 10), + delta = e.delta || (isKeyDown ? e.which === 38 ? 1 : -1 : 0); + var newValue = curValue + step * delta; + + if (typeof input.value !== "undefined" && input.value.length === 2) { + var isHourElem = input === self.hourElement, + isMinuteElem = input === self.minuteElement; + + if (newValue < min) { + newValue = max + newValue + int(!isHourElem) + (int(isHourElem) && int(!self.amPM)); + if (isMinuteElem) incrementNumInput(undefined, -1, self.hourElement); + } else if (newValue > max) { + newValue = input === self.hourElement ? newValue - max - int(!self.amPM) : min; + if (isMinuteElem) incrementNumInput(undefined, 1, self.hourElement); + } + + if (self.amPM && isHourElem && (step === 1 ? newValue + curValue === 23 : Math.abs(newValue - curValue) > step)) { + self.amPM.textContent = self.l10n.amPM[int(self.amPM.textContent === self.l10n.amPM[0])]; + } + + input.value = pad(newValue); + } + } + + init(); + return self; + } + + function _flatpickr(nodeList, config) { + var nodes = Array.prototype.slice.call(nodeList); + var instances = []; + + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + + try { + if (node.getAttribute("data-fp-omit") !== null) continue; + + if (node._flatpickr !== undefined) { + node._flatpickr.destroy(); + + node._flatpickr = undefined; + } + + node._flatpickr = FlatpickrInstance(node, config || {}); + instances.push(node._flatpickr); + } catch (e) { + console.error(e); + } + } + + return instances.length === 1 ? instances[0] : instances; + } + + if (typeof HTMLElement !== "undefined") { + HTMLCollection.prototype.flatpickr = NodeList.prototype.flatpickr = function (config) { + return _flatpickr(this, config); + }; + + HTMLElement.prototype.flatpickr = function (config) { + return _flatpickr([this], config); + }; + } + + var flatpickr = function flatpickr(selector, config) { + if (selector instanceof NodeList) return _flatpickr(selector, config);else if (typeof selector === "string") return _flatpickr(window.document.querySelectorAll(selector), config); + return _flatpickr([selector], config); + }; + + flatpickr.defaultConfig = defaults; + flatpickr.l10ns = { + en: Object.assign({}, english), + default: Object.assign({}, english) + }; + + flatpickr.localize = function (l10n) { + flatpickr.l10ns.default = Object.assign({}, flatpickr.l10ns.default, l10n); + }; + + flatpickr.setDefaults = function (config) { + flatpickr.defaultConfig = Object.assign({}, flatpickr.defaultConfig, config); + }; + + flatpickr.parseDate = createDateParser({}); + flatpickr.formatDate = createDateFormatter({}); + flatpickr.compareDates = compareDates; + + if (typeof jQuery !== "undefined") { + jQuery.fn.flatpickr = function (config) { + return _flatpickr(this, config); + }; + } + + Date.prototype.fp_incr = function (days) { + return new Date(this.getFullYear(), this.getMonth(), this.getDate() + (typeof days === "string" ? parseInt(days, 10) : days)); + }; + + if (typeof window !== "undefined") { + window.flatpickr = flatpickr; + } + + return flatpickr; + +}))); diff --git a/public/js/vendor/flatpickr.min.js b/public/js/vendor/flatpickr.min.js new file mode 100644 index 00000000..e9602014 --- /dev/null +++ b/public/js/vendor/flatpickr.min.js @@ -0,0 +1,2 @@ +/* flatpickr v4.5.2,, @license MIT */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.flatpickr=t()}(this,function(){"use strict";var X=function(e){return("0"+e).slice(-2)},ee=function(e){return!0===e?1:0};function te(n,a,i){var o;return void 0===i&&(i=!1),function(){var e=this,t=arguments;null!==o&&clearTimeout(o),o=window.setTimeout(function(){o=null,i||n.apply(e,t)},a),i&&!o&&n.apply(e,t)}}var ne=function(e){return e instanceof Array?e:[e]},e=function(){},ae=function(e,t,n){return n.months[t?"shorthand":"longhand"][e]},w={D:e,F:function(e,t,n){e.setMonth(n.months.longhand.indexOf(t))},G:function(e,t){e.setHours(parseFloat(t))},H:function(e,t){e.setHours(parseFloat(t))},J:function(e,t){e.setDate(parseFloat(t))},K:function(e,t,n){e.setHours(e.getHours()%12+12*ee(new RegExp(n.amPM[1],"i").test(t)))},M:function(e,t,n){e.setMonth(n.months.shorthand.indexOf(t))},S:function(e,t){e.setSeconds(parseFloat(t))},U:function(e,t){return new Date(1e3*parseFloat(t))},W:function(e,t){var n=parseInt(t);return new Date(e.getFullYear(),0,2+7*(n-1),0,0,0,0)},Y:function(e,t){e.setFullYear(parseFloat(t))},Z:function(e,t){return new Date(t)},d:function(e,t){e.setDate(parseFloat(t))},h:function(e,t){e.setHours(parseFloat(t))},i:function(e,t){e.setMinutes(parseFloat(t))},j:function(e,t){e.setDate(parseFloat(t))},l:e,m:function(e,t){e.setMonth(parseFloat(t)-1)},n:function(e,t){e.setMonth(parseFloat(t)-1)},s:function(e,t){e.setSeconds(parseFloat(t))},w:e,y:function(e,t){e.setFullYear(2e3+parseFloat(t))}},ie={D:"(\\w+)",F:"(\\w+)",G:"(\\d\\d|\\d)",H:"(\\d\\d|\\d)",J:"(\\d\\d|\\d)\\w+",K:"",M:"(\\w+)",S:"(\\d\\d|\\d)",U:"(.+)",W:"(\\d\\d|\\d)",Y:"(\\d{4})",Z:"(.+)",d:"(\\d\\d|\\d)",h:"(\\d\\d|\\d)",i:"(\\d\\d|\\d)",j:"(\\d\\d|\\d)",l:"(\\w+)",m:"(\\d\\d|\\d)",n:"(\\d\\d|\\d)",s:"(\\d\\d|\\d)",w:"(\\d\\d|\\d)",y:"(\\d{2})"},l={Z:function(e){return e.toISOString()},D:function(e,t,n){return t.weekdays.shorthand[l.w(e,t,n)]},F:function(e,t,n){return ae(l.n(e,t,n)-1,!1,t)},G:function(e,t,n){return X(l.h(e,t,n))},H:function(e){return X(e.getHours())},J:function(e,t){return void 0!==t.ordinal?e.getDate()+t.ordinal(e.getDate()):e.getDate()},K:function(e,t){return t.amPM[ee(11Math.min(t,n)&&e",noCalendar:!1,now:new Date,onChange:[],onClose:[],onDayCreate:[],onDestroy:[],onKeyDown:[],onMonthChange:[],onOpen:[],onParseConfig:[],onReady:[],onValueUpdate:[],onYearChange:[],onPreCalendarPosition:[],plugins:[],position:"auto",positionElement:void 0,prevArrow:"",shorthandCurrentMonth:!1,showMonths:1,static:!1,time_24hr:!1,weekNumbers:!1,wrap:!1};function fe(e,t,n){if(!0===n)return e.classList.add(t);e.classList.remove(t)}function me(e,t,n){var a=window.document.createElement(e);return t=t||"",n=n||"",a.className=t,void 0!==n&&(a.textContent=n),a}function ge(e){for(;e.firstChild;)e.removeChild(e.firstChild)}function pe(e,t){var n=me("div","numInputWrapper"),a=me("input","numInput "+e),i=me("span","arrowUp"),o=me("span","arrowDown");if(a.type="text",a.pattern="\\d*",void 0!==t)for(var r in t)a.setAttribute(r,t[r]);return n.appendChild(a),n.appendChild(i),n.appendChild(o),n}"function"!=typeof Object.assign&&(Object.assign=function(n){if(!n)throw TypeError("Cannot convert undefined or null to object");for(var e=arguments.length,a=new Array(1o)&&(h.amPM.textContent=h.l10n.amPM[ee(h.amPM.textContent===h.l10n.amPM[0])]),n.value=X(c)}}(e);var t=h._input.value;m(),Z(),h._input.value!==t&&h._debouncedChange()}}function m(){if(void 0!==h.hourElement&&void 0!==h.minuteElement){var e,t,n=(parseInt(h.hourElement.value.slice(-2),10)||0)%24,a=(parseInt(h.minuteElement.value,10)||0)%60,i=void 0!==h.secondElement?(parseInt(h.secondElement.value,10)||0)%60:0;void 0!==h.amPM&&(e=n,t=h.amPM.textContent,n=e%12+12*ee(t===h.l10n.amPM[1]));var o=void 0!==h.config.minTime||h.config.minDate&&h.minDateHasTime&&h.latestSelectedDateObj&&0===ce(h.latestSelectedDateObj,h.config.minDate,!0);if(void 0!==h.config.maxTime||h.config.maxDate&&h.maxDateHasTime&&h.latestSelectedDateObj&&0===ce(h.latestSelectedDateObj,h.config.maxDate,!0)){var r=void 0!==h.config.maxTime?h.config.maxTime:h.config.maxDate;(n=Math.min(n,r.getHours()))===r.getHours()&&(a=Math.min(a,r.getMinutes())),a===r.getMinutes()&&(i=Math.min(i,r.getSeconds()))}if(o){var l=void 0!==h.config.minTime?h.config.minTime:h.config.minDate;(n=Math.max(n,l.getHours()))===l.getHours()&&(a=Math.max(a,l.getMinutes())),a===l.getMinutes()&&(i=Math.max(i,l.getSeconds()))}c(n,a,i)}}function i(e){var t=e||h.latestSelectedDateObj;t&&c(t.getHours(),t.getMinutes(),t.getSeconds())}function a(){var e=h.config.defaultHour,t=h.config.defaultMinute,n=h.config.defaultSeconds;if(void 0!==h.config.minDate){var a=h.config.minDate.getHours(),i=h.config.minDate.getMinutes();(e=Math.max(e,a))===a&&(t=Math.max(i,t)),e===a&&t===i&&(n=h.config.minDate.getSeconds())}if(void 0!==h.config.maxDate){var o=h.config.maxDate.getHours(),r=h.config.maxDate.getMinutes();(e=Math.min(e,o))===o&&(t=Math.min(r,t)),e===o&&t===r&&(n=h.config.maxDate.getSeconds())}c(e,t,n)}function c(e,t,n){void 0!==h.latestSelectedDateObj&&h.latestSelectedDateObj.setHours(e%24,t,n||0,0),h.hourElement&&h.minuteElement&&!h.isMobile&&(h.hourElement.value=X(h.config.time_24hr?e:(12+e)%12+12*ee(e%12==0)),h.minuteElement.value=X(t),void 0!==h.amPM&&(h.amPM.textContent=h.l10n.amPM[ee(12<=e)]),void 0!==h.secondElement&&(h.secondElement.value=X(n)))}function n(e){var t=parseInt(e.target.value)+(e.delta||0);(1h.now?h.config.minDate:h.config.maxDate&&h.config.maxDate"+h.config.getWeek(t)+""),q("onDayCreate",r),r}function b(e){e.focus(),"range"===h.config.mode&&P(e)}function w(e){for(var t=0=Math.abs(t))return b(s)}h.changeMonth(i),C(w(i),0)}(a,t):b(a)}function y(e,t){for(var n=(new Date(e,t,1).getDay()-h.l10n.firstDayOfWeek+7)%7,a=h.utils.getDaysInMonth((t-1+12)%12),i=h.utils.getDaysInMonth(t),o=window.document.createDocumentFragment(),r=1\n "+t.join("")+"\n \n "}function I(e,t){void 0===t&&(t=!0);var n=t?e:e-h.currentMonth;n<0&&!0===h._hidePrevMonthArrow||0h.config.maxDate.getFullYear())){var t=e,n=h.currentYear!==t;h.currentYear=t||h.currentYear,h.config.maxDate&&h.currentYear===h.config.maxDate.getFullYear()?h.currentMonth=Math.min(h.config.maxDate.getMonth(),h.currentMonth):h.config.minDate&&h.currentYear===h.config.minDate.getFullYear()&&(h.currentMonth=Math.max(h.config.minDate.getMonth(),h.currentMonth)),n&&(h.redraw(),q("onYearChange"))}}function N(e,t){void 0===t&&(t=!0);var n=h.parseDate(e,void 0,t);if(h.config.minDate&&n&&ce(n,h.config.minDate,void 0!==t?t:!h.minDateHasTime)<0||h.config.maxDate&&n&&0=a.from.getTime()&&n.getTime()<=a.to.getTime())return i}return!i}function F(e){return void 0!==h.daysContainer&&(-1===e.className.indexOf("hidden")&&h.daysContainer.contains(e))}function A(e){var t=e.target===h._input,n=h.config.allowInput,a=h.isOpen&&(!n||!t),i=h.config.inline&&t&&!n;if(13===e.keyCode&&t){if(n)return h.setDate(h._input.value,!0,e.target===h.altInput?h.config.altFormat:h.config.dateFormat),e.target.blur();h.open()}else if(O(e.target)||a||i){var o=!!h.timeContainer&&h.timeContainer.contains(e.target);switch(e.keyCode){case 13:o?f():B(e);break;case 27:e.preventDefault(),R();break;case 8:case 46:t&&!h.config.allowInput&&(e.preventDefault(),h.clear());break;case 37:case 39:if(o)h.hourElement&&h.hourElement.focus();else if(e.preventDefault(),void 0!==h.daysContainer&&(!1===n||F(document.activeElement))){var r=39===e.keyCode?1:-1;e.ctrlKey?(I(r),C(w(1),0)):C(void 0,r)}break;case 38:case 40:e.preventDefault();var l=40===e.keyCode?1:-1;h.daysContainer&&void 0!==e.target.$i?e.ctrlKey?(_(h.currentYear-l),C(w(1),0)):o||C(void 0,7*l):h.config.enableTime&&(!o&&h.hourElement&&h.hourElement.focus(),f(e),h._debouncedChange());break;case 9:if(!o){h.element.focus();break}var c=[h.hourElement,h.minuteElement,h.secondElement,h.amPM].filter(function(e){return e}),d=c.indexOf(e.target);if(-1!==d){var s=c[d+(e.shiftKey?-1:1)];void 0!==s?(e.preventDefault(),s.focus()):h.element.focus()}}}if(void 0!==h.amPM&&e.target===h.amPM)switch(e.key){case h.l10n.amPM[0].charAt(0):case h.l10n.amPM[0].charAt(0).toLowerCase():h.amPM.textContent=h.l10n.amPM[0],m(),Z();break;case h.l10n.amPM[1].charAt(0):case h.l10n.amPM[1].charAt(0).toLowerCase():h.amPM.textContent=h.l10n.amPM[1],m(),Z()}q("onKeyDown",e)}function P(o){if(1===h.selectedDates.length&&(!o||o.classList.contains("flatpickr-day")&&!o.classList.contains("disabled"))){for(var r=o?o.dateObj.getTime():h.days.firstElementChild.dateObj.getTime(),l=h.parseDate(h.selectedDates[0],void 0,!0).getTime(),e=Math.min(r,h.selectedDates[0].getTime()),t=Math.max(r,h.selectedDates[0].getTime()),n=h.daysContainer.lastChild.lastChild.dateObj.getTime(),c=!1,d=0,s=0,a=e;a=a||(ln,s=window.pageYOffset+l.top+(d?-n-2:t.offsetHeight+2);if(fe(h.calendarContainer,"arrowTop",!d),fe(h.calendarContainer,"arrowBottom",d),!h.config.inline){var u=window.pageXOffset+l.left-(null!=r&&"center"===r?(a-l.width)/2:0),f=window.document.body.offsetWidth-l.right,m=u+a>window.document.body.offsetWidth;fe(h.calendarContainer,"rightMost",m),h.config.static||(h.calendarContainer.style.top=s+"px",m?(h.calendarContainer.style.left="auto",h.calendarContainer.style.right=f+"px"):(h.calendarContainer.style.left=u+"px",h.calendarContainer.style.right="auto"))}}}function W(){h.config.noCalendar||h.isMobile||(G(),M())}function R(){h._input.focus(),-1!==window.navigator.userAgent.indexOf("MSIE")||void 0!==navigator.msMaxTouchPoints?setTimeout(h.close,0):h.close()}function B(e){e.preventDefault(),e.stopPropagation();var t=function e(t,n){return n(t)?t:t.parentNode?e(t.parentNode,n):void 0}(e.target,function(e){return e.classList&&e.classList.contains("flatpickr-day")&&!e.classList.contains("disabled")&&!e.classList.contains("notAllowed")});if(void 0!==t){var n=t,a=h.latestSelectedDateObj=new Date(n.dateObj.getTime()),i=(a.getMonth()h.currentMonth+h.config.showMonths-1)&&"range"!==h.config.mode;if(h.selectedDateElem=n,"single"===h.config.mode)h.selectedDates=[a];else if("multiple"===h.config.mode){var o=z(a);o?h.selectedDates.splice(parseInt(o),1):h.selectedDates.push(a)}else"range"===h.config.mode&&(2===h.selectedDates.length&&h.clear(!1),h.selectedDates.push(a),0!==ce(a,h.selectedDates[0],!0)&&h.selectedDates.sort(function(e,t){return e.getTime()-t.getTime()}));if(m(),i){var r=h.currentYear!==a.getFullYear();h.currentYear=a.getFullYear(),h.currentMonth=a.getMonth(),r&&q("onYearChange"),q("onMonthChange")}if(G(),M(),Z(),h.config.enableTime&&setTimeout(function(){return h.showTimeInput=!0},50),i||"range"===h.config.mode||1!==h.config.showMonths?h.selectedDateElem&&h.selectedDateElem.focus():b(n),void 0!==h.hourElement&&setTimeout(function(){return void 0!==h.hourElement&&h.hourElement.select()},451),h.config.closeOnSelect){var l="single"===h.config.mode&&!h.config.enableTime,c="range"===h.config.mode&&2===h.selectedDates.length&&!h.config.enableTime;(l||c)&&R()}g()}}h.parseDate=le({config:h.config,l10n:h.l10n}),h._handlers=[],h._bind=o,h._setHoursFromDate=i,h._positionCalendar=L,h.changeMonth=I,h.changeYear=_,h.clear=function(e){void 0===e&&(e=!0);h.input.value="",void 0!==h.altInput&&(h.altInput.value="");void 0!==h.mobileInput&&(h.mobileInput.value="");h.selectedDates=[],h.latestSelectedDateObj=void 0,!(h.showTimeInput=!1)===h.config.enableTime&&a();h.redraw(),e&&q("onChange")},h.close=function(){h.isOpen=!1,h.isMobile||(h.calendarContainer.classList.remove("open"),h._input.classList.remove("active"));q("onClose")},h._createElement=me,h.destroy=function(){void 0!==h.config&&q("onDestroy");for(var e=h._handlers.length;e--;){var t=h._handlers[e];t.element.removeEventListener(t.event,t.handler,t.options)}if(h._handlers=[],h.mobileInput)h.mobileInput.parentNode&&h.mobileInput.parentNode.removeChild(h.mobileInput),h.mobileInput=void 0;else if(h.calendarContainer&&h.calendarContainer.parentNode)if(h.config.static&&h.calendarContainer.parentNode){var n=h.calendarContainer.parentNode;if(n.lastChild&&n.removeChild(n.lastChild),n.parentNode){for(;n.firstChild;)n.parentNode.insertBefore(n.firstChild,n);n.parentNode.removeChild(n)}}else h.calendarContainer.parentNode.removeChild(h.calendarContainer);h.altInput&&(h.input.type="text",h.altInput.parentNode&&h.altInput.parentNode.removeChild(h.altInput),delete h.altInput);h.input&&(h.input.type=h.input._type,h.input.classList.remove("flatpickr-input"),h.input.removeAttribute("readonly"),h.input.value="");["_showTimeInput","latestSelectedDateObj","_hideNextMonthArrow","_hidePrevMonthArrow","__hideNextMonthArrow","__hidePrevMonthArrow","isMobile","isOpen","selectedDateElem","minDateHasTime","maxDateHasTime","days","daysContainer","_input","_positionElement","innerContainer","rContainer","monthNav","todayDateElem","calendarContainer","weekdayContainer","prevMonthNav","nextMonthNav","currentMonthElement","currentYearElement","navigationCurrentMonth","selectedDateElem","config"].forEach(function(e){try{delete h[e]}catch(e){}})},h.isEnabled=N,h.jumpToDate=l,h.open=function(e,t){void 0===t&&(t=h._positionElement);if(!0===h.isMobile)return e&&(e.preventDefault(),e.target&&e.target.blur()),void 0!==h.mobileInput&&(h.mobileInput.focus(),h.mobileInput.click()),void q("onOpen");if(h._input.disabled||h.config.inline)return;var n=h.isOpen;h.isOpen=!0,n||(h.calendarContainer.classList.add("open"),h._input.classList.add("active"),q("onOpen"),L(t));!0===h.config.enableTime&&!0===h.config.noCalendar&&(0===h.selectedDates.length&&(h.setDate(void 0!==h.config.minDate?new Date(h.config.minDate.getTime()):new Date,!1),a(),Z()),!1!==h.config.allowInput||void 0!==e&&h.timeContainer.contains(e.relatedTarget)||setTimeout(function(){return h.hourElement.select()},50))},h.redraw=W,h.set=function(e,t){null!==e&&"object"==typeof e?Object.assign(h.config,e):(h.config[e]=t,void 0!==K[e]?K[e].forEach(function(e){return e()}):-1h.config.maxDate.getMonth():h.currentYear>h.config.maxDate.getFullYear()))}function V(t){return h.selectedDates.map(function(e){return h.formatDate(e,t)}).filter(function(e,t,n){return"range"!==h.config.mode||h.config.enableTime||n.indexOf(e)===t}).join("range"!==h.config.mode?h.config.conjunction:h.l10n.rangeSeparator)}function Z(e){if(void 0===e&&(e=!0),0===h.selectedDates.length)return h.clear(e);void 0!==h.mobileInput&&h.mobileFormatStr&&(h.mobileInput.value=void 0!==h.latestSelectedDateObj?h.formatDate(h.latestSelectedDateObj,h.mobileFormatStr):""),h.input.value=V(h.config.dateFormat),void 0!==h.altInput&&(h.altInput.value=V(h.config.altFormat)),!1!==e&&q("onValueUpdate")}function Q(e){e.preventDefault();var t=h.prevMonthNav.contains(e.target),n=h.nextMonthNav.contains(e.target);t||n?I(t?-1:1):0<=h.yearElements.indexOf(e.target)?e.target.select():e.target.classList.contains("arrowUp")?h.changeYear(h.currentYear+1):e.target.classList.contains("arrowDown")&&h.changeYear(h.currentYear-1)}return function(){h.element=h.input=d,h.isOpen=!1,function(){var e=["wrap","weekNumbers","allowInput","clickOpens","time_24hr","enableTime","noCalendar","altInput","shorthandCurrentMonth","inline","static","enableSeconds","disableMobile"],t=Object.assign({},s,JSON.parse(JSON.stringify(d.dataset||{}))),n={};h.config.parseDate=t.parseDate,h.config.formatDate=t.formatDate,Object.defineProperty(h.config,"enable",{get:function(){return h.config._enable},set:function(e){h.config._enable=U(e)}}),Object.defineProperty(h.config,"disable",{get:function(){return h.config._disable},set:function(e){h.config._disable=U(e)}});var a="time"===t.mode;t.dateFormat||!t.enableTime&&!a||(n.dateFormat=t.noCalendar||a?"H:i"+(t.enableSeconds?":S":""):ve.defaultConfig.dateFormat+" H:i"+(t.enableSeconds?":S":"")),t.altInput&&(t.enableTime||a)&&!t.altFormat&&(n.altFormat=t.noCalendar||a?"h:i"+(t.enableSeconds?":S K":" K"):ve.defaultConfig.altFormat+" h:i"+(t.enableSeconds?":S":"")+" K"),Object.defineProperty(h.config,"minDate",{get:function(){return h.config._minDate},set:Y("min")}),Object.defineProperty(h.config,"maxDate",{get:function(){return h.config._maxDate},set:Y("max")});var i=function(t){return function(e){h.config["min"===t?"_minTime":"_maxTime"]=h.parseDate(e,"H:i")}};Object.defineProperty(h.config,"minTime",{get:function(){return h.config._minTime},set:i("min")}),Object.defineProperty(h.config,"maxTime",{get:function(){return h.config._maxTime},set:i("max")}),"time"===t.mode&&(h.config.noCalendar=!0,h.config.enableTime=!0),Object.assign(h.config,n,t);for(var o=0;oh.now.getTime()?h.config.minDate:h.config.maxDate&&h.config.maxDate.getTime()provideHook('reporting/Report', '\\Icinga\\Module\\Reporting\\Reports\\SystemReport'); + + $this->provideHook('reporting/Action', '\\Icinga\\Module\\Reporting\\Actions\\SendMail'); + + Icinga::app()->getLoader()->registerNamespace('reportingipl\Web', __DIR__ . '/library/vendor/ipl/web/src'); +} diff --git a/schema/mysql.sql b/schema/mysql.sql new file mode 100644 index 00000000..7fcebb99 --- /dev/null +++ b/schema/mysql.sql @@ -0,0 +1,84 @@ +CREATE TABLE timeframe ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + name varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, + title varchar(255) NULL DEFAULT NULL COLLATE utf8mb4_unicode_ci, + start varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, + end varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, + ctime bigint(20) unsigned NOT NULL, + mtime bigint(20) unsigned NOT NULL, + PRIMARY KEY(id), + UNIQUE KEY timeframe (name) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=dynamic; + +INSERT INTO timeframe (name, title, start, end, ctime, mtime) VALUES + ('4 Hours', null, '-4 hours', 'now', CURRENT_TIMESTAMP() * 1000, CURRENT_TIMESTAMP() * 1000), + ('25 Hours', null, '-25 hours', 'now', CURRENT_TIMESTAMP() * 1000, CURRENT_TIMESTAMP() * 1000), + ('One Week', null, '-1 week', 'now', CURRENT_TIMESTAMP() * 1000, CURRENT_TIMESTAMP() * 1000), + ('One Month', null, '-1 month', 'now', CURRENT_TIMESTAMP() * 1000, CURRENT_TIMESTAMP() * 1000), + ('One Year', null, '-1 year', 'now', CURRENT_TIMESTAMP() * 1000, CURRENT_TIMESTAMP() * 1000), + ('Current Day', null, 'midnight', 'now', CURRENT_TIMESTAMP() * 1000, CURRENT_TIMESTAMP() * 1000), + ('Last Day', null, 'yesterday midnight', 'yesterday 23:59:59', CURRENT_TIMESTAMP() * 1000, CURRENT_TIMESTAMP() * 1000), + ('Current Week', null, 'monday this week midnight', 'sunday this week 23:59:59', CURRENT_TIMESTAMP() * 1000, CURRENT_TIMESTAMP() * 1000), + ('Last Week', null, 'monday last week midnight', 'sunday last week 23:59:59', CURRENT_TIMESTAMP() * 1000, CURRENT_TIMESTAMP() * 1000), + ('Current Month', null, 'first day of this month midnight', 'now', CURRENT_TIMESTAMP() * 1000, CURRENT_TIMESTAMP() * 1000), + ('Last Month', null, 'first day of last month midnight', 'last day of last month 23:59:59', CURRENT_TIMESTAMP() * 1000, CURRENT_TIMESTAMP() * 1000), + ('Current Year', null, 'first day of this year midnight', 'now', CURRENT_TIMESTAMP() * 1000, CURRENT_TIMESTAMP() * 1000), + ('Last Year', null, 'first day of last year midnight', 'last day of December last year 23:59:59', CURRENT_TIMESTAMP() * 1000, CURRENT_TIMESTAMP() * 1000); + +CREATE TABLE report ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + timeframe_id int(10) unsigned NOT NULL, + author varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, + name varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, + ctime bigint(20) unsigned NOT NULL, + mtime bigint(20) unsigned NOT NULL, + PRIMARY KEY(id), + UNIQUE KEY report (name), + CONSTRAINT report_timeframe FOREIGN KEY (timeframe_id) REFERENCES timeframe (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=dynamic; + +CREATE TABLE reportlet ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + report_id int(10) unsigned NOT NULL, + class varchar(255) NOT NULL, + ctime bigint(20) unsigned NOT NULL, + mtime bigint(20) unsigned NOT NULL, + PRIMARY KEY(id), + CONSTRAINT reportlet_report FOREIGN KEY (report_id) REFERENCES report (id) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +CREATE TABLE config ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + reportlet_id int(10) unsigned NOT NULL, + name varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, + value text NULL DEFAULT NULL, + ctime bigint(20) unsigned NOT NULL, + mtime bigint(20) unsigned NOT NULL, + PRIMARY KEY(id), + CONSTRAINT config_reportlet FOREIGN KEY (reportlet_id) REFERENCES reportlet (id) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +CREATE TABLE schedule ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + report_id int(10) unsigned NOT NULL, + author varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, + start bigint(20) unsigned NOT NULL, + frequency enum('minutely', 'hourly', 'daily', 'weekly', 'monthly'), + action varchar(255) NOT NULL, + config text NULL DEFAULT NULL, + ctime bigint(20) unsigned NOT NULL, + mtime bigint(20) unsigned NOT NULL, + PRIMARY KEY(id), + CONSTRAINT schedule_report FOREIGN KEY (report_id) REFERENCES report (id) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- CREATE TABLE share ( +-- id int(10) unsigned NOT NULL AUTO_INCREMENT, +-- report_id int(10) unsigned NOT NULL, +-- username varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, +-- restriction enum('none', 'owner', 'consumer'), +-- ctime bigint(20) unsigned NOT NULL, +-- mtime bigint(20) unsigned NOT NULL, +-- PRIMARY KEY(id), +-- CONSTRAINT share_report FOREIGN KEY (report_id) REFERENCES report (id) ON DELETE CASCADE ON UPDATE CASCADE +-- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 00000000..e74ef289 --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 00000000..f27399a0 --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +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/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 00000000..7a91153b --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ + $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', + 'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php', +); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php new file mode 100644 index 00000000..b7fc0125 --- /dev/null +++ b/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/psr/http-message/src'), + 'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 00000000..b8c603a6 --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,70 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInitae69f138d27af1289714a48b26fe4147::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + if ($useStaticLoader) { + $includeFiles = Composer\Autoload\ComposerStaticInitae69f138d27af1289714a48b26fe4147::$files; + } else { + $includeFiles = require __DIR__ . '/autoload_files.php'; + } + foreach ($includeFiles as $fileIdentifier => $file) { + composerRequireae69f138d27af1289714a48b26fe4147($fileIdentifier, $file); + } + + return $loader; + } +} + +function composerRequireae69f138d27af1289714a48b26fe4147($fileIdentifier, $file) +{ + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + require $file; + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 00000000..2c6b62f4 --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,44 @@ + __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', + 'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php', + ); + + public static $prefixLengthsPsr4 = array ( + 'P' => + array ( + 'Psr\\Http\\Message\\' => 17, + ), + 'G' => + array ( + 'GuzzleHttp\\Psr7\\' => 16, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Psr\\Http\\Message\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/http-message/src', + ), + 'GuzzleHttp\\Psr7\\' => + array ( + 0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src', + ), + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitae69f138d27af1289714a48b26fe4147::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitae69f138d27af1289714a48b26fe4147::$prefixDirsPsr4; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 00000000..8bbd2e59 --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,165 @@ +[ + { + "name": "guzzlehttp/psr7", + "version": "1.5.2", + "version_normalized": "1.5.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "9f83dded91781a01c63574e387eaa769be769115" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", + "reference": "9f83dded91781a01c63574e387eaa769be769115", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + }, + "time": "2018-12-04T20:46:45+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ] + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2016-08-06T14:39:51+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ] + }, + { + "name": "ralouphie/getallheaders", + "version": "2.0.5", + "version_normalized": "2.0.5.0", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~3.7.0", + "satooshi/php-coveralls": ">=1.0" + }, + "time": "2016-02-11T07:05:27+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders." + } +] diff --git a/vendor/guzzlehttp/psr7/CHANGELOG.md b/vendor/guzzlehttp/psr7/CHANGELOG.md new file mode 100644 index 00000000..27b65f09 --- /dev/null +++ b/vendor/guzzlehttp/psr7/CHANGELOG.md @@ -0,0 +1,225 @@ +# Change Log + + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + + +## [Unreleased] + + +## [1.5.2] - 2018-12-04 + +### Fixed + +- Check body size when getting the message summary + + +## [1.5.1] - 2018-12-04 + +### Fixed + +- Get the summary of a body only if it is readable + + +## [1.5.0] - 2018-12-03 + +### Added + +- Response first-line to response string exception (fixes #145) +- A test for #129 behavior +- `get_message_body_summary` function in order to get the message summary +- `3gp` and `mkv` mime types + +### Changed + +- Clarify exception message when stream is detached + +### Deprecated + +- Deprecated parsing folded header lines as per RFC 7230 + +### Fixed + +- Fix `AppendStream::detach` to not close streams +- `InflateStream` preserves `isSeekable` attribute of the underlying stream +- `ServerRequest::getUriFromGlobals` to support URLs in query parameters + + +Several other fixes and improvements. + + +## [1.4.2] - 2017-03-20 + +### Fixed + +- Reverted BC break to `Uri::resolve` and `Uri::removeDotSegments` by removing + calls to `trigger_error` when deprecated methods are invoked. + + +## [1.4.1] - 2017-02-27 + +### Added + +- Rriggering of silenced deprecation warnings. + +### Fixed + +- Reverted BC break by reintroducing behavior to automagically fix a URI with a + relative path and an authority by adding a leading slash to the path. It's only + deprecated now. + + +## [1.4.0] - 2017-02-21 + +### Added + +- Added common URI utility methods based on RFC 3986 (see documentation in the readme): + - `Uri::isDefaultPort` + - `Uri::isAbsolute` + - `Uri::isNetworkPathReference` + - `Uri::isAbsolutePathReference` + - `Uri::isRelativePathReference` + - `Uri::isSameDocumentReference` + - `Uri::composeComponents` + - `UriNormalizer::normalize` + - `UriNormalizer::isEquivalent` + - `UriResolver::relativize` + +### Changed + +- Ensure `ServerRequest::getUriFromGlobals` returns a URI in absolute form. +- Allow `parse_response` to parse a response without delimiting space and reason. +- Ensure each URI modification results in a valid URI according to PSR-7 discussions. + Invalid modifications will throw an exception instead of returning a wrong URI or + doing some magic. + - `(new Uri)->withPath('foo')->withHost('example.com')` will throw an exception + because the path of a URI with an authority must start with a slash "/" or be empty + - `(new Uri())->withScheme('http')` will return `'http://localhost'` + +### Deprecated + +- `Uri::resolve` in favor of `UriResolver::resolve` +- `Uri::removeDotSegments` in favor of `UriResolver::removeDotSegments` + +### Fixed + +- `Stream::read` when length parameter <= 0. +- `copy_to_stream` reads bytes in chunks instead of `maxLen` into memory. +- `ServerRequest::getUriFromGlobals` when `Host` header contains port. +- Compatibility of URIs with `file` scheme and empty host. + + +## [1.3.1] - 2016-06-25 + +### Fixed + +- `Uri::__toString` for network path references, e.g. `//example.org`. +- Missing lowercase normalization for host. +- Handling of URI components in case they are `'0'` in a lot of places, + e.g. as a user info password. +- `Uri::withAddedHeader` to correctly merge headers with different case. +- Trimming of header values in `Uri::withAddedHeader`. Header values may + be surrounded by whitespace which should be ignored according to RFC 7230 + Section 3.2.4. This does not apply to header names. +- `Uri::withAddedHeader` with an array of header values. +- `Uri::resolve` when base path has no slash and handling of fragment. +- Handling of encoding in `Uri::with(out)QueryValue` so one can pass the + key/value both in encoded as well as decoded form to those methods. This is + consistent with withPath, withQuery etc. +- `ServerRequest::withoutAttribute` when attribute value is null. + + +## [1.3.0] - 2016-04-13 + +### Added + +- Remaining interfaces needed for full PSR7 compatibility + (ServerRequestInterface, UploadedFileInterface, etc.). +- Support for stream_for from scalars. + +### Changed + +- Can now extend Uri. + +### Fixed +- A bug in validating request methods by making it more permissive. + + +## [1.2.3] - 2016-02-18 + +### Fixed + +- Support in `GuzzleHttp\Psr7\CachingStream` for seeking forward on remote + streams, which can sometimes return fewer bytes than requested with `fread`. +- Handling of gzipped responses with FNAME headers. + + +## [1.2.2] - 2016-01-22 + +### Added + +- Support for URIs without any authority. +- Support for HTTP 451 'Unavailable For Legal Reasons.' +- Support for using '0' as a filename. +- Support for including non-standard ports in Host headers. + + +## [1.2.1] - 2015-11-02 + +### Changes + +- Now supporting negative offsets when seeking to SEEK_END. + + +## [1.2.0] - 2015-08-15 + +### Changed + +- Body as `"0"` is now properly added to a response. +- Now allowing forward seeking in CachingStream. +- Now properly parsing HTTP requests that contain proxy targets in + `parse_request`. +- functions.php is now conditionally required. +- user-info is no longer dropped when resolving URIs. + + +## [1.1.0] - 2015-06-24 + +### Changed + +- URIs can now be relative. +- `multipart/form-data` headers are now overridden case-insensitively. +- URI paths no longer encode the following characters because they are allowed + in URIs: "(", ")", "*", "!", "'" +- A port is no longer added to a URI when the scheme is missing and no port is + present. + + +## 1.0.0 - 2015-05-19 + +Initial release. + +Currently unsupported: + +- `Psr\Http\Message\ServerRequestInterface` +- `Psr\Http\Message\UploadedFileInterface` + + + +[Unreleased]: https://github.com/guzzle/psr7/compare/1.5.2...HEAD +[1.5.2]: https://github.com/guzzle/psr7/compare/1.5.1...1.5.2 +[1.5.1]: https://github.com/guzzle/psr7/compare/1.5.0...1.5.1 +[1.5.0]: https://github.com/guzzle/psr7/compare/1.4.2...1.5.0 +[1.4.2]: https://github.com/guzzle/psr7/compare/1.4.1...1.4.2 +[1.4.1]: https://github.com/guzzle/psr7/compare/1.4.0...1.4.1 +[1.4.0]: https://github.com/guzzle/psr7/compare/1.3.1...1.4.0 +[1.3.1]: https://github.com/guzzle/psr7/compare/1.3.0...1.3.1 +[1.3.0]: https://github.com/guzzle/psr7/compare/1.2.3...1.3.0 +[1.2.3]: https://github.com/guzzle/psr7/compare/1.2.2...1.2.3 +[1.2.2]: https://github.com/guzzle/psr7/compare/1.2.1...1.2.2 +[1.2.1]: https://github.com/guzzle/psr7/compare/1.2.0...1.2.1 +[1.2.0]: https://github.com/guzzle/psr7/compare/1.1.0...1.2.0 +[1.1.0]: https://github.com/guzzle/psr7/compare/1.0.0...1.1.0 diff --git a/vendor/guzzlehttp/psr7/LICENSE b/vendor/guzzlehttp/psr7/LICENSE new file mode 100644 index 00000000..581d95f9 --- /dev/null +++ b/vendor/guzzlehttp/psr7/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Michael Dowling, https://github.com/mtdowling + +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/vendor/guzzlehttp/psr7/README.md b/vendor/guzzlehttp/psr7/README.md new file mode 100644 index 00000000..c60a6a38 --- /dev/null +++ b/vendor/guzzlehttp/psr7/README.md @@ -0,0 +1,745 @@ +# PSR-7 Message Implementation + +This repository contains a full [PSR-7](http://www.php-fig.org/psr/psr-7/) +message implementation, several stream decorators, and some helpful +functionality like query string parsing. + + +[![Build Status](https://travis-ci.org/guzzle/psr7.svg?branch=master)](https://travis-ci.org/guzzle/psr7) + + +# Stream implementation + +This package comes with a number of stream implementations and stream +decorators. + + +## AppendStream + +`GuzzleHttp\Psr7\AppendStream` + +Reads from multiple streams, one after the other. + +```php +use GuzzleHttp\Psr7; + +$a = Psr7\stream_for('abc, '); +$b = Psr7\stream_for('123.'); +$composed = new Psr7\AppendStream([$a, $b]); + +$composed->addStream(Psr7\stream_for(' Above all listen to me')); + +echo $composed; // abc, 123. Above all listen to me. +``` + + +## BufferStream + +`GuzzleHttp\Psr7\BufferStream` + +Provides a buffer stream that can be written to fill a buffer, and read +from to remove bytes from the buffer. + +This stream returns a "hwm" metadata value that tells upstream consumers +what the configured high water mark of the stream is, or the maximum +preferred size of the buffer. + +```php +use GuzzleHttp\Psr7; + +// When more than 1024 bytes are in the buffer, it will begin returning +// false to writes. This is an indication that writers should slow down. +$buffer = new Psr7\BufferStream(1024); +``` + + +## CachingStream + +The CachingStream is used to allow seeking over previously read bytes on +non-seekable streams. This can be useful when transferring a non-seekable +entity body fails due to needing to rewind the stream (for example, resulting +from a redirect). Data that is read from the remote stream will be buffered in +a PHP temp stream so that previously read bytes are cached first in memory, +then on disk. + +```php +use GuzzleHttp\Psr7; + +$original = Psr7\stream_for(fopen('http://www.google.com', 'r')); +$stream = new Psr7\CachingStream($original); + +$stream->read(1024); +echo $stream->tell(); +// 1024 + +$stream->seek(0); +echo $stream->tell(); +// 0 +``` + + +## DroppingStream + +`GuzzleHttp\Psr7\DroppingStream` + +Stream decorator that begins dropping data once the size of the underlying +stream becomes too full. + +```php +use GuzzleHttp\Psr7; + +// Create an empty stream +$stream = Psr7\stream_for(); + +// Start dropping data when the stream has more than 10 bytes +$dropping = new Psr7\DroppingStream($stream, 10); + +$dropping->write('01234567890123456789'); +echo $stream; // 0123456789 +``` + + +## FnStream + +`GuzzleHttp\Psr7\FnStream` + +Compose stream implementations based on a hash of functions. + +Allows for easy testing and extension of a provided stream without needing +to create a concrete class for a simple extension point. + +```php + +use GuzzleHttp\Psr7; + +$stream = Psr7\stream_for('hi'); +$fnStream = Psr7\FnStream::decorate($stream, [ + 'rewind' => function () use ($stream) { + echo 'About to rewind - '; + $stream->rewind(); + echo 'rewound!'; + } +]); + +$fnStream->rewind(); +// Outputs: About to rewind - rewound! +``` + + +## InflateStream + +`GuzzleHttp\Psr7\InflateStream` + +Uses PHP's zlib.inflate filter to inflate deflate or gzipped content. + +This stream decorator skips the first 10 bytes of the given stream to remove +the gzip header, converts the provided stream to a PHP stream resource, +then appends the zlib.inflate filter. The stream is then converted back +to a Guzzle stream resource to be used as a Guzzle stream. + + +## LazyOpenStream + +`GuzzleHttp\Psr7\LazyOpenStream` + +Lazily reads or writes to a file that is opened only after an IO operation +take place on the stream. + +```php +use GuzzleHttp\Psr7; + +$stream = new Psr7\LazyOpenStream('/path/to/file', 'r'); +// The file has not yet been opened... + +echo $stream->read(10); +// The file is opened and read from only when needed. +``` + + +## LimitStream + +`GuzzleHttp\Psr7\LimitStream` + +LimitStream can be used to read a subset or slice of an existing stream object. +This can be useful for breaking a large file into smaller pieces to be sent in +chunks (e.g. Amazon S3's multipart upload API). + +```php +use GuzzleHttp\Psr7; + +$original = Psr7\stream_for(fopen('/tmp/test.txt', 'r+')); +echo $original->getSize(); +// >>> 1048576 + +// Limit the size of the body to 1024 bytes and start reading from byte 2048 +$stream = new Psr7\LimitStream($original, 1024, 2048); +echo $stream->getSize(); +// >>> 1024 +echo $stream->tell(); +// >>> 0 +``` + + +## MultipartStream + +`GuzzleHttp\Psr7\MultipartStream` + +Stream that when read returns bytes for a streaming multipart or +multipart/form-data stream. + + +## NoSeekStream + +`GuzzleHttp\Psr7\NoSeekStream` + +NoSeekStream wraps a stream and does not allow seeking. + +```php +use GuzzleHttp\Psr7; + +$original = Psr7\stream_for('foo'); +$noSeek = new Psr7\NoSeekStream($original); + +echo $noSeek->read(3); +// foo +var_export($noSeek->isSeekable()); +// false +$noSeek->seek(0); +var_export($noSeek->read(3)); +// NULL +``` + + +## PumpStream + +`GuzzleHttp\Psr7\PumpStream` + +Provides a read only stream that pumps data from a PHP callable. + +When invoking the provided callable, the PumpStream will pass the amount of +data requested to read to the callable. The callable can choose to ignore +this value and return fewer or more bytes than requested. Any extra data +returned by the provided callable is buffered internally until drained using +the read() function of the PumpStream. The provided callable MUST return +false when there is no more data to read. + + +## Implementing stream decorators + +Creating a stream decorator is very easy thanks to the +`GuzzleHttp\Psr7\StreamDecoratorTrait`. This trait provides methods that +implement `Psr\Http\Message\StreamInterface` by proxying to an underlying +stream. Just `use` the `StreamDecoratorTrait` and implement your custom +methods. + +For example, let's say we wanted to call a specific function each time the last +byte is read from a stream. This could be implemented by overriding the +`read()` method. + +```php +use Psr\Http\Message\StreamInterface; +use GuzzleHttp\Psr7\StreamDecoratorTrait; + +class EofCallbackStream implements StreamInterface +{ + use StreamDecoratorTrait; + + private $callback; + + public function __construct(StreamInterface $stream, callable $cb) + { + $this->stream = $stream; + $this->callback = $cb; + } + + public function read($length) + { + $result = $this->stream->read($length); + + // Invoke the callback when EOF is hit. + if ($this->eof()) { + call_user_func($this->callback); + } + + return $result; + } +} +``` + +This decorator could be added to any existing stream and used like so: + +```php +use GuzzleHttp\Psr7; + +$original = Psr7\stream_for('foo'); + +$eofStream = new EofCallbackStream($original, function () { + echo 'EOF!'; +}); + +$eofStream->read(2); +$eofStream->read(1); +// echoes "EOF!" +$eofStream->seek(0); +$eofStream->read(3); +// echoes "EOF!" +``` + + +## PHP StreamWrapper + +You can use the `GuzzleHttp\Psr7\StreamWrapper` class if you need to use a +PSR-7 stream as a PHP stream resource. + +Use the `GuzzleHttp\Psr7\StreamWrapper::getResource()` method to create a PHP +stream from a PSR-7 stream. + +```php +use GuzzleHttp\Psr7\StreamWrapper; + +$stream = GuzzleHttp\Psr7\stream_for('hello!'); +$resource = StreamWrapper::getResource($stream); +echo fread($resource, 6); // outputs hello! +``` + + +# Function API + +There are various functions available under the `GuzzleHttp\Psr7` namespace. + + +## `function str` + +`function str(MessageInterface $message)` + +Returns the string representation of an HTTP message. + +```php +$request = new GuzzleHttp\Psr7\Request('GET', 'http://example.com'); +echo GuzzleHttp\Psr7\str($request); +``` + + +## `function uri_for` + +`function uri_for($uri)` + +This function accepts a string or `Psr\Http\Message\UriInterface` and returns a +UriInterface for the given value. If the value is already a `UriInterface`, it +is returned as-is. + +```php +$uri = GuzzleHttp\Psr7\uri_for('http://example.com'); +assert($uri === GuzzleHttp\Psr7\uri_for($uri)); +``` + + +## `function stream_for` + +`function stream_for($resource = '', array $options = [])` + +Create a new stream based on the input type. + +Options is an associative array that can contain the following keys: + +* - metadata: Array of custom metadata. +* - size: Size of the stream. + +This method accepts the following `$resource` types: + +- `Psr\Http\Message\StreamInterface`: Returns the value as-is. +- `string`: Creates a stream object that uses the given string as the contents. +- `resource`: Creates a stream object that wraps the given PHP stream resource. +- `Iterator`: If the provided value implements `Iterator`, then a read-only + stream object will be created that wraps the given iterable. Each time the + stream is read from, data from the iterator will fill a buffer and will be + continuously called until the buffer is equal to the requested read size. + Subsequent read calls will first read from the buffer and then call `next` + on the underlying iterator until it is exhausted. +- `object` with `__toString()`: If the object has the `__toString()` method, + the object will be cast to a string and then a stream will be returned that + uses the string value. +- `NULL`: When `null` is passed, an empty stream object is returned. +- `callable` When a callable is passed, a read-only stream object will be + created that invokes the given callable. The callable is invoked with the + number of suggested bytes to read. The callable can return any number of + bytes, but MUST return `false` when there is no more data to return. The + stream object that wraps the callable will invoke the callable until the + number of requested bytes are available. Any additional bytes will be + buffered and used in subsequent reads. + +```php +$stream = GuzzleHttp\Psr7\stream_for('foo'); +$stream = GuzzleHttp\Psr7\stream_for(fopen('/path/to/file', 'r')); + +$generator = function ($bytes) { + for ($i = 0; $i < $bytes; $i++) { + yield ' '; + } +} + +$stream = GuzzleHttp\Psr7\stream_for($generator(100)); +``` + + +## `function parse_header` + +`function parse_header($header)` + +Parse an array of header values containing ";" separated data into an array of +associative arrays representing the header key value pair data of the header. +When a parameter does not contain a value, but just contains a key, this +function will inject a key with a '' string value. + + +## `function normalize_header` + +`function normalize_header($header)` + +Converts an array of header values that may contain comma separated headers +into an array of headers with no comma separated values. + + +## `function modify_request` + +`function modify_request(RequestInterface $request, array $changes)` + +Clone and modify a request with the given changes. This method is useful for +reducing the number of clones needed to mutate a message. + +The changes can be one of: + +- method: (string) Changes the HTTP method. +- set_headers: (array) Sets the given headers. +- remove_headers: (array) Remove the given headers. +- body: (mixed) Sets the given body. +- uri: (UriInterface) Set the URI. +- query: (string) Set the query string value of the URI. +- version: (string) Set the protocol version. + + +## `function rewind_body` + +`function rewind_body(MessageInterface $message)` + +Attempts to rewind a message body and throws an exception on failure. The body +of the message will only be rewound if a call to `tell()` returns a value other +than `0`. + + +## `function try_fopen` + +`function try_fopen($filename, $mode)` + +Safely opens a PHP stream resource using a filename. + +When fopen fails, PHP normally raises a warning. This function adds an error +handler that checks for errors and throws an exception instead. + + +## `function copy_to_string` + +`function copy_to_string(StreamInterface $stream, $maxLen = -1)` + +Copy the contents of a stream into a string until the given number of bytes +have been read. + + +## `function copy_to_stream` + +`function copy_to_stream(StreamInterface $source, StreamInterface $dest, $maxLen = -1)` + +Copy the contents of a stream into another stream until the given number of +bytes have been read. + + +## `function hash` + +`function hash(StreamInterface $stream, $algo, $rawOutput = false)` + +Calculate a hash of a Stream. This method reads the entire stream to calculate +a rolling hash (based on PHP's hash_init functions). + + +## `function readline` + +`function readline(StreamInterface $stream, $maxLength = null)` + +Read a line from the stream up to the maximum allowed buffer length. + + +## `function parse_request` + +`function parse_request($message)` + +Parses a request message string into a request object. + + +## `function parse_response` + +`function parse_response($message)` + +Parses a response message string into a response object. + + +## `function parse_query` + +`function parse_query($str, $urlEncoding = true)` + +Parse a query string into an associative array. + +If multiple values are found for the same key, the value of that key value pair +will become an array. This function does not parse nested PHP style arrays into +an associative array (e.g., `foo[a]=1&foo[b]=2` will be parsed into +`['foo[a]' => '1', 'foo[b]' => '2']`). + + +## `function build_query` + +`function build_query(array $params, $encoding = PHP_QUERY_RFC3986)` + +Build a query string from an array of key value pairs. + +This function can use the return value of parse_query() to build a query string. +This function does not modify the provided keys when an array is encountered +(like http_build_query would). + + +## `function mimetype_from_filename` + +`function mimetype_from_filename($filename)` + +Determines the mimetype of a file by looking at its extension. + + +## `function mimetype_from_extension` + +`function mimetype_from_extension($extension)` + +Maps a file extensions to a mimetype. + + +# Additional URI Methods + +Aside from the standard `Psr\Http\Message\UriInterface` implementation in form of the `GuzzleHttp\Psr7\Uri` class, +this library also provides additional functionality when working with URIs as static methods. + +## URI Types + +An instance of `Psr\Http\Message\UriInterface` can either be an absolute URI or a relative reference. +An absolute URI has a scheme. A relative reference is used to express a URI relative to another URI, +the base URI. Relative references can be divided into several forms according to +[RFC 3986 Section 4.2](https://tools.ietf.org/html/rfc3986#section-4.2): + +- network-path references, e.g. `//example.com/path` +- absolute-path references, e.g. `/path` +- relative-path references, e.g. `subpath` + +The following methods can be used to identify the type of the URI. + +### `GuzzleHttp\Psr7\Uri::isAbsolute` + +`public static function isAbsolute(UriInterface $uri): bool` + +Whether the URI is absolute, i.e. it has a scheme. + +### `GuzzleHttp\Psr7\Uri::isNetworkPathReference` + +`public static function isNetworkPathReference(UriInterface $uri): bool` + +Whether the URI is a network-path reference. A relative reference that begins with two slash characters is +termed an network-path reference. + +### `GuzzleHttp\Psr7\Uri::isAbsolutePathReference` + +`public static function isAbsolutePathReference(UriInterface $uri): bool` + +Whether the URI is a absolute-path reference. A relative reference that begins with a single slash character is +termed an absolute-path reference. + +### `GuzzleHttp\Psr7\Uri::isRelativePathReference` + +`public static function isRelativePathReference(UriInterface $uri): bool` + +Whether the URI is a relative-path reference. A relative reference that does not begin with a slash character is +termed a relative-path reference. + +### `GuzzleHttp\Psr7\Uri::isSameDocumentReference` + +`public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null): bool` + +Whether the URI is a same-document reference. A same-document reference refers to a URI that is, aside from its +fragment component, identical to the base URI. When no base URI is given, only an empty URI reference +(apart from its fragment) is considered a same-document reference. + +## URI Components + +Additional methods to work with URI components. + +### `GuzzleHttp\Psr7\Uri::isDefaultPort` + +`public static function isDefaultPort(UriInterface $uri): bool` + +Whether the URI has the default port of the current scheme. `Psr\Http\Message\UriInterface::getPort` may return null +or the standard port. This method can be used independently of the implementation. + +### `GuzzleHttp\Psr7\Uri::composeComponents` + +`public static function composeComponents($scheme, $authority, $path, $query, $fragment): string` + +Composes a URI reference string from its various components according to +[RFC 3986 Section 5.3](https://tools.ietf.org/html/rfc3986#section-5.3). Usually this method does not need to be called +manually but instead is used indirectly via `Psr\Http\Message\UriInterface::__toString`. + +### `GuzzleHttp\Psr7\Uri::fromParts` + +`public static function fromParts(array $parts): UriInterface` + +Creates a URI from a hash of [`parse_url`](http://php.net/manual/en/function.parse-url.php) components. + + +### `GuzzleHttp\Psr7\Uri::withQueryValue` + +`public static function withQueryValue(UriInterface $uri, $key, $value): UriInterface` + +Creates a new URI with a specific query string value. Any existing query string values that exactly match the +provided key are removed and replaced with the given key value pair. A value of null will set the query string +key without a value, e.g. "key" instead of "key=value". + +### `GuzzleHttp\Psr7\Uri::withQueryValues` + +`public static function withQueryValues(UriInterface $uri, array $keyValueArray): UriInterface` + +Creates a new URI with multiple query string values. It has the same behavior as `withQueryValue()` but for an +associative array of key => value. + +### `GuzzleHttp\Psr7\Uri::withoutQueryValue` + +`public static function withoutQueryValue(UriInterface $uri, $key): UriInterface` + +Creates a new URI with a specific query string value removed. Any existing query string values that exactly match the +provided key are removed. + +## Reference Resolution + +`GuzzleHttp\Psr7\UriResolver` provides methods to resolve a URI reference in the context of a base URI according +to [RFC 3986 Section 5](https://tools.ietf.org/html/rfc3986#section-5). This is for example also what web browsers +do when resolving a link in a website based on the current request URI. + +### `GuzzleHttp\Psr7\UriResolver::resolve` + +`public static function resolve(UriInterface $base, UriInterface $rel): UriInterface` + +Converts the relative URI into a new URI that is resolved against the base URI. + +### `GuzzleHttp\Psr7\UriResolver::removeDotSegments` + +`public static function removeDotSegments(string $path): string` + +Removes dot segments from a path and returns the new path according to +[RFC 3986 Section 5.2.4](https://tools.ietf.org/html/rfc3986#section-5.2.4). + +### `GuzzleHttp\Psr7\UriResolver::relativize` + +`public static function relativize(UriInterface $base, UriInterface $target): UriInterface` + +Returns the target URI as a relative reference from the base URI. This method is the counterpart to resolve(): + +```php +(string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target)) +``` + +One use-case is to use the current request URI as base URI and then generate relative links in your documents +to reduce the document size or offer self-contained downloadable document archives. + +```php +$base = new Uri('http://example.com/a/b/'); +echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'. +echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'. +echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'. +echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'. +``` + +## Normalization and Comparison + +`GuzzleHttp\Psr7\UriNormalizer` provides methods to normalize and compare URIs according to +[RFC 3986 Section 6](https://tools.ietf.org/html/rfc3986#section-6). + +### `GuzzleHttp\Psr7\UriNormalizer::normalize` + +`public static function normalize(UriInterface $uri, $flags = self::PRESERVING_NORMALIZATIONS): UriInterface` + +Returns a normalized URI. The scheme and host component are already normalized to lowercase per PSR-7 UriInterface. +This methods adds additional normalizations that can be configured with the `$flags` parameter which is a bitmask +of normalizations to apply. The following normalizations are available: + +- `UriNormalizer::PRESERVING_NORMALIZATIONS` + + Default normalizations which only include the ones that preserve semantics. + +- `UriNormalizer::CAPITALIZE_PERCENT_ENCODING` + + All letters within a percent-encoding triplet (e.g., "%3A") are case-insensitive, and should be capitalized. + + Example: `http://example.org/a%c2%b1b` → `http://example.org/a%C2%B1b` + +- `UriNormalizer::DECODE_UNRESERVED_CHARACTERS` + + Decodes percent-encoded octets of unreserved characters. For consistency, percent-encoded octets in the ranges of + ALPHA (%41–%5A and %61–%7A), DIGIT (%30–%39), hyphen (%2D), period (%2E), underscore (%5F), or tilde (%7E) should + not be created by URI producers and, when found in a URI, should be decoded to their corresponding unreserved + characters by URI normalizers. + + Example: `http://example.org/%7Eusern%61me/` → `http://example.org/~username/` + +- `UriNormalizer::CONVERT_EMPTY_PATH` + + Converts the empty path to "/" for http and https URIs. + + Example: `http://example.org` → `http://example.org/` + +- `UriNormalizer::REMOVE_DEFAULT_HOST` + + Removes the default host of the given URI scheme from the URI. Only the "file" scheme defines the default host + "localhost". All of `file:/myfile`, `file:///myfile`, and `file://localhost/myfile` are equivalent according to + RFC 3986. + + Example: `file://localhost/myfile` → `file:///myfile` + +- `UriNormalizer::REMOVE_DEFAULT_PORT` + + Removes the default port of the given URI scheme from the URI. + + Example: `http://example.org:80/` → `http://example.org/` + +- `UriNormalizer::REMOVE_DOT_SEGMENTS` + + Removes unnecessary dot-segments. Dot-segments in relative-path references are not removed as it would + change the semantics of the URI reference. + + Example: `http://example.org/../a/b/../c/./d.html` → `http://example.org/a/c/d.html` + +- `UriNormalizer::REMOVE_DUPLICATE_SLASHES` + + Paths which include two or more adjacent slashes are converted to one. Webservers usually ignore duplicate slashes + and treat those URIs equivalent. But in theory those URIs do not need to be equivalent. So this normalization + may change the semantics. Encoded slashes (%2F) are not removed. + + Example: `http://example.org//foo///bar.html` → `http://example.org/foo/bar.html` + +- `UriNormalizer::SORT_QUERY_PARAMETERS` + + Sort query parameters with their values in alphabetical order. However, the order of parameters in a URI may be + significant (this is not defined by the standard). So this normalization is not safe and may change the semantics + of the URI. + + Example: `?lang=en&article=fred` → `?article=fred&lang=en` + +### `GuzzleHttp\Psr7\UriNormalizer::isEquivalent` + +`public static function isEquivalent(UriInterface $uri1, UriInterface $uri2, $normalizations = self::PRESERVING_NORMALIZATIONS): bool` + +Whether two URIs can be considered equivalent. Both URIs are normalized automatically before comparison with the given +`$normalizations` bitmask. The method also accepts relative URI references and returns true when they are equivalent. +This of course assumes they will be resolved against the same base URI. If this is not the case, determination of +equivalence or difference of relative references does not mean anything. diff --git a/vendor/guzzlehttp/psr7/composer.json b/vendor/guzzlehttp/psr7/composer.json new file mode 100644 index 00000000..2add153e --- /dev/null +++ b/vendor/guzzlehttp/psr7/composer.json @@ -0,0 +1,45 @@ +{ + "name": "guzzlehttp/psr7", + "type": "library", + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": ["request", "response", "message", "stream", "http", "uri", "url", "psr-7"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": ["src/functions_include.php"] + }, + "autoload-dev": { + "psr-4": { + "GuzzleHttp\\Tests\\Psr7\\": "tests/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + } +} diff --git a/vendor/guzzlehttp/psr7/src/AppendStream.php b/vendor/guzzlehttp/psr7/src/AppendStream.php new file mode 100644 index 00000000..472a0d61 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/AppendStream.php @@ -0,0 +1,241 @@ +addStream($stream); + } + } + + public function __toString() + { + try { + $this->rewind(); + return $this->getContents(); + } catch (\Exception $e) { + return ''; + } + } + + /** + * Add a stream to the AppendStream + * + * @param StreamInterface $stream Stream to append. Must be readable. + * + * @throws \InvalidArgumentException if the stream is not readable + */ + public function addStream(StreamInterface $stream) + { + if (!$stream->isReadable()) { + throw new \InvalidArgumentException('Each stream must be readable'); + } + + // The stream is only seekable if all streams are seekable + if (!$stream->isSeekable()) { + $this->seekable = false; + } + + $this->streams[] = $stream; + } + + public function getContents() + { + return copy_to_string($this); + } + + /** + * Closes each attached stream. + * + * {@inheritdoc} + */ + public function close() + { + $this->pos = $this->current = 0; + $this->seekable = true; + + foreach ($this->streams as $stream) { + $stream->close(); + } + + $this->streams = []; + } + + /** + * Detaches each attached stream. + * + * Returns null as it's not clear which underlying stream resource to return. + * + * {@inheritdoc} + */ + public function detach() + { + $this->pos = $this->current = 0; + $this->seekable = true; + + foreach ($this->streams as $stream) { + $stream->detach(); + } + + $this->streams = []; + } + + public function tell() + { + return $this->pos; + } + + /** + * Tries to calculate the size by adding the size of each stream. + * + * If any of the streams do not return a valid number, then the size of the + * append stream cannot be determined and null is returned. + * + * {@inheritdoc} + */ + public function getSize() + { + $size = 0; + + foreach ($this->streams as $stream) { + $s = $stream->getSize(); + if ($s === null) { + return null; + } + $size += $s; + } + + return $size; + } + + public function eof() + { + return !$this->streams || + ($this->current >= count($this->streams) - 1 && + $this->streams[$this->current]->eof()); + } + + public function rewind() + { + $this->seek(0); + } + + /** + * Attempts to seek to the given position. Only supports SEEK_SET. + * + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + if (!$this->seekable) { + throw new \RuntimeException('This AppendStream is not seekable'); + } elseif ($whence !== SEEK_SET) { + throw new \RuntimeException('The AppendStream can only seek with SEEK_SET'); + } + + $this->pos = $this->current = 0; + + // Rewind each stream + foreach ($this->streams as $i => $stream) { + try { + $stream->rewind(); + } catch (\Exception $e) { + throw new \RuntimeException('Unable to seek stream ' + . $i . ' of the AppendStream', 0, $e); + } + } + + // Seek to the actual position by reading from each stream + while ($this->pos < $offset && !$this->eof()) { + $result = $this->read(min(8096, $offset - $this->pos)); + if ($result === '') { + break; + } + } + } + + /** + * Reads from all of the appended streams until the length is met or EOF. + * + * {@inheritdoc} + */ + public function read($length) + { + $buffer = ''; + $total = count($this->streams) - 1; + $remaining = $length; + $progressToNext = false; + + while ($remaining > 0) { + + // Progress to the next stream if needed. + if ($progressToNext || $this->streams[$this->current]->eof()) { + $progressToNext = false; + if ($this->current === $total) { + break; + } + $this->current++; + } + + $result = $this->streams[$this->current]->read($remaining); + + // Using a loose comparison here to match on '', false, and null + if ($result == null) { + $progressToNext = true; + continue; + } + + $buffer .= $result; + $remaining = $length - strlen($buffer); + } + + $this->pos += strlen($buffer); + + return $buffer; + } + + public function isReadable() + { + return true; + } + + public function isWritable() + { + return false; + } + + public function isSeekable() + { + return $this->seekable; + } + + public function write($string) + { + throw new \RuntimeException('Cannot write to an AppendStream'); + } + + public function getMetadata($key = null) + { + return $key ? null : []; + } +} diff --git a/vendor/guzzlehttp/psr7/src/BufferStream.php b/vendor/guzzlehttp/psr7/src/BufferStream.php new file mode 100644 index 00000000..af4d4c22 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/BufferStream.php @@ -0,0 +1,137 @@ +hwm = $hwm; + } + + public function __toString() + { + return $this->getContents(); + } + + public function getContents() + { + $buffer = $this->buffer; + $this->buffer = ''; + + return $buffer; + } + + public function close() + { + $this->buffer = ''; + } + + public function detach() + { + $this->close(); + } + + public function getSize() + { + return strlen($this->buffer); + } + + public function isReadable() + { + return true; + } + + public function isWritable() + { + return true; + } + + public function isSeekable() + { + return false; + } + + public function rewind() + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET) + { + throw new \RuntimeException('Cannot seek a BufferStream'); + } + + public function eof() + { + return strlen($this->buffer) === 0; + } + + public function tell() + { + throw new \RuntimeException('Cannot determine the position of a BufferStream'); + } + + /** + * Reads data from the buffer. + */ + public function read($length) + { + $currentLength = strlen($this->buffer); + + if ($length >= $currentLength) { + // No need to slice the buffer because we don't have enough data. + $result = $this->buffer; + $this->buffer = ''; + } else { + // Slice up the result to provide a subset of the buffer. + $result = substr($this->buffer, 0, $length); + $this->buffer = substr($this->buffer, $length); + } + + return $result; + } + + /** + * Writes data to the buffer. + */ + public function write($string) + { + $this->buffer .= $string; + + // TODO: What should happen here? + if (strlen($this->buffer) >= $this->hwm) { + return false; + } + + return strlen($string); + } + + public function getMetadata($key = null) + { + if ($key == 'hwm') { + return $this->hwm; + } + + return $key ? null : []; + } +} diff --git a/vendor/guzzlehttp/psr7/src/CachingStream.php b/vendor/guzzlehttp/psr7/src/CachingStream.php new file mode 100644 index 00000000..ed68f086 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/CachingStream.php @@ -0,0 +1,138 @@ +remoteStream = $stream; + $this->stream = $target ?: new Stream(fopen('php://temp', 'r+')); + } + + public function getSize() + { + return max($this->stream->getSize(), $this->remoteStream->getSize()); + } + + public function rewind() + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET) + { + if ($whence == SEEK_SET) { + $byte = $offset; + } elseif ($whence == SEEK_CUR) { + $byte = $offset + $this->tell(); + } elseif ($whence == SEEK_END) { + $size = $this->remoteStream->getSize(); + if ($size === null) { + $size = $this->cacheEntireStream(); + } + $byte = $size + $offset; + } else { + throw new \InvalidArgumentException('Invalid whence'); + } + + $diff = $byte - $this->stream->getSize(); + + if ($diff > 0) { + // Read the remoteStream until we have read in at least the amount + // of bytes requested, or we reach the end of the file. + while ($diff > 0 && !$this->remoteStream->eof()) { + $this->read($diff); + $diff = $byte - $this->stream->getSize(); + } + } else { + // We can just do a normal seek since we've already seen this byte. + $this->stream->seek($byte); + } + } + + public function read($length) + { + // Perform a regular read on any previously read data from the buffer + $data = $this->stream->read($length); + $remaining = $length - strlen($data); + + // More data was requested so read from the remote stream + if ($remaining) { + // If data was written to the buffer in a position that would have + // been filled from the remote stream, then we must skip bytes on + // the remote stream to emulate overwriting bytes from that + // position. This mimics the behavior of other PHP stream wrappers. + $remoteData = $this->remoteStream->read( + $remaining + $this->skipReadBytes + ); + + if ($this->skipReadBytes) { + $len = strlen($remoteData); + $remoteData = substr($remoteData, $this->skipReadBytes); + $this->skipReadBytes = max(0, $this->skipReadBytes - $len); + } + + $data .= $remoteData; + $this->stream->write($remoteData); + } + + return $data; + } + + public function write($string) + { + // When appending to the end of the currently read stream, you'll want + // to skip bytes from being read from the remote stream to emulate + // other stream wrappers. Basically replacing bytes of data of a fixed + // length. + $overflow = (strlen($string) + $this->tell()) - $this->remoteStream->tell(); + if ($overflow > 0) { + $this->skipReadBytes += $overflow; + } + + return $this->stream->write($string); + } + + public function eof() + { + return $this->stream->eof() && $this->remoteStream->eof(); + } + + /** + * Close both the remote stream and buffer stream + */ + public function close() + { + $this->remoteStream->close() && $this->stream->close(); + } + + private function cacheEntireStream() + { + $target = new FnStream(['write' => 'strlen']); + copy_to_stream($this, $target); + + return $this->tell(); + } +} diff --git a/vendor/guzzlehttp/psr7/src/DroppingStream.php b/vendor/guzzlehttp/psr7/src/DroppingStream.php new file mode 100644 index 00000000..8935c80d --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/DroppingStream.php @@ -0,0 +1,42 @@ +stream = $stream; + $this->maxLength = $maxLength; + } + + public function write($string) + { + $diff = $this->maxLength - $this->stream->getSize(); + + // Begin returning 0 when the underlying stream is too large. + if ($diff <= 0) { + return 0; + } + + // Write the stream or a subset of the stream if needed. + if (strlen($string) < $diff) { + return $this->stream->write($string); + } + + return $this->stream->write(substr($string, 0, $diff)); + } +} diff --git a/vendor/guzzlehttp/psr7/src/FnStream.php b/vendor/guzzlehttp/psr7/src/FnStream.php new file mode 100644 index 00000000..73daea6f --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/FnStream.php @@ -0,0 +1,158 @@ +methods = $methods; + + // Create the functions on the class + foreach ($methods as $name => $fn) { + $this->{'_fn_' . $name} = $fn; + } + } + + /** + * Lazily determine which methods are not implemented. + * @throws \BadMethodCallException + */ + public function __get($name) + { + throw new \BadMethodCallException(str_replace('_fn_', '', $name) + . '() is not implemented in the FnStream'); + } + + /** + * The close method is called on the underlying stream only if possible. + */ + public function __destruct() + { + if (isset($this->_fn_close)) { + call_user_func($this->_fn_close); + } + } + + /** + * An unserialize would allow the __destruct to run when the unserialized value goes out of scope. + * @throws \LogicException + */ + public function __wakeup() + { + throw new \LogicException('FnStream should never be unserialized'); + } + + /** + * Adds custom functionality to an underlying stream by intercepting + * specific method calls. + * + * @param StreamInterface $stream Stream to decorate + * @param array $methods Hash of method name to a closure + * + * @return FnStream + */ + public static function decorate(StreamInterface $stream, array $methods) + { + // If any of the required methods were not provided, then simply + // proxy to the decorated stream. + foreach (array_diff(self::$slots, array_keys($methods)) as $diff) { + $methods[$diff] = [$stream, $diff]; + } + + return new self($methods); + } + + public function __toString() + { + return call_user_func($this->_fn___toString); + } + + public function close() + { + return call_user_func($this->_fn_close); + } + + public function detach() + { + return call_user_func($this->_fn_detach); + } + + public function getSize() + { + return call_user_func($this->_fn_getSize); + } + + public function tell() + { + return call_user_func($this->_fn_tell); + } + + public function eof() + { + return call_user_func($this->_fn_eof); + } + + public function isSeekable() + { + return call_user_func($this->_fn_isSeekable); + } + + public function rewind() + { + call_user_func($this->_fn_rewind); + } + + public function seek($offset, $whence = SEEK_SET) + { + call_user_func($this->_fn_seek, $offset, $whence); + } + + public function isWritable() + { + return call_user_func($this->_fn_isWritable); + } + + public function write($string) + { + return call_user_func($this->_fn_write, $string); + } + + public function isReadable() + { + return call_user_func($this->_fn_isReadable); + } + + public function read($length) + { + return call_user_func($this->_fn_read, $length); + } + + public function getContents() + { + return call_user_func($this->_fn_getContents); + } + + public function getMetadata($key = null) + { + return call_user_func($this->_fn_getMetadata, $key); + } +} diff --git a/vendor/guzzlehttp/psr7/src/InflateStream.php b/vendor/guzzlehttp/psr7/src/InflateStream.php new file mode 100644 index 00000000..5e4f6028 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/InflateStream.php @@ -0,0 +1,52 @@ +read(10); + $filenameHeaderLength = $this->getLengthOfPossibleFilenameHeader($stream, $header); + // Skip the header, that is 10 + length of filename + 1 (nil) bytes + $stream = new LimitStream($stream, -1, 10 + $filenameHeaderLength); + $resource = StreamWrapper::getResource($stream); + stream_filter_append($resource, 'zlib.inflate', STREAM_FILTER_READ); + $this->stream = $stream->isSeekable() ? new Stream($resource) : new NoSeekStream(new Stream($resource)); + } + + /** + * @param StreamInterface $stream + * @param $header + * @return int + */ + private function getLengthOfPossibleFilenameHeader(StreamInterface $stream, $header) + { + $filename_header_length = 0; + + if (substr(bin2hex($header), 6, 2) === '08') { + // we have a filename, read until nil + $filename_header_length = 1; + while ($stream->read(1) !== chr(0)) { + $filename_header_length++; + } + } + + return $filename_header_length; + } +} diff --git a/vendor/guzzlehttp/psr7/src/LazyOpenStream.php b/vendor/guzzlehttp/psr7/src/LazyOpenStream.php new file mode 100644 index 00000000..02cec3af --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/LazyOpenStream.php @@ -0,0 +1,39 @@ +filename = $filename; + $this->mode = $mode; + } + + /** + * Creates the underlying stream lazily when required. + * + * @return StreamInterface + */ + protected function createStream() + { + return stream_for(try_fopen($this->filename, $this->mode)); + } +} diff --git a/vendor/guzzlehttp/psr7/src/LimitStream.php b/vendor/guzzlehttp/psr7/src/LimitStream.php new file mode 100644 index 00000000..3c13d4f4 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/LimitStream.php @@ -0,0 +1,155 @@ +stream = $stream; + $this->setLimit($limit); + $this->setOffset($offset); + } + + public function eof() + { + // Always return true if the underlying stream is EOF + if ($this->stream->eof()) { + return true; + } + + // No limit and the underlying stream is not at EOF + if ($this->limit == -1) { + return false; + } + + return $this->stream->tell() >= $this->offset + $this->limit; + } + + /** + * Returns the size of the limited subset of data + * {@inheritdoc} + */ + public function getSize() + { + if (null === ($length = $this->stream->getSize())) { + return null; + } elseif ($this->limit == -1) { + return $length - $this->offset; + } else { + return min($this->limit, $length - $this->offset); + } + } + + /** + * Allow for a bounded seek on the read limited stream + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + if ($whence !== SEEK_SET || $offset < 0) { + throw new \RuntimeException(sprintf( + 'Cannot seek to offset % with whence %s', + $offset, + $whence + )); + } + + $offset += $this->offset; + + if ($this->limit !== -1) { + if ($offset > $this->offset + $this->limit) { + $offset = $this->offset + $this->limit; + } + } + + $this->stream->seek($offset); + } + + /** + * Give a relative tell() + * {@inheritdoc} + */ + public function tell() + { + return $this->stream->tell() - $this->offset; + } + + /** + * Set the offset to start limiting from + * + * @param int $offset Offset to seek to and begin byte limiting from + * + * @throws \RuntimeException if the stream cannot be seeked. + */ + public function setOffset($offset) + { + $current = $this->stream->tell(); + + if ($current !== $offset) { + // If the stream cannot seek to the offset position, then read to it + if ($this->stream->isSeekable()) { + $this->stream->seek($offset); + } elseif ($current > $offset) { + throw new \RuntimeException("Could not seek to stream offset $offset"); + } else { + $this->stream->read($offset - $current); + } + } + + $this->offset = $offset; + } + + /** + * Set the limit of bytes that the decorator allows to be read from the + * stream. + * + * @param int $limit Number of bytes to allow to be read from the stream. + * Use -1 for no limit. + */ + public function setLimit($limit) + { + $this->limit = $limit; + } + + public function read($length) + { + if ($this->limit == -1) { + return $this->stream->read($length); + } + + // Check if the current position is less than the total allowed + // bytes + original offset + $remaining = ($this->offset + $this->limit) - $this->stream->tell(); + if ($remaining > 0) { + // Only return the amount of requested data, ensuring that the byte + // limit is not exceeded + return $this->stream->read(min($remaining, $length)); + } + + return ''; + } +} diff --git a/vendor/guzzlehttp/psr7/src/MessageTrait.php b/vendor/guzzlehttp/psr7/src/MessageTrait.php new file mode 100644 index 00000000..1e4da649 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/MessageTrait.php @@ -0,0 +1,183 @@ + array of values */ + private $headers = []; + + /** @var array Map of lowercase header name => original name at registration */ + private $headerNames = []; + + /** @var string */ + private $protocol = '1.1'; + + /** @var StreamInterface */ + private $stream; + + public function getProtocolVersion() + { + return $this->protocol; + } + + public function withProtocolVersion($version) + { + if ($this->protocol === $version) { + return $this; + } + + $new = clone $this; + $new->protocol = $version; + return $new; + } + + public function getHeaders() + { + return $this->headers; + } + + public function hasHeader($header) + { + return isset($this->headerNames[strtolower($header)]); + } + + public function getHeader($header) + { + $header = strtolower($header); + + if (!isset($this->headerNames[$header])) { + return []; + } + + $header = $this->headerNames[$header]; + + return $this->headers[$header]; + } + + public function getHeaderLine($header) + { + return implode(', ', $this->getHeader($header)); + } + + public function withHeader($header, $value) + { + if (!is_array($value)) { + $value = [$value]; + } + + $value = $this->trimHeaderValues($value); + $normalized = strtolower($header); + + $new = clone $this; + if (isset($new->headerNames[$normalized])) { + unset($new->headers[$new->headerNames[$normalized]]); + } + $new->headerNames[$normalized] = $header; + $new->headers[$header] = $value; + + return $new; + } + + public function withAddedHeader($header, $value) + { + if (!is_array($value)) { + $value = [$value]; + } + + $value = $this->trimHeaderValues($value); + $normalized = strtolower($header); + + $new = clone $this; + if (isset($new->headerNames[$normalized])) { + $header = $this->headerNames[$normalized]; + $new->headers[$header] = array_merge($this->headers[$header], $value); + } else { + $new->headerNames[$normalized] = $header; + $new->headers[$header] = $value; + } + + return $new; + } + + public function withoutHeader($header) + { + $normalized = strtolower($header); + + if (!isset($this->headerNames[$normalized])) { + return $this; + } + + $header = $this->headerNames[$normalized]; + + $new = clone $this; + unset($new->headers[$header], $new->headerNames[$normalized]); + + return $new; + } + + public function getBody() + { + if (!$this->stream) { + $this->stream = stream_for(''); + } + + return $this->stream; + } + + public function withBody(StreamInterface $body) + { + if ($body === $this->stream) { + return $this; + } + + $new = clone $this; + $new->stream = $body; + return $new; + } + + private function setHeaders(array $headers) + { + $this->headerNames = $this->headers = []; + foreach ($headers as $header => $value) { + if (!is_array($value)) { + $value = [$value]; + } + + $value = $this->trimHeaderValues($value); + $normalized = strtolower($header); + if (isset($this->headerNames[$normalized])) { + $header = $this->headerNames[$normalized]; + $this->headers[$header] = array_merge($this->headers[$header], $value); + } else { + $this->headerNames[$normalized] = $header; + $this->headers[$header] = $value; + } + } + } + + /** + * Trims whitespace from the header values. + * + * Spaces and tabs ought to be excluded by parsers when extracting the field value from a header field. + * + * header-field = field-name ":" OWS field-value OWS + * OWS = *( SP / HTAB ) + * + * @param string[] $values Header values + * + * @return string[] Trimmed header values + * + * @see https://tools.ietf.org/html/rfc7230#section-3.2.4 + */ + private function trimHeaderValues(array $values) + { + return array_map(function ($value) { + return trim($value, " \t"); + }, $values); + } +} diff --git a/vendor/guzzlehttp/psr7/src/MultipartStream.php b/vendor/guzzlehttp/psr7/src/MultipartStream.php new file mode 100644 index 00000000..c0fd584f --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/MultipartStream.php @@ -0,0 +1,153 @@ +boundary = $boundary ?: sha1(uniqid('', true)); + $this->stream = $this->createStream($elements); + } + + /** + * Get the boundary + * + * @return string + */ + public function getBoundary() + { + return $this->boundary; + } + + public function isWritable() + { + return false; + } + + /** + * Get the headers needed before transferring the content of a POST file + */ + private function getHeaders(array $headers) + { + $str = ''; + foreach ($headers as $key => $value) { + $str .= "{$key}: {$value}\r\n"; + } + + return "--{$this->boundary}\r\n" . trim($str) . "\r\n\r\n"; + } + + /** + * Create the aggregate stream that will be used to upload the POST data + */ + protected function createStream(array $elements) + { + $stream = new AppendStream(); + + foreach ($elements as $element) { + $this->addElement($stream, $element); + } + + // Add the trailing boundary with CRLF + $stream->addStream(stream_for("--{$this->boundary}--\r\n")); + + return $stream; + } + + private function addElement(AppendStream $stream, array $element) + { + foreach (['contents', 'name'] as $key) { + if (!array_key_exists($key, $element)) { + throw new \InvalidArgumentException("A '{$key}' key is required"); + } + } + + $element['contents'] = stream_for($element['contents']); + + if (empty($element['filename'])) { + $uri = $element['contents']->getMetadata('uri'); + if (substr($uri, 0, 6) !== 'php://') { + $element['filename'] = $uri; + } + } + + list($body, $headers) = $this->createElement( + $element['name'], + $element['contents'], + isset($element['filename']) ? $element['filename'] : null, + isset($element['headers']) ? $element['headers'] : [] + ); + + $stream->addStream(stream_for($this->getHeaders($headers))); + $stream->addStream($body); + $stream->addStream(stream_for("\r\n")); + } + + /** + * @return array + */ + private function createElement($name, StreamInterface $stream, $filename, array $headers) + { + // Set a default content-disposition header if one was no provided + $disposition = $this->getHeader($headers, 'content-disposition'); + if (!$disposition) { + $headers['Content-Disposition'] = ($filename === '0' || $filename) + ? sprintf('form-data; name="%s"; filename="%s"', + $name, + basename($filename)) + : "form-data; name=\"{$name}\""; + } + + // Set a default content-length header if one was no provided + $length = $this->getHeader($headers, 'content-length'); + if (!$length) { + if ($length = $stream->getSize()) { + $headers['Content-Length'] = (string) $length; + } + } + + // Set a default Content-Type if one was not supplied + $type = $this->getHeader($headers, 'content-type'); + if (!$type && ($filename === '0' || $filename)) { + if ($type = mimetype_from_filename($filename)) { + $headers['Content-Type'] = $type; + } + } + + return [$stream, $headers]; + } + + private function getHeader(array $headers, $key) + { + $lowercaseHeader = strtolower($key); + foreach ($headers as $k => $v) { + if (strtolower($k) === $lowercaseHeader) { + return $v; + } + } + + return null; + } +} diff --git a/vendor/guzzlehttp/psr7/src/NoSeekStream.php b/vendor/guzzlehttp/psr7/src/NoSeekStream.php new file mode 100644 index 00000000..23322180 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/NoSeekStream.php @@ -0,0 +1,22 @@ +source = $source; + $this->size = isset($options['size']) ? $options['size'] : null; + $this->metadata = isset($options['metadata']) ? $options['metadata'] : []; + $this->buffer = new BufferStream(); + } + + public function __toString() + { + try { + return copy_to_string($this); + } catch (\Exception $e) { + return ''; + } + } + + public function close() + { + $this->detach(); + } + + public function detach() + { + $this->tellPos = false; + $this->source = null; + } + + public function getSize() + { + return $this->size; + } + + public function tell() + { + return $this->tellPos; + } + + public function eof() + { + return !$this->source; + } + + public function isSeekable() + { + return false; + } + + public function rewind() + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET) + { + throw new \RuntimeException('Cannot seek a PumpStream'); + } + + public function isWritable() + { + return false; + } + + public function write($string) + { + throw new \RuntimeException('Cannot write to a PumpStream'); + } + + public function isReadable() + { + return true; + } + + public function read($length) + { + $data = $this->buffer->read($length); + $readLen = strlen($data); + $this->tellPos += $readLen; + $remaining = $length - $readLen; + + if ($remaining) { + $this->pump($remaining); + $data .= $this->buffer->read($remaining); + $this->tellPos += strlen($data) - $readLen; + } + + return $data; + } + + public function getContents() + { + $result = ''; + while (!$this->eof()) { + $result .= $this->read(1000000); + } + + return $result; + } + + public function getMetadata($key = null) + { + if (!$key) { + return $this->metadata; + } + + return isset($this->metadata[$key]) ? $this->metadata[$key] : null; + } + + private function pump($length) + { + if ($this->source) { + do { + $data = call_user_func($this->source, $length); + if ($data === false || $data === null) { + $this->source = null; + return; + } + $this->buffer->write($data); + $length -= strlen($data); + } while ($length > 0); + } + } +} diff --git a/vendor/guzzlehttp/psr7/src/Request.php b/vendor/guzzlehttp/psr7/src/Request.php new file mode 100644 index 00000000..00066424 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/Request.php @@ -0,0 +1,142 @@ +method = strtoupper($method); + $this->uri = $uri; + $this->setHeaders($headers); + $this->protocol = $version; + + if (!isset($this->headerNames['host'])) { + $this->updateHostFromUri(); + } + + if ($body !== '' && $body !== null) { + $this->stream = stream_for($body); + } + } + + public function getRequestTarget() + { + if ($this->requestTarget !== null) { + return $this->requestTarget; + } + + $target = $this->uri->getPath(); + if ($target == '') { + $target = '/'; + } + if ($this->uri->getQuery() != '') { + $target .= '?' . $this->uri->getQuery(); + } + + return $target; + } + + public function withRequestTarget($requestTarget) + { + if (preg_match('#\s#', $requestTarget)) { + throw new InvalidArgumentException( + 'Invalid request target provided; cannot contain whitespace' + ); + } + + $new = clone $this; + $new->requestTarget = $requestTarget; + return $new; + } + + public function getMethod() + { + return $this->method; + } + + public function withMethod($method) + { + $new = clone $this; + $new->method = strtoupper($method); + return $new; + } + + public function getUri() + { + return $this->uri; + } + + public function withUri(UriInterface $uri, $preserveHost = false) + { + if ($uri === $this->uri) { + return $this; + } + + $new = clone $this; + $new->uri = $uri; + + if (!$preserveHost || !isset($this->headerNames['host'])) { + $new->updateHostFromUri(); + } + + return $new; + } + + private function updateHostFromUri() + { + $host = $this->uri->getHost(); + + if ($host == '') { + return; + } + + if (($port = $this->uri->getPort()) !== null) { + $host .= ':' . $port; + } + + if (isset($this->headerNames['host'])) { + $header = $this->headerNames['host']; + } else { + $header = 'Host'; + $this->headerNames['host'] = 'Host'; + } + // Ensure Host is the first header. + // See: http://tools.ietf.org/html/rfc7230#section-5.4 + $this->headers = [$header => [$host]] + $this->headers; + } +} diff --git a/vendor/guzzlehttp/psr7/src/Response.php b/vendor/guzzlehttp/psr7/src/Response.php new file mode 100644 index 00000000..6e72c06b --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/Response.php @@ -0,0 +1,136 @@ + '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', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'Switch Proxy', + 307 => 'Temporary 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 Time-out', + 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', + 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', + 451 => 'Unavailable For Legal Reasons', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'HTTP Version not supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 511 => 'Network Authentication Required', + ]; + + /** @var string */ + private $reasonPhrase = ''; + + /** @var int */ + private $statusCode = 200; + + /** + * @param int $status Status code + * @param array $headers Response headers + * @param string|null|resource|StreamInterface $body Response body + * @param string $version Protocol version + * @param string|null $reason Reason phrase (when empty a default will be used based on the status code) + */ + public function __construct( + $status = 200, + array $headers = [], + $body = null, + $version = '1.1', + $reason = null + ) { + if (filter_var($status, FILTER_VALIDATE_INT) === false) { + throw new \InvalidArgumentException('Status code must be an integer value.'); + } + + $this->statusCode = (int) $status; + + if ($body !== '' && $body !== null) { + $this->stream = stream_for($body); + } + + $this->setHeaders($headers); + if ($reason == '' && isset(self::$phrases[$this->statusCode])) { + $this->reasonPhrase = self::$phrases[$this->statusCode]; + } else { + $this->reasonPhrase = (string) $reason; + } + + $this->protocol = $version; + } + + public function getStatusCode() + { + return $this->statusCode; + } + + public function getReasonPhrase() + { + return $this->reasonPhrase; + } + + public function withStatus($code, $reasonPhrase = '') + { + $new = clone $this; + $new->statusCode = (int) $code; + if ($reasonPhrase == '' && isset(self::$phrases[$new->statusCode])) { + $reasonPhrase = self::$phrases[$new->statusCode]; + } + $new->reasonPhrase = $reasonPhrase; + return $new; + } +} diff --git a/vendor/guzzlehttp/psr7/src/Rfc7230.php b/vendor/guzzlehttp/psr7/src/Rfc7230.php new file mode 100644 index 00000000..505e4742 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/Rfc7230.php @@ -0,0 +1,18 @@ +@,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m"; + const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)"; +} diff --git a/vendor/guzzlehttp/psr7/src/ServerRequest.php b/vendor/guzzlehttp/psr7/src/ServerRequest.php new file mode 100644 index 00000000..99f453a5 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/ServerRequest.php @@ -0,0 +1,376 @@ +serverParams = $serverParams; + + parent::__construct($method, $uri, $headers, $body, $version); + } + + /** + * Return an UploadedFile instance array. + * + * @param array $files A array which respect $_FILES structure + * @throws InvalidArgumentException for unrecognized values + * @return array + */ + public static function normalizeFiles(array $files) + { + $normalized = []; + + foreach ($files as $key => $value) { + if ($value instanceof UploadedFileInterface) { + $normalized[$key] = $value; + } elseif (is_array($value) && isset($value['tmp_name'])) { + $normalized[$key] = self::createUploadedFileFromSpec($value); + } elseif (is_array($value)) { + $normalized[$key] = self::normalizeFiles($value); + continue; + } else { + throw new InvalidArgumentException('Invalid value in files specification'); + } + } + + return $normalized; + } + + /** + * Create and return an UploadedFile instance from a $_FILES specification. + * + * If the specification represents an array of values, this method will + * delegate to normalizeNestedFileSpec() and return that return value. + * + * @param array $value $_FILES struct + * @return array|UploadedFileInterface + */ + private static function createUploadedFileFromSpec(array $value) + { + if (is_array($value['tmp_name'])) { + return self::normalizeNestedFileSpec($value); + } + + return new UploadedFile( + $value['tmp_name'], + (int) $value['size'], + (int) $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 + * @return UploadedFileInterface[] + */ + private static function normalizeNestedFileSpec(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] = self::createUploadedFileFromSpec($spec); + } + + return $normalizedFiles; + } + + /** + * Return a ServerRequest populated with superglobals: + * $_GET + * $_POST + * $_COOKIE + * $_FILES + * $_SERVER + * + * @return ServerRequestInterface + */ + public static function fromGlobals() + { + $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + $headers = getallheaders(); + $uri = self::getUriFromGlobals(); + $body = new LazyOpenStream('php://input', 'r+'); + $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1'; + + $serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER); + + return $serverRequest + ->withCookieParams($_COOKIE) + ->withQueryParams($_GET) + ->withParsedBody($_POST) + ->withUploadedFiles(self::normalizeFiles($_FILES)); + } + + private static function extractHostAndPortFromAuthority($authority) + { + $uri = 'http://'.$authority; + $parts = parse_url($uri); + if (false === $parts) { + return [null, null]; + } + + $host = isset($parts['host']) ? $parts['host'] : null; + $port = isset($parts['port']) ? $parts['port'] : null; + + return [$host, $port]; + } + + /** + * Get a Uri populated with values from $_SERVER. + * + * @return UriInterface + */ + public static function getUriFromGlobals() + { + $uri = new Uri(''); + + $uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http'); + + $hasPort = false; + if (isset($_SERVER['HTTP_HOST'])) { + list($host, $port) = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']); + if ($host !== null) { + $uri = $uri->withHost($host); + } + + if ($port !== null) { + $hasPort = true; + $uri = $uri->withPort($port); + } + } elseif (isset($_SERVER['SERVER_NAME'])) { + $uri = $uri->withHost($_SERVER['SERVER_NAME']); + } elseif (isset($_SERVER['SERVER_ADDR'])) { + $uri = $uri->withHost($_SERVER['SERVER_ADDR']); + } + + if (!$hasPort && isset($_SERVER['SERVER_PORT'])) { + $uri = $uri->withPort($_SERVER['SERVER_PORT']); + } + + $hasQuery = false; + if (isset($_SERVER['REQUEST_URI'])) { + $requestUriParts = explode('?', $_SERVER['REQUEST_URI'], 2); + $uri = $uri->withPath($requestUriParts[0]); + if (isset($requestUriParts[1])) { + $hasQuery = true; + $uri = $uri->withQuery($requestUriParts[1]); + } + } + + if (!$hasQuery && isset($_SERVER['QUERY_STRING'])) { + $uri = $uri->withQuery($_SERVER['QUERY_STRING']); + } + + return $uri; + } + + + /** + * {@inheritdoc} + */ + public function getServerParams() + { + return $this->serverParams; + } + + /** + * {@inheritdoc} + */ + public function getUploadedFiles() + { + return $this->uploadedFiles; + } + + /** + * {@inheritdoc} + */ + public function withUploadedFiles(array $uploadedFiles) + { + $new = clone $this; + $new->uploadedFiles = $uploadedFiles; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getCookieParams() + { + return $this->cookieParams; + } + + /** + * {@inheritdoc} + */ + public function withCookieParams(array $cookies) + { + $new = clone $this; + $new->cookieParams = $cookies; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getQueryParams() + { + return $this->queryParams; + } + + /** + * {@inheritdoc} + */ + public function withQueryParams(array $query) + { + $new = clone $this; + $new->queryParams = $query; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getParsedBody() + { + return $this->parsedBody; + } + + /** + * {@inheritdoc} + */ + public function withParsedBody($data) + { + $new = clone $this; + $new->parsedBody = $data; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getAttributes() + { + return $this->attributes; + } + + /** + * {@inheritdoc} + */ + public function getAttribute($attribute, $default = null) + { + if (false === array_key_exists($attribute, $this->attributes)) { + return $default; + } + + return $this->attributes[$attribute]; + } + + /** + * {@inheritdoc} + */ + public function withAttribute($attribute, $value) + { + $new = clone $this; + $new->attributes[$attribute] = $value; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function withoutAttribute($attribute) + { + if (false === array_key_exists($attribute, $this->attributes)) { + return $this; + } + + $new = clone $this; + unset($new->attributes[$attribute]); + + return $new; + } +} diff --git a/vendor/guzzlehttp/psr7/src/Stream.php b/vendor/guzzlehttp/psr7/src/Stream.php new file mode 100644 index 00000000..111795eb --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/Stream.php @@ -0,0 +1,270 @@ + [ + 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true, + 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true, + 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true, + 'x+t' => true, 'c+t' => true, 'a+' => true, 'rb+' => true, + ], + 'write' => [ + 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true, + 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true, 'rb+' => true, + 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true, + 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true + ] + ]; + + /** + * This constructor accepts an associative array of options. + * + * - size: (int) If a read stream would otherwise have an indeterminate + * size, but the size is known due to foreknowledge, then you can + * provide that size, in bytes. + * - metadata: (array) Any additional metadata to return when the metadata + * of the stream is accessed. + * + * @param resource $stream Stream resource to wrap. + * @param array $options Associative array of options. + * + * @throws \InvalidArgumentException if the stream is not a stream resource + */ + public function __construct($stream, $options = []) + { + if (!is_resource($stream)) { + throw new \InvalidArgumentException('Stream must be a resource'); + } + + if (isset($options['size'])) { + $this->size = $options['size']; + } + + $this->customMetadata = isset($options['metadata']) + ? $options['metadata'] + : []; + + $this->stream = $stream; + $meta = stream_get_meta_data($this->stream); + $this->seekable = $meta['seekable']; + $this->readable = isset(self::$readWriteHash['read'][$meta['mode']]); + $this->writable = isset(self::$readWriteHash['write'][$meta['mode']]); + $this->uri = $this->getMetadata('uri'); + } + + /** + * Closes the stream when the destructed + */ + public function __destruct() + { + $this->close(); + } + + public function __toString() + { + try { + $this->seek(0); + return (string) stream_get_contents($this->stream); + } catch (\Exception $e) { + return ''; + } + } + + public function getContents() + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + $contents = stream_get_contents($this->stream); + + if ($contents === false) { + throw new \RuntimeException('Unable to read stream contents'); + } + + return $contents; + } + + public function close() + { + if (isset($this->stream)) { + if (is_resource($this->stream)) { + fclose($this->stream); + } + $this->detach(); + } + } + + public function detach() + { + if (!isset($this->stream)) { + return null; + } + + $result = $this->stream; + unset($this->stream); + $this->size = $this->uri = null; + $this->readable = $this->writable = $this->seekable = false; + + return $result; + } + + public function getSize() + { + if ($this->size !== null) { + return $this->size; + } + + if (!isset($this->stream)) { + return null; + } + + // Clear the stat cache if the stream has a URI + if ($this->uri) { + clearstatcache(true, $this->uri); + } + + $stats = fstat($this->stream); + if (isset($stats['size'])) { + $this->size = $stats['size']; + return $this->size; + } + + return null; + } + + public function isReadable() + { + return $this->readable; + } + + public function isWritable() + { + return $this->writable; + } + + public function isSeekable() + { + return $this->seekable; + } + + public function eof() + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + return feof($this->stream); + } + + public function tell() + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + $result = ftell($this->stream); + + if ($result === false) { + throw new \RuntimeException('Unable to determine stream position'); + } + + return $result; + } + + public function rewind() + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET) + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + if (!$this->seekable) { + throw new \RuntimeException('Stream is not seekable'); + } + if (fseek($this->stream, $offset, $whence) === -1) { + throw new \RuntimeException('Unable to seek to stream position ' + . $offset . ' with whence ' . var_export($whence, true)); + } + } + + public function read($length) + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + if (!$this->readable) { + throw new \RuntimeException('Cannot read from non-readable stream'); + } + if ($length < 0) { + throw new \RuntimeException('Length parameter cannot be negative'); + } + + if (0 === $length) { + return ''; + } + + $string = fread($this->stream, $length); + if (false === $string) { + throw new \RuntimeException('Unable to read from stream'); + } + + return $string; + } + + public function write($string) + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + if (!$this->writable) { + throw new \RuntimeException('Cannot write to a non-writable stream'); + } + + // We can't know the size after writing anything + $this->size = null; + $result = fwrite($this->stream, $string); + + if ($result === false) { + throw new \RuntimeException('Unable to write to stream'); + } + + return $result; + } + + public function getMetadata($key = null) + { + if (!isset($this->stream)) { + return $key ? null : []; + } elseif (!$key) { + return $this->customMetadata + stream_get_meta_data($this->stream); + } elseif (isset($this->customMetadata[$key])) { + return $this->customMetadata[$key]; + } + + $meta = stream_get_meta_data($this->stream); + + return isset($meta[$key]) ? $meta[$key] : null; + } +} diff --git a/vendor/guzzlehttp/psr7/src/StreamDecoratorTrait.php b/vendor/guzzlehttp/psr7/src/StreamDecoratorTrait.php new file mode 100644 index 00000000..daec6f52 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/StreamDecoratorTrait.php @@ -0,0 +1,149 @@ +stream = $stream; + } + + /** + * Magic method used to create a new stream if streams are not added in + * the constructor of a decorator (e.g., LazyOpenStream). + * + * @param string $name Name of the property (allows "stream" only). + * + * @return StreamInterface + */ + public function __get($name) + { + if ($name == 'stream') { + $this->stream = $this->createStream(); + return $this->stream; + } + + throw new \UnexpectedValueException("$name not found on class"); + } + + public function __toString() + { + try { + if ($this->isSeekable()) { + $this->seek(0); + } + return $this->getContents(); + } catch (\Exception $e) { + // Really, PHP? https://bugs.php.net/bug.php?id=53648 + trigger_error('StreamDecorator::__toString exception: ' + . (string) $e, E_USER_ERROR); + return ''; + } + } + + public function getContents() + { + return copy_to_string($this); + } + + /** + * Allow decorators to implement custom methods + * + * @param string $method Missing method name + * @param array $args Method arguments + * + * @return mixed + */ + public function __call($method, array $args) + { + $result = call_user_func_array([$this->stream, $method], $args); + + // Always return the wrapped object if the result is a return $this + return $result === $this->stream ? $this : $result; + } + + public function close() + { + $this->stream->close(); + } + + public function getMetadata($key = null) + { + return $this->stream->getMetadata($key); + } + + public function detach() + { + return $this->stream->detach(); + } + + public function getSize() + { + return $this->stream->getSize(); + } + + public function eof() + { + return $this->stream->eof(); + } + + public function tell() + { + return $this->stream->tell(); + } + + public function isReadable() + { + return $this->stream->isReadable(); + } + + public function isWritable() + { + return $this->stream->isWritable(); + } + + public function isSeekable() + { + return $this->stream->isSeekable(); + } + + public function rewind() + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET) + { + $this->stream->seek($offset, $whence); + } + + public function read($length) + { + return $this->stream->read($length); + } + + public function write($string) + { + return $this->stream->write($string); + } + + /** + * Implement in subclasses to dynamically create streams when requested. + * + * @return StreamInterface + * @throws \BadMethodCallException + */ + protected function createStream() + { + throw new \BadMethodCallException('Not implemented'); + } +} diff --git a/vendor/guzzlehttp/psr7/src/StreamWrapper.php b/vendor/guzzlehttp/psr7/src/StreamWrapper.php new file mode 100644 index 00000000..0f3a2856 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/StreamWrapper.php @@ -0,0 +1,161 @@ +isReadable()) { + $mode = $stream->isWritable() ? 'r+' : 'r'; + } elseif ($stream->isWritable()) { + $mode = 'w'; + } else { + throw new \InvalidArgumentException('The stream must be readable, ' + . 'writable, or both.'); + } + + return fopen('guzzle://stream', $mode, null, self::createStreamContext($stream)); + } + + /** + * Creates a stream context that can be used to open a stream as a php stream resource. + * + * @param StreamInterface $stream + * + * @return resource + */ + public static function createStreamContext(StreamInterface $stream) + { + return stream_context_create([ + 'guzzle' => ['stream' => $stream] + ]); + } + + /** + * Registers the stream wrapper if needed + */ + public static function register() + { + if (!in_array('guzzle', stream_get_wrappers())) { + stream_wrapper_register('guzzle', __CLASS__); + } + } + + public function stream_open($path, $mode, $options, &$opened_path) + { + $options = stream_context_get_options($this->context); + + if (!isset($options['guzzle']['stream'])) { + return false; + } + + $this->mode = $mode; + $this->stream = $options['guzzle']['stream']; + + return true; + } + + public function stream_read($count) + { + return $this->stream->read($count); + } + + public function stream_write($data) + { + return (int) $this->stream->write($data); + } + + public function stream_tell() + { + return $this->stream->tell(); + } + + public function stream_eof() + { + return $this->stream->eof(); + } + + public function stream_seek($offset, $whence) + { + $this->stream->seek($offset, $whence); + + return true; + } + + public function stream_cast($cast_as) + { + $stream = clone($this->stream); + + return $stream->detach(); + } + + public function stream_stat() + { + static $modeMap = [ + 'r' => 33060, + 'rb' => 33060, + 'r+' => 33206, + 'w' => 33188, + 'wb' => 33188 + ]; + + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => $modeMap[$this->mode], + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => $this->stream->getSize() ?: 0, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0 + ]; + } + + public function url_stat($path, $flags) + { + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => 0, + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => 0, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0 + ]; + } +} diff --git a/vendor/guzzlehttp/psr7/src/UploadedFile.php b/vendor/guzzlehttp/psr7/src/UploadedFile.php new file mode 100644 index 00000000..e62bd5c8 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/UploadedFile.php @@ -0,0 +1,316 @@ +setError($errorStatus); + $this->setSize($size); + $this->setClientFilename($clientFilename); + $this->setClientMediaType($clientMediaType); + + if ($this->isOk()) { + $this->setStreamOrFile($streamOrFile); + } + } + + /** + * Depending on the value set file or stream variable + * + * @param mixed $streamOrFile + * @throws InvalidArgumentException + */ + private function setStreamOrFile($streamOrFile) + { + if (is_string($streamOrFile)) { + $this->file = $streamOrFile; + } elseif (is_resource($streamOrFile)) { + $this->stream = new Stream($streamOrFile); + } elseif ($streamOrFile instanceof StreamInterface) { + $this->stream = $streamOrFile; + } else { + throw new InvalidArgumentException( + 'Invalid stream or file provided for UploadedFile' + ); + } + } + + /** + * @param int $error + * @throws InvalidArgumentException + */ + private function setError($error) + { + if (false === is_int($error)) { + throw new InvalidArgumentException( + 'Upload file error status must be an integer' + ); + } + + if (false === in_array($error, UploadedFile::$errors)) { + throw new InvalidArgumentException( + 'Invalid error status for UploadedFile' + ); + } + + $this->error = $error; + } + + /** + * @param int $size + * @throws InvalidArgumentException + */ + private function setSize($size) + { + if (false === is_int($size)) { + throw new InvalidArgumentException( + 'Upload file size must be an integer' + ); + } + + $this->size = $size; + } + + /** + * @param mixed $param + * @return boolean + */ + private function isStringOrNull($param) + { + return in_array(gettype($param), ['string', 'NULL']); + } + + /** + * @param mixed $param + * @return boolean + */ + private function isStringNotEmpty($param) + { + return is_string($param) && false === empty($param); + } + + /** + * @param string|null $clientFilename + * @throws InvalidArgumentException + */ + private function setClientFilename($clientFilename) + { + if (false === $this->isStringOrNull($clientFilename)) { + throw new InvalidArgumentException( + 'Upload file client filename must be a string or null' + ); + } + + $this->clientFilename = $clientFilename; + } + + /** + * @param string|null $clientMediaType + * @throws InvalidArgumentException + */ + private function setClientMediaType($clientMediaType) + { + if (false === $this->isStringOrNull($clientMediaType)) { + throw new InvalidArgumentException( + 'Upload file client media type must be a string or null' + ); + } + + $this->clientMediaType = $clientMediaType; + } + + /** + * Return true if there is no upload error + * + * @return boolean + */ + private function isOk() + { + return $this->error === UPLOAD_ERR_OK; + } + + /** + * @return boolean + */ + public function isMoved() + { + return $this->moved; + } + + /** + * @throws RuntimeException if is moved or not ok + */ + private function validateActive() + { + if (false === $this->isOk()) { + throw new RuntimeException('Cannot retrieve stream due to upload error'); + } + + if ($this->isMoved()) { + throw new RuntimeException('Cannot retrieve stream after it has already been moved'); + } + } + + /** + * {@inheritdoc} + * @throws RuntimeException if the upload was not successful. + */ + public function getStream() + { + $this->validateActive(); + + if ($this->stream instanceof StreamInterface) { + return $this->stream; + } + + return new LazyOpenStream($this->file, 'r+'); + } + + /** + * {@inheritdoc} + * + * @see http://php.net/is_uploaded_file + * @see http://php.net/move_uploaded_file + * @param string $targetPath Path to which to move the uploaded file. + * @throws RuntimeException if the upload was not successful. + * @throws InvalidArgumentException if the $path specified is invalid. + * @throws RuntimeException on any error during the move operation, or on + * the second or subsequent call to the method. + */ + public function moveTo($targetPath) + { + $this->validateActive(); + + if (false === $this->isStringNotEmpty($targetPath)) { + throw new InvalidArgumentException( + 'Invalid path provided for move operation; must be a non-empty string' + ); + } + + if ($this->file) { + $this->moved = php_sapi_name() == 'cli' + ? rename($this->file, $targetPath) + : move_uploaded_file($this->file, $targetPath); + } else { + copy_to_stream( + $this->getStream(), + new LazyOpenStream($targetPath, 'w') + ); + + $this->moved = true; + } + + if (false === $this->moved) { + throw new RuntimeException( + sprintf('Uploaded file could not be moved to %s', $targetPath) + ); + } + } + + /** + * {@inheritdoc} + * + * @return int|null The file size in bytes or null if unknown. + */ + public function getSize() + { + return $this->size; + } + + /** + * {@inheritdoc} + * + * @see http://php.net/manual/en/features.file-upload.errors.php + * @return int One of PHP's UPLOAD_ERR_XXX constants. + */ + public function getError() + { + return $this->error; + } + + /** + * {@inheritdoc} + * + * @return string|null The filename sent by the client or null if none + * was provided. + */ + public function getClientFilename() + { + return $this->clientFilename; + } + + /** + * {@inheritdoc} + */ + public function getClientMediaType() + { + return $this->clientMediaType; + } +} diff --git a/vendor/guzzlehttp/psr7/src/Uri.php b/vendor/guzzlehttp/psr7/src/Uri.php new file mode 100644 index 00000000..36219568 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/Uri.php @@ -0,0 +1,738 @@ + 80, + 'https' => 443, + 'ftp' => 21, + 'gopher' => 70, + 'nntp' => 119, + 'news' => 119, + 'telnet' => 23, + 'tn3270' => 23, + 'imap' => 143, + 'pop' => 110, + 'ldap' => 389, + ]; + + private static $charUnreserved = 'a-zA-Z0-9_\-\.~'; + private static $charSubDelims = '!\$&\'\(\)\*\+,;='; + private static $replaceQuery = ['=' => '%3D', '&' => '%26']; + + /** @var string Uri scheme. */ + private $scheme = ''; + + /** @var string Uri user info. */ + private $userInfo = ''; + + /** @var string Uri host. */ + private $host = ''; + + /** @var int|null Uri port. */ + private $port; + + /** @var string Uri path. */ + private $path = ''; + + /** @var string Uri query string. */ + private $query = ''; + + /** @var string Uri fragment. */ + private $fragment = ''; + + /** + * @param string $uri URI to parse + */ + public function __construct($uri = '') + { + // weak type check to also accept null until we can add scalar type hints + if ($uri != '') { + $parts = parse_url($uri); + if ($parts === false) { + throw new \InvalidArgumentException("Unable to parse URI: $uri"); + } + $this->applyParts($parts); + } + } + + public function __toString() + { + return self::composeComponents( + $this->scheme, + $this->getAuthority(), + $this->path, + $this->query, + $this->fragment + ); + } + + /** + * Composes a URI reference string from its various components. + * + * Usually this method does not need to be called manually but instead is used indirectly via + * `Psr\Http\Message\UriInterface::__toString`. + * + * PSR-7 UriInterface treats an empty component the same as a missing component as + * getQuery(), getFragment() etc. always return a string. This explains the slight + * difference to RFC 3986 Section 5.3. + * + * Another adjustment is that the authority separator is added even when the authority is missing/empty + * for the "file" scheme. This is because PHP stream functions like `file_get_contents` only work with + * `file:///myfile` but not with `file:/myfile` although they are equivalent according to RFC 3986. But + * `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to + * that format). + * + * @param string $scheme + * @param string $authority + * @param string $path + * @param string $query + * @param string $fragment + * + * @return string + * + * @link https://tools.ietf.org/html/rfc3986#section-5.3 + */ + public static function composeComponents($scheme, $authority, $path, $query, $fragment) + { + $uri = ''; + + // weak type checks to also accept null until we can add scalar type hints + if ($scheme != '') { + $uri .= $scheme . ':'; + } + + if ($authority != ''|| $scheme === 'file') { + $uri .= '//' . $authority; + } + + $uri .= $path; + + if ($query != '') { + $uri .= '?' . $query; + } + + if ($fragment != '') { + $uri .= '#' . $fragment; + } + + return $uri; + } + + /** + * Whether the URI has the default port of the current scheme. + * + * `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used + * independently of the implementation. + * + * @param UriInterface $uri + * + * @return bool + */ + public static function isDefaultPort(UriInterface $uri) + { + return $uri->getPort() === null + || (isset(self::$defaultPorts[$uri->getScheme()]) && $uri->getPort() === self::$defaultPorts[$uri->getScheme()]); + } + + /** + * Whether the URI is absolute, i.e. it has a scheme. + * + * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true + * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative + * to another URI, the base URI. Relative references can be divided into several forms: + * - network-path references, e.g. '//example.com/path' + * - absolute-path references, e.g. '/path' + * - relative-path references, e.g. 'subpath' + * + * @param UriInterface $uri + * + * @return bool + * @see Uri::isNetworkPathReference + * @see Uri::isAbsolutePathReference + * @see Uri::isRelativePathReference + * @link https://tools.ietf.org/html/rfc3986#section-4 + */ + public static function isAbsolute(UriInterface $uri) + { + return $uri->getScheme() !== ''; + } + + /** + * Whether the URI is a network-path reference. + * + * A relative reference that begins with two slash characters is termed an network-path reference. + * + * @param UriInterface $uri + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public static function isNetworkPathReference(UriInterface $uri) + { + return $uri->getScheme() === '' && $uri->getAuthority() !== ''; + } + + /** + * Whether the URI is a absolute-path reference. + * + * A relative reference that begins with a single slash character is termed an absolute-path reference. + * + * @param UriInterface $uri + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public static function isAbsolutePathReference(UriInterface $uri) + { + return $uri->getScheme() === '' + && $uri->getAuthority() === '' + && isset($uri->getPath()[0]) + && $uri->getPath()[0] === '/'; + } + + /** + * Whether the URI is a relative-path reference. + * + * A relative reference that does not begin with a slash character is termed a relative-path reference. + * + * @param UriInterface $uri + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public static function isRelativePathReference(UriInterface $uri) + { + return $uri->getScheme() === '' + && $uri->getAuthority() === '' + && (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/'); + } + + /** + * Whether the URI is a same-document reference. + * + * A same-document reference refers to a URI that is, aside from its fragment + * component, identical to the base URI. When no base URI is given, only an empty + * URI reference (apart from its fragment) is considered a same-document reference. + * + * @param UriInterface $uri The URI to check + * @param UriInterface|null $base An optional base URI to compare against + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.4 + */ + public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null) + { + if ($base !== null) { + $uri = UriResolver::resolve($base, $uri); + + return ($uri->getScheme() === $base->getScheme()) + && ($uri->getAuthority() === $base->getAuthority()) + && ($uri->getPath() === $base->getPath()) + && ($uri->getQuery() === $base->getQuery()); + } + + return $uri->getScheme() === '' && $uri->getAuthority() === '' && $uri->getPath() === '' && $uri->getQuery() === ''; + } + + /** + * Removes dot segments from a path and returns the new path. + * + * @param string $path + * + * @return string + * + * @deprecated since version 1.4. Use UriResolver::removeDotSegments instead. + * @see UriResolver::removeDotSegments + */ + public static function removeDotSegments($path) + { + return UriResolver::removeDotSegments($path); + } + + /** + * Converts the relative URI into a new URI that is resolved against the base URI. + * + * @param UriInterface $base Base URI + * @param string|UriInterface $rel Relative URI + * + * @return UriInterface + * + * @deprecated since version 1.4. Use UriResolver::resolve instead. + * @see UriResolver::resolve + */ + public static function resolve(UriInterface $base, $rel) + { + if (!($rel instanceof UriInterface)) { + $rel = new self($rel); + } + + return UriResolver::resolve($base, $rel); + } + + /** + * Creates a new URI with a specific query string value removed. + * + * Any existing query string values that exactly match the provided key are + * removed. + * + * @param UriInterface $uri URI to use as a base. + * @param string $key Query string key to remove. + * + * @return UriInterface + */ + public static function withoutQueryValue(UriInterface $uri, $key) + { + $result = self::getFilteredQueryString($uri, [$key]); + + return $uri->withQuery(implode('&', $result)); + } + + /** + * Creates a new URI with a specific query string value. + * + * Any existing query string values that exactly match the provided key are + * removed and replaced with the given key value pair. + * + * A value of null will set the query string key without a value, e.g. "key" + * instead of "key=value". + * + * @param UriInterface $uri URI to use as a base. + * @param string $key Key to set. + * @param string|null $value Value to set + * + * @return UriInterface + */ + public static function withQueryValue(UriInterface $uri, $key, $value) + { + $result = self::getFilteredQueryString($uri, [$key]); + + $result[] = self::generateQueryString($key, $value); + + return $uri->withQuery(implode('&', $result)); + } + + /** + * Creates a new URI with multiple specific query string values. + * + * It has the same behavior as withQueryValue() but for an associative array of key => value. + * + * @param UriInterface $uri URI to use as a base. + * @param array $keyValueArray Associative array of key and values + * + * @return UriInterface + */ + public static function withQueryValues(UriInterface $uri, array $keyValueArray) + { + $result = self::getFilteredQueryString($uri, array_keys($keyValueArray)); + + foreach ($keyValueArray as $key => $value) { + $result[] = self::generateQueryString($key, $value); + } + + return $uri->withQuery(implode('&', $result)); + } + + /** + * Creates a URI from a hash of `parse_url` components. + * + * @param array $parts + * + * @return UriInterface + * @link http://php.net/manual/en/function.parse-url.php + * + * @throws \InvalidArgumentException If the components do not form a valid URI. + */ + public static function fromParts(array $parts) + { + $uri = new self(); + $uri->applyParts($parts); + $uri->validateState(); + + return $uri; + } + + public function getScheme() + { + return $this->scheme; + } + + public function getAuthority() + { + $authority = $this->host; + if ($this->userInfo !== '') { + $authority = $this->userInfo . '@' . $authority; + } + + if ($this->port !== null) { + $authority .= ':' . $this->port; + } + + return $authority; + } + + public function getUserInfo() + { + return $this->userInfo; + } + + public function getHost() + { + return $this->host; + } + + public function getPort() + { + return $this->port; + } + + public function getPath() + { + return $this->path; + } + + public function getQuery() + { + return $this->query; + } + + public function getFragment() + { + return $this->fragment; + } + + public function withScheme($scheme) + { + $scheme = $this->filterScheme($scheme); + + if ($this->scheme === $scheme) { + return $this; + } + + $new = clone $this; + $new->scheme = $scheme; + $new->removeDefaultPort(); + $new->validateState(); + + return $new; + } + + public function withUserInfo($user, $password = null) + { + $info = $user; + if ($password != '') { + $info .= ':' . $password; + } + + if ($this->userInfo === $info) { + return $this; + } + + $new = clone $this; + $new->userInfo = $info; + $new->validateState(); + + return $new; + } + + public function withHost($host) + { + $host = $this->filterHost($host); + + if ($this->host === $host) { + return $this; + } + + $new = clone $this; + $new->host = $host; + $new->validateState(); + + return $new; + } + + public function withPort($port) + { + $port = $this->filterPort($port); + + if ($this->port === $port) { + return $this; + } + + $new = clone $this; + $new->port = $port; + $new->removeDefaultPort(); + $new->validateState(); + + return $new; + } + + public function withPath($path) + { + $path = $this->filterPath($path); + + if ($this->path === $path) { + return $this; + } + + $new = clone $this; + $new->path = $path; + $new->validateState(); + + return $new; + } + + public function withQuery($query) + { + $query = $this->filterQueryAndFragment($query); + + if ($this->query === $query) { + return $this; + } + + $new = clone $this; + $new->query = $query; + + return $new; + } + + public function withFragment($fragment) + { + $fragment = $this->filterQueryAndFragment($fragment); + + if ($this->fragment === $fragment) { + return $this; + } + + $new = clone $this; + $new->fragment = $fragment; + + return $new; + } + + /** + * Apply parse_url parts to a URI. + * + * @param array $parts Array of parse_url parts to apply. + */ + private function applyParts(array $parts) + { + $this->scheme = isset($parts['scheme']) + ? $this->filterScheme($parts['scheme']) + : ''; + $this->userInfo = isset($parts['user']) ? $parts['user'] : ''; + $this->host = isset($parts['host']) + ? $this->filterHost($parts['host']) + : ''; + $this->port = isset($parts['port']) + ? $this->filterPort($parts['port']) + : null; + $this->path = isset($parts['path']) + ? $this->filterPath($parts['path']) + : ''; + $this->query = isset($parts['query']) + ? $this->filterQueryAndFragment($parts['query']) + : ''; + $this->fragment = isset($parts['fragment']) + ? $this->filterQueryAndFragment($parts['fragment']) + : ''; + if (isset($parts['pass'])) { + $this->userInfo .= ':' . $parts['pass']; + } + + $this->removeDefaultPort(); + } + + /** + * @param string $scheme + * + * @return string + * + * @throws \InvalidArgumentException If the scheme is invalid. + */ + private function filterScheme($scheme) + { + if (!is_string($scheme)) { + throw new \InvalidArgumentException('Scheme must be a string'); + } + + return strtolower($scheme); + } + + /** + * @param string $host + * + * @return string + * + * @throws \InvalidArgumentException If the host is invalid. + */ + private function filterHost($host) + { + if (!is_string($host)) { + throw new \InvalidArgumentException('Host must be a string'); + } + + return strtolower($host); + } + + /** + * @param int|null $port + * + * @return int|null + * + * @throws \InvalidArgumentException If the port is invalid. + */ + private function filterPort($port) + { + if ($port === null) { + return null; + } + + $port = (int) $port; + if (1 > $port || 0xffff < $port) { + throw new \InvalidArgumentException( + sprintf('Invalid port: %d. Must be between 1 and 65535', $port) + ); + } + + return $port; + } + + /** + * @param UriInterface $uri + * @param array $keys + * + * @return array + */ + private static function getFilteredQueryString(UriInterface $uri, array $keys) + { + $current = $uri->getQuery(); + + if ($current === '') { + return []; + } + + $decodedKeys = array_map('rawurldecode', $keys); + + return array_filter(explode('&', $current), function ($part) use ($decodedKeys) { + return !in_array(rawurldecode(explode('=', $part)[0]), $decodedKeys, true); + }); + } + + /** + * @param string $key + * @param string|null $value + * + * @return string + */ + private static function generateQueryString($key, $value) + { + // Query string separators ("=", "&") within the key or value need to be encoded + // (while preventing double-encoding) before setting the query string. All other + // chars that need percent-encoding will be encoded by withQuery(). + $queryString = strtr($key, self::$replaceQuery); + + if ($value !== null) { + $queryString .= '=' . strtr($value, self::$replaceQuery); + } + + return $queryString; + } + + private function removeDefaultPort() + { + if ($this->port !== null && self::isDefaultPort($this)) { + $this->port = null; + } + } + + /** + * Filters the path of a URI + * + * @param string $path + * + * @return string + * + * @throws \InvalidArgumentException If the path is invalid. + */ + private function filterPath($path) + { + if (!is_string($path)) { + throw new \InvalidArgumentException('Path must be a string'); + } + + return preg_replace_callback( + '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', + [$this, 'rawurlencodeMatchZero'], + $path + ); + } + + /** + * Filters the query string or fragment of a URI. + * + * @param string $str + * + * @return string + * + * @throws \InvalidArgumentException If the query or fragment is invalid. + */ + private function filterQueryAndFragment($str) + { + if (!is_string($str)) { + throw new \InvalidArgumentException('Query and fragment must be a string'); + } + + return preg_replace_callback( + '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', + [$this, 'rawurlencodeMatchZero'], + $str + ); + } + + private function rawurlencodeMatchZero(array $match) + { + return rawurlencode($match[0]); + } + + private function validateState() + { + if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) { + $this->host = self::HTTP_DEFAULT_HOST; + } + + if ($this->getAuthority() === '') { + if (0 === strpos($this->path, '//')) { + throw new \InvalidArgumentException('The path of a URI without an authority must not start with two slashes "//"'); + } + if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) { + throw new \InvalidArgumentException('A relative URI must not have a path beginning with a segment containing a colon'); + } + } elseif (isset($this->path[0]) && $this->path[0] !== '/') { + @trigger_error( + 'The path of a URI with an authority must start with a slash "/" or be empty. Automagically fixing the URI ' . + 'by adding a leading slash to the path is deprecated since version 1.4 and will throw an exception instead.', + E_USER_DEPRECATED + ); + $this->path = '/'. $this->path; + //throw new \InvalidArgumentException('The path of a URI with an authority must start with a slash "/" or be empty'); + } + } +} diff --git a/vendor/guzzlehttp/psr7/src/UriNormalizer.php b/vendor/guzzlehttp/psr7/src/UriNormalizer.php new file mode 100644 index 00000000..384c29e5 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/UriNormalizer.php @@ -0,0 +1,216 @@ +getPath() === '' && + ($uri->getScheme() === 'http' || $uri->getScheme() === 'https') + ) { + $uri = $uri->withPath('/'); + } + + if ($flags & self::REMOVE_DEFAULT_HOST && $uri->getScheme() === 'file' && $uri->getHost() === 'localhost') { + $uri = $uri->withHost(''); + } + + if ($flags & self::REMOVE_DEFAULT_PORT && $uri->getPort() !== null && Uri::isDefaultPort($uri)) { + $uri = $uri->withPort(null); + } + + if ($flags & self::REMOVE_DOT_SEGMENTS && !Uri::isRelativePathReference($uri)) { + $uri = $uri->withPath(UriResolver::removeDotSegments($uri->getPath())); + } + + if ($flags & self::REMOVE_DUPLICATE_SLASHES) { + $uri = $uri->withPath(preg_replace('#//++#', '/', $uri->getPath())); + } + + if ($flags & self::SORT_QUERY_PARAMETERS && $uri->getQuery() !== '') { + $queryKeyValues = explode('&', $uri->getQuery()); + sort($queryKeyValues); + $uri = $uri->withQuery(implode('&', $queryKeyValues)); + } + + return $uri; + } + + /** + * Whether two URIs can be considered equivalent. + * + * Both URIs are normalized automatically before comparison with the given $normalizations bitmask. The method also + * accepts relative URI references and returns true when they are equivalent. This of course assumes they will be + * resolved against the same base URI. If this is not the case, determination of equivalence or difference of + * relative references does not mean anything. + * + * @param UriInterface $uri1 An URI to compare + * @param UriInterface $uri2 An URI to compare + * @param int $normalizations A bitmask of normalizations to apply, see constants + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-6.1 + */ + public static function isEquivalent(UriInterface $uri1, UriInterface $uri2, $normalizations = self::PRESERVING_NORMALIZATIONS) + { + return (string) self::normalize($uri1, $normalizations) === (string) self::normalize($uri2, $normalizations); + } + + private static function capitalizePercentEncoding(UriInterface $uri) + { + $regex = '/(?:%[A-Fa-f0-9]{2})++/'; + + $callback = function (array $match) { + return strtoupper($match[0]); + }; + + return + $uri->withPath( + preg_replace_callback($regex, $callback, $uri->getPath()) + )->withQuery( + preg_replace_callback($regex, $callback, $uri->getQuery()) + ); + } + + private static function decodeUnreservedCharacters(UriInterface $uri) + { + $regex = '/%(?:2D|2E|5F|7E|3[0-9]|[46][1-9A-F]|[57][0-9A])/i'; + + $callback = function (array $match) { + return rawurldecode($match[0]); + }; + + return + $uri->withPath( + preg_replace_callback($regex, $callback, $uri->getPath()) + )->withQuery( + preg_replace_callback($regex, $callback, $uri->getQuery()) + ); + } + + private function __construct() + { + // cannot be instantiated + } +} diff --git a/vendor/guzzlehttp/psr7/src/UriResolver.php b/vendor/guzzlehttp/psr7/src/UriResolver.php new file mode 100644 index 00000000..c1cb8a27 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/UriResolver.php @@ -0,0 +1,219 @@ +getScheme() != '') { + return $rel->withPath(self::removeDotSegments($rel->getPath())); + } + + if ($rel->getAuthority() != '') { + $targetAuthority = $rel->getAuthority(); + $targetPath = self::removeDotSegments($rel->getPath()); + $targetQuery = $rel->getQuery(); + } else { + $targetAuthority = $base->getAuthority(); + if ($rel->getPath() === '') { + $targetPath = $base->getPath(); + $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery(); + } else { + if ($rel->getPath()[0] === '/') { + $targetPath = $rel->getPath(); + } else { + if ($targetAuthority != '' && $base->getPath() === '') { + $targetPath = '/' . $rel->getPath(); + } else { + $lastSlashPos = strrpos($base->getPath(), '/'); + if ($lastSlashPos === false) { + $targetPath = $rel->getPath(); + } else { + $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1) . $rel->getPath(); + } + } + } + $targetPath = self::removeDotSegments($targetPath); + $targetQuery = $rel->getQuery(); + } + } + + return new Uri(Uri::composeComponents( + $base->getScheme(), + $targetAuthority, + $targetPath, + $targetQuery, + $rel->getFragment() + )); + } + + /** + * Returns the target URI as a relative reference from the base URI. + * + * This method is the counterpart to resolve(): + * + * (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target)) + * + * One use-case is to use the current request URI as base URI and then generate relative links in your documents + * to reduce the document size or offer self-contained downloadable document archives. + * + * $base = new Uri('http://example.com/a/b/'); + * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'. + * echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'. + * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'. + * echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'. + * + * This method also accepts a target that is already relative and will try to relativize it further. Only a + * relative-path reference will be returned as-is. + * + * echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well + * + * @param UriInterface $base Base URI + * @param UriInterface $target Target URI + * + * @return UriInterface The relative URI reference + */ + public static function relativize(UriInterface $base, UriInterface $target) + { + if ($target->getScheme() !== '' && + ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '') + ) { + return $target; + } + + if (Uri::isRelativePathReference($target)) { + // As the target is already highly relative we return it as-is. It would be possible to resolve + // the target with `$target = self::resolve($base, $target);` and then try make it more relative + // by removing a duplicate query. But let's not do that automatically. + return $target; + } + + if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) { + return $target->withScheme(''); + } + + // We must remove the path before removing the authority because if the path starts with two slashes, the URI + // would turn invalid. And we also cannot set a relative path before removing the authority, as that is also + // invalid. + $emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost(''); + + if ($base->getPath() !== $target->getPath()) { + return $emptyPathUri->withPath(self::getRelativePath($base, $target)); + } + + if ($base->getQuery() === $target->getQuery()) { + // Only the target fragment is left. And it must be returned even if base and target fragment are the same. + return $emptyPathUri->withQuery(''); + } + + // If the base URI has a query but the target has none, we cannot return an empty path reference as it would + // inherit the base query component when resolving. + if ($target->getQuery() === '') { + $segments = explode('/', $target->getPath()); + $lastSegment = end($segments); + + return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment); + } + + return $emptyPathUri; + } + + private static function getRelativePath(UriInterface $base, UriInterface $target) + { + $sourceSegments = explode('/', $base->getPath()); + $targetSegments = explode('/', $target->getPath()); + array_pop($sourceSegments); + $targetLastSegment = array_pop($targetSegments); + foreach ($sourceSegments as $i => $segment) { + if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) { + unset($sourceSegments[$i], $targetSegments[$i]); + } else { + break; + } + } + $targetSegments[] = $targetLastSegment; + $relativePath = str_repeat('../', count($sourceSegments)) . implode('/', $targetSegments); + + // A reference to am empty last segment or an empty first sub-segment must be prefixed with "./". + // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used + // as the first segment of a relative-path reference, as it would be mistaken for a scheme name. + if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) { + $relativePath = "./$relativePath"; + } elseif ('/' === $relativePath[0]) { + if ($base->getAuthority() != '' && $base->getPath() === '') { + // In this case an extra slash is added by resolve() automatically. So we must not add one here. + $relativePath = ".$relativePath"; + } else { + $relativePath = "./$relativePath"; + } + } + + return $relativePath; + } + + private function __construct() + { + // cannot be instantiated + } +} diff --git a/vendor/guzzlehttp/psr7/src/functions.php b/vendor/guzzlehttp/psr7/src/functions.php new file mode 100644 index 00000000..957bfb42 --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/functions.php @@ -0,0 +1,898 @@ +getMethod() . ' ' + . $message->getRequestTarget()) + . ' HTTP/' . $message->getProtocolVersion(); + if (!$message->hasHeader('host')) { + $msg .= "\r\nHost: " . $message->getUri()->getHost(); + } + } elseif ($message instanceof ResponseInterface) { + $msg = 'HTTP/' . $message->getProtocolVersion() . ' ' + . $message->getStatusCode() . ' ' + . $message->getReasonPhrase(); + } else { + throw new \InvalidArgumentException('Unknown message type'); + } + + foreach ($message->getHeaders() as $name => $values) { + $msg .= "\r\n{$name}: " . implode(', ', $values); + } + + return "{$msg}\r\n\r\n" . $message->getBody(); +} + +/** + * Returns a UriInterface for the given value. + * + * This function accepts a string or {@see Psr\Http\Message\UriInterface} and + * returns a UriInterface for the given value. If the value is already a + * `UriInterface`, it is returned as-is. + * + * @param string|UriInterface $uri + * + * @return UriInterface + * @throws \InvalidArgumentException + */ +function uri_for($uri) +{ + if ($uri instanceof UriInterface) { + return $uri; + } elseif (is_string($uri)) { + return new Uri($uri); + } + + throw new \InvalidArgumentException('URI must be a string or UriInterface'); +} + +/** + * Create a new stream based on the input type. + * + * Options is an associative array that can contain the following keys: + * - metadata: Array of custom metadata. + * - size: Size of the stream. + * + * @param resource|string|null|int|float|bool|StreamInterface|callable|\Iterator $resource Entity body data + * @param array $options Additional options + * + * @return StreamInterface + * @throws \InvalidArgumentException if the $resource arg is not valid. + */ +function stream_for($resource = '', array $options = []) +{ + if (is_scalar($resource)) { + $stream = fopen('php://temp', 'r+'); + if ($resource !== '') { + fwrite($stream, $resource); + fseek($stream, 0); + } + return new Stream($stream, $options); + } + + switch (gettype($resource)) { + case 'resource': + return new Stream($resource, $options); + case 'object': + if ($resource instanceof StreamInterface) { + return $resource; + } elseif ($resource instanceof \Iterator) { + return new PumpStream(function () use ($resource) { + if (!$resource->valid()) { + return false; + } + $result = $resource->current(); + $resource->next(); + return $result; + }, $options); + } elseif (method_exists($resource, '__toString')) { + return stream_for((string) $resource, $options); + } + break; + case 'NULL': + return new Stream(fopen('php://temp', 'r+'), $options); + } + + if (is_callable($resource)) { + return new PumpStream($resource, $options); + } + + throw new \InvalidArgumentException('Invalid resource type: ' . gettype($resource)); +} + +/** + * Parse an array of header values containing ";" separated data into an + * array of associative arrays representing the header key value pair + * data of the header. When a parameter does not contain a value, but just + * contains a key, this function will inject a key with a '' string value. + * + * @param string|array $header Header to parse into components. + * + * @return array Returns the parsed header values. + */ +function parse_header($header) +{ + static $trimmed = "\"' \n\t\r"; + $params = $matches = []; + + foreach (normalize_header($header) as $val) { + $part = []; + foreach (preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $val) as $kvp) { + if (preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) { + $m = $matches[0]; + if (isset($m[1])) { + $part[trim($m[0], $trimmed)] = trim($m[1], $trimmed); + } else { + $part[] = trim($m[0], $trimmed); + } + } + } + if ($part) { + $params[] = $part; + } + } + + return $params; +} + +/** + * Converts an array of header values that may contain comma separated + * headers into an array of headers with no comma separated values. + * + * @param string|array $header Header to normalize. + * + * @return array Returns the normalized header field values. + */ +function normalize_header($header) +{ + if (!is_array($header)) { + return array_map('trim', explode(',', $header)); + } + + $result = []; + foreach ($header as $value) { + foreach ((array) $value as $v) { + if (strpos($v, ',') === false) { + $result[] = $v; + continue; + } + foreach (preg_split('/,(?=([^"]*"[^"]*")*[^"]*$)/', $v) as $vv) { + $result[] = trim($vv); + } + } + } + + return $result; +} + +/** + * Clone and modify a request with the given changes. + * + * The changes can be one of: + * - method: (string) Changes the HTTP method. + * - set_headers: (array) Sets the given headers. + * - remove_headers: (array) Remove the given headers. + * - body: (mixed) Sets the given body. + * - uri: (UriInterface) Set the URI. + * - query: (string) Set the query string value of the URI. + * - version: (string) Set the protocol version. + * + * @param RequestInterface $request Request to clone and modify. + * @param array $changes Changes to apply. + * + * @return RequestInterface + */ +function modify_request(RequestInterface $request, array $changes) +{ + if (!$changes) { + return $request; + } + + $headers = $request->getHeaders(); + + if (!isset($changes['uri'])) { + $uri = $request->getUri(); + } else { + // Remove the host header if one is on the URI + if ($host = $changes['uri']->getHost()) { + $changes['set_headers']['Host'] = $host; + + if ($port = $changes['uri']->getPort()) { + $standardPorts = ['http' => 80, 'https' => 443]; + $scheme = $changes['uri']->getScheme(); + if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) { + $changes['set_headers']['Host'] .= ':'.$port; + } + } + } + $uri = $changes['uri']; + } + + if (!empty($changes['remove_headers'])) { + $headers = _caseless_remove($changes['remove_headers'], $headers); + } + + if (!empty($changes['set_headers'])) { + $headers = _caseless_remove(array_keys($changes['set_headers']), $headers); + $headers = $changes['set_headers'] + $headers; + } + + if (isset($changes['query'])) { + $uri = $uri->withQuery($changes['query']); + } + + if ($request instanceof ServerRequestInterface) { + return (new ServerRequest( + isset($changes['method']) ? $changes['method'] : $request->getMethod(), + $uri, + $headers, + isset($changes['body']) ? $changes['body'] : $request->getBody(), + isset($changes['version']) + ? $changes['version'] + : $request->getProtocolVersion(), + $request->getServerParams() + )) + ->withParsedBody($request->getParsedBody()) + ->withQueryParams($request->getQueryParams()) + ->withCookieParams($request->getCookieParams()) + ->withUploadedFiles($request->getUploadedFiles()); + } + + return new Request( + isset($changes['method']) ? $changes['method'] : $request->getMethod(), + $uri, + $headers, + isset($changes['body']) ? $changes['body'] : $request->getBody(), + isset($changes['version']) + ? $changes['version'] + : $request->getProtocolVersion() + ); +} + +/** + * Attempts to rewind a message body and throws an exception on failure. + * + * The body of the message will only be rewound if a call to `tell()` returns a + * value other than `0`. + * + * @param MessageInterface $message Message to rewind + * + * @throws \RuntimeException + */ +function rewind_body(MessageInterface $message) +{ + $body = $message->getBody(); + + if ($body->tell()) { + $body->rewind(); + } +} + +/** + * Safely opens a PHP stream resource using a filename. + * + * When fopen fails, PHP normally raises a warning. This function adds an + * error handler that checks for errors and throws an exception instead. + * + * @param string $filename File to open + * @param string $mode Mode used to open the file + * + * @return resource + * @throws \RuntimeException if the file cannot be opened + */ +function try_fopen($filename, $mode) +{ + $ex = null; + set_error_handler(function () use ($filename, $mode, &$ex) { + $ex = new \RuntimeException(sprintf( + 'Unable to open %s using mode %s: %s', + $filename, + $mode, + func_get_args()[1] + )); + }); + + $handle = fopen($filename, $mode); + restore_error_handler(); + + if ($ex) { + /** @var $ex \RuntimeException */ + throw $ex; + } + + return $handle; +} + +/** + * Copy the contents of a stream into a string until the given number of + * bytes have been read. + * + * @param StreamInterface $stream Stream to read + * @param int $maxLen Maximum number of bytes to read. Pass -1 + * to read the entire stream. + * @return string + * @throws \RuntimeException on error. + */ +function copy_to_string(StreamInterface $stream, $maxLen = -1) +{ + $buffer = ''; + + if ($maxLen === -1) { + while (!$stream->eof()) { + $buf = $stream->read(1048576); + // Using a loose equality here to match on '' and false. + if ($buf == null) { + break; + } + $buffer .= $buf; + } + return $buffer; + } + + $len = 0; + while (!$stream->eof() && $len < $maxLen) { + $buf = $stream->read($maxLen - $len); + // Using a loose equality here to match on '' and false. + if ($buf == null) { + break; + } + $buffer .= $buf; + $len = strlen($buffer); + } + + return $buffer; +} + +/** + * Copy the contents of a stream into another stream until the given number + * of bytes have been read. + * + * @param StreamInterface $source Stream to read from + * @param StreamInterface $dest Stream to write to + * @param int $maxLen Maximum number of bytes to read. Pass -1 + * to read the entire stream. + * + * @throws \RuntimeException on error. + */ +function copy_to_stream( + StreamInterface $source, + StreamInterface $dest, + $maxLen = -1 +) { + $bufferSize = 8192; + + if ($maxLen === -1) { + while (!$source->eof()) { + if (!$dest->write($source->read($bufferSize))) { + break; + } + } + } else { + $remaining = $maxLen; + while ($remaining > 0 && !$source->eof()) { + $buf = $source->read(min($bufferSize, $remaining)); + $len = strlen($buf); + if (!$len) { + break; + } + $remaining -= $len; + $dest->write($buf); + } + } +} + +/** + * Calculate a hash of a Stream + * + * @param StreamInterface $stream Stream to calculate the hash for + * @param string $algo Hash algorithm (e.g. md5, crc32, etc) + * @param bool $rawOutput Whether or not to use raw output + * + * @return string Returns the hash of the stream + * @throws \RuntimeException on error. + */ +function hash( + StreamInterface $stream, + $algo, + $rawOutput = false +) { + $pos = $stream->tell(); + + if ($pos > 0) { + $stream->rewind(); + } + + $ctx = hash_init($algo); + while (!$stream->eof()) { + hash_update($ctx, $stream->read(1048576)); + } + + $out = hash_final($ctx, (bool) $rawOutput); + $stream->seek($pos); + + return $out; +} + +/** + * Read a line from the stream up to the maximum allowed buffer length + * + * @param StreamInterface $stream Stream to read from + * @param int $maxLength Maximum buffer length + * + * @return string + */ +function readline(StreamInterface $stream, $maxLength = null) +{ + $buffer = ''; + $size = 0; + + while (!$stream->eof()) { + // Using a loose equality here to match on '' and false. + if (null == ($byte = $stream->read(1))) { + return $buffer; + } + $buffer .= $byte; + // Break when a new line is found or the max length - 1 is reached + if ($byte === "\n" || ++$size === $maxLength - 1) { + break; + } + } + + return $buffer; +} + +/** + * Parses a request message string into a request object. + * + * @param string $message Request message string. + * + * @return Request + */ +function parse_request($message) +{ + $data = _parse_message($message); + $matches = []; + if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) { + throw new \InvalidArgumentException('Invalid request string'); + } + $parts = explode(' ', $data['start-line'], 3); + $version = isset($parts[2]) ? explode('/', $parts[2])[1] : '1.1'; + + $request = new Request( + $parts[0], + $matches[1] === '/' ? _parse_request_uri($parts[1], $data['headers']) : $parts[1], + $data['headers'], + $data['body'], + $version + ); + + return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]); +} + +/** + * Parses a response message string into a response object. + * + * @param string $message Response message string. + * + * @return Response + */ +function parse_response($message) +{ + $data = _parse_message($message); + // According to https://tools.ietf.org/html/rfc7230#section-3.1.2 the space + // between status-code and reason-phrase is required. But browsers accept + // responses without space and reason as well. + if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) { + throw new \InvalidArgumentException('Invalid response string: ' . $data['start-line']); + } + $parts = explode(' ', $data['start-line'], 3); + + return new Response( + $parts[1], + $data['headers'], + $data['body'], + explode('/', $parts[0])[1], + isset($parts[2]) ? $parts[2] : null + ); +} + +/** + * Parse a query string into an associative array. + * + * If multiple values are found for the same key, the value of that key + * value pair will become an array. This function does not parse nested + * PHP style arrays into an associative array (e.g., foo[a]=1&foo[b]=2 will + * be parsed into ['foo[a]' => '1', 'foo[b]' => '2']). + * + * @param string $str Query string to parse + * @param int|bool $urlEncoding How the query string is encoded + * + * @return array + */ +function parse_query($str, $urlEncoding = true) +{ + $result = []; + + if ($str === '') { + return $result; + } + + if ($urlEncoding === true) { + $decoder = function ($value) { + return rawurldecode(str_replace('+', ' ', $value)); + }; + } elseif ($urlEncoding === PHP_QUERY_RFC3986) { + $decoder = 'rawurldecode'; + } elseif ($urlEncoding === PHP_QUERY_RFC1738) { + $decoder = 'urldecode'; + } else { + $decoder = function ($str) { return $str; }; + } + + foreach (explode('&', $str) as $kvp) { + $parts = explode('=', $kvp, 2); + $key = $decoder($parts[0]); + $value = isset($parts[1]) ? $decoder($parts[1]) : null; + if (!isset($result[$key])) { + $result[$key] = $value; + } else { + if (!is_array($result[$key])) { + $result[$key] = [$result[$key]]; + } + $result[$key][] = $value; + } + } + + return $result; +} + +/** + * Build a query string from an array of key value pairs. + * + * This function can use the return value of parse_query() to build a query + * string. This function does not modify the provided keys when an array is + * encountered (like http_build_query would). + * + * @param array $params Query string parameters. + * @param int|false $encoding Set to false to not encode, PHP_QUERY_RFC3986 + * to encode using RFC3986, or PHP_QUERY_RFC1738 + * to encode using RFC1738. + * @return string + */ +function build_query(array $params, $encoding = PHP_QUERY_RFC3986) +{ + if (!$params) { + return ''; + } + + if ($encoding === false) { + $encoder = function ($str) { return $str; }; + } elseif ($encoding === PHP_QUERY_RFC3986) { + $encoder = 'rawurlencode'; + } elseif ($encoding === PHP_QUERY_RFC1738) { + $encoder = 'urlencode'; + } else { + throw new \InvalidArgumentException('Invalid type'); + } + + $qs = ''; + foreach ($params as $k => $v) { + $k = $encoder($k); + if (!is_array($v)) { + $qs .= $k; + if ($v !== null) { + $qs .= '=' . $encoder($v); + } + $qs .= '&'; + } else { + foreach ($v as $vv) { + $qs .= $k; + if ($vv !== null) { + $qs .= '=' . $encoder($vv); + } + $qs .= '&'; + } + } + } + + return $qs ? (string) substr($qs, 0, -1) : ''; +} + +/** + * Determines the mimetype of a file by looking at its extension. + * + * @param $filename + * + * @return null|string + */ +function mimetype_from_filename($filename) +{ + return mimetype_from_extension(pathinfo($filename, PATHINFO_EXTENSION)); +} + +/** + * Maps a file extensions to a mimetype. + * + * @param $extension string The file extension. + * + * @return string|null + * @link http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types + */ +function mimetype_from_extension($extension) +{ + static $mimetypes = [ + '3gp' => 'video/3gpp', + '7z' => 'application/x-7z-compressed', + 'aac' => 'audio/x-aac', + 'ai' => 'application/postscript', + 'aif' => 'audio/x-aiff', + 'asc' => 'text/plain', + 'asf' => 'video/x-ms-asf', + 'atom' => 'application/atom+xml', + 'avi' => 'video/x-msvideo', + 'bmp' => 'image/bmp', + 'bz2' => 'application/x-bzip2', + 'cer' => 'application/pkix-cert', + 'crl' => 'application/pkix-crl', + 'crt' => 'application/x-x509-ca-cert', + 'css' => 'text/css', + 'csv' => 'text/csv', + 'cu' => 'application/cu-seeme', + 'deb' => 'application/x-debian-package', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dvi' => 'application/x-dvi', + 'eot' => 'application/vnd.ms-fontobject', + 'eps' => 'application/postscript', + 'epub' => 'application/epub+zip', + 'etx' => 'text/x-setext', + 'flac' => 'audio/flac', + 'flv' => 'video/x-flv', + 'gif' => 'image/gif', + 'gz' => 'application/gzip', + 'htm' => 'text/html', + 'html' => 'text/html', + 'ico' => 'image/x-icon', + 'ics' => 'text/calendar', + 'ini' => 'text/plain', + 'iso' => 'application/x-iso9660-image', + 'jar' => 'application/java-archive', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'js' => 'text/javascript', + 'json' => 'application/json', + 'latex' => 'application/x-latex', + 'log' => 'text/plain', + 'm4a' => 'audio/mp4', + 'm4v' => 'video/mp4', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mov' => 'video/quicktime', + 'mkv' => 'video/x-matroska', + 'mp3' => 'audio/mpeg', + 'mp4' => 'video/mp4', + 'mp4a' => 'audio/mp4', + 'mp4v' => 'video/mp4', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpg4' => 'video/mp4', + 'oga' => 'audio/ogg', + 'ogg' => 'audio/ogg', + 'ogv' => 'video/ogg', + 'ogx' => 'application/ogg', + 'pbm' => 'image/x-portable-bitmap', + 'pdf' => 'application/pdf', + 'pgm' => 'image/x-portable-graymap', + 'png' => 'image/png', + 'pnm' => 'image/x-portable-anymap', + 'ppm' => 'image/x-portable-pixmap', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'ps' => 'application/postscript', + 'qt' => 'video/quicktime', + 'rar' => 'application/x-rar-compressed', + 'ras' => 'image/x-cmu-raster', + 'rss' => 'application/rss+xml', + 'rtf' => 'application/rtf', + 'sgm' => 'text/sgml', + 'sgml' => 'text/sgml', + 'svg' => 'image/svg+xml', + 'swf' => 'application/x-shockwave-flash', + 'tar' => 'application/x-tar', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'torrent' => 'application/x-bittorrent', + 'ttf' => 'application/x-font-ttf', + 'txt' => 'text/plain', + 'wav' => 'audio/x-wav', + 'webm' => 'video/webm', + 'wma' => 'audio/x-ms-wma', + 'wmv' => 'video/x-ms-wmv', + 'woff' => 'application/x-font-woff', + 'wsdl' => 'application/wsdl+xml', + 'xbm' => 'image/x-xbitmap', + 'xls' => 'application/vnd.ms-excel', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xml' => 'application/xml', + 'xpm' => 'image/x-xpixmap', + 'xwd' => 'image/x-xwindowdump', + 'yaml' => 'text/yaml', + 'yml' => 'text/yaml', + 'zip' => 'application/zip', + ]; + + $extension = strtolower($extension); + + return isset($mimetypes[$extension]) + ? $mimetypes[$extension] + : null; +} + +/** + * Parses an HTTP message into an associative array. + * + * The array contains the "start-line" key containing the start line of + * the message, "headers" key containing an associative array of header + * array values, and a "body" key containing the body of the message. + * + * @param string $message HTTP request or response to parse. + * + * @return array + * @internal + */ +function _parse_message($message) +{ + if (!$message) { + throw new \InvalidArgumentException('Invalid message'); + } + + $message = ltrim($message, "\r\n"); + + $messageParts = preg_split("/\r?\n\r?\n/", $message, 2); + + if ($messageParts === false || count($messageParts) !== 2) { + throw new \InvalidArgumentException('Invalid message: Missing header delimiter'); + } + + list($rawHeaders, $body) = $messageParts; + $rawHeaders .= "\r\n"; // Put back the delimiter we split previously + $headerParts = preg_split("/\r?\n/", $rawHeaders, 2); + + if ($headerParts === false || count($headerParts) !== 2) { + throw new \InvalidArgumentException('Invalid message: Missing status line'); + } + + list($startLine, $rawHeaders) = $headerParts; + + if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') { + // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0 + $rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders); + } + + /** @var array[] $headerLines */ + $count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER); + + // If these aren't the same, then one line didn't match and there's an invalid header. + if ($count !== substr_count($rawHeaders, "\n")) { + // Folding is deprecated, see https://tools.ietf.org/html/rfc7230#section-3.2.4 + if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) { + throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding'); + } + + throw new \InvalidArgumentException('Invalid header syntax'); + } + + $headers = []; + + foreach ($headerLines as $headerLine) { + $headers[$headerLine[1]][] = $headerLine[2]; + } + + return [ + 'start-line' => $startLine, + 'headers' => $headers, + 'body' => $body, + ]; +} + +/** + * Constructs a URI for an HTTP request message. + * + * @param string $path Path from the start-line + * @param array $headers Array of headers (each value an array). + * + * @return string + * @internal + */ +function _parse_request_uri($path, array $headers) +{ + $hostKey = array_filter(array_keys($headers), function ($k) { + return strtolower($k) === 'host'; + }); + + // If no host is found, then a full URI cannot be constructed. + if (!$hostKey) { + return $path; + } + + $host = $headers[reset($hostKey)][0]; + $scheme = substr($host, -4) === ':443' ? 'https' : 'http'; + + return $scheme . '://' . $host . '/' . ltrim($path, '/'); +} + +/** + * Get a short summary of the message body + * + * Will return `null` if the response is not printable. + * + * @param MessageInterface $message The message to get the body summary + * @param int $truncateAt The maximum allowed size of the summary + * + * @return null|string + */ +function get_message_body_summary(MessageInterface $message, $truncateAt = 120) +{ + $body = $message->getBody(); + + if (!$body->isSeekable() || !$body->isReadable()) { + return null; + } + + $size = $body->getSize(); + + if ($size === 0) { + return null; + } + + $summary = $body->read($truncateAt); + $body->rewind(); + + if ($size > $truncateAt) { + $summary .= ' (truncated...)'; + } + + // Matches any printable character, including unicode characters: + // letters, marks, numbers, punctuation, spacing, and separators. + if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/', $summary)) { + return null; + } + + return $summary; +} + +/** @internal */ +function _caseless_remove($keys, array $data) +{ + $result = []; + + foreach ($keys as &$key) { + $key = strtolower($key); + } + + foreach ($data as $k => $v) { + if (!in_array(strtolower($k), $keys)) { + $result[$k] = $v; + } + } + + return $result; +} diff --git a/vendor/guzzlehttp/psr7/src/functions_include.php b/vendor/guzzlehttp/psr7/src/functions_include.php new file mode 100644 index 00000000..96a4a83a --- /dev/null +++ b/vendor/guzzlehttp/psr7/src/functions_include.php @@ -0,0 +1,6 @@ +=5.3.0" + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/vendor/psr/http-message/src/MessageInterface.php b/vendor/psr/http-message/src/MessageInterface.php new file mode 100644 index 00000000..dd46e5ec --- /dev/null +++ b/vendor/psr/http-message/src/MessageInterface.php @@ -0,0 +1,187 @@ +getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return string[][] Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getHeaders(); + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader($name); + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader($name); + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function getHeaderLine($name); + + /** + * Return an instance with the provided value replacing the specified header. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * 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 + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withHeader($name, $value); + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * 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 + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withAddedHeader($name, $value); + + /** + * Return an instance without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the named header. + * + * @param string $name Case-insensitive header field name to remove. + * @return static + */ + public function withoutHeader($name); + + /** + * Gets the body of the message. + * + * @return StreamInterface Returns the body as a stream. + */ + public function getBody(); + + /** + * Return an instance with the specified message body. + * + * The body MUST be a StreamInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamInterface $body Body. + * @return static + * @throws \InvalidArgumentException When the body is not valid. + */ + public function withBody(StreamInterface $body); +} diff --git a/vendor/psr/http-message/src/RequestInterface.php b/vendor/psr/http-message/src/RequestInterface.php new file mode 100644 index 00000000..a96d4fd6 --- /dev/null +++ b/vendor/psr/http-message/src/RequestInterface.php @@ -0,0 +1,129 @@ +getQuery()` + * or from the `QUERY_STRING` server param. + * + * @return array + */ + public function getQueryParams(); + + /** + * Return an instance with the specified query string arguments. + * + * These values SHOULD remain immutable over the course of the incoming + * request. They MAY be injected during instantiation, such as from PHP's + * $_GET superglobal, or MAY be derived from some other value such as the + * URI. In cases where the arguments are parsed from the URI, the data + * MUST be compatible with what PHP's parse_str() would return for + * purposes of how duplicate query parameters are handled, and how nested + * sets are handled. + * + * Setting query string arguments MUST NOT change the URI stored by the + * request, nor the values in the server params. + * + * 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 query string arguments. + * + * @param array $query Array of query string arguments, typically from + * $_GET. + * @return static + */ + public function withQueryParams(array $query); + + /** + * Retrieve normalized file upload data. + * + * This method returns upload metadata in a normalized tree, with each leaf + * an instance of Psr\Http\Message\UploadedFileInterface. + * + * These values MAY be prepared from $_FILES or the message body during + * instantiation, or MAY be injected via withUploadedFiles(). + * + * @return array An array tree of UploadedFileInterface instances; an empty + * array MUST be returned if no data is present. + */ + public function getUploadedFiles(); + + /** + * Create a new instance with the specified uploaded files. + * + * 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 body parameters. + * + * @param array $uploadedFiles An array tree of UploadedFileInterface instances. + * @return static + * @throws \InvalidArgumentException if an invalid structure is provided. + */ + public function withUploadedFiles(array $uploadedFiles); + + /** + * Retrieve any parameters provided in the request body. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, this method MUST + * return the contents of $_POST. + * + * Otherwise, this method may return any results of deserializing + * the request body content; as parsing returns structured content, the + * potential types MUST be arrays or objects only. A null value indicates + * the absence of body content. + * + * @return null|array|object The deserialized body parameters, if any. + * These will typically be an array or object. + */ + public function getParsedBody(); + + /** + * Return an instance with the specified body parameters. + * + * These MAY be injected during instantiation. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, use this method + * ONLY to inject the contents of $_POST. + * + * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of + * deserializing the request body content. Deserialization/parsing returns + * structured data, and, as such, this method ONLY accepts arrays or objects, + * or a null value if nothing was available to parse. + * + * As an example, if content negotiation determines that the request data + * is a JSON payload, this method could be used to create a request + * instance with the deserialized parameters. + * + * 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 body parameters. + * + * @param null|array|object $data The deserialized body data. This will + * typically be in an array or object. + * @return static + * @throws \InvalidArgumentException if an unsupported argument type is + * provided. + */ + public function withParsedBody($data); + + /** + * Retrieve attributes derived from the request. + * + * The request "attributes" may be used to allow injection of any + * parameters derived from the request: e.g., the results of path + * match operations; the results of decrypting cookies; the results of + * deserializing non-form-encoded message bodies; etc. Attributes + * will be application and request specific, and CAN be mutable. + * + * @return array Attributes derived from the request. + */ + public function getAttributes(); + + /** + * Retrieve a single derived request attribute. + * + * Retrieves a single derived request attribute as described in + * getAttributes(). If the attribute has not been previously set, returns + * the default value as provided. + * + * This method obviates the need for a hasAttribute() method, as it allows + * specifying a default value to return if the attribute is not found. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * @return mixed + */ + public function getAttribute($name, $default = null); + + /** + * Return an instance with the specified derived request attribute. + * + * This method allows setting a single derived request attribute as + * described in getAttributes(). + * + * 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 attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * @return static + */ + public function withAttribute($name, $value); + + /** + * Return an instance that removes the specified derived request attribute. + * + * This method allows removing a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @return static + */ + public function withoutAttribute($name); +} diff --git a/vendor/psr/http-message/src/StreamInterface.php b/vendor/psr/http-message/src/StreamInterface.php new file mode 100644 index 00000000..f68f3912 --- /dev/null +++ b/vendor/psr/http-message/src/StreamInterface.php @@ -0,0 +1,158 @@ + + * [user-info@]host[:port] + * + * + * If the port component is not set or is the standard port for the current + * scheme, it SHOULD NOT be included. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2 + * @return string The URI authority, in "[user-info@]host[:port]" format. + */ + public function getAuthority(); + + /** + * Retrieve the user information component of the URI. + * + * If no user information is present, this method MUST return an empty + * string. + * + * If a user is present in the URI, this will return that value; + * additionally, if the password is also present, it will be appended to the + * user value, with a colon (":") separating the values. + * + * The trailing "@" character is not part of the user information and MUST + * NOT be added. + * + * @return string The URI user information, in "username[:password]" format. + */ + public function getUserInfo(); + + /** + * Retrieve the host component of the URI. + * + * If no host is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.2.2. + * + * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 + * @return string The URI host. + */ + public function getHost(); + + /** + * Retrieve the port component of the URI. + * + * If a port is present, and it is non-standard for the current scheme, + * this method MUST return it as an integer. If the port is the standard port + * used with the current scheme, this method SHOULD return null. + * + * If no port is present, and no scheme is present, this method MUST return + * a null value. + * + * If no port is present, but a scheme is present, this method MAY return + * the standard port for that scheme, but SHOULD return null. + * + * @return null|int The URI port. + */ + public function getPort(); + + /** + * Retrieve the path component of the URI. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * Normally, the empty path "" and absolute path "/" are considered equal as + * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically + * do this normalization because in contexts with a trimmed base path, e.g. + * the front controller, this difference becomes significant. It's the task + * of the user to handle both "" and "/". + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.3. + * + * As an example, if the value should include a slash ("/") not intended as + * delimiter between path segments, that value MUST be passed in encoded + * form (e.g., "%2F") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.3 + * @return string The URI path. + */ + public function getPath(); + + /** + * Retrieve the query string of the URI. + * + * If no query string is present, this method MUST return an empty string. + * + * The leading "?" character is not part of the query and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.4. + * + * As an example, if a value in a key/value pair of the query string should + * include an ampersand ("&") not intended as a delimiter between values, + * that value MUST be passed in encoded form (e.g., "%26") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.4 + * @return string The URI query string. + */ + public function getQuery(); + + /** + * Retrieve the fragment component of the URI. + * + * If no fragment is present, this method MUST return an empty string. + * + * The leading "#" character is not part of the fragment and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.5. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.5 + * @return string The URI fragment. + */ + public function getFragment(); + + /** + * Return an instance with the specified scheme. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified scheme. + * + * Implementations MUST support the schemes "http" and "https" case + * insensitively, and MAY accommodate other schemes if required. + * + * An empty scheme is equivalent to removing the scheme. + * + * @param string $scheme The scheme to use with the new instance. + * @return static A new instance with the specified scheme. + * @throws \InvalidArgumentException for invalid or unsupported schemes. + */ + public function withScheme($scheme); + + /** + * Return an instance with the specified user information. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified user information. + * + * Password is optional, but the user information MUST include the + * user; an empty string for the user is equivalent to removing user + * information. + * + * @param string $user The user name to use for authority. + * @param null|string $password The password associated with $user. + * @return static A new instance with the specified user information. + */ + public function withUserInfo($user, $password = null); + + /** + * Return an instance with the specified host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified host. + * + * An empty host value is equivalent to removing the host. + * + * @param string $host The hostname to use with the new instance. + * @return static A new instance with the specified host. + * @throws \InvalidArgumentException for invalid hostnames. + */ + public function withHost($host); + + /** + * Return an instance with the specified port. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified port. + * + * Implementations MUST raise an exception for ports outside the + * established TCP and UDP port ranges. + * + * A null value provided for the port is equivalent to removing the port + * information. + * + * @param null|int $port The port to use with the new instance; a null value + * removes the port information. + * @return static A new instance with the specified port. + * @throws \InvalidArgumentException for invalid ports. + */ + public function withPort($port); + + /** + * Return an instance with the specified path. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified path. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * If the path is intended to be domain-relative rather than path relative then + * it must begin with a slash ("/"). Paths not starting with a slash ("/") + * are assumed to be relative to some base path known to the application or + * consumer. + * + * Users can provide both encoded and decoded path characters. + * Implementations ensure the correct encoding as outlined in getPath(). + * + * @param string $path The path to use with the new instance. + * @return static A new instance with the specified path. + * @throws \InvalidArgumentException for invalid paths. + */ + public function withPath($path); + + /** + * Return an instance with the specified query string. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified query string. + * + * Users can provide both encoded and decoded query characters. + * Implementations ensure the correct encoding as outlined in getQuery(). + * + * An empty query string value is equivalent to removing the query string. + * + * @param string $query The query string to use with the new instance. + * @return static A new instance with the specified query string. + * @throws \InvalidArgumentException for invalid query strings. + */ + public function withQuery($query); + + /** + * Return an instance with the specified URI fragment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified URI fragment. + * + * Users can provide both encoded and decoded fragment characters. + * Implementations ensure the correct encoding as outlined in getFragment(). + * + * An empty fragment value is equivalent to removing the fragment. + * + * @param string $fragment The fragment to use with the new instance. + * @return static A new instance with the specified fragment. + */ + public function withFragment($fragment); + + /** + * Return the string representation as a URI reference. + * + * Depending on which components of the URI are present, the resulting + * string is either a full URI or relative reference according to RFC 3986, + * Section 4.1. The method concatenates the various components of the URI, + * using the appropriate delimiters: + * + * - If a scheme is present, it MUST be suffixed by ":". + * - If an authority is present, it MUST be prefixed by "//". + * - The path can be concatenated without delimiters. But there are two + * cases where the path has to be adjusted to make the URI reference + * valid as PHP does not allow to throw an exception in __toString(): + * - If the path is rootless and an authority is present, the path MUST + * be prefixed by "/". + * - If the path is starting with more than one "/" and no authority is + * present, the starting slashes MUST be reduced to one. + * - If a query is present, it MUST be prefixed by "?". + * - If a fragment is present, it MUST be prefixed by "#". + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + * @return string + */ + public function __toString(); +} diff --git a/vendor/ralouphie/getallheaders/.gitignore b/vendor/ralouphie/getallheaders/.gitignore new file mode 100644 index 00000000..1324e7d3 --- /dev/null +++ b/vendor/ralouphie/getallheaders/.gitignore @@ -0,0 +1,5 @@ +.idea +.DS_store +/vendor/ +composer.phar +composer.lock diff --git a/vendor/ralouphie/getallheaders/LICENSE b/vendor/ralouphie/getallheaders/LICENSE new file mode 100644 index 00000000..be5540c2 --- /dev/null +++ b/vendor/ralouphie/getallheaders/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ralph Khattar + +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/vendor/ralouphie/getallheaders/README.md b/vendor/ralouphie/getallheaders/README.md new file mode 100644 index 00000000..f3329d66 --- /dev/null +++ b/vendor/ralouphie/getallheaders/README.md @@ -0,0 +1,19 @@ +getallheaders +============= + +PHP `getallheaders()` polyfill. Compatible with PHP >= 5.3. + +[![Build Status](https://travis-ci.org/ralouphie/getallheaders.svg?branch=master)](https://travis-ci.org/ralouphie/getallheaders) +[![Coverage Status](https://coveralls.io/repos/ralouphie/getallheaders/badge.png?branch=master)](https://coveralls.io/r/ralouphie/getallheaders?branch=master) +[![Latest Stable Version](https://poser.pugx.org/ralouphie/getallheaders/v/stable.png)](https://packagist.org/packages/ralouphie/getallheaders) +[![Latest Unstable Version](https://poser.pugx.org/ralouphie/getallheaders/v/unstable.png)](https://packagist.org/packages/ralouphie/getallheaders) +[![License](https://poser.pugx.org/ralouphie/getallheaders/license.png)](https://packagist.org/packages/ralouphie/getallheaders) + + +This is a simple polyfill for [`getallheaders()`](http://www.php.net/manual/en/function.getallheaders.php). + +## Install + +``` +composer require ralouphie/getallheaders +``` diff --git a/vendor/ralouphie/getallheaders/composer.json b/vendor/ralouphie/getallheaders/composer.json new file mode 100644 index 00000000..5a0d595c --- /dev/null +++ b/vendor/ralouphie/getallheaders/composer.json @@ -0,0 +1,21 @@ +{ + "name": "ralouphie/getallheaders", + "description": "A polyfill for getallheaders.", + "license": "MIT", + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~3.7.0", + "satooshi/php-coveralls": ">=1.0" + }, + "autoload": { + "files": ["src/getallheaders.php"] + } +} \ No newline at end of file diff --git a/vendor/ralouphie/getallheaders/phpunit.xml b/vendor/ralouphie/getallheaders/phpunit.xml new file mode 100644 index 00000000..7255b23d --- /dev/null +++ b/vendor/ralouphie/getallheaders/phpunit.xml @@ -0,0 +1,22 @@ + + + + ./tests + + + + + src + + + + + + + + \ No newline at end of file diff --git a/vendor/ralouphie/getallheaders/src/getallheaders.php b/vendor/ralouphie/getallheaders/src/getallheaders.php new file mode 100644 index 00000000..c7285a5b --- /dev/null +++ b/vendor/ralouphie/getallheaders/src/getallheaders.php @@ -0,0 +1,46 @@ + 'Content-Type', + 'CONTENT_LENGTH' => 'Content-Length', + 'CONTENT_MD5' => 'Content-Md5', + ); + + foreach ($_SERVER as $key => $value) { + if (substr($key, 0, 5) === 'HTTP_') { + $key = substr($key, 5); + if (!isset($copy_server[$key]) || !isset($_SERVER[$key])) { + $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key)))); + $headers[$key] = $value; + } + } elseif (isset($copy_server[$key])) { + $headers[$copy_server[$key]] = $value; + } + } + + if (!isset($headers['Authorization'])) { + if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { + $headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; + } elseif (isset($_SERVER['PHP_AUTH_USER'])) { + $basic_pass = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : ''; + $headers['Authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $basic_pass); + } elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) { + $headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST']; + } + } + + return $headers; + } + +} diff --git a/vendor/ralouphie/getallheaders/tests/GetAllHeadersTest.php b/vendor/ralouphie/getallheaders/tests/GetAllHeadersTest.php new file mode 100644 index 00000000..8e3d1790 --- /dev/null +++ b/vendor/ralouphie/getallheaders/tests/GetAllHeadersTest.php @@ -0,0 +1,121 @@ + $val) { + $_SERVER[$key] = $val; + } + $result = getallheaders(); + $this->assertEquals($expected, $result, "Error testing $test_type works."); + } + + public function testWorksData() + { + return array( + array( + 'normal case', + array( + 'Key-One' => 'foo', + 'Key-Two' => 'bar', + 'Another-Key-For-Testing' => 'baz' + ), + array( + 'HTTP_KEY_ONE' => 'foo', + 'HTTP_KEY_TWO' => 'bar', + 'HTTP_ANOTHER_KEY_FOR_TESTING' => 'baz' + ) + ), + array( + 'Content-Type', + array( + 'Content-Type' => 'two' + ), + array( + 'HTTP_CONTENT_TYPE' => 'one', + 'CONTENT_TYPE' => 'two' + ) + ), + array( + 'Content-Length', + array( + 'Content-Length' => '222' + ), + array( + 'CONTENT_LENGTH' => '222', + 'HTTP_CONTENT_LENGTH' => '111' + ) + ), + array( + 'Content-Length (HTTP_CONTENT_LENGTH only)', + array( + 'Content-Length' => '111' + ), + array( + 'HTTP_CONTENT_LENGTH' => '111' + ) + ), + array( + 'Content-MD5', + array( + 'Content-Md5' => 'aef123' + ), + array( + 'CONTENT_MD5' => 'aef123', + 'HTTP_CONTENT_MD5' => 'fea321' + ) + ), + array( + 'Content-MD5 (HTTP_CONTENT_MD5 only)', + array( + 'Content-Md5' => 'f123' + ), + array( + 'HTTP_CONTENT_MD5' => 'f123' + ) + ), + array( + 'Authorization (normal)', + array( + 'Authorization' => 'testing' + ), + array( + 'HTTP_AUTHORIZATION' => 'testing', + ) + ), + array( + 'Authorization (redirect)', + array( + 'Authorization' => 'testing redirect' + ), + array( + 'REDIRECT_HTTP_AUTHORIZATION' => 'testing redirect', + ) + ), + array( + 'Authorization (PHP_AUTH_USER + PHP_AUTH_PW)', + array( + 'Authorization' => 'Basic ' . base64_encode('foo:bar') + ), + array( + 'PHP_AUTH_USER' => 'foo', + 'PHP_AUTH_PW' => 'bar' + ) + ), + array( + 'Authorization (PHP_AUTH_DIGEST)', + array( + 'Authorization' => 'example-digest' + ), + array( + 'PHP_AUTH_DIGEST' => 'example-digest' + ) + ) + ); + } +}