From 94d7b80b5a547cc69f14ac8dc8fcb36c5fe74607 Mon Sep 17 00:00:00 2001 From: Johan Dufau Date: Sat, 4 Feb 2017 14:33:25 +0100 Subject: [PATCH 01/10] Updates phpmailer library --- helpers/phpmailer/LICENSE | 504 ++++ helpers/phpmailer/PHPMailerAutoload.php | 13 +- helpers/phpmailer/VERSION | 1 + helpers/phpmailer/class.phpmailer.php | 2188 +++++++++++------ helpers/phpmailer/class.phpmaileroauth.php | 197 ++ .../phpmailer/class.phpmaileroauthgoogle.php | 77 + helpers/phpmailer/class.pop3.php | 202 +- helpers/phpmailer/class.smtp.php | 909 ++++--- helpers/phpmailer/extras/EasyPeasyICS.php | 148 ++ helpers/phpmailer/extras/README.md | 17 + helpers/phpmailer/extras/htmlfilter.php | 1159 +++++++++ helpers/phpmailer/extras/ntlm_sasl_client.php | 185 ++ helpers/phpmailer/get_oauth_token.php | 162 ++ .../phpmailer/language/phpmailer.lang-am.php | 26 + .../phpmailer/language/phpmailer.lang-ar.php | 41 +- .../phpmailer/language/phpmailer.lang-az.php | 26 + .../phpmailer/language/phpmailer.lang-be.php | 26 + .../phpmailer/language/phpmailer.lang-bg.php | 26 + .../phpmailer/language/phpmailer.lang-br.php | 26 - .../phpmailer/language/phpmailer.lang-ca.php | 31 +- .../phpmailer/language/phpmailer.lang-ch.php | 33 +- ...iler.lang-cz.php => phpmailer.lang-cs.php} | 7 +- ...iler.lang-dk.php => phpmailer.lang-da.php} | 11 +- .../phpmailer/language/phpmailer.lang-de.php | 31 +- .../phpmailer/language/phpmailer.lang-el.php | 25 + .../phpmailer/language/phpmailer.lang-eo.php | 7 +- .../phpmailer/language/phpmailer.lang-es.php | 32 +- .../phpmailer/language/phpmailer.lang-et.php | 13 +- .../phpmailer/language/phpmailer.lang-fa.php | 26 +- .../phpmailer/language/phpmailer.lang-fi.php | 11 +- .../phpmailer/language/phpmailer.lang-fo.php | 12 +- .../phpmailer/language/phpmailer.lang-fr.php | 14 +- .../phpmailer/language/phpmailer.lang-gl.php | 26 + .../phpmailer/language/phpmailer.lang-he.php | 11 +- .../phpmailer/language/phpmailer.lang-hr.php | 26 + .../phpmailer/language/phpmailer.lang-hu.php | 8 +- .../phpmailer/language/phpmailer.lang-id.php | 26 + .../phpmailer/language/phpmailer.lang-it.php | 13 +- .../phpmailer/language/phpmailer.lang-ja.php | 35 +- .../phpmailer/language/phpmailer.lang-ka.php | 26 + .../phpmailer/language/phpmailer.lang-ko.php | 26 + .../phpmailer/language/phpmailer.lang-lt.php | 10 +- .../phpmailer/language/phpmailer.lang-lv.php | 26 + .../phpmailer/language/phpmailer.lang-ms.php | 26 + ...iler.lang-no.php => phpmailer.lang-nb.php} | 13 +- .../phpmailer/language/phpmailer.lang-nl.php | 42 +- .../phpmailer/language/phpmailer.lang-pl.php | 14 +- .../phpmailer/language/phpmailer.lang-pt.php | 26 + .../language/phpmailer.lang-pt_br.php | 28 + .../phpmailer/language/phpmailer.lang-ro.php | 44 +- .../phpmailer/language/phpmailer.lang-ru.php | 17 +- .../phpmailer/language/phpmailer.lang-sk.php | 11 +- .../phpmailer/language/phpmailer.lang-sl.php | 26 + .../phpmailer/language/phpmailer.lang-sr.php | 26 + ...iler.lang-se.php => phpmailer.lang-sv.php} | 19 +- .../phpmailer/language/phpmailer.lang-tr.php | 43 +- .../phpmailer/language/phpmailer.lang-uk.php | 17 +- .../phpmailer/language/phpmailer.lang-vi.php | 26 + .../phpmailer/language/phpmailer.lang-zh.php | 47 +- .../language/phpmailer.lang-zh_cn.php | 44 +- 60 files changed, 5397 insertions(+), 1491 deletions(-) create mode 100755 helpers/phpmailer/LICENSE mode change 100644 => 100755 helpers/phpmailer/PHPMailerAutoload.php create mode 100755 helpers/phpmailer/VERSION mode change 100644 => 100755 helpers/phpmailer/class.phpmailer.php create mode 100755 helpers/phpmailer/class.phpmaileroauth.php create mode 100755 helpers/phpmailer/class.phpmaileroauthgoogle.php mode change 100644 => 100755 helpers/phpmailer/class.pop3.php mode change 100644 => 100755 helpers/phpmailer/class.smtp.php create mode 100755 helpers/phpmailer/extras/EasyPeasyICS.php create mode 100755 helpers/phpmailer/extras/README.md create mode 100755 helpers/phpmailer/extras/htmlfilter.php create mode 100755 helpers/phpmailer/extras/ntlm_sasl_client.php create mode 100755 helpers/phpmailer/get_oauth_token.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-am.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-ar.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-az.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-be.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-bg.php delete mode 100644 helpers/phpmailer/language/phpmailer.lang-br.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-ca.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-ch.php rename helpers/phpmailer/language/{phpmailer.lang-cz.php => phpmailer.lang-cs.php} (89%) mode change 100644 => 100755 rename helpers/phpmailer/language/{phpmailer.lang-dk.php => phpmailer.lang-da.php} (81%) mode change 100644 => 100755 mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-de.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-el.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-eo.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-es.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-et.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-fa.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-fi.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-fo.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-fr.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-gl.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-he.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-hr.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-hu.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-id.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-it.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-ja.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-ka.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-ko.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-lt.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-lv.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-ms.php rename helpers/phpmailer/language/{phpmailer.lang-no.php => phpmailer.lang-nb.php} (84%) mode change 100644 => 100755 mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-nl.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-pl.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-pt.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-pt_br.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-ro.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-ru.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-sk.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-sl.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-sr.php rename helpers/phpmailer/language/{phpmailer.lang-se.php => phpmailer.lang-sv.php} (63%) mode change 100644 => 100755 mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-tr.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-uk.php create mode 100755 helpers/phpmailer/language/phpmailer.lang-vi.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-zh.php mode change 100644 => 100755 helpers/phpmailer/language/phpmailer.lang-zh_cn.php diff --git a/helpers/phpmailer/LICENSE b/helpers/phpmailer/LICENSE new file mode 100755 index 00000000..8e0763d1 --- /dev/null +++ b/helpers/phpmailer/LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. 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 library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/helpers/phpmailer/PHPMailerAutoload.php b/helpers/phpmailer/PHPMailerAutoload.php old mode 100644 new mode 100755 index be0dad25..eaa2e303 --- a/helpers/phpmailer/PHPMailerAutoload.php +++ b/helpers/phpmailer/PHPMailerAutoload.php @@ -1,14 +1,14 @@ + * @link https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project + * @author Marcus Bointon (Synchro/coolbru) * @author Jim Jagielski (jimjag) * @author Andy Prevost (codeworxtech) * @author Brent R. Matzelle (original founder) - * @copyright 2013 Marcus Bointon + * @copyright 2012 - 2014 Marcus Bointon * @copyright 2010 - 2012 Jim Jagielski * @copyright 2004 - 2009 Andy Prevost * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License @@ -38,7 +38,10 @@ function PHPMailerAutoload($classname) spl_autoload_register('PHPMailerAutoload'); } } else { - //Fall back to traditional autoload for old PHP versions + /** + * Fall back to traditional autoload for old PHP versions + * @param string $classname The name of the class to load + */ function __autoload($classname) { PHPMailerAutoload($classname); diff --git a/helpers/phpmailer/VERSION b/helpers/phpmailer/VERSION new file mode 100755 index 00000000..567eefa2 --- /dev/null +++ b/helpers/phpmailer/VERSION @@ -0,0 +1 @@ +5.2.21 diff --git a/helpers/phpmailer/class.phpmailer.php b/helpers/phpmailer/class.phpmailer.php old mode 100644 new mode 100755 index 2f6831a8..8ff13f11 --- a/helpers/phpmailer/class.phpmailer.php +++ b/helpers/phpmailer/class.phpmailer.php @@ -1,15 +1,14 @@ + * @link https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project + * @author Marcus Bointon (Synchro/coolbru) * @author Jim Jagielski (jimjag) * @author Andy Prevost (codeworxtech) * @author Brent R. Matzelle (original founder) - * @copyright 2013 Marcus Bointon + * @copyright 2012 - 2014 Marcus Bointon * @copyright 2010 - 2012 Jim Jagielski * @copyright 2004 - 2009 Andy Prevost * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License @@ -18,98 +17,94 @@ * FITNESS FOR A PARTICULAR PURPOSE. */ -if (version_compare(PHP_VERSION, '5.0.0', '<')) { - exit("Sorry, PHPMailer will only run on PHP version 5 or greater!\n"); -} - /** * PHPMailer - PHP email creation and transport class. - * PHP Version 5.0.0 * @package PHPMailer - * @author Marcus Bointon (coolbru) + * @author Marcus Bointon (Synchro/coolbru) * @author Jim Jagielski (jimjag) * @author Andy Prevost (codeworxtech) * @author Brent R. Matzelle (original founder) - * @copyright 2013 Marcus Bointon - * @copyright 2010 - 2012 Jim Jagielski - * @copyright 2004 - 2009 Andy Prevost */ class PHPMailer { /** * The PHPMailer Version number. - * @type string + * @var string */ - public $Version = '5.2.7'; + public $Version = '5.2.21'; /** * Email priority. - * Options: 1 = High, 3 = Normal, 5 = low. - * @type int + * Options: null (default), 1 = High, 3 = Normal, 5 = low. + * When null, the header is not set at all. + * @var integer */ - public $Priority = 3; + public $Priority = null; /** * The character set of the message. - * @type string + * @var string */ public $CharSet = 'iso-8859-1'; /** * The MIME Content-type of the message. - * @type string + * @var string */ public $ContentType = 'text/plain'; /** * The message encoding. * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable". - * @type string + * @var string */ public $Encoding = '8bit'; /** * Holds the most recent mailer error message. - * @type string + * @var string */ public $ErrorInfo = ''; /** * The From email address for the message. - * @type string + * @var string */ public $From = 'root@localhost'; /** * The From name of the message. - * @type string + * @var string */ public $FromName = 'Root User'; /** * The Sender email (Return-Path) of the message. * If not empty, will be sent via -f to sendmail or as 'MAIL FROM' in smtp mode. - * @type string + * @var string */ public $Sender = ''; /** * The Return-Path of the message. * If empty, it will be set to either From or Sender. - * @type string + * @var string + * @deprecated Email senders should never set a return-path header; + * it's the receiver's job (RFC5321 section 4.4), so this no longer does anything. + * @link https://tools.ietf.org/html/rfc5321#section-4.4 RFC5321 reference */ public $ReturnPath = ''; /** * The Subject of the message. - * @type string + * @var string */ public $Subject = ''; /** * An HTML or plain text message body. * If HTML then call isHTML(true). - * @type string + * @var string */ public $Body = ''; @@ -118,7 +113,7 @@ class PHPMailer * This body can be read by mail clients that do not have HTML email * capability such as mutt & Eudora. * Clients that can read HTML will view the normal Body. - * @type string + * @var string */ public $AltBody = ''; @@ -128,91 +123,95 @@ class PHPMailer * To generate iCal events, use the bundled extras/EasyPeasyICS.php class or iCalcreator * @link http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/ * @link http://kigkonsult.se/iCalcreator/ - * @type string + * @var string */ public $Ical = ''; /** * The complete compiled MIME message body. * @access protected - * @type string + * @var string */ protected $MIMEBody = ''; /** * The complete compiled MIME message headers. - * @type string + * @var string * @access protected */ protected $MIMEHeader = ''; /** * Extra headers that createHeader() doesn't fold in. - * @type string + * @var string * @access protected */ protected $mailHeader = ''; /** * Word-wrap the message body to this number of chars. - * @type int + * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance. + * @var integer */ public $WordWrap = 0; /** * Which method to use to send mail. * Options: "mail", "sendmail", or "smtp". - * @type string + * @var string */ public $Mailer = 'mail'; /** * The path to the sendmail program. - * @type string + * @var string */ public $Sendmail = '/usr/sbin/sendmail'; /** * Whether mail() uses a fully sendmail-compatible MTA. * One which supports sendmail's "-oi -f" options. - * @type bool + * @var boolean */ public $UseSendmailOptions = true; /** * Path to PHPMailer plugins. * Useful if the SMTP class is not in the PHP include path. - * @type string + * @var string * @deprecated Should not be needed now there is an autoloader. */ public $PluginDir = ''; /** - * The email address that a reading confirmation should be sent to. - * @type string + * The email address that a reading confirmation should be sent to, also known as read receipt. + * @var string */ public $ConfirmReadingTo = ''; /** - * The hostname to use in Message-Id and Received headers - * and as default HELO string. - * If empty, the value returned - * by SERVER_NAME is used or 'localhost.localdomain'. - * @type string + * The hostname to use in the Message-ID header and as default HELO string. + * If empty, PHPMailer attempts to find one with, in order, + * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value + * 'localhost.localdomain'. + * @var string */ public $Hostname = ''; /** - * An ID to be used in the Message-Id header. + * An ID to be used in the Message-ID header. * If empty, a unique id will be generated. - * @type string + * You can set your own, but it must be in the format "", + * as defined in RFC5322 section 3.6.4 or it will be ignored. + * @see https://tools.ietf.org/html/rfc5322#section-3.6.4 + * @var string */ public $MessageID = ''; /** * The message Date to be used in the Date header. * If empty, the current date will be added. - * @type string + * @var string */ public $MessageDate = ''; @@ -222,130 +221,164 @@ class PHPMailer * You can also specify a different port * for each host by using this format: [hostname:port] * (e.g. "smtp1.example.com:25;smtp2.example.com"). + * You can also specify encryption type, for example: + * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465"). * Hosts will be tried in order. - * @type string + * @var string */ public $Host = 'localhost'; /** * The default SMTP server port. - * @type int - * @Todo Why is this needed when the SMTP class takes care of it? + * @var integer + * @TODO Why is this needed when the SMTP class takes care of it? */ public $Port = 25; /** * The SMTP HELO of the message. - * Default is $Hostname. - * @type string + * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find + * one with the same method described above for $Hostname. + * @var string * @see PHPMailer::$Hostname */ public $Helo = ''; /** - * The secure connection prefix. - * Options: "", "ssl" or "tls" - * @type string + * What kind of encryption to use on the SMTP connection. + * Options: '', 'ssl' or 'tls' + * @var string */ public $SMTPSecure = ''; + /** + * Whether to enable TLS encryption automatically if a server supports it, + * even if `SMTPSecure` is not set to 'tls'. + * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid. + * @var boolean + */ + public $SMTPAutoTLS = true; + /** * Whether to use SMTP authentication. * Uses the Username and Password properties. - * @type bool + * @var boolean * @see PHPMailer::$Username * @see PHPMailer::$Password */ public $SMTPAuth = false; + /** + * Options array passed to stream_context_create when connecting via SMTP. + * @var array + */ + public $SMTPOptions = array(); + /** * SMTP username. - * @type string + * @var string */ public $Username = ''; /** * SMTP password. - * @type string + * @var string */ public $Password = ''; /** * SMTP auth type. - * Options are LOGIN (default), PLAIN, NTLM, CRAM-MD5 - * @type string + * Options are CRAM-MD5, LOGIN, PLAIN, NTLM, XOAUTH2, attempted in that order if not specified + * @var string */ public $AuthType = ''; /** * SMTP realm. * Used for NTLM auth - * @type string + * @var string */ public $Realm = ''; /** * SMTP workstation. * Used for NTLM auth - * @type string + * @var string */ public $Workstation = ''; /** * The SMTP server timeout in seconds. - * @type int + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2 + * @var integer */ - public $Timeout = 10; + public $Timeout = 300; /** * SMTP class debug output mode. - * Options: 0 = off, 1 = commands, 2 = commands and data - * @type int + * Debug output level. + * Options: + * * `0` No output + * * `1` Commands + * * `2` Data and commands + * * `3` As 2 plus connection status + * * `4` Low-level data output + * @var integer * @see SMTP::$do_debug */ public $SMTPDebug = 0; /** - * The function/method to use for debugging output. - * Options: "echo" or "error_log" - * @type string + * How to handle debug output. + * Options: + * * `echo` Output plain-text as-is, appropriate for CLI + * * `html` Output escaped, line breaks converted to `
`, appropriate for browser output + * * `error_log` Output to error log as configured in php.ini + * + * Alternatively, you can provide a callable expecting two params: a message string and the debug level: + * + * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; + * + * @var string|callable * @see SMTP::$Debugoutput */ - public $Debugoutput = "echo"; + public $Debugoutput = 'echo'; /** * Whether to keep SMTP connection open after each message. * If this is set to true then to close the connection * requires an explicit call to smtpClose(). - * @type bool + * @var boolean */ public $SMTPKeepAlive = false; /** * Whether to split multiple to addresses into multiple messages * or send them all in one message. - * @type bool + * Only supported in `mail` and `sendmail` transports, not in SMTP. + * @var boolean */ public $SingleTo = false; /** * Storage for addresses when SingleTo is enabled. - * @type array - * @todo This should really not be public + * @var array + * @TODO This should really not be public */ public $SingleToArray = array(); /** * Whether to generate VERP addresses on send. * Only applicable when sending via SMTP. - * @link http://en.wikipedia.org/wiki/Variable_envelope_return_path - * @type bool + * @link https://en.wikipedia.org/wiki/Variable_envelope_return_path + * @link http://www.postfix.org/VERP_README.html Postfix VERP info + * @var boolean */ public $do_verp = false; /** * Whether to allow sending messages with an empty body. - * @type bool + * @var boolean */ public $AllowEmpty = false; @@ -353,227 +386,278 @@ class PHPMailer * The default line ending. * @note The default remains "\n". We force CRLF where we know * it must be used via self::CRLF. - * @type string + * @var string */ public $LE = "\n"; /** * DKIM selector. - * @type string + * @var string */ public $DKIM_selector = ''; /** * DKIM Identity. - * Usually the email address used as the source of the email - * @type string + * Usually the email address used as the source of the email. + * @var string */ public $DKIM_identity = ''; /** * DKIM passphrase. * Used if your key is encrypted. - * @type string + * @var string */ public $DKIM_passphrase = ''; /** * DKIM signing domain name. * @example 'example.com' - * @type string + * @var string */ public $DKIM_domain = ''; /** * DKIM private key file path. - * @type string + * @var string */ public $DKIM_private = ''; + /** + * DKIM private key string. + * If set, takes precedence over `$DKIM_private`. + * @var string + */ + public $DKIM_private_string = ''; + /** * Callback Action function name. * * The function that handles the result of the send email action. * It is called out by send() for each email sent. * - * Value can be: - * - 'function_name' for function names - * - 'Class::Method' for static method calls - * - array($object, 'Method') for calling methods on $object - * See http://php.net/is_callable manual page for more details. + * Value can be any php callable: http://www.php.net/is_callable * * Parameters: - * bool $result result of the send action + * boolean $result result of the send action * string $to email address of the recipient * string $cc cc email addresses * string $bcc bcc email addresses * string $subject the subject * string $body the email body * string $from email address of sender - * - * @type string + * @var string */ public $action_function = ''; /** - * What to use in the X-Mailer header. - * Options: null for default, whitespace for none, or a string to use - * @type string + * What to put in the X-Mailer header. + * Options: An empty string for PHPMailer default, whitespace for none, or a string to use + * @var string */ public $XMailer = ''; + /** + * Which validator to use by default when validating email addresses. + * May be a callable to inject your own validator, but there are several built-in validators. + * @see PHPMailer::validateAddress() + * @var string|callable + * @static + */ + public static $validator = 'auto'; + /** * An instance of the SMTP sender class. - * @type SMTP + * @var SMTP * @access protected */ protected $smtp = null; /** - * The array of 'to' addresses. - * @type array + * The array of 'to' names and addresses. + * @var array * @access protected */ protected $to = array(); /** - * The array of 'cc' addresses. - * @type array + * The array of 'cc' names and addresses. + * @var array * @access protected */ protected $cc = array(); /** - * The array of 'bcc' addresses. - * @type array + * The array of 'bcc' names and addresses. + * @var array * @access protected */ protected $bcc = array(); /** * The array of reply-to names and addresses. - * @type array + * @var array * @access protected */ protected $ReplyTo = array(); /** * An array of all kinds of addresses. - * Includes all of $to, $cc, $bcc, $replyto - * @type array + * Includes all of $to, $cc, $bcc + * @var array * @access protected + * @see PHPMailer::$to @see PHPMailer::$cc @see PHPMailer::$bcc */ protected $all_recipients = array(); + /** + * An array of names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $all_recipients + * and one of $to, $cc, or $bcc. + * This array is used only for addresses with IDN. + * @var array + * @access protected + * @see PHPMailer::$to @see PHPMailer::$cc @see PHPMailer::$bcc + * @see PHPMailer::$all_recipients + */ + protected $RecipientsQueue = array(); + + /** + * An array of reply-to names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $ReplyTo. + * This array is used only for addresses with IDN. + * @var array + * @access protected + * @see PHPMailer::$ReplyTo + */ + protected $ReplyToQueue = array(); + /** * The array of attachments. - * @type array + * @var array * @access protected */ protected $attachment = array(); /** * The array of custom headers. - * @type array + * @var array * @access protected */ protected $CustomHeader = array(); /** * The most recent Message-ID (including angular brackets). - * @type string + * @var string * @access protected */ protected $lastMessageID = ''; /** * The message's MIME type. - * @type string + * @var string * @access protected */ protected $message_type = ''; /** * The array of MIME boundary strings. - * @type array + * @var array * @access protected */ protected $boundary = array(); /** * The array of available languages. - * @type array + * @var array * @access protected */ protected $language = array(); /** * The number of errors encountered. - * @type integer + * @var integer * @access protected */ protected $error_count = 0; /** * The S/MIME certificate file path. - * @type string + * @var string * @access protected */ protected $sign_cert_file = ''; /** * The S/MIME key file path. - * @type string + * @var string * @access protected */ protected $sign_key_file = ''; + /** + * The optional S/MIME extra certificates ("CA Chain") file path. + * @var string + * @access protected + */ + protected $sign_extracerts_file = ''; + /** * The S/MIME password for the key. * Used only if the key is encrypted. - * @type string + * @var string * @access protected */ protected $sign_key_pass = ''; /** * Whether to throw exceptions for errors. - * @type bool + * @var boolean * @access protected */ protected $exceptions = false; /** - * Error severity: message only, continue processing + * Unique ID used for message ID and boundaries. + * @var string + * @access protected + */ + protected $uniqueid = ''; + + /** + * Error severity: message only, continue processing. */ const STOP_MESSAGE = 0; /** - * Error severity: message, likely ok to continue processing + * Error severity: message, likely ok to continue processing. */ const STOP_CONTINUE = 1; /** - * Error severity: message, plus full stop, critical error reached + * Error severity: message, plus full stop, critical error reached. */ const STOP_CRITICAL = 2; /** - * SMTP RFC standard line ending + * SMTP RFC standard line ending. */ const CRLF = "\r\n"; /** - * Constructor - * @param bool $exceptions Should we throw external exceptions? + * The maximum line length allowed by RFC 2822 section 2.1.1 + * @var integer */ - public function __construct($exceptions = false) + const MAX_LINE_LENGTH = 998; + + /** + * Constructor. + * @param boolean $exceptions Should we throw external exceptions? + */ + public function __construct($exceptions = null) { - $this->exceptions = ($exceptions == true); - //Make sure our autoloader is loaded - if (version_compare(PHP_VERSION, '5.1.2', '>=') and - !spl_autoload_functions() || !in_array('PHPMailerAutoload', spl_autoload_functions())) { - require 'PHPMailerAutoload.php'; + if ($exceptions !== null) { + $this->exceptions = (boolean)$exceptions; } } @@ -582,9 +666,8 @@ public function __construct($exceptions = false) */ public function __destruct() { - if ($this->Mailer == 'smtp') { //close any open SMTP connection nicely - $this->smtpClose(); - } + //Close any open SMTP connection nicely + $this->smtpClose(); } /** @@ -598,7 +681,7 @@ public function __destruct() * @param string $header Additional Header(s) * @param string $params Params * @access private - * @return bool + * @return boolean */ private function mailPassthru($to, $subject, $body, $header, $params) { @@ -608,49 +691,67 @@ private function mailPassthru($to, $subject, $body, $header, $params) } else { $subject = $this->encodeHeader($this->secureHeader($subject)); } - if (ini_get('safe_mode') || !($this->UseSendmailOptions)) { - $rt = @mail($to, $subject, $body, $header); + + //Can't use additional_parameters in safe_mode, calling mail() with null params breaks + //@link http://php.net/manual/en/function.mail.php + if (ini_get('safe_mode') or !$this->UseSendmailOptions or is_null($params)) { + $result = @mail($to, $subject, $body, $header); } else { - $rt = @mail($to, $subject, $body, $header, $params); + $result = @mail($to, $subject, $body, $header, $params); } - return $rt; + return $result; } - /** * Output debugging info via user-defined method. - * Only if debug output is enabled. + * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug). * @see PHPMailer::$Debugoutput * @see PHPMailer::$SMTPDebug * @param string $str */ protected function edebug($str) { - if (!$this->SMTPDebug) { + if ($this->SMTPDebug <= 0) { + return; + } + //Avoid clash with built-in function names + if (!in_array($this->Debugoutput, array('error_log', 'html', 'echo')) and is_callable($this->Debugoutput)) { + call_user_func($this->Debugoutput, $str, $this->SMTPDebug); return; } switch ($this->Debugoutput) { case 'error_log': + //Don't output, just log error_log($str); break; case 'html': - //Cleans up output a bit for a better looking display that's HTML-safe - echo htmlentities(preg_replace('/[\r\n]+/', '', $str), ENT_QUOTES, $this->CharSet) . "
\n"; + //Cleans up output a bit for a better looking, HTML-safe output + echo htmlentities( + preg_replace('/[\r\n]+/', '', $str), + ENT_QUOTES, + 'UTF-8' + ) + . "
\n"; break; case 'echo': default: - //Just echoes exactly what was received - echo $str; + //Normalize line breaks + $str = preg_replace('/\r\n?/ms', "\n", $str); + echo gmdate('Y-m-d H:i:s') . "\t" . str_replace( + "\n", + "\n \t ", + trim($str) + ) . "\n"; } } /** * Sets message type to HTML or plain. - * @param bool $ishtml True for HTML mode. + * @param boolean $isHtml True for HTML mode. * @return void */ - public function isHTML($ishtml = true) + public function isHTML($isHtml = true) { - if ($ishtml) { + if ($isHtml) { $this->ContentType = 'text/html'; } else { $this->ContentType = 'text/plain'; @@ -681,8 +782,12 @@ public function isMail() */ public function isSendmail() { - if (!stristr(ini_get('sendmail_path'), 'sendmail')) { + $ini_sendmail_path = ini_get('sendmail_path'); + + if (!stristr($ini_sendmail_path, 'sendmail')) { $this->Sendmail = '/usr/sbin/sendmail'; + } else { + $this->Sendmail = $ini_sendmail_path; } $this->Mailer = 'sendmail'; } @@ -693,90 +798,140 @@ public function isSendmail() */ public function isQmail() { - if (!stristr(ini_get('sendmail_path'), 'qmail')) { + $ini_sendmail_path = ini_get('sendmail_path'); + + if (!stristr($ini_sendmail_path, 'qmail')) { $this->Sendmail = '/var/qmail/bin/qmail-inject'; + } else { + $this->Sendmail = $ini_sendmail_path; } $this->Mailer = 'qmail'; } /** * Add a "To" address. - * @param string $address + * @param string $address The email address to send to * @param string $name - * @return bool true on success, false if address already used + * @return boolean true on success, false if address already used or invalid in some way */ public function addAddress($address, $name = '') { - return $this->addAnAddress('to', $address, $name); + return $this->addOrEnqueueAnAddress('to', $address, $name); } /** * Add a "CC" address. * @note: This function works with the SMTP mailer on win32, not with the "mail" mailer. - * @param string $address + * @param string $address The email address to send to * @param string $name - * @return bool true on success, false if address already used + * @return boolean true on success, false if address already used or invalid in some way */ public function addCC($address, $name = '') { - return $this->addAnAddress('cc', $address, $name); + return $this->addOrEnqueueAnAddress('cc', $address, $name); } /** * Add a "BCC" address. * @note: This function works with the SMTP mailer on win32, not with the "mail" mailer. - * @param string $address + * @param string $address The email address to send to * @param string $name - * @return bool true on success, false if address already used + * @return boolean true on success, false if address already used or invalid in some way */ public function addBCC($address, $name = '') { - return $this->addAnAddress('bcc', $address, $name); + return $this->addOrEnqueueAnAddress('bcc', $address, $name); } /** - * Add a "Reply-to" address. - * @param string $address + * Add a "Reply-To" address. + * @param string $address The email address to reply to * @param string $name - * @return bool + * @return boolean true on success, false if address already used or invalid in some way */ public function addReplyTo($address, $name = '') { - return $this->addAnAddress('Reply-To', $address, $name); + return $this->addOrEnqueueAnAddress('Reply-To', $address, $name); } /** - * Add an address to one of the recipient arrays. - * Addresses that have been added already return false, but do not throw exceptions - * @param string $kind One of 'to', 'cc', 'bcc', 'ReplyTo' - * @param string $address The email address to send to + * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer + * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still + * be modified after calling this function), addition of such addresses is delayed until send(). + * Addresses that have been added already return false, but do not throw exceptions. + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to + * @param string $name + * @throws phpmailerException + * @return boolean true on success, false if address already used or invalid in some way + * @access protected + */ + protected function addOrEnqueueAnAddress($kind, $address, $name) + { + $address = trim($address); + $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + if (($pos = strrpos($address, '@')) === false) { + // At-sign is misssing. + $error_message = $this->lang('invalid_address') . " (addAnAddress $kind): $address"; + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new phpmailerException($error_message); + } + return false; + } + $params = array($kind, $address, $name); + // Enqueue addresses with IDN until we know the PHPMailer::$CharSet. + if ($this->has8bitChars(substr($address, ++$pos)) and $this->idnSupported()) { + if ($kind != 'Reply-To') { + if (!array_key_exists($address, $this->RecipientsQueue)) { + $this->RecipientsQueue[$address] = $params; + return true; + } + } else { + if (!array_key_exists($address, $this->ReplyToQueue)) { + $this->ReplyToQueue[$address] = $params; + return true; + } + } + return false; + } + // Immediately add standard addresses without IDN. + return call_user_func_array(array($this, 'addAnAddress'), $params); + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. + * Addresses that have been added already return false, but do not throw exceptions. + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to * @param string $name * @throws phpmailerException - * @return bool true on success, false if address already used or invalid in some way + * @return boolean true on success, false if address already used or invalid in some way * @access protected */ protected function addAnAddress($kind, $address, $name = '') { - if (!preg_match('/^(to|cc|bcc|Reply-To)$/', $kind)) { - $this->setError($this->lang('Invalid recipient array') . ': ' . $kind); + if (!in_array($kind, array('to', 'cc', 'bcc', 'Reply-To'))) { + $error_message = $this->lang('Invalid recipient kind: ') . $kind; + $this->setError($error_message); + $this->edebug($error_message); if ($this->exceptions) { - throw new phpmailerException('Invalid recipient array: ' . $kind); + throw new phpmailerException($error_message); } - $this->edebug($this->lang('Invalid recipient array') . ': ' . $kind); return false; } - $address = trim($address); - $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim if (!$this->validateAddress($address)) { - $this->setError($this->lang('invalid_address') . ': ' . $address); + $error_message = $this->lang('invalid_address') . " (addAnAddress $kind): $address"; + $this->setError($error_message); + $this->edebug($error_message); if ($this->exceptions) { - throw new phpmailerException($this->lang('invalid_address') . ': ' . $address); + throw new phpmailerException($error_message); } - $this->edebug($this->lang('invalid_address') . ': ' . $address); return false; } if ($kind != 'Reply-To') { - if (!isset($this->all_recipients[strtolower($address)])) { + if (!array_key_exists(strtolower($address), $this->all_recipients)) { array_push($this->$kind, array($address, $name)); $this->all_recipients[strtolower($address)] = true; return true; @@ -790,24 +945,83 @@ protected function addAnAddress($kind, $address, $name = '') return false; } + /** + * Parse and validate a string containing one or more RFC822-style comma-separated email addresses + * of the form "display name
" into an array of name/address pairs. + * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available. + * Note that quotes in the name part are removed. + * @param string $addrstr The address list string + * @param bool $useimap Whether to use the IMAP extension to parse the list + * @return array + * @link http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation + */ + public function parseAddresses($addrstr, $useimap = true) + { + $addresses = array(); + if ($useimap and function_exists('imap_rfc822_parse_adrlist')) { + //Use this built-in parser if it's available + $list = imap_rfc822_parse_adrlist($addrstr, ''); + foreach ($list as $address) { + if ($address->host != '.SYNTAX-ERROR.') { + if ($this->validateAddress($address->mailbox . '@' . $address->host)) { + $addresses[] = array( + 'name' => (property_exists($address, 'personal') ? $address->personal : ''), + 'address' => $address->mailbox . '@' . $address->host + ); + } + } + } + } else { + //Use this simpler parser + $list = explode(',', $addrstr); + foreach ($list as $address) { + $address = trim($address); + //Is there a separate name part? + if (strpos($address, '<') === false) { + //No separate name, just use the whole thing + if ($this->validateAddress($address)) { + $addresses[] = array( + 'name' => '', + 'address' => $address + ); + } + } else { + list($name, $email) = explode('<', $address); + $email = trim(str_replace('>', '', $email)); + if ($this->validateAddress($email)) { + $addresses[] = array( + 'name' => trim(str_replace(array('"', "'"), '', $name)), + 'address' => $email + ); + } + } + } + } + return $addresses; + } + /** * Set the From and FromName properties. * @param string $address * @param string $name - * @param bool $auto Whether to also set the Sender address, defaults to true + * @param boolean $auto Whether to also set the Sender address, defaults to true * @throws phpmailerException - * @return bool + * @return boolean */ public function setFrom($address, $name = '', $auto = true) { $address = trim($address); $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim - if (!$this->validateAddress($address)) { - $this->setError($this->lang('invalid_address') . ': ' . $address); + // Don't validate now addresses with IDN. Will be done in send(). + if (($pos = strrpos($address, '@')) === false or + (!$this->has8bitChars(substr($address, ++$pos)) or !$this->idnSupported()) and + !$this->validateAddress($address)) { + $error_message = $this->lang('invalid_address') . " (setFrom) $address"; + $this->setError($error_message); + $this->edebug($error_message); if ($this->exceptions) { - throw new phpmailerException($this->lang('invalid_address') . ': ' . $address); + throw new phpmailerException($error_message); } - $this->edebug($this->lang('invalid_address') . ': ' . $address); return false; } $this->From = $address; @@ -835,28 +1049,47 @@ public function getLastMessageID() /** * Check that a string looks like an email address. * @param string $address The email address to check - * @param string $patternselect A selector for the validation pattern to use : - * 'auto' - pick best one automatically; - * 'pcre8' - use the squiloople.com pattern, requires PCRE > 8.0, PHP >= 5.3.2, 5.2.14; - * 'pcre' - use old PCRE implementation; - * 'php' - use PHP built-in FILTER_VALIDATE_EMAIL; faster, less thorough; - * 'noregex' - super fast, really dumb. - * @return bool + * @param string|callable $patternselect A selector for the validation pattern to use : + * * `auto` Pick best pattern automatically; + * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0, PHP >= 5.3.2, 5.2.14; + * * `pcre` Use old PCRE implementation; + * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL; + * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements. + * * `noregex` Don't use a regex: super fast, really dumb. + * Alternatively you may pass in a callable to inject your own validator, for example: + * PHPMailer::validateAddress('user@example.com', function($address) { + * return (strpos($address, '@') !== false); + * }); + * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator. + * @return boolean * @static * @access public */ - public static function validateAddress($address, $patternselect = 'auto') + public static function validateAddress($address, $patternselect = null) { - if ($patternselect == 'auto') { - if (defined( - 'PCRE_VERSION' - ) - ) { //Check this instead of extension_loaded so it works when that function is disabled - if (version_compare(PCRE_VERSION, '8.0') >= 0) { + if (is_null($patternselect)) { + $patternselect = self::$validator; + } + if (is_callable($patternselect)) { + return call_user_func($patternselect, $address); + } + //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321 + if (strpos($address, "\n") !== false or strpos($address, "\r") !== false) { + return false; + } + if (!$patternselect or $patternselect == 'auto') { + //Check this constant first so it works when extension_loaded() is disabled by safe mode + //Constant was added in PHP 5.2.4 + if (defined('PCRE_VERSION')) { + //This pattern can get stuck in a recursive loop in PCRE <= 8.0.2 + if (version_compare(PCRE_VERSION, '8.0.3') >= 0) { $patternselect = 'pcre8'; } else { $patternselect = 'pcre'; } + } elseif (function_exists('extension_loaded') and extension_loaded('pcre')) { + //Fall back to older PCRE + $patternselect = 'pcre'; } else { //Filter_var appeared in PHP 5.2.0 and does not require the PCRE extension if (version_compare(PHP_VERSION, '5.2.0') >= 0) { @@ -869,14 +1102,12 @@ public static function validateAddress($address, $patternselect = 'auto') switch ($patternselect) { case 'pcre8': /** - * Conforms to RFC5322: Uses *correct* regex on which FILTER_VALIDATE_EMAIL is - * based; So why not use FILTER_VALIDATE_EMAIL? Because it was broken to - * not allow a@b type valid addresses :( + * Uses the same RFC5322 regex on which FILTER_VALIDATE_EMAIL is based, but allows dotless domains. * @link http://squiloople.com/2009/12/20/email-address-validation/ * @copyright 2009-2010 Michael Rushton * Feel free to use and redistribute this code. But please keep this copyright notice. */ - return (bool)preg_match( + return (boolean)preg_match( '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' . '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' . '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' . @@ -888,10 +1119,9 @@ public static function validateAddress($address, $patternselect = 'auto') '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD', $address ); - break; case 'pcre': //An older regex that doesn't need a recent PCRE - return (bool)preg_match( + return (boolean)preg_match( '/^(?!(?>"?(?>\\\[ -~]|[^"])"?){255,})(?!(?>"?(?>\\\[ -~]|[^"])"?){65,}@)(?>' . '[!#-\'*+\/-9=?^-~-]+|"(?>(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\xFF]))*")' . '(?>\.(?>[!#-\'*+\/-9=?^-~-]+|"(?>(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\xFF]))*"))*' . @@ -904,26 +1134,75 @@ public static function validateAddress($address, $patternselect = 'auto') '|[1-9]?[0-9])(?>\.(?>25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}))\])$/isD', $address ); - break; - case 'php': - default: - return (bool)filter_var($address, FILTER_VALIDATE_EMAIL); - break; + case 'html5': + /** + * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements. + * @link http://www.whatwg.org/specs/web-apps/current-work/#e-mail-state-(type=email) + */ + return (boolean)preg_match( + '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' . + '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD', + $address + ); case 'noregex': //No PCRE! Do something _very_ approximate! //Check the address is 3 chars or longer and contains an @ that's not the first or last char return (strlen($address) >= 3 and strpos($address, '@') >= 1 and strpos($address, '@') != strlen($address) - 1); - break; + case 'php': + default: + return (boolean)filter_var($address, FILTER_VALIDATE_EMAIL); + } + } + + /** + * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the + * "intl" and "mbstring" PHP extensions. + * @return bool "true" if required functions for IDN support are present + */ + public function idnSupported() + { + // @TODO: Write our own "idn_to_ascii" function for PHP <= 5.2. + return function_exists('idn_to_ascii') and function_exists('mb_convert_encoding'); + } + + /** + * Converts IDN in given email address to its ASCII form, also known as punycode, if possible. + * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet. + * This function silently returns unmodified address if: + * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form) + * - Conversion to punycode is impossible (e.g. required PHP functions are not available) + * or fails for any reason (e.g. domain has characters not allowed in an IDN) + * @see PHPMailer::$CharSet + * @param string $address The email address to convert + * @return string The encoded address in ASCII form + */ + public function punyencodeAddress($address) + { + // Verify we have required functions, CharSet, and at-sign. + if ($this->idnSupported() and + !empty($this->CharSet) and + ($pos = strrpos($address, '@')) !== false) { + $domain = substr($address, ++$pos); + // Verify CharSet string is a valid one, and domain properly encoded in this CharSet. + if ($this->has8bitChars($domain) and @mb_check_encoding($domain, $this->CharSet)) { + $domain = mb_convert_encoding($domain, 'UTF-8', $this->CharSet); + if (($punycode = defined('INTL_IDNA_VARIANT_UTS46') ? + idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46) : + idn_to_ascii($domain)) !== false) { + return substr($address, 0, $pos) . $punycode; + } + } } + return $address; } /** * Create a message and send it. * Uses the sending method specified by $Mailer. * @throws phpmailerException - * @return bool false on error - See the ErrorInfo property for details of the error. + * @return boolean false on error - See the ErrorInfo property for details of the error. */ public function send() { @@ -932,11 +1211,11 @@ public function send() return false; } return $this->postSend(); - } catch (phpmailerException $e) { + } catch (phpmailerException $exc) { $this->mailHeader = ''; - $this->setError($e->getMessage()); + $this->setError($exc->getMessage()); if ($this->exceptions) { - throw $e; + throw $exc; } return false; } @@ -945,38 +1224,67 @@ public function send() /** * Prepare a message for sending. * @throws phpmailerException - * @return bool + * @return boolean */ public function preSend() { try { - $this->mailHeader = ""; + $this->error_count = 0; // Reset errors + $this->mailHeader = ''; + + // Dequeue recipient and Reply-To addresses with IDN + foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) { + $params[1] = $this->punyencodeAddress($params[1]); + call_user_func_array(array($this, 'addAnAddress'), $params); + } if ((count($this->to) + count($this->cc) + count($this->bcc)) < 1) { throw new phpmailerException($this->lang('provide_address'), self::STOP_CRITICAL); } + // Validate From, Sender, and ConfirmReadingTo addresses + foreach (array('From', 'Sender', 'ConfirmReadingTo') as $address_kind) { + $this->$address_kind = trim($this->$address_kind); + if (empty($this->$address_kind)) { + continue; + } + $this->$address_kind = $this->punyencodeAddress($this->$address_kind); + if (!$this->validateAddress($this->$address_kind)) { + $error_message = $this->lang('invalid_address') . ' (punyEncode) ' . $this->$address_kind; + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new phpmailerException($error_message); + } + return false; + } + } + // Set whether the message is multipart/alternative - if (!empty($this->AltBody)) { + if ($this->alternativeExists()) { $this->ContentType = 'multipart/alternative'; } - $this->error_count = 0; // reset errors $this->setMessageType(); // Refuse to send an empty message unless we are specifically allowing it if (!$this->AllowEmpty and empty($this->Body)) { throw new phpmailerException($this->lang('empty_message'), self::STOP_CRITICAL); } - $this->MIMEHeader = $this->createHeader(); + // Create body before headers in case body makes changes to headers (e.g. altering transfer encoding) + $this->MIMEHeader = ''; $this->MIMEBody = $this->createBody(); + // createBody may have added some headers, so retain them + $tempheaders = $this->MIMEHeader; + $this->MIMEHeader = $this->createHeader(); + $this->MIMEHeader .= $tempheaders; // To capture the complete message when using mail(), create // an extra header list which createHeader() doesn't fold in if ($this->Mailer == 'mail') { if (count($this->to) > 0) { - $this->mailHeader .= $this->addrAppend("To", $this->to); + $this->mailHeader .= $this->addrAppend('To', $this->to); } else { - $this->mailHeader .= $this->headerLine("To", "undisclosed-recipients:;"); + $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;'); } $this->mailHeader .= $this->headerLine( 'Subject', @@ -986,10 +1294,11 @@ public function preSend() // Sign with DKIM if enabled if (!empty($this->DKIM_domain) - && !empty($this->DKIM_private) && !empty($this->DKIM_selector) - && !empty($this->DKIM_domain) - && file_exists($this->DKIM_private)) { + && (!empty($this->DKIM_private_string) + || (!empty($this->DKIM_private) && file_exists($this->DKIM_private)) + ) + ) { $header_dkim = $this->DKIM_Add( $this->MIMEHeader . $this->mailHeader, $this->encodeHeader($this->secureHeader($this->Subject)), @@ -999,11 +1308,10 @@ public function preSend() str_replace("\r\n", "\n", $header_dkim) . self::CRLF; } return true; - - } catch (phpmailerException $e) { - $this->setError($e->getMessage()); + } catch (phpmailerException $exc) { + $this->setError($exc->getMessage()); if ($this->exceptions) { - throw $e; + throw $exc; } return false; } @@ -1013,7 +1321,7 @@ public function preSend() * Actually send a message. * Send the email via the selected mechanism * @throws phpmailerException - * @return bool + * @return boolean */ public function postSend() { @@ -1028,19 +1336,19 @@ public function postSend() case 'mail': return $this->mailSend($this->MIMEHeader, $this->MIMEBody); default: - if (method_exists($this, $this->Mailer.'Send')) { - $sendMethod = $this->Mailer.'Send'; + $sendMethod = $this->Mailer.'Send'; + if (method_exists($this, $sendMethod)) { return $this->$sendMethod($this->MIMEHeader, $this->MIMEBody); - } else { - return $this->mailSend($this->MIMEHeader, $this->MIMEBody); } + + return $this->mailSend($this->MIMEHeader, $this->MIMEBody); } - } catch (phpmailerException $e) { - $this->setError($e->getMessage()); + } catch (phpmailerException $exc) { + $this->setError($exc->getMessage()); + $this->edebug($exc->getMessage()); if ($this->exceptions) { - throw $e; + throw $exc; } - $this->edebug($e->getMessage() . "\n"); } return false; } @@ -1052,35 +1360,46 @@ public function postSend() * @see PHPMailer::$Sendmail * @throws phpmailerException * @access protected - * @return bool + * @return boolean */ protected function sendmailSend($header, $body) { - if ($this->Sender != '') { + // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + if (!empty($this->Sender) and self::isShellSafe($this->Sender)) { if ($this->Mailer == 'qmail') { - $sendmail = sprintf("%s -f%s", escapeshellcmd($this->Sendmail), escapeshellarg($this->Sender)); + $sendmailFmt = '%s -f%s'; } else { - $sendmail = sprintf("%s -oi -f%s -t", escapeshellcmd($this->Sendmail), escapeshellarg($this->Sender)); + $sendmailFmt = '%s -oi -f%s -t'; } } else { if ($this->Mailer == 'qmail') { - $sendmail = sprintf("%s", escapeshellcmd($this->Sendmail)); + $sendmailFmt = '%s'; } else { - $sendmail = sprintf("%s -oi -t", escapeshellcmd($this->Sendmail)); + $sendmailFmt = '%s -oi -t'; } } - if ($this->SingleTo === true) { - foreach ($this->SingleToArray as $val) { + + // TODO: If possible, this should be changed to escapeshellarg. Needs thorough testing. + $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender); + + if ($this->SingleTo) { + foreach ($this->SingleToArray as $toAddr) { if (!@$mail = popen($sendmail, 'w')) { throw new phpmailerException($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } - fputs($mail, "To: " . $val . "\n"); + fputs($mail, 'To: ' . $toAddr . "\n"); fputs($mail, $header); fputs($mail, $body); $result = pclose($mail); - // implement call back function if it exists - $isSent = ($result == 0) ? 1 : 0; - $this->doCallback($isSent, $val, $this->cc, $this->bcc, $this->Subject, $body, $this->From); + $this->doCallback( + ($result == 0), + array($toAddr), + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From + ); if ($result != 0) { throw new phpmailerException($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } @@ -1092,9 +1411,15 @@ protected function sendmailSend($header, $body) fputs($mail, $header); fputs($mail, $body); $result = pclose($mail); - // implement call back function if it exists - $isSent = ($result == 0) ? 1 : 0; - $this->doCallback($isSent, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From); + $this->doCallback( + ($result == 0), + $this->to, + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From + ); if ($result != 0) { throw new phpmailerException($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } @@ -1102,6 +1427,40 @@ protected function sendmailSend($header, $body) return true; } + /** + * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters. + * + * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows. + * @param string $string The string to be validated + * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report + * @access protected + * @return boolean + */ + protected static function isShellSafe($string) + { + // Future-proof + if (escapeshellcmd($string) !== $string + or !in_array(escapeshellarg($string), array("'$string'", "\"$string\"")) + ) { + return false; + } + + $length = strlen($string); + + for ($i = 0; $i < $length; $i++) { + $c = $string[$i]; + + // All other characters have a special meaning in at least one common shell, including = and +. + // Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here. + // Note that this does permit non-Latin alphanumeric characters based on the current locale. + if (!ctype_alnum($c) && strpos('@_-.', $c) === false) { + return false; + } + } + + return true; + } + /** * Send mail using the PHP mail() function. * @param string $header The message headers @@ -1109,43 +1468,42 @@ protected function sendmailSend($header, $body) * @link http://www.php.net/manual/en/book.mail.php * @throws phpmailerException * @access protected - * @return bool + * @return boolean */ protected function mailSend($header, $body) { $toArr = array(); - foreach ($this->to as $t) { - $toArr[] = $this->addrFormat($t); + foreach ($this->to as $toaddr) { + $toArr[] = $this->addrFormat($toaddr); } $to = implode(', ', $toArr); - if (empty($this->Sender)) { - $params = " "; - } else { - $params = sprintf("-f%s", $this->Sender); + $params = null; + //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver + if (!empty($this->Sender) and $this->validateAddress($this->Sender)) { + // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + if (self::isShellSafe($this->Sender)) { + $params = sprintf('-f%s', $this->Sender); + } } - if ($this->Sender != '' and !ini_get('safe_mode')) { + if (!empty($this->Sender) and !ini_get('safe_mode') and $this->validateAddress($this->Sender)) { $old_from = ini_get('sendmail_from'); ini_set('sendmail_from', $this->Sender); } - $rt = false; - if ($this->SingleTo === true && count($toArr) > 1) { - foreach ($toArr as $val) { - $rt = $this->mailPassthru($val, $this->Subject, $body, $header, $params); - // implement call back function if it exists - $isSent = ($rt == 1) ? 1 : 0; - $this->doCallback($isSent, $val, $this->cc, $this->bcc, $this->Subject, $body, $this->From); + $result = false; + if ($this->SingleTo and count($toArr) > 1) { + foreach ($toArr as $toAddr) { + $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params); + $this->doCallback($result, array($toAddr), $this->cc, $this->bcc, $this->Subject, $body, $this->From); } } else { - $rt = $this->mailPassthru($to, $this->Subject, $body, $header, $params); - // implement call back function if it exists - $isSent = ($rt == 1) ? 1 : 0; - $this->doCallback($isSent, $to, $this->cc, $this->bcc, $this->Subject, $body, $this->From); + $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params); + $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From); } if (isset($old_from)) { ini_set('sendmail_from', $old_from); } - if (!$rt) { + if (!$result) { throw new phpmailerException($this->lang('instantiate'), self::STOP_CRITICAL); } return true; @@ -1174,62 +1532,56 @@ public function getSMTPInstance() * @throws phpmailerException * @uses SMTP * @access protected - * @return bool + * @return boolean */ protected function smtpSend($header, $body) { $bad_rcpt = array(); - - if (!$this->smtpConnect()) { + if (!$this->smtpConnect($this->SMTPOptions)) { throw new phpmailerException($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); } - $smtp_from = ($this->Sender == '') ? $this->From : $this->Sender; + if (!empty($this->Sender) and $this->validateAddress($this->Sender)) { + $smtp_from = $this->Sender; + } else { + $smtp_from = $this->From; + } if (!$this->smtp->mail($smtp_from)) { $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); throw new phpmailerException($this->ErrorInfo, self::STOP_CRITICAL); } - // Attempt to send attach all recipients - foreach ($this->to as $to) { - if (!$this->smtp->recipient($to[0])) { - $bad_rcpt[] = $to[0]; - $isSent = 0; - } else { - $isSent = 1; - } - $this->doCallback($isSent, $to[0], '', '', $this->Subject, $body, $this->From); - } - foreach ($this->cc as $cc) { - if (!$this->smtp->recipient($cc[0])) { - $bad_rcpt[] = $cc[0]; - $isSent = 0; - } else { - $isSent = 1; - } - $this->doCallback($isSent, '', $cc[0], '', $this->Subject, $body, $this->From); - } - foreach ($this->bcc as $bcc) { - if (!$this->smtp->recipient($bcc[0])) { - $bad_rcpt[] = $bcc[0]; - $isSent = 0; - } else { - $isSent = 1; + // Attempt to send to all recipients + foreach (array($this->to, $this->cc, $this->bcc) as $togroup) { + foreach ($togroup as $to) { + if (!$this->smtp->recipient($to[0])) { + $error = $this->smtp->getError(); + $bad_rcpt[] = array('to' => $to[0], 'error' => $error['detail']); + $isSent = false; + } else { + $isSent = true; + } + $this->doCallback($isSent, array($to[0]), array(), array(), $this->Subject, $body, $this->From); } - $this->doCallback($isSent, '', '', $bcc[0], $this->Subject, $body, $this->From); } - if (!$this->smtp->data($header . $body)) { + // Only send the DATA command if we have viable recipients + if ((count($this->all_recipients) > count($bad_rcpt)) and !$this->smtp->data($header . $body)) { throw new phpmailerException($this->lang('data_not_accepted'), self::STOP_CRITICAL); } - if ($this->SMTPKeepAlive == true) { + if ($this->SMTPKeepAlive) { $this->smtp->reset(); } else { $this->smtp->quit(); $this->smtp->close(); } - if (count($bad_rcpt) > 0) { //Create error message for any bad addresses + //Create error message for any bad addresses + if (count($bad_rcpt) > 0) { + $errstr = ''; + foreach ($bad_rcpt as $bad) { + $errstr .= $bad['to'] . ': ' . $bad['error']; + } throw new phpmailerException( - $this->lang('recipients_failed') . implode(', ', $bad_rcpt), + $this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE ); } @@ -1243,15 +1595,20 @@ protected function smtpSend($header, $body) * @uses SMTP * @access public * @throws phpmailerException - * @return bool + * @return boolean */ - public function smtpConnect($options = array()) + public function smtpConnect($options = null) { if (is_null($this->smtp)) { $this->smtp = $this->getSMTPInstance(); } - //Already connected? + //If no options are provided, use whatever is set in the instance + if (is_null($options)) { + $options = $this->SMTPOptions; + } + + // Already connected? if ($this->smtp->connected()) { return true; } @@ -1266,22 +1623,33 @@ public function smtpConnect($options = array()) foreach ($hosts as $hostentry) { $hostinfo = array(); if (!preg_match('/^((ssl|tls):\/\/)*([a-zA-Z0-9\.-]*):?([0-9]*)$/', trim($hostentry), $hostinfo)) { - //Not a valid host entry + // Not a valid host entry continue; } - //$hostinfo[2]: optional ssl or tls prefix - //$hostinfo[3]: the hostname - //$hostinfo[4]: optional port number - //The host string prefix can temporarily override the current setting for SMTPSecure - //If it's not specified, the default value is used + // $hostinfo[2]: optional ssl or tls prefix + // $hostinfo[3]: the hostname + // $hostinfo[4]: optional port number + // The host string prefix can temporarily override the current setting for SMTPSecure + // If it's not specified, the default value is used $prefix = ''; + $secure = $this->SMTPSecure; $tls = ($this->SMTPSecure == 'tls'); - if ($hostinfo[2] == 'ssl' or ($hostinfo[2] == '' and $this->SMTPSecure == 'ssl')) { + if ('ssl' == $hostinfo[2] or ('' == $hostinfo[2] and 'ssl' == $this->SMTPSecure)) { $prefix = 'ssl://'; - $tls = false; //Can't have SSL and TLS at once + $tls = false; // Can't have SSL and TLS at the same time + $secure = 'ssl'; } elseif ($hostinfo[2] == 'tls') { $tls = true; - //tls doesn't use a prefix + // tls doesn't use a prefix + $secure = 'tls'; + } + //Do we need the OpenSSL extension? + $sslext = defined('OPENSSL_ALGO_SHA1'); + if ('tls' === $secure or 'ssl' === $secure) { + //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled + if (!$sslext) { + throw new phpmailerException($this->lang('extension_missing').'openssl', self::STOP_CRITICAL); + } } $host = $hostinfo[3]; $port = $this->Port; @@ -1297,12 +1665,19 @@ public function smtpConnect($options = array()) $hello = $this->serverHostname(); } $this->smtp->hello($hello); - + //Automatically enable TLS encryption if: + // * it's not disabled + // * we have openssl extension + // * we are not already using SSL + // * the server offers STARTTLS + if ($this->SMTPAutoTLS and $sslext and $secure != 'ssl' and $this->smtp->getServerExt('STARTTLS')) { + $tls = true; + } if ($tls) { if (!$this->smtp->startTLS()) { throw new phpmailerException($this->lang('connect_host')); } - //We must resend HELO after tls negotiation + // We must resend EHLO after TLS negotiation $this->smtp->hello($hello); } if ($this->SMTPAuth) { @@ -1318,16 +1693,17 @@ public function smtpConnect($options = array()) } } return true; - } catch (phpmailerException $e) { - $lastexception = $e; - //We must have connected, but then failed TLS or Auth, so close connection nicely + } catch (phpmailerException $exc) { + $lastexception = $exc; + $this->edebug($exc->getMessage()); + // We must have connected, but then failed TLS or Auth, so close connection nicely $this->smtp->quit(); } } } - //If we get here, all connection attempts have failed, so close connection hard + // If we get here, all connection attempts have failed, so close connection hard $this->smtp->close(); - //As we've caught all exceptions, just report whatever the last one was + // As we've caught all exceptions, just report whatever the last one was if ($this->exceptions and !is_null($lastexception)) { throw $lastexception; } @@ -1340,7 +1716,7 @@ public function smtpConnect($options = array()) */ public function smtpClose() { - if ($this->smtp !== null) { + if (is_a($this->smtp, 'SMTP')) { if ($this->smtp->connected()) { $this->smtp->quit(); $this->smtp->close(); @@ -1354,12 +1730,25 @@ public function smtpClose() * The default language is English. * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr") * @param string $lang_path Path to the language file directory, with trailing separator (slash) - * @return bool + * @return boolean * @access public */ - public function setLanguage($langcode = 'en', $lang_path = 'language/') + public function setLanguage($langcode = 'en', $lang_path = '') { - //Define full set of translatable strings + // Backwards compatibility for renamed language codes + $renamed_langcodes = array( + 'br' => 'pt_br', + 'cz' => 'cs', + 'dk' => 'da', + 'no' => 'nb', + 'se' => 'sv', + ); + + if (isset($renamed_langcodes[$langcode])) { + $langcode = $renamed_langcodes[$langcode]; + } + + // Define full set of translatable strings in English $PHPMAILER_LANG = array( 'authenticate' => 'SMTP Error: Could not authenticate.', 'connect_host' => 'SMTP Error: Could not connect to SMTP host.', @@ -1371,23 +1760,39 @@ public function setLanguage($langcode = 'en', $lang_path = 'language/') 'file_open' => 'File Error: Could not open file: ', 'from_failed' => 'The following From address failed: ', 'instantiate' => 'Could not instantiate mail function.', - 'invalid_address' => 'Invalid address', + 'invalid_address' => 'Invalid address: ', 'mailer_not_supported' => ' mailer is not supported.', 'provide_address' => 'You must provide at least one recipient email address.', 'recipients_failed' => 'SMTP Error: The following recipients failed: ', 'signing' => 'Signing Error: ', 'smtp_connect_failed' => 'SMTP connect() failed.', 'smtp_error' => 'SMTP server error: ', - 'variable_set' => 'Cannot set or reset variable: ' + 'variable_set' => 'Cannot set or reset variable: ', + 'extension_missing' => 'Extension missing: ' ); - //Overwrite language-specific strings. - //This way we'll never have missing translations - no more "language string failed to load"! - $l = true; - if ($langcode != 'en') { //There is no English translation file - $l = @include $lang_path . 'phpmailer.lang-' . $langcode . '.php'; + if (empty($lang_path)) { + // Calculate an absolute path so it can work if CWD is not here + $lang_path = dirname(__FILE__). DIRECTORY_SEPARATOR . 'language'. DIRECTORY_SEPARATOR; + } + //Validate $langcode + if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) { + $langcode = 'en'; + } + $foundlang = true; + $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php'; + // There is no English translation file + if ($langcode != 'en') { + // Make sure language file path is readable + if (!is_readable($lang_file)) { + $foundlang = false; + } else { + // Overwrite language-specific strings. + // This way we'll never have missing translation keys. + $foundlang = include $lang_file; + } } $this->language = $PHPMAILER_LANG; - return ($l == true); //Returns false if language not found + return (boolean)$foundlang; // Returns false if language not found } /** @@ -1412,8 +1817,8 @@ public function getTranslations() public function addrAppend($type, $addr) { $addresses = array(); - foreach ($addr as $a) { - $addresses[] = $this->addrFormat($a); + foreach ($addr as $address) { + $addresses[] = $this->addrFormat($address); } return $type . ': ' . implode(', ', $addresses) . $this->LE; } @@ -1430,9 +1835,9 @@ public function addrFormat($addr) if (empty($addr[1])) { // No name provided return $this->secureHeader($addr[0]); } else { - return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . " <" . $this->secureHeader( + return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . ' <' . $this->secureHeader( $addr[0] - ) . ">"; + ) . '>'; } } @@ -1443,47 +1848,54 @@ public function addrFormat($addr) * Original written by philippe. * @param string $message The message to wrap * @param integer $length The line length to wrap to - * @param bool $qp_mode Whether to run in Quoted-Printable mode + * @param boolean $qp_mode Whether to run in Quoted-Printable mode * @access public * @return string */ public function wrapText($message, $length, $qp_mode = false) { - $soft_break = ($qp_mode) ? sprintf(" =%s", $this->LE) : $this->LE; + if ($qp_mode) { + $soft_break = sprintf(' =%s', $this->LE); + } else { + $soft_break = $this->LE; + } // If utf-8 encoding is used, we will need to make sure we don't // split multibyte characters when we wrap - $is_utf8 = (strtolower($this->CharSet) == "utf-8"); + $is_utf8 = (strtolower($this->CharSet) == 'utf-8'); $lelen = strlen($this->LE); $crlflen = strlen(self::CRLF); $message = $this->fixEOL($message); + //Remove a trailing line break if (substr($message, -$lelen) == $this->LE) { $message = substr($message, 0, -$lelen); } - $line = explode($this->LE, $message); // Magic. We know fixEOL uses $LE + //Split message into lines + $lines = explode($this->LE, $message); + //Message will be rebuilt in here $message = ''; - for ($i = 0; $i < count($line); $i++) { - $line_part = explode(' ', $line[$i]); + foreach ($lines as $line) { + $words = explode(' ', $line); $buf = ''; - for ($e = 0; $e < count($line_part); $e++) { - $word = $line_part[$e]; + $firstword = true; + foreach ($words as $word) { if ($qp_mode and (strlen($word) > $length)) { $space_left = $length - strlen($buf) - $crlflen; - if ($e != 0) { + if (!$firstword) { if ($space_left > 20) { $len = $space_left; if ($is_utf8) { $len = $this->utf8CharBoundary($word, $len); - } elseif (substr($word, $len - 1, 1) == "=") { + } elseif (substr($word, $len - 1, 1) == '=') { $len--; - } elseif (substr($word, $len - 2, 1) == "=") { + } elseif (substr($word, $len - 2, 1) == '=') { $len -= 2; } $part = substr($word, 0, $len); $word = substr($word, $len); $buf .= ' ' . $part; - $message .= $buf . sprintf("=%s", self::CRLF); + $message .= $buf . sprintf('=%s', self::CRLF); } else { $message .= $buf . $soft_break; } @@ -1496,29 +1908,33 @@ public function wrapText($message, $length, $qp_mode = false) $len = $length; if ($is_utf8) { $len = $this->utf8CharBoundary($word, $len); - } elseif (substr($word, $len - 1, 1) == "=") { + } elseif (substr($word, $len - 1, 1) == '=') { $len--; - } elseif (substr($word, $len - 2, 1) == "=") { + } elseif (substr($word, $len - 2, 1) == '=') { $len -= 2; } $part = substr($word, 0, $len); $word = substr($word, $len); if (strlen($word) > 0) { - $message .= $part . sprintf("=%s", self::CRLF); + $message .= $part . sprintf('=%s', self::CRLF); } else { $buf = $part; } } } else { $buf_o = $buf; - $buf .= ($e == 0) ? $word : (' ' . $word); + if (!$firstword) { + $buf .= ' '; + } + $buf .= $word; if (strlen($buf) > $length and $buf_o != '') { $message .= $buf_o . $soft_break; $buf = $word; } } + $firstword = false; } $message .= $buf . self::CRLF; } @@ -1528,12 +1944,12 @@ public function wrapText($message, $length, $qp_mode = false) /** * Find the last character boundary prior to $maxLength in a utf-8 - * quoted (printable) encoded string. + * quoted-printable encoded string. * Original written by Colin Brown. * @access public * @param string $encodedText utf-8 QP text - * @param int $maxLength find last character boundary prior to this length - * @return int + * @param integer $maxLength Find the last character boundary prior to this length + * @return integer */ public function utf8CharBoundary($encodedText, $maxLength) { @@ -1541,23 +1957,27 @@ public function utf8CharBoundary($encodedText, $maxLength) $lookBack = 3; while (!$foundSplitPos) { $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack); - $encodedCharPos = strpos($lastChunk, "="); - if ($encodedCharPos !== false) { + $encodedCharPos = strpos($lastChunk, '='); + if (false !== $encodedCharPos) { // Found start of encoded character byte within $lookBack block. // Check the encoded byte value (the 2 chars after the '=') $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2); $dec = hexdec($hex); - if ($dec < 128) { // Single byte character. + if ($dec < 128) { + // Single byte character. // If the encoded char was found at pos 0, it will fit // otherwise reduce maxLength to start of the encoded char - $maxLength = ($encodedCharPos == 0) ? $maxLength : - $maxLength - ($lookBack - $encodedCharPos); + if ($encodedCharPos > 0) { + $maxLength = $maxLength - ($lookBack - $encodedCharPos); + } $foundSplitPos = true; - } elseif ($dec >= 192) { // First byte of a multi byte character + } elseif ($dec >= 192) { + // First byte of a multi byte character // Reduce maxLength to split at start of character $maxLength = $maxLength - ($lookBack - $encodedCharPos); $foundSplitPos = true; - } elseif ($dec < 192) { // Middle byte of a multi byte character, look further back + } elseif ($dec < 192) { + // Middle byte of a multi byte character, look further back $lookBack += 3; } } else { @@ -1568,9 +1988,11 @@ public function utf8CharBoundary($encodedText, $maxLength) return $maxLength; } - /** - * Set the body wrapping. + * Apply word wrapping to the message body. + * Wraps the message body to the number of chars set in the WordWrap property. + * You should only do this to plain-text bodies as wrapping HTML tags may break them. + * This is called automatically by createBody(), so you don't need to call it yourself. * @access public * @return void */ @@ -1602,38 +2024,25 @@ public function createHeader() { $result = ''; - // Set the boundaries - $uniq_id = md5(uniqid(time())); - $this->boundary[1] = 'b1_' . $uniq_id; - $this->boundary[2] = 'b2_' . $uniq_id; - $this->boundary[3] = 'b3_' . $uniq_id; - if ($this->MessageDate == '') { - $result .= $this->headerLine('Date', self::rfcDate()); - } else { - $result .= $this->headerLine('Date', $this->MessageDate); - } - - if ($this->ReturnPath) { - $result .= $this->headerLine('Return-Path', '<' . trim($this->ReturnPath) . '>'); - } elseif ($this->Sender == '') { - $result .= $this->headerLine('Return-Path', '<' . trim($this->From) . '>'); - } else { - $result .= $this->headerLine('Return-Path', '<' . trim($this->Sender) . '>'); + $this->MessageDate = self::rfcDate(); } + $result .= $this->headerLine('Date', $this->MessageDate); // To be created automatically by mail() - if ($this->Mailer != 'mail') { - if ($this->SingleTo === true) { - foreach ($this->to as $t) { - $this->SingleToArray[] = $this->addrFormat($t); + if ($this->SingleTo) { + if ($this->Mailer != 'mail') { + foreach ($this->to as $toaddr) { + $this->SingleToArray[] = $this->addrFormat($toaddr); } - } else { - if (count($this->to) > 0) { + } + } else { + if (count($this->to) > 0) { + if ($this->Mailer != 'mail') { $result .= $this->addrAppend('To', $this->to); - } elseif (count($this->cc) == 0) { - $result .= $this->headerLine('To', 'undisclosed-recipients:;'); } + } elseif (count($this->cc) == 0) { + $result .= $this->headerLine('To', 'undisclosed-recipients:;'); } } @@ -1662,17 +2071,21 @@ public function createHeader() $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject))); } - if ($this->MessageID != '') { + // Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4 + // https://tools.ietf.org/html/rfc5322#section-3.6.4 + if ('' != $this->MessageID and preg_match('/^<.*@.*>$/', $this->MessageID)) { $this->lastMessageID = $this->MessageID; } else { - $this->lastMessageID = sprintf("<%s@%s>", $uniq_id, $this->ServerHostname()); + $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname()); + } + $result .= $this->headerLine('Message-ID', $this->lastMessageID); + if (!is_null($this->Priority)) { + $result .= $this->headerLine('X-Priority', $this->Priority); } - $result .= $this->HeaderLine('Message-ID', $this->lastMessageID); - $result .= $this->headerLine('X-Priority', $this->Priority); if ($this->XMailer == '') { $result .= $this->headerLine( 'X-Mailer', - 'PHPMailer ' . $this->Version . ' (https://github.com/PHPMailer/PHPMailer/)' + 'PHPMailer ' . $this->Version . ' (https://github.com/PHPMailer/PHPMailer)' ); } else { $myXmailer = trim($this->XMailer); @@ -1682,14 +2095,14 @@ public function createHeader() } if ($this->ConfirmReadingTo != '') { - $result .= $this->headerLine('Disposition-Notification-To', '<' . trim($this->ConfirmReadingTo) . '>'); + $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>'); } // Add custom headers - for ($index = 0; $index < count($this->CustomHeader); $index++) { + foreach ($this->CustomHeader as $header) { $result .= $this->headerLine( - trim($this->CustomHeader[$index][0]), - $this->encodeHeader(trim($this->CustomHeader[$index][1])) + trim($header[0]), + $this->encodeHeader(trim($header[1])) ); } if (!$this->sign_key_file) { @@ -1708,6 +2121,7 @@ public function createHeader() public function getMailMIME() { $result = ''; + $ismultipart = true; switch ($this->message_type) { case 'inline': $result .= $this->headerLine('Content-Type', 'multipart/related;'); @@ -1728,11 +2142,20 @@ public function getMailMIME() default: // Catches case 'plain': and case '': $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet); + $ismultipart = false; break; } - //RFC1341 part 5 says 7bit is assumed if not specified + // RFC1341 part 5 says 7bit is assumed if not specified if ($this->Encoding != '7bit') { - $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding); + // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE + if ($ismultipart) { + if ($this->Encoding == '8bit') { + $result .= $this->headerLine('Content-Transfer-Encoding', '8bit'); + } + // The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible + } else { + $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding); + } } if ($this->Mailer != 'mail') { @@ -1745,16 +2168,23 @@ public function getMailMIME() /** * Returns the whole MIME message. * Includes complete headers and body. - * Only valid post PreSend(). - * @see PHPMailer::PreSend() + * Only valid post preSend(). + * @see PHPMailer::preSend() * @access public * @return string */ public function getSentMIMEMessage() { - return $this->MIMEHeader . $this->mailHeader . self::CRLF . $this->MIMEBody; + return rtrim($this->MIMEHeader . $this->mailHeader, "\n\r") . self::CRLF . self::CRLF . $this->MIMEBody; } + /** + * Create unique ID + * @return string + */ + protected function generateId() { + return md5(uniqid(time())); + } /** * Assemble the message body. @@ -1766,6 +2196,11 @@ public function getSentMIMEMessage() public function createBody() { $body = ''; + //Create unique IDs and preset boundaries + $this->uniqueid = $this->generateId(); + $this->boundary[1] = 'b1_' . $this->uniqueid; + $this->boundary[2] = 'b2_' . $this->uniqueid; + $this->boundary[3] = 'b3_' . $this->uniqueid; if ($this->sign_key_file) { $body .= $this->getMailMIME() . $this->LE; @@ -1773,37 +2208,70 @@ public function createBody() $this->setWordWrap(); + $bodyEncoding = $this->Encoding; + $bodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? + if ($bodyEncoding == '8bit' and !$this->has8bitChars($this->Body)) { + $bodyEncoding = '7bit'; + //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit + $bodyCharSet = 'us-ascii'; + } + //If lines are too long, and we're not already using an encoding that will shorten them, + //change to quoted-printable transfer encoding for the body part only + if ('base64' != $this->Encoding and self::hasLineLongerThanMax($this->Body)) { + $bodyEncoding = 'quoted-printable'; + } + + $altBodyEncoding = $this->Encoding; + $altBodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? + if ($altBodyEncoding == '8bit' and !$this->has8bitChars($this->AltBody)) { + $altBodyEncoding = '7bit'; + //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit + $altBodyCharSet = 'us-ascii'; + } + //If lines are too long, and we're not already using an encoding that will shorten them, + //change to quoted-printable transfer encoding for the alt body part only + if ('base64' != $altBodyEncoding and self::hasLineLongerThanMax($this->AltBody)) { + $altBodyEncoding = 'quoted-printable'; + } + //Use this as a preamble in all multipart message types + $mimepre = "This is a multi-part message in MIME format." . $this->LE . $this->LE; switch ($this->message_type) { case 'inline': - $body .= $this->getBoundary($this->boundary[1], '', '', ''); - $body .= $this->encodeString($this->Body, $this->Encoding); + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->attachAll('inline', $this->boundary[1]); break; case 'attach': - $body .= $this->getBoundary($this->boundary[1], '', '', ''); - $body .= $this->encodeString($this->Body, $this->Encoding); + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->attachAll('attachment', $this->boundary[1]); break; case 'inline_attach': + $body .= $mimepre; $body .= $this->textLine('--' . $this->boundary[1]); $body .= $this->headerLine('Content-Type', 'multipart/related;'); $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); $body .= $this->LE; - $body .= $this->getBoundary($this->boundary[2], '', '', ''); - $body .= $this->encodeString($this->Body, $this->Encoding); + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->attachAll('inline', $this->boundary[2]); $body .= $this->LE; $body .= $this->attachAll('attachment', $this->boundary[1]); break; case 'alt': - $body .= $this->getBoundary($this->boundary[1], '', 'text/plain', ''); - $body .= $this->encodeString($this->AltBody, $this->Encoding); + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, 'text/plain', $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); $body .= $this->LE . $this->LE; - $body .= $this->getBoundary($this->boundary[1], '', 'text/html', ''); - $body .= $this->encodeString($this->Body, $this->Encoding); + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, 'text/html', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= $this->LE . $this->LE; if (!empty($this->Ical)) { $body .= $this->getBoundary($this->boundary[1], '', 'text/calendar; method=REQUEST', ''); @@ -1813,49 +2281,52 @@ public function createBody() $body .= $this->endBoundary($this->boundary[1]); break; case 'alt_inline': - $body .= $this->getBoundary($this->boundary[1], '', 'text/plain', ''); - $body .= $this->encodeString($this->AltBody, $this->Encoding); + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, 'text/plain', $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->textLine('--' . $this->boundary[1]); $body .= $this->headerLine('Content-Type', 'multipart/related;'); $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); $body .= $this->LE; - $body .= $this->getBoundary($this->boundary[2], '', 'text/html', ''); - $body .= $this->encodeString($this->Body, $this->Encoding); + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, 'text/html', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->attachAll('inline', $this->boundary[2]); $body .= $this->LE; $body .= $this->endBoundary($this->boundary[1]); break; case 'alt_attach': + $body .= $mimepre; $body .= $this->textLine('--' . $this->boundary[1]); $body .= $this->headerLine('Content-Type', 'multipart/alternative;'); $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); $body .= $this->LE; - $body .= $this->getBoundary($this->boundary[2], '', 'text/plain', ''); - $body .= $this->encodeString($this->AltBody, $this->Encoding); + $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, 'text/plain', $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); $body .= $this->LE . $this->LE; - $body .= $this->getBoundary($this->boundary[2], '', 'text/html', ''); - $body .= $this->encodeString($this->Body, $this->Encoding); + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, 'text/html', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->endBoundary($this->boundary[2]); $body .= $this->LE; $body .= $this->attachAll('attachment', $this->boundary[1]); break; case 'alt_inline_attach': + $body .= $mimepre; $body .= $this->textLine('--' . $this->boundary[1]); $body .= $this->headerLine('Content-Type', 'multipart/alternative;'); $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); $body .= $this->LE; - $body .= $this->getBoundary($this->boundary[2], '', 'text/plain', ''); - $body .= $this->encodeString($this->AltBody, $this->Encoding); + $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, 'text/plain', $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->textLine('--' . $this->boundary[2]); $body .= $this->headerLine('Content-Type', 'multipart/related;'); $body .= $this->textLine("\tboundary=\"" . $this->boundary[3] . '"'); $body .= $this->LE; - $body .= $this->getBoundary($this->boundary[3], '', 'text/html', ''); - $body .= $this->encodeString($this->Body, $this->Encoding); + $body .= $this->getBoundary($this->boundary[3], $bodyCharSet, 'text/html', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->attachAll('inline', $this->boundary[3]); $body .= $this->LE; @@ -1864,7 +2335,9 @@ public function createBody() $body .= $this->attachAll('attachment', $this->boundary[1]); break; default: - // catch case 'plain' and case '' + // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types + //Reset the `Encoding` property in case we changed it for line length reasons + $this->Encoding = $bodyEncoding; $body .= $this->encodeString($this->Body, $this->Encoding); break; } @@ -1874,32 +2347,51 @@ public function createBody() } elseif ($this->sign_key_file) { try { if (!defined('PKCS7_TEXT')) { - throw new phpmailerException($this->lang('signing') . ' OpenSSL extension missing.'); + throw new phpmailerException($this->lang('extension_missing') . 'openssl'); } - //TODO would be nice to use php://temp streams here, but need to wrap for PHP < 5.1 + // @TODO would be nice to use php://temp streams here, but need to wrap for PHP < 5.1 $file = tempnam(sys_get_temp_dir(), 'mail'); - file_put_contents($file, $body); //TODO check this worked + if (false === file_put_contents($file, $body)) { + throw new phpmailerException($this->lang('signing') . ' Could not write temp file'); + } $signed = tempnam(sys_get_temp_dir(), 'signed'); - if (@openssl_pkcs7_sign( - $file, - $signed, - 'file://' . realpath($this->sign_cert_file), - array('file://' . realpath($this->sign_key_file), $this->sign_key_pass), - null - ) - ) { + //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197 + if (empty($this->sign_extracerts_file)) { + $sign = @openssl_pkcs7_sign( + $file, + $signed, + 'file://' . realpath($this->sign_cert_file), + array('file://' . realpath($this->sign_key_file), $this->sign_key_pass), + null + ); + } else { + $sign = @openssl_pkcs7_sign( + $file, + $signed, + 'file://' . realpath($this->sign_cert_file), + array('file://' . realpath($this->sign_key_file), $this->sign_key_pass), + null, + PKCS7_DETACHED, + $this->sign_extracerts_file + ); + } + if ($sign) { @unlink($file); $body = file_get_contents($signed); @unlink($signed); + //The message returned by openssl contains both headers and body, so need to split them up + $parts = explode("\n\n", $body, 2); + $this->MIMEHeader .= $parts[0] . $this->LE . $this->LE; + $body = $parts[1]; } else { @unlink($file); @unlink($signed); throw new phpmailerException($this->lang('signing') . openssl_error_string()); } - } catch (phpmailerException $e) { + } catch (phpmailerException $exc) { $body = ''; if ($this->exceptions) { - throw $e; + throw $exc; } } } @@ -1928,9 +2420,12 @@ protected function getBoundary($boundary, $charSet, $contentType, $encoding) $encoding = $this->Encoding; } $result .= $this->textLine('--' . $boundary); - $result .= sprintf("Content-Type: %s; charset=%s", $contentType, $charSet); + $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet); $result .= $this->LE; - $result .= $this->headerLine('Content-Transfer-Encoding', $encoding); + // RFC1341 part 5 says 7bit is assumed if not specified + if ($encoding != '7bit') { + $result .= $this->headerLine('Content-Transfer-Encoding', $encoding); + } $result .= $this->LE; return $result; @@ -1949,26 +2444,26 @@ protected function endBoundary($boundary) /** * Set the message type. - * PHPMailer only supports some preset message types, - * not arbitrary MIME structures. + * PHPMailer only supports some preset message types, not arbitrary MIME structures. * @access protected * @return void */ protected function setMessageType() { - $this->message_type = array(); + $type = array(); if ($this->alternativeExists()) { - $this->message_type[] = "alt"; + $type[] = 'alt'; } if ($this->inlineImageExists()) { - $this->message_type[] = "inline"; + $type[] = 'inline'; } if ($this->attachmentExists()) { - $this->message_type[] = "attach"; + $type[] = 'attach'; } - $this->message_type = implode("_", $this->message_type); - if ($this->message_type == "") { - $this->message_type = "plain"; + $this->message_type = implode('_', $type); + if ($this->message_type == '') { + //The 'plain' message_type refers to the message having a single body element, not that it is plain-text + $this->message_type = 'plain'; } } @@ -2004,7 +2499,7 @@ public function textLine($value) * @param string $type File extension (MIME) type. * @param string $disposition Disposition to use * @throws phpmailerException - * @return bool + * @return boolean */ public function addAttachment($path, $name = '', $encoding = 'base64', $type = '', $disposition = 'attachment') { @@ -2013,7 +2508,7 @@ public function addAttachment($path, $name = '', $encoding = 'base64', $type = ' throw new phpmailerException($this->lang('file_access') . $path, self::STOP_CONTINUE); } - //If a MIME type is not specified, try to work it out from the file name + // If a MIME type is not specified, try to work it out from the file name if ($type == '') { $type = self::filenameToType($path); } @@ -2034,12 +2529,12 @@ public function addAttachment($path, $name = '', $encoding = 'base64', $type = ' 7 => 0 ); - } catch (phpmailerException $e) { - $this->setError($e->getMessage()); + } catch (phpmailerException $exc) { + $this->setError($exc->getMessage()); + $this->edebug($exc->getMessage()); if ($this->exceptions) { - throw $e; + throw $exc; } - $this->edebug($e->getMessage() . "\n"); return false; } return true; @@ -2093,22 +2588,34 @@ protected function attachAll($disposition_type, $boundary) $type = $attachment[4]; $disposition = $attachment[6]; $cid = $attachment[7]; - if ($disposition == 'inline' && isset($cidUniq[$cid])) { + if ($disposition == 'inline' && array_key_exists($cid, $cidUniq)) { continue; } $cidUniq[$cid] = true; - $mime[] = sprintf("--%s%s", $boundary, $this->LE); - $mime[] = sprintf( - "Content-Type: %s; name=\"%s\"%s", - $type, - $this->encodeHeader($this->secureHeader($name)), - $this->LE - ); - $mime[] = sprintf("Content-Transfer-Encoding: %s%s", $encoding, $this->LE); + $mime[] = sprintf('--%s%s', $boundary, $this->LE); + //Only include a filename property if we have one + if (!empty($name)) { + $mime[] = sprintf( + 'Content-Type: %s; name="%s"%s', + $type, + $this->encodeHeader($this->secureHeader($name)), + $this->LE + ); + } else { + $mime[] = sprintf( + 'Content-Type: %s%s', + $type, + $this->LE + ); + } + // RFC1341 part 5 says 7bit is assumed if not specified + if ($encoding != '7bit') { + $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, $this->LE); + } if ($disposition == 'inline') { - $mime[] = sprintf("Content-ID: <%s>%s", $cid, $this->LE); + $mime[] = sprintf('Content-ID: <%s>%s', $cid, $this->LE); } // If a filename contains any of these chars, it should be quoted, @@ -2116,20 +2623,29 @@ protected function attachAll($disposition_type, $boundary) // Fixes a warning in IETF's msglint MIME checker // Allow for bypassing the Content-Disposition header totally if (!(empty($disposition))) { - if (preg_match('/[ \(\)<>@,;:\\"\/\[\]\?=]/', $name)) { + $encoded_name = $this->encodeHeader($this->secureHeader($name)); + if (preg_match('/[ \(\)<>@,;:\\"\/\[\]\?=]/', $encoded_name)) { $mime[] = sprintf( - "Content-Disposition: %s; filename=\"%s\"%s", + 'Content-Disposition: %s; filename="%s"%s', $disposition, - $this->encodeHeader($this->secureHeader($name)), + $encoded_name, $this->LE . $this->LE ); } else { - $mime[] = sprintf( - "Content-Disposition: %s; filename=%s%s", - $disposition, - $this->encodeHeader($this->secureHeader($name)), - $this->LE . $this->LE - ); + if (!empty($encoded_name)) { + $mime[] = sprintf( + 'Content-Disposition: %s; filename=%s%s', + $disposition, + $encoded_name, + $this->LE . $this->LE + ); + } else { + $mime[] = sprintf( + 'Content-Disposition: %s%s', + $disposition, + $this->LE . $this->LE + ); + } } } else { $mime[] = $this->LE; @@ -2152,9 +2668,9 @@ protected function attachAll($disposition_type, $boundary) } } - $mime[] = sprintf("--%s--%s", $boundary, $this->LE); + $mime[] = sprintf('--%s--%s', $boundary, $this->LE); - return implode("", $mime); + return implode('', $mime); } /** @@ -2163,7 +2679,6 @@ protected function attachAll($disposition_type, $boundary) * @param string $path The full path to the file * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' * @throws phpmailerException - * @see EncodeFile(encodeFile * @access protected * @return string */ @@ -2176,9 +2691,12 @@ protected function encodeFile($path, $encoding = 'base64') $magic_quotes = get_magic_quotes_runtime(); if ($magic_quotes) { if (version_compare(PHP_VERSION, '5.3.0', '<')) { - set_magic_quotes_runtime(0); + set_magic_quotes_runtime(false); } else { - ini_set('magic_quotes_runtime', 0); + //Doesn't exist in PHP 5.4, but we don't need to check because + //get_magic_quotes_runtime always returns false in 5.4+ + //so it will never get here + ini_set('magic_quotes_runtime', false); } } $file_buffer = file_get_contents($path); @@ -2191,8 +2709,8 @@ protected function encodeFile($path, $encoding = 'base64') } } return $file_buffer; - } catch (Exception $e) { - $this->setError($e->getMessage()); + } catch (Exception $exc) { + $this->setError($exc->getMessage()); return ''; } } @@ -2215,7 +2733,7 @@ public function encodeString($str, $encoding = 'base64') case '7bit': case '8bit': $encoded = $this->fixEOL($str); - //Make sure it ends with a line break + // Make sure it ends with a line break if (substr($encoded, -(strlen($this->LE))) != $this->LE) { $encoded .= $this->LE; } @@ -2243,11 +2761,11 @@ public function encodeString($str, $encoding = 'base64') */ public function encodeHeader($str, $position = 'text') { - $x = 0; + $matchcount = 0; switch (strtolower($position)) { case 'phrase': if (!preg_match('/[\200-\377]/', $str)) { - // Can't use addslashes as we don't know what value has magic_quotes_sybase + // Can't use addslashes as we don't know the value of magic_quotes_sybase $encoded = addcslashes($str, "\0..\37\177\\\""); if (($str == $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) { return ($encoded); @@ -2255,26 +2773,27 @@ public function encodeHeader($str, $position = 'text') return ("\"$encoded\""); } } - $x = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches); + $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches); break; /** @noinspection PhpMissingBreakStatementInspection */ case 'comment': - $x = preg_match_all('/[()"]/', $str, $matches); + $matchcount = preg_match_all('/[()"]/', $str, $matches); // Intentional fall-through case 'text': default: - $x += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches); + $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches); break; } - if ($x == 0) { //There are no chars that need encoding + //There are no chars that need encoding + if ($matchcount == 0) { return ($str); } $maxlen = 75 - 7 - strlen($this->CharSet); // Try to select the encoding which should produce the shortest output - if ($x > strlen($str) / 3) { - //More than a third of the content will need encoding, so B encoding will be most efficient + if ($matchcount > strlen($str) / 3) { + // More than a third of the content will need encoding, so B encoding will be most efficient $encoding = 'B'; if (function_exists('mb_strlen') && $this->hasMultiBytes($str)) { // Use a custom function which correctly encodes and wraps long @@ -2292,7 +2811,7 @@ public function encodeHeader($str, $position = 'text') $encoded = str_replace('=' . self::CRLF, "\n", trim($encoded)); } - $encoded = preg_replace('/^(.*)$/m', " =?" . $this->CharSet . "?$encoding?\\1?=", $encoded); + $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); $encoded = trim(str_replace("\n", $this->LE, $encoded)); return $encoded; @@ -2302,7 +2821,7 @@ public function encodeHeader($str, $position = 'text') * Check if a string contains multi-byte characters. * @access public * @param string $str multi-byte text to wrap encode - * @return bool + * @return boolean */ public function hasMultiBytes($str) { @@ -2313,22 +2832,33 @@ public function hasMultiBytes($str) } } + /** + * Does a string contain any 8-bit chars (in any charset)? + * @param string $text + * @return boolean + */ + public function has8bitChars($text) + { + return (boolean)preg_match('/[\x80-\xFF]/', $text); + } + /** * Encode and wrap long multibyte strings for mail headers * without breaking lines within a character. - * Adapted from a function by paravoid at http://uk.php.net/manual/en/function.mb-encode-mimeheader.php + * Adapted from a function by paravoid + * @link http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283 * @access public * @param string $str multi-byte text to wrap encode - * @param string $lf string to use as linefeed/end-of-line + * @param string $linebreak string to use as linefeed/end-of-line * @return string */ - public function base64EncodeWrapMB($str, $lf = null) + public function base64EncodeWrapMB($str, $linebreak = null) { - $start = "=?" . $this->CharSet . "?B?"; - $end = "?="; - $encoded = ""; - if ($lf === null) { - $lf = $this->LE; + $start = '=?' . $this->CharSet . '?B?'; + $end = '?='; + $encoded = ''; + if ($linebreak === null) { + $linebreak = $this->LE; } $mb_length = mb_strlen($str, $this->CharSet); @@ -2347,11 +2877,11 @@ public function base64EncodeWrapMB($str, $lf = null) $chunk = base64_encode($chunk); $lookBack++; } while (strlen($chunk) > $length); - $encoded .= $chunk . $lf; + $encoded .= $chunk . $linebreak; } // Chomp the last linefeed - $encoded = substr($encoded, 0, -strlen($lf)); + $encoded = substr($encoded, 0, -strlen($linebreak)); return $encoded; } @@ -2362,21 +2892,21 @@ public function base64EncodeWrapMB($str, $lf = null) * @param string $string The text to encode * @param integer $line_max Number of chars allowed on a line before wrapping * @return string - * @link PHP version adapted from http://www.php.net/manual/en/function.quoted-printable-decode.php#89417 + * @link http://www.php.net/manual/en/function.quoted-printable-decode.php#89417 Adapted from this comment */ public function encodeQP($string, $line_max = 76) { - if (function_exists('quoted_printable_encode')) { //Use native function if it's available (>= PHP5.3) + // Use native function if it's available (>= PHP5.3) + if (function_exists('quoted_printable_encode')) { return quoted_printable_encode($string); } - //Fall back to a pure PHP implementation + // Fall back to a pure PHP implementation $string = str_replace( array('%20', '%0D%0A.', '%0D%0A', '%'), array(' ', "\r\n=2E", "\r\n", '='), rawurlencode($string) ); - $string = preg_replace('/[^\r\n]{' . ($line_max - 3) . '}[^=\r\n]{2}/', "$0=\r\n", $string); - return $string; + return preg_replace('/[^\r\n]{' . ($line_max - 3) . '}[^=\r\n]{2}/', "$0=\r\n", $string); } /** @@ -2385,7 +2915,7 @@ public function encodeQP($string, $line_max = 76) * @access public * @param string $string * @param integer $line_max - * @param bool $space_conv + * @param boolean $space_conv * @return string * @deprecated Use encodeQP instead. */ @@ -2407,45 +2937,44 @@ public function encodeQPphp( */ public function encodeQ($str, $position = 'text') { - //There should not be any EOL in the string + // There should not be any EOL in the string $pattern = ''; $encoded = str_replace(array("\r", "\n"), '', $str); switch (strtolower($position)) { case 'phrase': - //RFC 2047 section 5.3 + // RFC 2047 section 5.3 $pattern = '^A-Za-z0-9!*+\/ -'; break; /** @noinspection PhpMissingBreakStatementInspection */ case 'comment': - //RFC 2047 section 5.2 + // RFC 2047 section 5.2 $pattern = '\(\)"'; - //intentional fall-through - //for this reason we build the $pattern without including delimiters and [] + // intentional fall-through + // for this reason we build the $pattern without including delimiters and [] case 'text': default: - //RFC 2047 section 5.1 - //Replace every high ascii, control, =, ? and _ characters + // RFC 2047 section 5.1 + // Replace every high ascii, control, =, ? and _ characters $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern; break; } $matches = array(); if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) { - //If the string contains an '=', make sure it's the first thing we replace - //so as to avoid double-encoding - $s = array_search('=', $matches[0]); - if ($s !== false) { - unset($matches[0][$s]); + // If the string contains an '=', make sure it's the first thing we replace + // so as to avoid double-encoding + $eqkey = array_search('=', $matches[0]); + if (false !== $eqkey) { + unset($matches[0][$eqkey]); array_unshift($matches[0], '='); } foreach (array_unique($matches[0]) as $char) { $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded); } } - //Replace every spaces to _ (more readable than =20) + // Replace every spaces to _ (more readable than =20) return str_replace(' ', '_', $encoded); } - /** * Add a string or binary attachment (non-filesystem). * This method can be used to attach ascii or binary data, @@ -2464,7 +2993,7 @@ public function addStringAttachment( $type = '', $disposition = 'attachment' ) { - //If a MIME type is not specified, try to work it out from the file name + // If a MIME type is not specified, try to work it out from the file name if ($type == '') { $type = self::filenameToType($filename); } @@ -2484,7 +3013,7 @@ public function addStringAttachment( /** * Add an embedded (inline) attachment from a file. * This can include images, sounds, and just about any other document type. - * These differ from 'regular' attachmants in that they are intended to be + * These differ from 'regular' attachments in that they are intended to be * displayed inline with the message, not just attached for download. * This is used in HTML messages that embed the images * the HTML refers to using the $cid value. @@ -2495,7 +3024,7 @@ public function addStringAttachment( * @param string $encoding File encoding (see $Encoding). * @param string $type File MIME type. * @param string $disposition Disposition to use - * @return bool True on successfully adding an attachment + * @return boolean True on successfully adding an attachment */ public function addEmbeddedImage($path, $cid, $name = '', $encoding = 'base64', $type = '', $disposition = 'inline') { @@ -2504,7 +3033,7 @@ public function addEmbeddedImage($path, $cid, $name = '', $encoding = 'base64', return false; } - //If a MIME type is not specified, try to work it out from the file name + // If a MIME type is not specified, try to work it out from the file name if ($type == '') { $type = self::filenameToType($path); } @@ -2540,7 +3069,7 @@ public function addEmbeddedImage($path, $cid, $name = '', $encoding = 'base64', * @param string $encoding File encoding (see $Encoding). * @param string $type MIME type. * @param string $disposition Disposition to use - * @return bool True on successfully adding an attachment + * @return boolean True on successfully adding an attachment */ public function addStringEmbeddedImage( $string, @@ -2550,8 +3079,8 @@ public function addStringEmbeddedImage( $type = '', $disposition = 'inline' ) { - //If a MIME type is not specified, try to work it out from the name - if ($type == '') { + // If a MIME type is not specified, try to work it out from the name + if ($type == '' and !empty($name)) { $type = self::filenameToType($name); } @@ -2572,7 +3101,7 @@ public function addStringEmbeddedImage( /** * Check if an inline attachment is present. * @access public - * @return bool + * @return boolean */ public function inlineImageExists() { @@ -2586,7 +3115,7 @@ public function inlineImageExists() /** * Check if an attachment (non-inline) is present. - * @return bool + * @return boolean */ public function attachmentExists() { @@ -2600,13 +3129,29 @@ public function attachmentExists() /** * Check if this message has an alternative body set. - * @return bool + * @return boolean */ public function alternativeExists() { return !empty($this->AltBody); } + /** + * Clear queued addresses of given kind. + * @access protected + * @param string $kind 'to', 'cc', or 'bcc' + * @return void + */ + public function clearQueuedAddresses($kind) + { + $RecipientsQueue = $this->RecipientsQueue; + foreach ($RecipientsQueue as $address => $params) { + if ($params[0] == $kind) { + unset($this->RecipientsQueue[$address]); + } + } + } + /** * Clear all To recipients. * @return void @@ -2617,6 +3162,7 @@ public function clearAddresses() unset($this->all_recipients[strtolower($to[0])]); } $this->to = array(); + $this->clearQueuedAddresses('to'); } /** @@ -2629,6 +3175,7 @@ public function clearCCs() unset($this->all_recipients[strtolower($cc[0])]); } $this->cc = array(); + $this->clearQueuedAddresses('cc'); } /** @@ -2641,6 +3188,7 @@ public function clearBCCs() unset($this->all_recipients[strtolower($bcc[0])]); } $this->bcc = array(); + $this->clearQueuedAddresses('bcc'); } /** @@ -2650,6 +3198,7 @@ public function clearBCCs() public function clearReplyTos() { $this->ReplyTo = array(); + $this->ReplyToQueue = array(); } /** @@ -2662,6 +3211,7 @@ public function clearAllRecipients() $this->cc = array(); $this->bcc = array(); $this->all_recipients = array(); + $this->RecipientsQueue = array(); } /** @@ -2693,8 +3243,17 @@ protected function setError($msg) $this->error_count++; if ($this->Mailer == 'smtp' and !is_null($this->smtp)) { $lasterror = $this->smtp->getError(); - if (!empty($lasterror) and array_key_exists('smtp_msg', $lasterror)) { - $msg .= '

' . $this->lang('smtp_error') . $lasterror['smtp_msg'] . "

\n"; + if (!empty($lasterror['error'])) { + $msg .= $this->lang('smtp_error') . $lasterror['error']; + if (!empty($lasterror['detail'])) { + $msg .= ' Detail: '. $lasterror['detail']; + } + if (!empty($lasterror['smtp_code'])) { + $msg .= ' SMTP code: ' . $lasterror['smtp_code']; + } + if (!empty($lasterror['smtp_code_ex'])) { + $msg .= ' Additional SMTP info: ' . $lasterror['smtp_code_ex']; + } } } $this->ErrorInfo = $msg; @@ -2708,8 +3267,8 @@ protected function setError($msg) */ public static function rfcDate() { - //Set the time zone to whatever the default is to avoid 500 errors - //Will default to UTC if it's not set properly in php.ini + // Set the time zone to whatever the default is to avoid 500 errors + // Will default to UTC if it's not set properly in php.ini date_default_timezone_set(@date_default_timezone_get()); return date('D, j M Y H:i:s O'); } @@ -2722,14 +3281,16 @@ public static function rfcDate() */ protected function serverHostname() { + $result = 'localhost.localdomain'; if (!empty($this->Hostname)) { $result = $this->Hostname; - } elseif (isset($_SERVER['SERVER_NAME'])) { + } elseif (isset($_SERVER) and array_key_exists('SERVER_NAME', $_SERVER) and !empty($_SERVER['SERVER_NAME'])) { $result = $_SERVER['SERVER_NAME']; - } else { - $result = 'localhost.localdomain'; + } elseif (function_exists('gethostname') && gethostname() !== false) { + $result = gethostname(); + } elseif (php_uname('n') !== false) { + $result = php_uname('n'); } - return $result; } @@ -2745,17 +3306,24 @@ protected function lang($key) $this->setLanguage('en'); // set the default language } - if (isset($this->language[$key])) { + if (array_key_exists($key, $this->language)) { + if ($key == 'smtp_connect_failed') { + //Include a link to troubleshooting docs on SMTP connection failure + //this is by far the biggest cause of support questions + //but it's usually not PHPMailer's fault. + return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; + } return $this->language[$key]; } else { - return 'Language string failed to load: ' . $key; + //Return the key as a fallback + return $key; } } /** * Check if an error occurred. * @access public - * @return bool True if an error did occur. + * @return boolean True if an error did occur. */ public function isError() { @@ -2800,29 +3368,58 @@ public function addCustomHeader($name, $value = null) } /** - * Create a message from an HTML string. - * Automatically makes modifications for inline images and backgrounds - * and creates a plain-text version by converting the HTML. - * Overwrites any existing values in $this->Body and $this->AltBody + * Returns all custom headers. + * @return array + */ + public function getCustomHeaders() + { + return $this->CustomHeader; + } + + /** + * Create a message body from an HTML string. + * Automatically inlines images and creates a plain-text version by converting the HTML, + * overwriting any existing values in Body and AltBody. + * $basedir is used when handling relative image paths, e.g. + * will look for an image file in $basedir/images/a.png and convert it to inline. + * If you don't want to apply these transformations to your HTML, just set Body and AltBody yourself. * @access public * @param string $message HTML message string - * @param string $basedir baseline directory for path - * @param bool $advanced Whether to use the advanced HTML to text converter - * @return string $message + * @param string $basedir base directory for relative paths to images + * @param boolean|callable $advanced Whether to use the internal HTML to text converter + * or your own custom converter @see PHPMailer::html2text() + * @return string $message The transformed message Body */ public function msgHTML($message, $basedir = '', $advanced = false) { - preg_match_all("/(src|background)=[\"'](.*)[\"']/Ui", $message, $images); - if (isset($images[2])) { - foreach ($images[2] as $i => $url) { - // do not change urls for absolute images (thanks to corvuscorax) - if (!preg_match('#^[A-z]+://#', $url)) { + preg_match_all('/(src|background)=["\'](.*)["\']/Ui', $message, $images); + if (array_key_exists(2, $images)) { + foreach ($images[2] as $imgindex => $url) { + // Convert data URIs into embedded images + if (preg_match('#^data:(image[^;,]*)(;base64)?,#', $url, $match)) { + $data = substr($url, strpos($url, ',')); + if ($match[2]) { + $data = base64_decode($data); + } else { + $data = rawurldecode($data); + } + $cid = md5($url) . '@phpmailer.0'; // RFC2392 S 2 + if ($this->addStringEmbeddedImage($data, $cid, 'embed' . $imgindex, 'base64', $match[1])) { + $message = str_replace( + $images[0][$imgindex], + $images[1][$imgindex] . '="cid:' . $cid . '"', + $message + ); + } + } elseif (substr($url, 0, 4) !== 'cid:' && !preg_match('#^[a-z][a-z0-9+.-]*://#i', $url)) { + // Do not change urls for absolute images (thanks to corvuscorax) + // Do not change urls that are already inline images $filename = basename($url); $directory = dirname($url); if ($directory == '.') { $directory = ''; } - $cid = md5($url) . '@phpmailer.0'; //RFC2392 S 2 + $cid = md5($url) . '@phpmailer.0'; // RFC2392 S 2 if (strlen($basedir) > 1 && substr($basedir, -1) != '/') { $basedir .= '/'; } @@ -2834,12 +3431,12 @@ public function msgHTML($message, $basedir = '', $advanced = false) $cid, $filename, 'base64', - self::_mime_types(self::mb_pathinfo($filename, PATHINFO_EXTENSION)) + self::_mime_types((string)self::mb_pathinfo($filename, PATHINFO_EXTENSION)) ) ) { $message = preg_replace( - "/" . $images[1][$i] . "=[\"']" . preg_quote($url, '/') . "[\"']/Ui", - $images[1][$i] . "=\"cid:" . $cid . "\"", + '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui', + $images[1][$imgindex] . '="cid:' . $cid . '"', $message ); } @@ -2847,27 +3444,40 @@ public function msgHTML($message, $basedir = '', $advanced = false) } } $this->isHTML(true); - if (empty($this->AltBody)) { - $this->AltBody = 'To view this email message, open it in a program that understands HTML!' . "\n\n"; - } - //Convert all message body line breaks to CRLF, makes quoted-printable encoding work much better + // Convert all message body line breaks to CRLF, makes quoted-printable encoding work much better $this->Body = $this->normalizeBreaks($message); $this->AltBody = $this->normalizeBreaks($this->html2text($message, $advanced)); + if (!$this->alternativeExists()) { + $this->AltBody = 'To view this email message, open it in a program that understands HTML!' . + self::CRLF . self::CRLF; + } return $this->Body; } /** * Convert an HTML string into plain text. + * This is used by msgHTML(). + * Note - older versions of this function used a bundled advanced converter + * which was been removed for license reasons in #232. + * Example usage: + * + * // Use default conversion + * $plain = $mail->html2text($html); + * // Use your own custom converter + * $plain = $mail->html2text($html, function($html) { + * $converter = new MyHtml2text($html); + * return $converter->get_text(); + * }); + * * @param string $html The HTML text to convert - * @param bool $advanced Should this use the more complex html2text converter or just a simple one? + * @param boolean|callable $advanced Any boolean value to use the internal converter, + * or provide your own callable for custom conversion. * @return string */ public function html2text($html, $advanced = false) { - if ($advanced) { - require_once 'extras/class.html2text.php'; - $h = new html2text($html); - return $h->get_text(); + if (is_callable($advanced)) { + return call_user_func($advanced, $html); } return html_entity_decode( trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))), @@ -2886,94 +3496,109 @@ public function html2text($html, $advanced = false) public static function _mime_types($ext = '') { $mimes = array( - 'xl' => 'application/excel', - 'hqx' => 'application/mac-binhex40', - 'cpt' => 'application/mac-compactpro', - 'bin' => 'application/macbinary', - 'doc' => 'application/msword', - 'word' => 'application/msword', + 'xl' => 'application/excel', + 'js' => 'application/javascript', + 'hqx' => 'application/mac-binhex40', + 'cpt' => 'application/mac-compactpro', + 'bin' => 'application/macbinary', + 'doc' => 'application/msword', + 'word' => 'application/msword', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', 'class' => 'application/octet-stream', - 'dll' => 'application/octet-stream', - 'dms' => 'application/octet-stream', - 'exe' => 'application/octet-stream', - 'lha' => 'application/octet-stream', - 'lzh' => 'application/octet-stream', - 'psd' => 'application/octet-stream', - 'sea' => 'application/octet-stream', - 'so' => 'application/octet-stream', - 'oda' => 'application/oda', - 'pdf' => 'application/pdf', - 'ai' => 'application/postscript', - 'eps' => 'application/postscript', - 'ps' => 'application/postscript', - 'smi' => 'application/smil', - 'smil' => 'application/smil', - 'mif' => 'application/vnd.mif', - 'xls' => 'application/vnd.ms-excel', - 'ppt' => 'application/vnd.ms-powerpoint', + 'dll' => 'application/octet-stream', + 'dms' => 'application/octet-stream', + 'exe' => 'application/octet-stream', + 'lha' => 'application/octet-stream', + 'lzh' => 'application/octet-stream', + 'psd' => 'application/octet-stream', + 'sea' => 'application/octet-stream', + 'so' => 'application/octet-stream', + 'oda' => 'application/oda', + 'pdf' => 'application/pdf', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'mif' => 'application/vnd.mif', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', 'wbxml' => 'application/vnd.wap.wbxml', - 'wmlc' => 'application/vnd.wap.wmlc', - 'dcr' => 'application/x-director', - 'dir' => 'application/x-director', - 'dxr' => 'application/x-director', - 'dvi' => 'application/x-dvi', - 'gtar' => 'application/x-gtar', - 'php3' => 'application/x-httpd-php', - 'php4' => 'application/x-httpd-php', - 'php' => 'application/x-httpd-php', + 'wmlc' => 'application/vnd.wap.wmlc', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dxr' => 'application/x-director', + 'dvi' => 'application/x-dvi', + 'gtar' => 'application/x-gtar', + 'php3' => 'application/x-httpd-php', + 'php4' => 'application/x-httpd-php', + 'php' => 'application/x-httpd-php', 'phtml' => 'application/x-httpd-php', - 'phps' => 'application/x-httpd-php-source', - 'js' => 'application/x-javascript', - 'swf' => 'application/x-shockwave-flash', - 'sit' => 'application/x-stuffit', - 'tar' => 'application/x-tar', - 'tgz' => 'application/x-tar', - 'xht' => 'application/xhtml+xml', + 'phps' => 'application/x-httpd-php-source', + 'swf' => 'application/x-shockwave-flash', + 'sit' => 'application/x-stuffit', + 'tar' => 'application/x-tar', + 'tgz' => 'application/x-tar', + 'xht' => 'application/xhtml+xml', 'xhtml' => 'application/xhtml+xml', - 'zip' => 'application/zip', - 'mid' => 'audio/midi', - 'midi' => 'audio/midi', - 'mp2' => 'audio/mpeg', - 'mp3' => 'audio/mpeg', - 'mpga' => 'audio/mpeg', - 'aif' => 'audio/x-aiff', - 'aifc' => 'audio/x-aiff', - 'aiff' => 'audio/x-aiff', - 'ram' => 'audio/x-pn-realaudio', - 'rm' => 'audio/x-pn-realaudio', - 'rpm' => 'audio/x-pn-realaudio-plugin', - 'ra' => 'audio/x-realaudio', - 'wav' => 'audio/x-wav', - 'bmp' => 'image/bmp', - 'gif' => 'image/gif', - 'jpeg' => 'image/jpeg', - 'jpe' => 'image/jpeg', - 'jpg' => 'image/jpeg', - 'png' => 'image/png', - 'tiff' => 'image/tiff', - 'tif' => 'image/tiff', - 'eml' => 'message/rfc822', - 'css' => 'text/css', - 'html' => 'text/html', - 'htm' => 'text/html', + 'zip' => 'application/zip', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mp2' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mpga' => 'audio/mpeg', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'ram' => 'audio/x-pn-realaudio', + 'rm' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'ra' => 'audio/x-realaudio', + 'wav' => 'audio/x-wav', + 'bmp' => 'image/bmp', + 'gif' => 'image/gif', + 'jpeg' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'png' => 'image/png', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'eml' => 'message/rfc822', + 'css' => 'text/css', + 'html' => 'text/html', + 'htm' => 'text/html', 'shtml' => 'text/html', - 'log' => 'text/plain', - 'text' => 'text/plain', - 'txt' => 'text/plain', - 'rtx' => 'text/richtext', - 'rtf' => 'text/rtf', - 'xml' => 'text/xml', - 'xsl' => 'text/xml', - 'mpeg' => 'video/mpeg', - 'mpe' => 'video/mpeg', - 'mpg' => 'video/mpeg', - 'mov' => 'video/quicktime', - 'qt' => 'video/quicktime', - 'rv' => 'video/vnd.rn-realvideo', - 'avi' => 'video/x-msvideo', + 'log' => 'text/plain', + 'text' => 'text/plain', + 'txt' => 'text/plain', + 'rtx' => 'text/richtext', + 'rtf' => 'text/rtf', + 'vcf' => 'text/vcard', + 'vcard' => 'text/vcard', + 'xml' => 'text/xml', + 'xsl' => 'text/xml', + 'mpeg' => 'video/mpeg', + 'mpe' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mov' => 'video/quicktime', + 'qt' => 'video/quicktime', + 'rv' => 'video/vnd.rn-realvideo', + 'avi' => 'video/x-msvideo', 'movie' => 'video/x-sgi-movie' ); - return (array_key_exists(strtolower($ext), $mimes) ? $mimes[strtolower($ext)]: 'application/octet-stream'); + if (array_key_exists(strtolower($ext), $mimes)) { + return $mimes[strtolower($ext)]; + } + return 'application/octet-stream'; } /** @@ -2985,9 +3610,9 @@ public static function _mime_types($ext = '') */ public static function filenameToType($filename) { - //In case the path is a URL, strip any query string before getting extension + // In case the path is a URL, strip any query string before getting extension $qpos = strpos($filename, '?'); - if ($qpos !== false) { + if (false !== $qpos) { $filename = substr($filename, 0, $qpos); } $pathinfo = self::mb_pathinfo($filename); @@ -3008,37 +3633,34 @@ public static function filenameToType($filename) public static function mb_pathinfo($path, $options = null) { $ret = array('dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''); - $m = array(); - preg_match('%^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$%im', $path, $m); - if (array_key_exists(1, $m)) { - $ret['dirname'] = $m[1]; - } - if (array_key_exists(2, $m)) { - $ret['basename'] = $m[2]; - } - if (array_key_exists(5, $m)) { - $ret['extension'] = $m[5]; - } - if (array_key_exists(3, $m)) { - $ret['filename'] = $m[3]; + $pathinfo = array(); + if (preg_match('%^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$%im', $path, $pathinfo)) { + if (array_key_exists(1, $pathinfo)) { + $ret['dirname'] = $pathinfo[1]; + } + if (array_key_exists(2, $pathinfo)) { + $ret['basename'] = $pathinfo[2]; + } + if (array_key_exists(5, $pathinfo)) { + $ret['extension'] = $pathinfo[5]; + } + if (array_key_exists(3, $pathinfo)) { + $ret['filename'] = $pathinfo[3]; + } } switch ($options) { case PATHINFO_DIRNAME: case 'dirname': return $ret['dirname']; - break; case PATHINFO_BASENAME: case 'basename': return $ret['basename']; - break; case PATHINFO_EXTENSION: case 'extension': return $ret['extension']; - break; case PATHINFO_FILENAME: case 'filename': return $ret['filename']; - break; default: return $ret; } @@ -3046,33 +3668,27 @@ public static function mb_pathinfo($path, $options = null) /** * Set or reset instance properties. - * + * You should avoid this function - it's more verbose, less efficient, more error-prone and + * harder to debug than setting properties directly. * Usage Example: - * $page->set('X-Priority', '3'); - * + * `$mail->set('SMTPSecure', 'tls');` + * is the same as: + * `$mail->SMTPSecure = 'tls';` * @access public - * @param string $name - * @param mixed $value - * NOTE: will not work with arrays, there are no arrays to set/reset - * @throws phpmailerException - * @return bool - * @todo Should this not be using __set() magic function? + * @param string $name The property name to set + * @param mixed $value The value to set the property to + * @return boolean + * @TODO Should this not be using the __set() magic function? */ public function set($name, $value = '') { - try { - if (isset($this->$name)) { - $this->$name = $value; - } else { - throw new phpmailerException($this->lang('variable_set') . $name, self::STOP_CRITICAL); - } - } catch (Exception $e) { - $this->setError($e->getMessage()); - if ($e->getCode() == self::STOP_CRITICAL) { - return false; - } + if (property_exists($this, $name)) { + $this->$name = $value; + return true; + } else { + $this->setError($this->lang('variable_set') . $name); + return false; } - return true; } /** @@ -3101,19 +3717,20 @@ public static function normalizeBreaks($text, $breaktype = "\r\n") return preg_replace('/(\r\n|\r|\n)/ms', $breaktype, $text); } - /** * Set the public and private key files and password for S/MIME signing. * @access public * @param string $cert_filename * @param string $key_filename * @param string $key_pass Password for private key + * @param string $extracerts_filename Optional path to chain certificate */ - public function sign($cert_filename, $key_filename, $key_pass) + public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '') { $this->sign_cert_file = $cert_filename; $this->sign_key_file = $key_filename; $this->sign_key_pass = $key_pass; + $this->sign_extracerts_file = $extracerts_filename; } /** @@ -3130,7 +3747,7 @@ public function DKIM_QP($txt) if (((0x21 <= $ord) && ($ord <= 0x3A)) || $ord == 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E))) { $line .= $txt[$i]; } else { - $line .= "=" . sprintf("%02X", $ord); + $line .= '=' . sprintf('%02X', $ord); } } return $line; @@ -3139,48 +3756,68 @@ public function DKIM_QP($txt) /** * Generate a DKIM signature. * @access public - * @param string $s Header + * @param string $signHeader * @throws phpmailerException - * @return string + * @return string The DKIM signature value */ - public function DKIM_Sign($s) + public function DKIM_Sign($signHeader) { if (!defined('PKCS7_TEXT')) { if ($this->exceptions) { - throw new phpmailerException($this->lang("signing") . ' OpenSSL extension missing.'); + throw new phpmailerException($this->lang('extension_missing') . 'openssl'); } return ''; } - $privKeyStr = file_get_contents($this->DKIM_private); - if ($this->DKIM_passphrase != '') { + $privKeyStr = !empty($this->DKIM_private_string) ? $this->DKIM_private_string : file_get_contents($this->DKIM_private); + if ('' != $this->DKIM_passphrase) { $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase); } else { - $privKey = $privKeyStr; - } - if (openssl_sign($s, $signature, $privKey)) { - return base64_encode($signature); + $privKey = openssl_pkey_get_private($privKeyStr); + } + //Workaround for missing digest algorithms in old PHP & OpenSSL versions + //@link http://stackoverflow.com/a/11117338/333340 + if (version_compare(PHP_VERSION, '5.3.0') >= 0 and + in_array('sha256WithRSAEncryption', openssl_get_md_methods(true))) { + if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) { + openssl_pkey_free($privKey); + return base64_encode($signature); + } + } else { + $pinfo = openssl_pkey_get_details($privKey); + $hash = hash('sha256', $signHeader); + //'Magic' constant for SHA256 from RFC3447 + //@link https://tools.ietf.org/html/rfc3447#page-43 + $t = '3031300d060960864801650304020105000420' . $hash; + $pslen = $pinfo['bits'] / 8 - (strlen($t) / 2 + 3); + $eb = pack('H*', '0001' . str_repeat('FF', $pslen) . '00' . $t); + + if (openssl_private_encrypt($eb, $signature, $privKey, OPENSSL_NO_PADDING)) { + openssl_pkey_free($privKey); + return base64_encode($signature); + } } + openssl_pkey_free($privKey); return ''; } /** * Generate a DKIM canonicalization header. * @access public - * @param string $s Header + * @param string $signHeader Header * @return string */ - public function DKIM_HeaderC($s) + public function DKIM_HeaderC($signHeader) { - $s = preg_replace("/\r\n\s+/", " ", $s); - $lines = explode("\r\n", $s); + $signHeader = preg_replace('/\r\n\s+/', ' ', $signHeader); + $lines = explode("\r\n", $signHeader); foreach ($lines as $key => $line) { - list($heading, $value) = explode(":", $line, 2); + list($heading, $value) = explode(':', $line, 2); $heading = strtolower($heading); - $value = preg_replace("/\s+/", " ", $value); // Compress useless spaces - $lines[$key] = $heading . ":" . trim($value); // Don't forget to remove WSP around the value + $value = preg_replace('/\s{2,}/', ' ', $value); // Compress useless spaces + $lines[$key] = $heading . ':' . trim($value); // Don't forget to remove WSP around the value } - $s = implode("\r\n", $lines); - return $s; + $signHeader = implode("\r\n", $lines); + return $signHeader; } /** @@ -3214,7 +3851,7 @@ public function DKIM_BodyC($body) */ public function DKIM_Add($headers_line, $subject, $body) { - $DKIMsignatureType = 'rsa-sha1'; // Signature & hash algorithms + $DKIMsignatureType = 'rsa-sha256'; // Signature & hash algorithms $DKIMcanonicalization = 'relaxed/simple'; // Canonicalization of header/body $DKIMquery = 'dns/txt'; // Query method $DKIMtime = time(); // Signature Timestamp = seconds since 00:00:00 - Jan 1, 1970 (UTC time zone) @@ -3222,6 +3859,7 @@ public function DKIM_Add($headers_line, $subject, $body) $headers = explode($this->LE, $headers_line); $from_header = ''; $to_header = ''; + $date_header = ''; $current = ''; foreach ($headers as $header) { if (strpos($header, 'From:') === 0) { @@ -3230,9 +3868,12 @@ public function DKIM_Add($headers_line, $subject, $body) } elseif (strpos($header, 'To:') === 0) { $to_header = $header; $current = 'to_header'; + } elseif (strpos($header, 'Date:') === 0) { + $date_header = $header; + $current = 'date_header'; } else { - if ($current && strpos($header, ' =?') === 0) { - $current .= $header; + if (!empty($$current) && strpos($header, ' =?') === 0) { + $$current .= $header; } else { $current = ''; } @@ -3240,6 +3881,7 @@ public function DKIM_Add($headers_line, $subject, $body) } $from = str_replace('|', '=7C', $this->DKIM_QP($from_header)); $to = str_replace('|', '=7C', $this->DKIM_QP($to_header)); + $date = str_replace('|', '=7C', $this->DKIM_QP($date_header)); $subject = str_replace( '|', '=7C', @@ -3247,40 +3889,116 @@ public function DKIM_Add($headers_line, $subject, $body) ); // Copied header fields (dkim-quoted-printable) $body = $this->DKIM_BodyC($body); $DKIMlen = strlen($body); // Length of body - $DKIMb64 = base64_encode(pack("H*", sha1($body))); // Base64 of packed binary SHA-1 hash of body - $ident = ($this->DKIM_identity == '') ? '' : " i=" . $this->DKIM_identity . ";"; - $dkimhdrs = "DKIM-Signature: v=1; a=" . - $DKIMsignatureType . "; q=" . - $DKIMquery . "; l=" . - $DKIMlen . "; s=" . + $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body))); // Base64 of packed binary SHA-256 hash of body + if ('' == $this->DKIM_identity) { + $ident = ''; + } else { + $ident = ' i=' . $this->DKIM_identity . ';'; + } + $dkimhdrs = 'DKIM-Signature: v=1; a=' . + $DKIMsignatureType . '; q=' . + $DKIMquery . '; l=' . + $DKIMlen . '; s=' . $this->DKIM_selector . ";\r\n" . - "\tt=" . $DKIMtime . "; c=" . $DKIMcanonicalization . ";\r\n" . - "\th=From:To:Subject;\r\n" . - "\td=" . $this->DKIM_domain . ";" . $ident . "\r\n" . + "\tt=" . $DKIMtime . '; c=' . $DKIMcanonicalization . ";\r\n" . + "\th=From:To:Date:Subject;\r\n" . + "\td=" . $this->DKIM_domain . ';' . $ident . "\r\n" . "\tz=$from\r\n" . "\t|$to\r\n" . + "\t|$date\r\n" . "\t|$subject;\r\n" . "\tbh=" . $DKIMb64 . ";\r\n" . "\tb="; $toSign = $this->DKIM_HeaderC( - $from_header . "\r\n" . $to_header . "\r\n" . $subject_header . "\r\n" . $dkimhdrs + $from_header . "\r\n" . + $to_header . "\r\n" . + $date_header . "\r\n" . + $subject_header . "\r\n" . + $dkimhdrs ); $signed = $this->DKIM_Sign($toSign); return $dkimhdrs . $signed . "\r\n"; } + /** + * Detect if a string contains a line longer than the maximum line length allowed. + * @param string $str + * @return boolean + * @static + */ + public static function hasLineLongerThanMax($str) + { + //+2 to include CRLF line break for a 1000 total + return (boolean)preg_match('/^(.{'.(self::MAX_LINE_LENGTH + 2).',})/m', $str); + } + + /** + * Allows for public read access to 'to' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * @access public + * @return array + */ + public function getToAddresses() + { + return $this->to; + } + + /** + * Allows for public read access to 'cc' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * @access public + * @return array + */ + public function getCcAddresses() + { + return $this->cc; + } + + /** + * Allows for public read access to 'bcc' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * @access public + * @return array + */ + public function getBccAddresses() + { + return $this->bcc; + } + + /** + * Allows for public read access to 'ReplyTo' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * @access public + * @return array + */ + public function getReplyToAddresses() + { + return $this->ReplyTo; + } + + /** + * Allows for public read access to 'all_recipients' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * @access public + * @return array + */ + public function getAllRecipientAddresses() + { + return $this->all_recipients; + } + /** * Perform a callback. - * @param bool $isSent - * @param string $to - * @param string $cc - * @param string $bcc + * @param boolean $isSent + * @param array $to + * @param array $cc + * @param array $bcc * @param string $subject * @param string $body * @param string $from */ - protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from = null) + protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from) { if (!empty($this->action_function) && is_callable($this->action_function)) { $params = array($isSent, $to, $cc, $bcc, $subject, $body, $from); diff --git a/helpers/phpmailer/class.phpmaileroauth.php b/helpers/phpmailer/class.phpmaileroauth.php new file mode 100755 index 00000000..b1bb09f0 --- /dev/null +++ b/helpers/phpmailer/class.phpmaileroauth.php @@ -0,0 +1,197 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2014 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +/** + * PHPMailerOAuth - PHPMailer subclass adding OAuth support. + * @package PHPMailer + * @author @sherryl4george + * @author Marcus Bointon (@Synchro) + */ +class PHPMailerOAuth extends PHPMailer +{ + /** + * The OAuth user's email address + * @var string + */ + public $oauthUserEmail = ''; + + /** + * The OAuth refresh token + * @var string + */ + public $oauthRefreshToken = ''; + + /** + * The OAuth client ID + * @var string + */ + public $oauthClientId = ''; + + /** + * The OAuth client secret + * @var string + */ + public $oauthClientSecret = ''; + + /** + * An instance of the PHPMailerOAuthGoogle class. + * @var PHPMailerOAuthGoogle + * @access protected + */ + protected $oauth = null; + + /** + * Get a PHPMailerOAuthGoogle instance to use. + * @return PHPMailerOAuthGoogle + */ + public function getOAUTHInstance() + { + if (!is_object($this->oauth)) { + $this->oauth = new PHPMailerOAuthGoogle( + $this->oauthUserEmail, + $this->oauthClientSecret, + $this->oauthClientId, + $this->oauthRefreshToken + ); + } + return $this->oauth; + } + + /** + * Initiate a connection to an SMTP server. + * Overrides the original smtpConnect method to add support for OAuth. + * @param array $options An array of options compatible with stream_context_create() + * @uses SMTP + * @access public + * @return bool + * @throws phpmailerException + */ + public function smtpConnect($options = array()) + { + if (is_null($this->smtp)) { + $this->smtp = $this->getSMTPInstance(); + } + + if (is_null($this->oauth)) { + $this->oauth = $this->getOAUTHInstance(); + } + + // Already connected? + if ($this->smtp->connected()) { + return true; + } + + $this->smtp->setTimeout($this->Timeout); + $this->smtp->setDebugLevel($this->SMTPDebug); + $this->smtp->setDebugOutput($this->Debugoutput); + $this->smtp->setVerp($this->do_verp); + $hosts = explode(';', $this->Host); + $lastexception = null; + + foreach ($hosts as $hostentry) { + $hostinfo = array(); + if (!preg_match('/^((ssl|tls):\/\/)*([a-zA-Z0-9\.-]*):?([0-9]*)$/', trim($hostentry), $hostinfo)) { + // Not a valid host entry + continue; + } + // $hostinfo[2]: optional ssl or tls prefix + // $hostinfo[3]: the hostname + // $hostinfo[4]: optional port number + // The host string prefix can temporarily override the current setting for SMTPSecure + // If it's not specified, the default value is used + $prefix = ''; + $secure = $this->SMTPSecure; + $tls = ($this->SMTPSecure == 'tls'); + if ('ssl' == $hostinfo[2] or ('' == $hostinfo[2] and 'ssl' == $this->SMTPSecure)) { + $prefix = 'ssl://'; + $tls = false; // Can't have SSL and TLS at the same time + $secure = 'ssl'; + } elseif ($hostinfo[2] == 'tls') { + $tls = true; + // tls doesn't use a prefix + $secure = 'tls'; + } + //Do we need the OpenSSL extension? + $sslext = defined('OPENSSL_ALGO_SHA1'); + if ('tls' === $secure or 'ssl' === $secure) { + //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled + if (!$sslext) { + throw new phpmailerException($this->lang('extension_missing').'openssl', self::STOP_CRITICAL); + } + } + $host = $hostinfo[3]; + $port = $this->Port; + $tport = (integer)$hostinfo[4]; + if ($tport > 0 and $tport < 65536) { + $port = $tport; + } + if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) { + try { + if ($this->Helo) { + $hello = $this->Helo; + } else { + $hello = $this->serverHostname(); + } + $this->smtp->hello($hello); + //Automatically enable TLS encryption if: + // * it's not disabled + // * we have openssl extension + // * we are not already using SSL + // * the server offers STARTTLS + if ($this->SMTPAutoTLS and $sslext and $secure != 'ssl' and $this->smtp->getServerExt('STARTTLS')) { + $tls = true; + } + if ($tls) { + if (!$this->smtp->startTLS()) { + throw new phpmailerException($this->lang('connect_host')); + } + // We must resend HELO after tls negotiation + $this->smtp->hello($hello); + } + if ($this->SMTPAuth) { + if (!$this->smtp->authenticate( + $this->Username, + $this->Password, + $this->AuthType, + $this->Realm, + $this->Workstation, + $this->oauth + ) + ) { + throw new phpmailerException($this->lang('authenticate')); + } + } + return true; + } catch (phpmailerException $exc) { + $lastexception = $exc; + $this->edebug($exc->getMessage()); + // We must have connected, but then failed TLS or Auth, so close connection nicely + $this->smtp->quit(); + } + } + } + // If we get here, all connection attempts have failed, so close connection hard + $this->smtp->close(); + // As we've caught all exceptions, just report whatever the last one was + if ($this->exceptions and !is_null($lastexception)) { + throw $lastexception; + } + return false; + } +} diff --git a/helpers/phpmailer/class.phpmaileroauthgoogle.php b/helpers/phpmailer/class.phpmaileroauthgoogle.php new file mode 100755 index 00000000..71c9bd32 --- /dev/null +++ b/helpers/phpmailer/class.phpmaileroauthgoogle.php @@ -0,0 +1,77 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2014 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +/** + * PHPMailerOAuthGoogle - Wrapper for League OAuth2 Google provider. + * @package PHPMailer + * @author @sherryl4george + * @author Marcus Bointon (@Synchro) + * @link https://github.com/thephpleague/oauth2-client + */ +class PHPMailerOAuthGoogle +{ + private $oauthUserEmail = ''; + private $oauthRefreshToken = ''; + private $oauthClientId = ''; + private $oauthClientSecret = ''; + + /** + * @param string $UserEmail + * @param string $ClientSecret + * @param string $ClientId + * @param string $RefreshToken + */ + public function __construct( + $UserEmail, + $ClientSecret, + $ClientId, + $RefreshToken + ) { + $this->oauthClientId = $ClientId; + $this->oauthClientSecret = $ClientSecret; + $this->oauthRefreshToken = $RefreshToken; + $this->oauthUserEmail = $UserEmail; + } + + private function getProvider() + { + return new League\OAuth2\Client\Provider\Google([ + 'clientId' => $this->oauthClientId, + 'clientSecret' => $this->oauthClientSecret + ]); + } + + private function getGrant() + { + return new \League\OAuth2\Client\Grant\RefreshToken(); + } + + private function getToken() + { + $provider = $this->getProvider(); + $grant = $this->getGrant(); + return $provider->getAccessToken($grant, ['refresh_token' => $this->oauthRefreshToken]); + } + + public function getOauth64() + { + $token = $this->getToken(); + return base64_encode("user=" . $this->oauthUserEmail . "\001auth=Bearer " . $token . "\001\001"); + } +} diff --git a/helpers/phpmailer/class.pop3.php b/helpers/phpmailer/class.pop3.php old mode 100644 new mode 100755 index 042b93ac..373c886c --- a/helpers/phpmailer/class.pop3.php +++ b/helpers/phpmailer/class.pop3.php @@ -1,15 +1,14 @@ + * @author Marcus Bointon (Synchro/coolbru) * @author Jim Jagielski (jimjag) * @author Andy Prevost (codeworxtech) * @author Brent R. Matzelle (original founder) - * @copyright 2013 Marcus Bointon + * @copyright 2012 - 2014 Marcus Bointon * @copyright 2010 - 2012 Jim Jagielski * @copyright 2004 - 2009 Andy Prevost * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License @@ -24,37 +23,36 @@ * Does not support APOP. * @package PHPMailer * @author Richard Davey (original author) - * @author Marcus Bointon (coolbru) + * @author Marcus Bointon (Synchro/coolbru) * @author Jim Jagielski (jimjag) * @author Andy Prevost (codeworxtech) */ - class POP3 { /** * The POP3 PHPMailer Version number. - * @type string + * @var string * @access public */ - public $Version = '5.2.7'; + public $Version = '5.2.21'; /** * Default POP3 port number. - * @type int + * @var integer * @access public */ public $POP3_PORT = 110; /** * Default timeout in seconds. - * @type int + * @var integer * @access public */ public $POP3_TIMEOUT = 30; /** * POP3 Carriage Return + Line Feed. - * @type string + * @var string * @access public * @deprecated Use the constant instead */ @@ -63,103 +61,92 @@ class POP3 /** * Debug display level. * Options: 0 = no, 1+ = yes - * @type int + * @var integer * @access public */ public $do_debug = 0; /** * POP3 mail server hostname. - * @type string + * @var string * @access public */ public $host; /** * POP3 port number. - * @type int + * @var integer * @access public */ public $port; /** * POP3 Timeout Value in seconds. - * @type int + * @var integer * @access public */ public $tval; /** * POP3 username - * @type string + * @var string * @access public */ public $username; /** * POP3 password. - * @type string + * @var string * @access public */ public $password; /** * Resource handle for the POP3 connection socket. - * @type resource - * @access private + * @var resource + * @access protected */ - private $pop_conn; + protected $pop_conn; /** * Are we connected? - * @type bool - * @access private + * @var boolean + * @access protected */ - private $connected; + protected $connected = false; /** * Error container. - * @type array - * @access private + * @var array + * @access protected */ - private $error; + protected $errors = array(); /** * Line break constant */ const CRLF = "\r\n"; - /** - * Constructor. - * @access public - */ - public function __construct() - { - $this->pop_conn = 0; - $this->connected = false; - $this->error = null; - } - /** * Simple static wrapper for all-in-one POP before SMTP * @param $host - * @param bool $port - * @param bool $tval + * @param integer|boolean $port The port number to connect to + * @param integer|boolean $timeout The timeout value * @param string $username * @param string $password - * @param int $debug_level - * @return bool + * @param integer $debug_level + * @return boolean */ public static function popBeforeSmtp( $host, $port = false, - $tval = false, + $timeout = false, $username = '', $password = '', $debug_level = 0 ) { $pop = new POP3; - return $pop->authorise($host, $port, $tval, $username, $password, $debug_level); + return $pop->authorise($host, $port, $timeout, $username, $password, $debug_level); } /** @@ -167,34 +154,34 @@ public static function popBeforeSmtp( * A connect, login, disconnect sequence * appropriate for POP-before SMTP authorisation. * @access public - * @param string $host - * @param bool|int $port - * @param bool|int $tval + * @param string $host The hostname to connect to + * @param integer|boolean $port The port number to connect to + * @param integer|boolean $timeout The timeout value * @param string $username * @param string $password - * @param int $debug_level - * @return bool + * @param integer $debug_level + * @return boolean */ - public function authorise($host, $port = false, $tval = false, $username = '', $password = '', $debug_level = 0) + public function authorise($host, $port = false, $timeout = false, $username = '', $password = '', $debug_level = 0) { $this->host = $host; // If no port value provided, use default - if ($port === false) { + if (false === $port) { $this->port = $this->POP3_PORT; } else { - $this->port = $port; + $this->port = (integer)$port; } // If no timeout value provided, use default - if ($tval === false) { + if (false === $timeout) { $this->tval = $this->POP3_TIMEOUT; } else { - $this->tval = $tval; + $this->tval = (integer)$timeout; } $this->do_debug = $debug_level; $this->username = $username; $this->password = $password; - // Refresh the error log - $this->error = null; + // Reset the error log + $this->errors = array(); // connect $result = $this->connect($this->host, $this->port, $this->tval); if ($result) { @@ -213,7 +200,7 @@ public function authorise($host, $port = false, $tval = false, $username = '', $ * Connect to a POP3 server. * @access public * @param string $host - * @param bool|int $port + * @param integer|boolean $port * @param integer $tval * @return boolean */ @@ -228,6 +215,10 @@ public function connect($host, $port = false, $tval = 30) //Rather than suppress it with @fsockopen, capture it cleanly instead set_error_handler(array($this, 'catchWarning')); + if (false === $port) { + $port = $this->POP3_PORT; + } + // connect to the POP3 server $this->pop_conn = fsockopen( $host, // POP3 Host @@ -238,34 +229,20 @@ public function connect($host, $port = false, $tval = 30) ); // Timeout (seconds) // Restore the error handler restore_error_handler(); - // Does the Error Log now contain anything? - if ($this->error && $this->do_debug >= 1) { - $this->displayErrors(); - } + // Did we connect? - if ($this->pop_conn == false) { + if (false === $this->pop_conn) { // It would appear not... - $this->error = array( + $this->setError(array( 'error' => "Failed to connect to server $host on port $port", 'errno' => $errno, 'errstr' => $errstr - ); - if ($this->do_debug >= 1) { - $this->displayErrors(); - } + )); return false; } // Increase the stream time-out - // Check for PHP 4.3.0 or later - if (version_compare(phpversion(), '5.0.0', 'ge')) { - stream_set_timeout($this->pop_conn, $tval, 0); - } else { - // Does not work on Windows - if (substr(PHP_OS, 0, 3) !== 'WIN') { - socket_set_timeout($this->pop_conn, $tval, 0); - } - } + stream_set_timeout($this->pop_conn, $tval, 0); // Get the POP3 server response $pop3_response = $this->getResponse(); @@ -288,12 +265,8 @@ public function connect($host, $port = false, $tval = 30) */ public function login($username = '', $password = '') { - if ($this->connected == false) { - $this->error = 'Not connected to POP3 server'; - - if ($this->do_debug >= 1) { - $this->displayErrors(); - } + if (!$this->connected) { + $this->setError('Not connected to POP3 server'); } if (empty($username)) { $username = $this->username; @@ -325,7 +298,11 @@ public function disconnect() $this->sendString('QUIT'); //The QUIT command may cause the daemon to exit, which will kill our connection //So ignore errors here - @fclose($this->pop_conn); + try { + @fclose($this->pop_conn); + } catch (Exception $e) { + //Do nothing + }; } /** @@ -333,24 +310,24 @@ public function disconnect() * $size is the maximum number of bytes to retrieve * @param integer $size * @return string - * @access private + * @access protected */ - private function getResponse($size = 128) + protected function getResponse($size = 128) { - $r = fgets($this->pop_conn, $size); + $response = fgets($this->pop_conn, $size); if ($this->do_debug >= 1) { - echo "Server -> Client: $r"; + echo "Server -> Client: $response"; } - return $r; + return $response; } /** * Send raw data to the POP3 server. * @param string $string * @return integer - * @access private + * @access protected */ - private function sendString($string) + protected function sendString($string) { if ($this->pop_conn) { if ($this->do_debug >= 2) { //Show client messages when debug >= 2 @@ -366,19 +343,16 @@ private function sendString($string) * Looks for for +OK or -ERR. * @param string $string * @return boolean - * @access private + * @access protected */ - private function checkResponse($string) + protected function checkResponse($string) { if (substr($string, 0, 3) !== '+OK') { - $this->error = array( + $this->setError(array( 'error' => "Server reported an error: $string", 'errno' => 0, 'errstr' => '' - ); - if ($this->do_debug >= 1) { - $this->displayErrors(); - } + )); return false; } else { return true; @@ -386,16 +360,30 @@ private function checkResponse($string) } /** - * Display errors if debug is enabled. - * @access private + * Add an error to the internal error store. + * Also display debug output if it's enabled. + * @param $error + * @access protected */ - private function displayErrors() + protected function setError($error) { - echo '
';
-        foreach ($this->error as $single_error) {
-            print_r($single_error);
+        $this->errors[] = $error;
+        if ($this->do_debug >= 1) {
+            echo '
';
+            foreach ($this->errors as $error) {
+                print_r($error);
+            }
+            echo '
'; } - echo '
'; + } + + /** + * Get an array of error messages, if any. + * @return array + */ + public function getErrors() + { + return $this->errors; } /** @@ -404,16 +392,16 @@ private function displayErrors() * @param string $errstr * @param string $errfile * @param integer $errline - * @access private + * @access protected */ - private function catchWarning($errno, $errstr, $errfile, $errline) + protected function catchWarning($errno, $errstr, $errfile, $errline) { - $this->error[] = array( + $this->setError(array( 'error' => "Connecting to the POP3 server raised a PHP warning: ", 'errno' => $errno, 'errstr' => $errstr, 'errfile' => $errfile, 'errline' => $errline - ); + )); } } diff --git a/helpers/phpmailer/class.smtp.php b/helpers/phpmailer/class.smtp.php old mode 100644 new mode 100755 index 6366724a..270162b2 --- a/helpers/phpmailer/class.smtp.php +++ b/helpers/phpmailer/class.smtp.php @@ -1,73 +1,100 @@ + * PHP Version 5 + * @package PHPMailer + * @link https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project + * @author Marcus Bointon (Synchro/coolbru) * @author Jim Jagielski (jimjag) * @author Andy Prevost (codeworxtech) - * @copyright 2013 Marcus Bointon - * @copyright 2004 - 2008 Andy Prevost + * @author Brent R. Matzelle (original founder) + * @copyright 2014 Marcus Bointon * @copyright 2010 - 2012 Jim Jagielski - * @license http://www.gnu.org/copyleft/lesser.html Distributed under the Lesser General Public License (LGPL) + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. */ /** * PHPMailer RFC821 SMTP email transport class. - * - * Implements RFC 821 SMTP commands - * and provides some utility methods for sending mail to an SMTP server. - * - * PHP Version 5.0.0 - * - * @category PHP - * @package PHPMailer - * @link https://github.com/PHPMailer/PHPMailer/blob/master/class.smtp.php - * @author Chris Ryan - * @author Marcus Bointon - * @license http://www.gnu.org/copyleft/lesser.html Distributed under the Lesser General Public License (LGPL) + * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server. + * @package PHPMailer + * @author Chris Ryan + * @author Marcus Bointon */ - class SMTP { /** - * The PHPMailer SMTP Version number. + * The PHPMailer SMTP version number. + * @var string */ - const VERSION = '5.2.7'; + const VERSION = '5.2.21'; /** * SMTP line break constant. + * @var string */ const CRLF = "\r\n"; /** * The SMTP port to use if one is not specified. + * @var integer */ const DEFAULT_SMTP_PORT = 25; + /** + * The maximum line length allowed by RFC 2822 section 2.1.1 + * @var integer + */ + const MAX_LINE_LENGTH = 998; + + /** + * Debug level for no output + */ + const DEBUG_OFF = 0; + + /** + * Debug level to show client -> server messages + */ + const DEBUG_CLIENT = 1; + + /** + * Debug level to show client -> server and server -> client messages + */ + const DEBUG_SERVER = 2; + + /** + * Debug level to show connection status, client -> server and server -> client messages + */ + const DEBUG_CONNECTION = 3; + + /** + * Debug level to show all messages + */ + const DEBUG_LOWLEVEL = 4; + /** * The PHPMailer SMTP Version number. - * @type string - * @deprecated This should be a constant + * @var string + * @deprecated Use the `VERSION` constant instead * @see SMTP::VERSION */ - public $Version = '5.2.7'; + public $Version = '5.2.21'; /** * SMTP server port number. - * @type int - * @deprecated This is only ever ued as default value, so should be a constant + * @var integer + * @deprecated This is only ever used as a default value, so use the `DEFAULT_SMTP_PORT` constant instead * @see SMTP::DEFAULT_SMTP_PORT */ public $SMTP_PORT = 25; /** - * SMTP reply line ending - * @type string - * @deprecated Use the class constant instead + * SMTP reply line ending. + * @var string + * @deprecated Use the `CRLF` constant instead * @see SMTP::CRLF */ public $CRLF = "\r\n"; @@ -75,85 +102,124 @@ class SMTP /** * Debug output level. * Options: - * 0: no output - * 1: commands - * 2: data and commands - * 3: as 2 plus connection status - * 4: low level data output - * @type int + * * self::DEBUG_OFF (`0`) No debug output, default + * * self::DEBUG_CLIENT (`1`) Client commands + * * self::DEBUG_SERVER (`2`) Client commands and server responses + * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status + * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages + * @var integer */ - public $do_debug = 0; + public $do_debug = self::DEBUG_OFF; /** - * The function/method to use for debugging output. - * Options: 'echo', 'html' or 'error_log' - * @type string + * How to handle debug output. + * Options: + * * `echo` Output plain-text as-is, appropriate for CLI + * * `html` Output escaped, line breaks converted to `
`, appropriate for browser output + * * `error_log` Output to error log as configured in php.ini + * + * Alternatively, you can provide a callable expecting two params: a message string and the debug level: + * + * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; + * + * @var string|callable */ public $Debugoutput = 'echo'; /** * Whether to use VERP. - * @type bool + * @link http://en.wikipedia.org/wiki/Variable_envelope_return_path + * @link http://www.postfix.org/VERP_README.html Info on VERP + * @var boolean */ public $do_verp = false; /** * The timeout value for connection, in seconds. * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2 - * @type int + * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure. + * @link http://tools.ietf.org/html/rfc2821#section-4.5.3.2 + * @var integer */ public $Timeout = 300; /** - * The SMTP timelimit value for reads, in seconds. - * @type int + * How long to wait for commands to complete, in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2 + * @var integer */ - public $Timelimit = 30; + public $Timelimit = 300; + + /** + * @var array patterns to extract smtp transaction id from smtp reply + * Only first capture group will be use, use non-capturing group to deal with it + * Extend this class to override this property to fulfil your needs. + */ + protected $smtp_transaction_id_patterns = array( + 'exim' => '/[0-9]{3} OK id=(.*)/', + 'sendmail' => '/[0-9]{3} 2.0.0 (.*) Message/', + 'postfix' => '/[0-9]{3} 2.0.0 Ok: queued as (.*)/' + ); /** * The socket for the server connection. - * @type resource + * @var resource */ protected $smtp_conn; /** - * Error message, if any, for the last call. - * @type string + * Error information, if any, for the last SMTP command. + * @var array */ - protected $error = ''; + protected $error = array( + 'error' => '', + 'detail' => '', + 'smtp_code' => '', + 'smtp_code_ex' => '' + ); /** * The reply the server sent to us for HELO. - * @type string + * If null, no HELO string has yet been received. + * @var string|null */ - protected $helo_rply = ''; + protected $helo_rply = null; /** - * The most recent reply received from the server. - * @type string + * The set of SMTP extensions sent in reply to EHLO command. + * Indexes of the array are extension names. + * Value at index 'HELO' or 'EHLO' (according to command that was sent) + * represents the server name. In case of HELO it is the only element of the array. + * Other values can be boolean TRUE or an array containing extension options. + * If null, no HELO/EHLO string has yet been received. + * @var array|null */ - protected $last_reply = ''; + protected $server_caps = null; /** - * Constructor. - * @access public + * The most recent reply received from the server. + * @var string */ - public function __construct() - { - $this->smtp_conn = 0; - $this->error = null; - $this->helo_rply = null; - - $this->do_debug = 0; - } + protected $last_reply = ''; /** * Output debugging info via a user-selected method. + * @see SMTP::$Debugoutput + * @see SMTP::$do_debug * @param string $str Debug string to output + * @param integer $level The debug level of this message; see DEBUG_* constants * @return void */ - protected function edebug($str) + protected function edebug($str, $level = 0) { + if ($level > $this->do_debug) { + return; + } + //Avoid clash with built-in function names + if (!in_array($this->Debugoutput, array('error_log', 'html', 'echo')) and is_callable($this->Debugoutput)) { + call_user_func($this->Debugoutput, $str, $level); + return; + } switch ($this->Debugoutput) { case 'error_log': //Don't output, just log @@ -170,109 +236,137 @@ protected function edebug($str) break; case 'echo': default: - echo gmdate('Y-m-d H:i:s')."\t".trim($str)."\n"; + //Normalize line breaks + $str = preg_replace('/(\r\n|\r|\n)/ms', "\n", $str); + echo gmdate('Y-m-d H:i:s') . "\t" . str_replace( + "\n", + "\n \t ", + trim($str) + )."\n"; } } /** * Connect to an SMTP server. - * @param string $host SMTP server IP or host name - * @param int $port The port number to connect to - * @param int $timeout How long to wait for the connection to open + * @param string $host SMTP server IP or host name + * @param integer $port The port number to connect to + * @param integer $timeout How long to wait for the connection to open * @param array $options An array of options for stream_context_create() * @access public - * @return bool + * @return boolean */ public function connect($host, $port = null, $timeout = 30, $options = array()) { + static $streamok; + //This is enabled by default since 5.0.0 but some providers disable it + //Check this once and cache the result + if (is_null($streamok)) { + $streamok = function_exists('stream_socket_client'); + } // Clear errors to avoid confusion - $this->error = null; - + $this->setError(''); // Make sure we are __not__ connected if ($this->connected()) { // Already connected, generate error - $this->error = array('error' => 'Already connected to a server'); + $this->setError('Already connected to a server'); return false; } - if (empty($port)) { $port = self::DEFAULT_SMTP_PORT; } - // Connect to the SMTP server - if ($this->do_debug >= 3) { - $this->edebug('Connection: opening'); - } - + $this->edebug( + "Connection: opening to $host:$port, timeout=$timeout, options=".var_export($options, true), + self::DEBUG_CONNECTION + ); $errno = 0; $errstr = ''; - $socket_context = stream_context_create($options); - //Suppress errors; connection failures are handled at a higher level - $this->smtp_conn = @stream_socket_client( - $host . ":" . $port, - $errno, - $errstr, - $timeout, - STREAM_CLIENT_CONNECT, - $socket_context - ); - + if ($streamok) { + $socket_context = stream_context_create($options); + set_error_handler(array($this, 'errorHandler')); + $this->smtp_conn = stream_socket_client( + $host . ":" . $port, + $errno, + $errstr, + $timeout, + STREAM_CLIENT_CONNECT, + $socket_context + ); + restore_error_handler(); + } else { + //Fall back to fsockopen which should work in more places, but is missing some features + $this->edebug( + "Connection: stream_socket_client not available, falling back to fsockopen", + self::DEBUG_CONNECTION + ); + set_error_handler(array($this, 'errorHandler')); + $this->smtp_conn = fsockopen( + $host, + $port, + $errno, + $errstr, + $timeout + ); + restore_error_handler(); + } // Verify we connected properly - if (empty($this->smtp_conn)) { - $this->error = array( - 'error' => 'Failed to connect to server', - 'errno' => $errno, - 'errstr' => $errstr + if (!is_resource($this->smtp_conn)) { + $this->setError( + 'Failed to connect to server', + $errno, + $errstr + ); + $this->edebug( + 'SMTP ERROR: ' . $this->error['error'] + . ": $errstr ($errno)", + self::DEBUG_CLIENT ); - if ($this->do_debug >= 1) { - $this->edebug( - 'SMTP ERROR: ' . $this->error['error'] - . ": $errstr ($errno)" - ); - } return false; } - if ($this->do_debug >= 3) { - $this->edebug('Connection: opened'); - } - + $this->edebug('Connection: opened', self::DEBUG_CONNECTION); // SMTP server can take longer to respond, give longer timeout for first read // Windows does not have support for this timeout function if (substr(PHP_OS, 0, 3) != 'WIN') { $max = ini_get('max_execution_time'); - if ($max != 0 && $timeout > $max) { // Don't bother if unlimited + // Don't bother if unlimited + if ($max != 0 && $timeout > $max) { @set_time_limit($timeout); } stream_set_timeout($this->smtp_conn, $timeout, 0); } - // Get any announcement $announce = $this->get_lines(); - - if ($this->do_debug >= 2) { - $this->edebug('SERVER -> CLIENT: ' . $announce); - } - + $this->edebug('SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER); return true; } /** * Initiate a TLS (encrypted) session. * @access public - * @return bool + * @return boolean */ public function startTLS() { - if (!$this->sendCommand("STARTTLS", "STARTTLS", 220)) { + if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) { return false; } + + //Allow the best TLS version(s) we can + $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT; + + //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT + //so add them back in manually if we can + if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { + $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; + } + // Begin encrypted connection if (!stream_socket_enable_crypto( $this->smtp_conn, true, - STREAM_CRYPTO_METHOD_TLS_CLIENT - ) - ) { + $crypto_method + )) { return false; } return true; @@ -282,25 +376,64 @@ public function startTLS() * Perform SMTP authentication. * Must be run after hello(). * @see hello() - * @param string $username The user name - * @param string $password The password - * @param string $authtype The auth type (PLAIN, LOGIN, NTLM, CRAM-MD5) - * @param string $realm The auth realm for NTLM + * @param string $username The user name + * @param string $password The password + * @param string $authtype The auth type (PLAIN, LOGIN, NTLM, CRAM-MD5, XOAUTH2) + * @param string $realm The auth realm for NTLM * @param string $workstation The auth workstation for NTLM - * @access public - * @return bool True if successfully authenticated. + * @param null|OAuth $OAuth An optional OAuth instance (@see PHPMailerOAuth) + * @return bool True if successfully authenticated.* @access public */ public function authenticate( $username, $password, - $authtype = 'LOGIN', + $authtype = null, $realm = '', - $workstation = '' + $workstation = '', + $OAuth = null ) { - if (empty($authtype)) { - $authtype = 'LOGIN'; + if (!$this->server_caps) { + $this->setError('Authentication is not allowed before HELO/EHLO'); + return false; } + if (array_key_exists('EHLO', $this->server_caps)) { + // SMTP extensions are available. Let's try to find a proper authentication method + + if (!array_key_exists('AUTH', $this->server_caps)) { + $this->setError('Authentication is not allowed at this stage'); + // 'at this stage' means that auth may be allowed after the stage changes + // e.g. after STARTTLS + return false; + } + + self::edebug('Auth method requested: ' . ($authtype ? $authtype : 'UNKNOWN'), self::DEBUG_LOWLEVEL); + self::edebug( + 'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']), + self::DEBUG_LOWLEVEL + ); + + if (empty($authtype)) { + foreach (array('CRAM-MD5', 'LOGIN', 'PLAIN', 'NTLM', 'XOAUTH2') as $method) { + if (in_array($method, $this->server_caps['AUTH'])) { + $authtype = $method; + break; + } + } + if (empty($authtype)) { + $this->setError('No supported authentication methods found'); + return false; + } + self::edebug('Auth method selected: '.$authtype, self::DEBUG_LOWLEVEL); + } + + if (!in_array($authtype, $this->server_caps['AUTH'])) { + $this->setError("The requested authentication method \"$authtype\" is not supported by the server"); + return false; + } + } elseif (empty($authtype)) { + $authtype = 'LOGIN'; + } switch ($authtype) { case 'PLAIN': // Start authentication @@ -329,6 +462,19 @@ public function authenticate( return false; } break; + case 'XOAUTH2': + //If the OAuth Instance is not set. Can be a case when PHPMailer is used + //instead of PHPMailerOAuth + if (is_null($OAuth)) { + return false; + } + $oauth = $OAuth->getOauth64(); + + // Start authentication + if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) { + return false; + } + break; case 'NTLM': /* * ntlm_sasl_client.php @@ -339,21 +485,20 @@ public function authenticate( * PROTOCOL Docs http://curl.haxx.se/rfc/ntlm.html#ntlmSmtpAuthentication */ require_once 'extras/ntlm_sasl_client.php'; - $temp = new stdClass(); + $temp = new stdClass; $ntlm_client = new ntlm_sasl_client_class; //Check that functions are available - if (!$ntlm_client->Initialize($temp)) { - $this->error = array('error' => $temp->error); - if ($this->do_debug >= 1) { - $this->edebug( - 'You need to enable some modules in your php.ini file: ' - . $this->error['error'] - ); - } + if (!$ntlm_client->initialize($temp)) { + $this->setError($temp->error); + $this->edebug( + 'You need to enable some modules in your php.ini file: ' + . $this->error['error'], + self::DEBUG_CLIENT + ); return false; } //msg1 - $msg1 = $ntlm_client->TypeMsg1($realm, $workstation); //msg1 + $msg1 = $ntlm_client->typeMsg1($realm, $workstation); //msg1 if (!$this->sendCommand( 'AUTH NTLM', @@ -363,7 +508,6 @@ public function authenticate( ) { return false; } - //Though 0 based, there is a white space after the 3 digit number //msg2 $challenge = substr($this->last_reply, 3); @@ -373,7 +517,7 @@ public function authenticate( $password ); //msg3 - $msg3 = $ntlm_client->TypeMsg3( + $msg3 = $ntlm_client->typeMsg3( $ntlm_res, $username, $realm, @@ -381,7 +525,6 @@ public function authenticate( ); // send encoded username return $this->sendCommand('Username', base64_encode($msg3), 235); - break; case 'CRAM-MD5': // Start authentication if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) { @@ -395,7 +538,9 @@ public function authenticate( // send encoded credentials return $this->sendCommand('Username', base64_encode($response), 235); - break; + default: + $this->setError("Authentication method \"$authtype\" is not supported"); + return false; } return true; } @@ -421,15 +566,15 @@ protected function hmac($data, $key) // RFC 2104 HMAC implementation for php. // Creates an md5 HMAC. // Eliminates the need to install mhash to compute a HMAC - // Hacked by Lance Rushing + // by Lance Rushing - $b = 64; // byte length for md5 - if (strlen($key) > $b) { + $bytelen = 64; // byte length for md5 + if (strlen($key) > $bytelen) { $key = pack('H*', md5($key)); } - $key = str_pad($key, $b, chr(0x00)); - $ipad = str_pad('', $b, chr(0x36)); - $opad = str_pad('', $b, chr(0x5c)); + $key = str_pad($key, $bytelen, chr(0x00)); + $ipad = str_pad('', $bytelen, chr(0x36)); + $opad = str_pad('', $bytelen, chr(0x5c)); $k_ipad = $key ^ $ipad; $k_opad = $key ^ $opad; @@ -439,19 +584,18 @@ protected function hmac($data, $key) /** * Check connection state. * @access public - * @return bool True if connected. + * @return boolean True if connected. */ public function connected() { - if (!empty($this->smtp_conn)) { + if (is_resource($this->smtp_conn)) { $sock_status = stream_get_meta_data($this->smtp_conn); if ($sock_status['eof']) { - // the socket is valid but we are not connected - if ($this->do_debug >= 1) { - $this->edebug( - 'SMTP NOTICE: EOF caught while checking if connected' - ); - } + // The socket is valid but we are not connected + $this->edebug( + 'SMTP NOTICE: EOF caught while checking if connected', + self::DEBUG_CLIENT + ); $this->close(); return false; } @@ -469,15 +613,14 @@ public function connected() */ public function close() { - $this->error = null; // so there is no confusion + $this->setError(''); + $this->server_caps = null; $this->helo_rply = null; - if (!empty($this->smtp_conn)) { + if (is_resource($this->smtp_conn)) { // close the connection and cleanup fclose($this->smtp_conn); - if ($this->do_debug >= 3) { - $this->edebug('Connection: closed'); - } - $this->smtp_conn = 0; + $this->smtp_conn = null; //Makes for cleaner serialization + $this->edebug('Connection: closed', self::DEBUG_CONNECTION); } } @@ -491,111 +634,101 @@ public function close() * Implements rfc 821: DATA * @param string $msg_data Message data to send * @access public - * @return bool + * @return boolean */ public function data($msg_data) { + //This will use the standard timelimit if (!$this->sendCommand('DATA', 'DATA', 354)) { return false; } /* The server is ready to accept data! - * according to rfc821 we should not send more than 1000 - * including the CRLF - * characters on a single line so we will break the data up - * into lines by \r and/or \n then if needed we will break - * each of those into smaller lines to fit within the limit. - * in addition we will be looking for lines that start with - * a period '.' and append and additional period '.' to that - * line. NOTE: this does not count towards limit. + * According to rfc821 we should not send more than 1000 characters on a single line (including the CRLF) + * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into + * smaller lines to fit within the limit. + * We will also look for lines that start with a '.' and prepend an additional '.'. + * NOTE: this does not count towards line-length limit. */ - // Normalize the line breaks before exploding - $msg_data = str_replace("\r\n", "\n", $msg_data); - $msg_data = str_replace("\r", "\n", $msg_data); - $lines = explode("\n", $msg_data); - - /* We need to find a good way to determine if headers are - * in the msg_data or if it is a straight msg body - * currently I am assuming rfc822 definitions of msg headers - * and if the first field of the first line (':' separated) - * does not contain a space then it _should_ be a header - * and we can process all lines before a blank "" line as - * headers. + // Normalize line breaks before exploding + $lines = explode("\n", str_replace(array("\r\n", "\r"), "\n", $msg_data)); + + /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field + * of the first line (':' separated) does not contain a space then it _should_ be a header and we will + * process all lines before a blank line as headers. */ $field = substr($lines[0], 0, strpos($lines[0], ':')); $in_headers = false; - if (!empty($field) && !strstr($field, ' ')) { + if (!empty($field) && strpos($field, ' ') === false) { $in_headers = true; } - //RFC 2822 section 2.1.1 limit - $max_line_length = 998; - foreach ($lines as $line) { - $lines_out = null; - if ($line == '' && $in_headers) { + $lines_out = array(); + if ($in_headers and $line == '') { $in_headers = false; } - // ok we need to break this line up into several smaller lines - while (strlen($line) > $max_line_length) { - $pos = strrpos(substr($line, 0, $max_line_length), ' '); - - // Patch to fix DOS attack + //Break this line up into several smaller lines if it's too long + //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len), + while (isset($line[self::MAX_LINE_LENGTH])) { + //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on + //so as to avoid breaking in the middle of a word + $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' '); + //Deliberately matches both false and 0 if (!$pos) { - $pos = $max_line_length - 1; + //No nice break found, add a hard break + $pos = self::MAX_LINE_LENGTH - 1; $lines_out[] = substr($line, 0, $pos); $line = substr($line, $pos); } else { + //Break at the found point $lines_out[] = substr($line, 0, $pos); + //Move along by the amount we dealt with $line = substr($line, $pos + 1); } - - /* If processing headers add a LWSP-char to the front of new line - * rfc822 on long msg headers - */ + //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1 if ($in_headers) { $line = "\t" . $line; } } $lines_out[] = $line; - // send the lines to the server - while (list(, $line_out) = @each($lines_out)) { - if (strlen($line_out) > 0) { - if (substr($line_out, 0, 1) == '.') { - $line_out = '.' . $line_out; - } + //Send the lines to the server + foreach ($lines_out as $line_out) { + //RFC2821 section 4.5.2 + if (!empty($line_out) and $line_out[0] == '.') { + $line_out = '.' . $line_out; } $this->client_send($line_out . self::CRLF); } } - // Message data has been sent, complete the command - return $this->sendCommand('DATA END', '.', 250); + //Message data has been sent, complete the command + //Increase timelimit for end of DATA command + $savetimelimit = $this->Timelimit; + $this->Timelimit = $this->Timelimit * 2; + $result = $this->sendCommand('DATA END', '.', 250); + //Restore timelimit + $this->Timelimit = $savetimelimit; + return $result; } /** * Send an SMTP HELO or EHLO command. * Used to identify the sending server to the receiving server. * This makes sure that client and server are in a known state. - * Implements from RFC 821: HELO + * Implements RFC 821: HELO * and RFC 2821 EHLO. * @param string $host The host name or IP to connect to * @access public - * @return bool + * @return boolean */ public function hello($host = '') { - // Try extended hello first (RFC 2821) - if (!$this->sendHello('EHLO', $host)) { - if (!$this->sendHello('HELO', $host)) { - return false; - } - } - - return true; + //Try extended hello first (RFC 2821) + return (boolean)($this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host)); } /** @@ -603,17 +736,64 @@ public function hello($host = '') * Low-level implementation used by hello() * @see hello() * @param string $hello The HELO string - * @param string $host The hostname to say we are + * @param string $host The hostname to say we are * @access protected - * @return bool + * @return boolean */ protected function sendHello($hello, $host) { $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250); $this->helo_rply = $this->last_reply; + if ($noerror) { + $this->parseHelloFields($hello); + } else { + $this->server_caps = null; + } return $noerror; } + /** + * Parse a reply to HELO/EHLO command to discover server extensions. + * In case of HELO, the only parameter that can be discovered is a server name. + * @access protected + * @param string $type - 'HELO' or 'EHLO' + */ + protected function parseHelloFields($type) + { + $this->server_caps = array(); + $lines = explode("\n", $this->helo_rply); + + foreach ($lines as $n => $s) { + //First 4 chars contain response code followed by - or space + $s = trim(substr($s, 4)); + if (empty($s)) { + continue; + } + $fields = explode(' ', $s); + if (!empty($fields)) { + if (!$n) { + $name = $type; + $fields = $fields[0]; + } else { + $name = array_shift($fields); + switch ($name) { + case 'SIZE': + $fields = ($fields ? $fields[0] : 0); + break; + case 'AUTH': + if (!is_array($fields)) { + $fields = array(); + } + break; + default: + $fields = true; + } + } + $this->server_caps[$name] = $fields; + } + } + } + /** * Send an SMTP MAIL command. * Starts a mail transaction from the email address specified in @@ -623,7 +803,7 @@ protected function sendHello($hello, $host) * Implements rfc 821: MAIL FROM: * @param string $from Source address of this message * @access public - * @return bool + * @return boolean */ public function mail($from) { @@ -639,35 +819,35 @@ public function mail($from) * Send an SMTP QUIT command. * Closes the socket if there is no error or the $close_on_error argument is true. * Implements from rfc 821: QUIT - * @param bool $close_on_error Should the connection close if an error occurs? + * @param boolean $close_on_error Should the connection close if an error occurs? * @access public - * @return bool + * @return boolean */ public function quit($close_on_error = true) { $noerror = $this->sendCommand('QUIT', 'QUIT', 221); - $e = $this->error; //Save any error + $err = $this->error; //Save any error if ($noerror or $close_on_error) { $this->close(); - $this->error = $e; //Restore any error from the quit command + $this->error = $err; //Restore any error from the quit command } return $noerror; } /** * Send an SMTP RCPT command. - * Sets the TO argument to $to. + * Sets the TO argument to $toaddr. * Returns true if the recipient was accepted false if it was rejected. * Implements from rfc 821: RCPT TO: - * @param string $to The address the message is being sent to + * @param string $address The address the message is being sent to * @access public - * @return bool + * @return boolean */ - public function recipient($to) + public function recipient($address) { return $this->sendCommand( - 'RCPT TO ', - 'RCPT TO:<' . $to . '>', + 'RCPT TO', + 'RCPT TO:<' . $address . '>', array(250, 251) ); } @@ -677,7 +857,7 @@ public function recipient($to) * Abort any transaction that is currently in progress. * Implements rfc 821: RSET * @access public - * @return bool True on success. + * @return boolean True on success. */ public function reset() { @@ -686,46 +866,61 @@ public function reset() /** * Send a command to an SMTP server and check its return code. - * @param string $command The command name - not sent to the server + * @param string $command The command name - not sent to the server * @param string $commandstring The actual command to send - * @param int|array $expect One or more expected integer success codes + * @param integer|array $expect One or more expected integer success codes * @access protected - * @return bool True on success. + * @return boolean True on success. */ protected function sendCommand($command, $commandstring, $expect) { if (!$this->connected()) { - $this->error = array( - "error" => "Called $command without being connected" - ); + $this->setError("Called $command without being connected"); + return false; + } + //Reject line breaks in all commands + if (strpos($commandstring, "\n") !== false or strpos($commandstring, "\r") !== false) { + $this->setError("Command '$command' contained line breaks"); return false; } $this->client_send($commandstring . self::CRLF); - $reply = $this->get_lines(); - $code = substr($reply, 0, 3); - - if ($this->do_debug >= 2) { - $this->edebug('SERVER -> CLIENT: ' . $reply); + $this->last_reply = $this->get_lines(); + // Fetch SMTP code and possible error code explanation + $matches = array(); + if (preg_match("/^([0-9]{3})[ -](?:([0-9]\\.[0-9]\\.[0-9]) )?/", $this->last_reply, $matches)) { + $code = $matches[1]; + $code_ex = (count($matches) > 2 ? $matches[2] : null); + // Cut off error code from each response line + $detail = preg_replace( + "/{$code}[ -]".($code_ex ? str_replace('.', '\\.', $code_ex).' ' : '')."/m", + '', + $this->last_reply + ); + } else { + // Fall back to simple parsing if regex fails + $code = substr($this->last_reply, 0, 3); + $code_ex = null; + $detail = substr($this->last_reply, 4); } + $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER); + if (!in_array($code, (array)$expect)) { - $this->last_reply = null; - $this->error = array( - "error" => "$command command failed", - "smtp_code" => $code, - "detail" => substr($reply, 4) + $this->setError( + "$command command failed", + $detail, + $code, + $code_ex + ); + $this->edebug( + 'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply, + self::DEBUG_CLIENT ); - if ($this->do_debug >= 1) { - $this->edebug( - 'SMTP ERROR: ' . $this->error['error'] . ': ' . $reply - ); - } return false; } - $this->last_reply = $reply; - $this->error = null; + $this->setError(''); return true; } @@ -740,52 +935,48 @@ protected function sendCommand($command, $commandstring, $expect) * Implements rfc 821: SAML FROM: * @param string $from The address the message is from * @access public - * @return bool + * @return boolean */ public function sendAndMail($from) { - return $this->sendCommand("SAML", "SAML FROM:$from", 250); + return $this->sendCommand('SAML', "SAML FROM:$from", 250); } /** * Send an SMTP VRFY command. * @param string $name The name to verify * @access public - * @return bool + * @return boolean */ public function verify($name) { - return $this->sendCommand("VRFY", "VRFY $name", array(250, 251)); + return $this->sendCommand('VRFY', "VRFY $name", array(250, 251)); } /** * Send an SMTP NOOP command. * Used to keep keep-alives alive, doesn't actually do anything * @access public - * @return bool + * @return boolean */ public function noop() { - return $this->sendCommand("NOOP", "NOOP", 250); + return $this->sendCommand('NOOP', 'NOOP', 250); } /** * Send an SMTP TURN command. * This is an optional command for SMTP that this class does not support. - * This method is here to make the RFC821 Definition - * complete for this class and __may__ be implemented in future + * This method is here to make the RFC821 Definition complete for this class + * and _may_ be implemented in future * Implements from rfc 821: TURN * @access public - * @return bool + * @return boolean */ public function turn() { - $this->error = array( - 'error' => 'The SMTP TURN command is not implemented' - ); - if ($this->do_debug >= 1) { - $this->edebug('SMTP NOTICE: ' . $this->error['error']); - } + $this->setError('The SMTP TURN command is not implemented'); + $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT); return false; } @@ -793,13 +984,11 @@ public function turn() * Send raw data to the server. * @param string $data The data to send * @access public - * @return int|bool The number of bytes sent to the server or FALSE on error + * @return integer|boolean The number of bytes sent to the server or false on error */ public function client_send($data) { - if ($this->do_debug >= 1) { - $this->edebug("CLIENT -> SERVER: $data"); - } + $this->edebug("CLIENT -> SERVER: $data", self::DEBUG_CLIENT); return fwrite($this->smtp_conn, $data); } @@ -813,6 +1002,57 @@ public function getError() return $this->error; } + /** + * Get SMTP extensions available on the server + * @access public + * @return array|null + */ + public function getServerExtList() + { + return $this->server_caps; + } + + /** + * A multipurpose method + * The method works in three ways, dependent on argument value and current state + * 1. HELO/EHLO was not sent - returns null and set up $this->error + * 2. HELO was sent + * $name = 'HELO': returns server name + * $name = 'EHLO': returns boolean false + * $name = any string: returns null and set up $this->error + * 3. EHLO was sent + * $name = 'HELO'|'EHLO': returns server name + * $name = any string: if extension $name exists, returns boolean True + * or its options. Otherwise returns boolean False + * In other words, one can use this method to detect 3 conditions: + * - null returned: handshake was not or we don't know about ext (refer to $this->error) + * - false returned: the requested feature exactly not exists + * - positive value returned: the requested feature exists + * @param string $name Name of SMTP extension or 'HELO'|'EHLO' + * @return mixed + */ + public function getServerExt($name) + { + if (!$this->server_caps) { + $this->setError('No HELO/EHLO was sent'); + return null; + } + + // the tight logic knot ;) + if (!array_key_exists($name, $this->server_caps)) { + if ($name == 'HELO') { + return $this->server_caps['EHLO']; + } + if ($name == 'EHLO' || array_key_exists('EHLO', $this->server_caps)) { + return false; + } + $this->setError('HELO handshake was used. Client knows nothing about server extensions'); + return null; + } + + return $this->server_caps[$name]; + } + /** * Get the last reply from the server. * @access public @@ -834,51 +1074,42 @@ public function getLastReply() */ protected function get_lines() { - $data = ''; - $endtime = 0; - // If the connection is bad, give up now + // If the connection is bad, give up straight away if (!is_resource($this->smtp_conn)) { - return $data; + return ''; } + $data = ''; + $endtime = 0; stream_set_timeout($this->smtp_conn, $this->Timeout); if ($this->Timelimit > 0) { $endtime = time() + $this->Timelimit; } while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) { $str = @fgets($this->smtp_conn, 515); - if ($this->do_debug >= 4) { - $this->edebug("SMTP -> get_lines(): \$data was \"$data\""); - $this->edebug("SMTP -> get_lines(): \$str is \"$str\""); - } + $this->edebug("SMTP -> get_lines(): \$data is \"$data\"", self::DEBUG_LOWLEVEL); + $this->edebug("SMTP -> get_lines(): \$str is \"$str\"", self::DEBUG_LOWLEVEL); $data .= $str; - if ($this->do_debug >= 4) { - $this->edebug("SMTP -> get_lines(): \$data is \"$data\""); - } - // if 4th character is a space, we are done reading, break the loop - if (substr($str, 3, 1) == ' ') { + // If 4th character is a space, we are done reading, break the loop, micro-optimisation over strlen + if ((isset($str[3]) and $str[3] == ' ')) { break; } // Timed-out? Log and break $info = stream_get_meta_data($this->smtp_conn); if ($info['timed_out']) { - if ($this->do_debug >= 4) { - $this->edebug( - 'SMTP -> get_lines(): timed-out (' . $this->Timeout . ' sec)' - ); - } + $this->edebug( + 'SMTP -> get_lines(): timed-out (' . $this->Timeout . ' sec)', + self::DEBUG_LOWLEVEL + ); break; } // Now check if reads took too long - if ($endtime) { - if (time() > $endtime) { - if ($this->do_debug >= 4) { - $this->edebug( - 'SMTP -> get_lines(): timelimit reached (' - . $this->Timelimit . ' sec)' - ); - } - break; - } + if ($endtime and time() > $endtime) { + $this->edebug( + 'SMTP -> get_lines(): timelimit reached ('. + $this->Timelimit . ' sec)', + self::DEBUG_LOWLEVEL + ); + break; } } return $data; @@ -886,7 +1117,7 @@ protected function get_lines() /** * Enable or disable VERP address generation. - * @param bool $enabled + * @param boolean $enabled */ public function setVerp($enabled = false) { @@ -895,16 +1126,33 @@ public function setVerp($enabled = false) /** * Get VERP address generation mode. - * @return bool + * @return boolean */ public function getVerp() { return $this->do_verp; } + /** + * Set error messages and codes. + * @param string $message The error message + * @param string $detail Further detail on the error + * @param string $smtp_code An associated SMTP error code + * @param string $smtp_code_ex Extended SMTP code + */ + protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '') + { + $this->error = array( + 'error' => $message, + 'detail' => $detail, + 'smtp_code' => $smtp_code, + 'smtp_code_ex' => $smtp_code_ex + ); + } + /** * Set debug output method. - * @param string $method The function/method to use for debugging output. + * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it. */ public function setDebugOutput($method = 'echo') { @@ -922,7 +1170,7 @@ public function getDebugOutput() /** * Set debug output level. - * @param int $level + * @param integer $level */ public function setDebugLevel($level = 0) { @@ -931,7 +1179,7 @@ public function setDebugLevel($level = 0) /** * Get debug output level. - * @return int + * @return integer */ public function getDebugLevel() { @@ -940,7 +1188,7 @@ public function getDebugLevel() /** * Set SMTP timeout. - * @param int $timeout + * @param integer $timeout */ public function setTimeout($timeout = 0) { @@ -949,10 +1197,53 @@ public function setTimeout($timeout = 0) /** * Get SMTP timeout. - * @return int + * @return integer */ public function getTimeout() { return $this->Timeout; } + + /** + * Reports an error number and string. + * @param integer $errno The error number returned by PHP. + * @param string $errmsg The error message returned by PHP. + */ + protected function errorHandler($errno, $errmsg) + { + $notice = 'Connection: Failed to connect to server.'; + $this->setError( + $notice, + $errno, + $errmsg + ); + $this->edebug( + $notice . ' Error number ' . $errno . '. "Error notice: ' . $errmsg, + self::DEBUG_CONNECTION + ); + } + + /** + * Will return the ID of the last smtp transaction based on a list of patterns provided + * in SMTP::$smtp_transaction_id_patterns. + * If no reply has been received yet, it will return null. + * If no pattern has been matched, it will return false. + * @return bool|null|string + */ + public function getLastTransactionID() + { + $reply = $this->getLastReply(); + + if (empty($reply)) { + return null; + } + + foreach($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) { + if(preg_match($smtp_transaction_id_pattern, $reply, $matches)) { + return $matches[1]; + } + } + + return false; + } } diff --git a/helpers/phpmailer/extras/EasyPeasyICS.php b/helpers/phpmailer/extras/EasyPeasyICS.php new file mode 100755 index 00000000..d8bfcfae --- /dev/null +++ b/helpers/phpmailer/extras/EasyPeasyICS.php @@ -0,0 +1,148 @@ + + * @author Manuel Reinhard + * + * Built with inspiration from + * http://stackoverflow.com/questions/1463480/how-can-i-use-php-to-dynamically-publish-an-ical-file-to-be-read-by-google-calend/1464355#1464355 + * History: + * 2010/12/17 - Manuel Reinhard - when it all started + * 2014 PHPMailer project becomes maintainer + */ + +/** + * Class EasyPeasyICS. + * Simple ICS data generator + * @package phpmailer + * @subpackage easypeasyics + */ +class EasyPeasyICS +{ + /** + * The name of the calendar + * @var string + */ + protected $calendarName; + /** + * The array of events to add to this calendar + * @var array + */ + protected $events = array(); + + /** + * Constructor + * @param string $calendarName + */ + public function __construct($calendarName = "") + { + $this->calendarName = $calendarName; + } + + /** + * Add an event to this calendar. + * @param string $start The start date and time as a unix timestamp + * @param string $end The end date and time as a unix timestamp + * @param string $summary A summary or title for the event + * @param string $description A description of the event + * @param string $url A URL for the event + * @param string $uid A unique identifier for the event - generated automatically if not provided + * @return array An array of event details, including any generated UID + */ + public function addEvent($start, $end, $summary = '', $description = '', $url = '', $uid = '') + { + if (empty($uid)) { + $uid = md5(uniqid(mt_rand(), true)) . '@EasyPeasyICS'; + } + $event = array( + 'start' => gmdate('Ymd', $start) . 'T' . gmdate('His', $start) . 'Z', + 'end' => gmdate('Ymd', $end) . 'T' . gmdate('His', $end) . 'Z', + 'summary' => $summary, + 'description' => $description, + 'url' => $url, + 'uid' => $uid + ); + $this->events[] = $event; + return $event; + } + + /** + * @return array Get the array of events. + */ + public function getEvents() + { + return $this->events; + } + + /** + * Clear all events. + */ + public function clearEvents() + { + $this->events = array(); + } + + /** + * Get the name of the calendar. + * @return string + */ + public function getName() + { + return $this->calendarName; + } + + /** + * Set the name of the calendar. + * @param $name + */ + public function setName($name) + { + $this->calendarName = $name; + } + + /** + * Render and optionally output a vcal string. + * @param bool $output Whether to output the calendar data directly (the default). + * @return string The complete rendered vlal + */ + public function render($output = true) + { + //Add header + $ics = 'BEGIN:VCALENDAR +METHOD:PUBLISH +VERSION:2.0 +X-WR-CALNAME:' . $this->calendarName . ' +PRODID:-//hacksw/handcal//NONSGML v1.0//EN'; + + //Add events + foreach ($this->events as $event) { + $ics .= ' +BEGIN:VEVENT +UID:' . $event['uid'] . ' +DTSTAMP:' . gmdate('Ymd') . 'T' . gmdate('His') . 'Z +DTSTART:' . $event['start'] . ' +DTEND:' . $event['end'] . ' +SUMMARY:' . str_replace("\n", "\\n", $event['summary']) . ' +DESCRIPTION:' . str_replace("\n", "\\n", $event['description']) . ' +URL;VALUE=URI:' . $event['url'] . ' +END:VEVENT'; + } + + //Add footer + $ics .= ' +END:VCALENDAR'; + + if ($output) { + //Output + $filename = $this->calendarName; + //Filename needs quoting if it contains spaces + if (strpos($filename, ' ') !== false) { + $filename = '"'.$filename.'"'; + } + header('Content-type: text/calendar; charset=utf-8'); + header('Content-Disposition: inline; filename=' . $filename . '.ics'); + echo $ics; + } + return $ics; + } +} diff --git a/helpers/phpmailer/extras/README.md b/helpers/phpmailer/extras/README.md new file mode 100755 index 00000000..dac79e05 --- /dev/null +++ b/helpers/phpmailer/extras/README.md @@ -0,0 +1,17 @@ +#PHPMailer Extras + +These classes provide optional additional functions to PHPMailer. + +These are not loaded by the PHPMailer autoloader, so in some cases you may need to `require` them yourself before using them. + +##EasyPeasyICS + +This class was originally written by Manuel Reinhard and provides a simple means of generating ICS/vCal files that are used in sending calendar events. PHPMailer does not use it directly, but you can use it to generate content appropriate for placing in the `Ical` property of PHPMailer. The PHPMailer project is now its official home as Manuel has given permission for that and is no longer maintaining it himself. + +##htmlfilter + +This class by Konstantin Riabitsev and Jim Jagielski implements HTML filtering to remove potentially malicious tags, such as `)#si', + array($this, 'scriptCallback'), + $html + ); + } + + $html = $this->normalize($html, $config, $context); + + $cursor = 0; // our location in the text + $inside_tag = false; // whether or not we're parsing the inside of a tag + $array = array(); // result array + + // This is also treated to mean maintain *column* numbers too + $maintain_line_numbers = $config->get('Core.MaintainLineNumbers'); + + if ($maintain_line_numbers === null) { + // automatically determine line numbering by checking + // if error collection is on + $maintain_line_numbers = $config->get('Core.CollectErrors'); + } + + if ($maintain_line_numbers) { + $current_line = 1; + $current_col = 0; + $length = strlen($html); + } else { + $current_line = false; + $current_col = false; + $length = false; + } + $context->register('CurrentLine', $current_line); + $context->register('CurrentCol', $current_col); + $nl = "\n"; + // how often to manually recalculate. This will ALWAYS be right, + // but it's pretty wasteful. Set to 0 to turn off + $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval'); + + $e = false; + if ($config->get('Core.CollectErrors')) { + $e =& $context->get('ErrorCollector'); + } + + // for testing synchronization + $loops = 0; + + while (++$loops) { + // $cursor is either at the start of a token, or inside of + // a tag (i.e. there was a < immediately before it), as indicated + // by $inside_tag + + if ($maintain_line_numbers) { + // $rcursor, however, is always at the start of a token. + $rcursor = $cursor - (int)$inside_tag; + + // Column number is cheap, so we calculate it every round. + // We're interested at the *end* of the newline string, so + // we need to add strlen($nl) == 1 to $nl_pos before subtracting it + // from our "rcursor" position. + $nl_pos = strrpos($html, $nl, $rcursor - $length); + $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1); + + // recalculate lines + if ($synchronize_interval && // synchronization is on + $cursor > 0 && // cursor is further than zero + $loops % $synchronize_interval === 0) { // time to synchronize! + $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor); + } + } + + $position_next_lt = strpos($html, '<', $cursor); + $position_next_gt = strpos($html, '>', $cursor); + + // triggers on "asdf" but not "asdf " + // special case to set up context + if ($position_next_lt === $cursor) { + $inside_tag = true; + $cursor++; + } + + if (!$inside_tag && $position_next_lt !== false) { + // We are not inside tag and there still is another tag to parse + $token = new + HTMLPurifier_Token_Text( + $this->parseData( + substr( + $html, + $cursor, + $position_next_lt - $cursor + ) + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor); + } + $array[] = $token; + $cursor = $position_next_lt + 1; + $inside_tag = true; + continue; + } elseif (!$inside_tag) { + // We are not inside tag but there are no more tags + // If we're already at the end, break + if ($cursor === strlen($html)) { + break; + } + // Create Text of rest of string + $token = new + HTMLPurifier_Token_Text( + $this->parseData( + substr( + $html, + $cursor + ) + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + } + $array[] = $token; + break; + } elseif ($inside_tag && $position_next_gt !== false) { + // We are in tag and it is well formed + // Grab the internals of the tag + $strlen_segment = $position_next_gt - $cursor; + + if ($strlen_segment < 1) { + // there's nothing to process! + $token = new HTMLPurifier_Token_Text('<'); + $cursor++; + continue; + } + + $segment = substr($html, $cursor, $strlen_segment); + + if ($segment === false) { + // somehow, we attempted to access beyond the end of + // the string, defense-in-depth, reported by Nate Abele + break; + } + + // Check if it's a comment + if (substr($segment, 0, 3) === '!--') { + // re-determine segment length, looking for --> + $position_comment_end = strpos($html, '-->', $cursor); + if ($position_comment_end === false) { + // uh oh, we have a comment that extends to + // infinity. Can't be helped: set comment + // end position to end of string + if ($e) { + $e->send(E_WARNING, 'Lexer: Unclosed comment'); + } + $position_comment_end = strlen($html); + $end = true; + } else { + $end = false; + } + $strlen_segment = $position_comment_end - $cursor; + $segment = substr($html, $cursor, $strlen_segment); + $token = new + HTMLPurifier_Token_Comment( + substr( + $segment, + 3, + $strlen_segment - 3 + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment); + } + $array[] = $token; + $cursor = $end ? $position_comment_end : $position_comment_end + 3; + $inside_tag = false; + continue; + } + + // Check if it's an end tag + $is_end_tag = (strpos($segment, '/') === 0); + if ($is_end_tag) { + $type = substr($segment, 1); + $token = new HTMLPurifier_Token_End($type); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $inside_tag = false; + $cursor = $position_next_gt + 1; + continue; + } + + // Check leading character is alnum, if not, we may + // have accidently grabbed an emoticon. Translate into + // text and go our merry way + if (!ctype_alpha($segment[0])) { + // XML: $segment[0] !== '_' && $segment[0] !== ':' + if ($e) { + $e->send(E_NOTICE, 'Lexer: Unescaped lt'); + } + $token = new HTMLPurifier_Token_Text('<'); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $inside_tag = false; + continue; + } + + // Check if it is explicitly self closing, if so, remove + // trailing slash. Remember, we could have a tag like
, so + // any later token processing scripts must convert improperly + // classified EmptyTags from StartTags. + $is_self_closing = (strrpos($segment, '/') === $strlen_segment - 1); + if ($is_self_closing) { + $strlen_segment--; + $segment = substr($segment, 0, $strlen_segment); + } + + // Check if there are any attributes + $position_first_space = strcspn($segment, $this->_whitespace); + + if ($position_first_space >= $strlen_segment) { + if ($is_self_closing) { + $token = new HTMLPurifier_Token_Empty($segment); + } else { + $token = new HTMLPurifier_Token_Start($segment); + } + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $inside_tag = false; + $cursor = $position_next_gt + 1; + continue; + } + + // Grab out all the data + $type = substr($segment, 0, $position_first_space); + $attribute_string = + trim( + substr( + $segment, + $position_first_space + ) + ); + if ($attribute_string) { + $attr = $this->parseAttributeString( + $attribute_string, + $config, + $context + ); + } else { + $attr = array(); + } + + if ($is_self_closing) { + $token = new HTMLPurifier_Token_Empty($type, $attr); + } else { + $token = new HTMLPurifier_Token_Start($type, $attr); + } + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $cursor = $position_next_gt + 1; + $inside_tag = false; + continue; + } else { + // inside tag, but there's no ending > sign + if ($e) { + $e->send(E_WARNING, 'Lexer: Missing gt'); + } + $token = new + HTMLPurifier_Token_Text( + '<' . + $this->parseData( + substr($html, $cursor) + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + } + // no cursor scroll? Hmm... + $array[] = $token; + break; + } + break; + } + + $context->destroy('CurrentLine'); + $context->destroy('CurrentCol'); + return $array; + } + + /** + * PHP 5.0.x compatible substr_count that implements offset and length + * @param string $haystack + * @param string $needle + * @param int $offset + * @param int $length + * @return int + */ + protected function substrCount($haystack, $needle, $offset, $length) + { + static $oldVersion; + if ($oldVersion === null) { + $oldVersion = version_compare(PHP_VERSION, '5.1', '<'); + } + if ($oldVersion) { + $haystack = substr($haystack, $offset, $length); + return substr_count($haystack, $needle); + } else { + return substr_count($haystack, $needle, $offset, $length); + } + } + + /** + * Takes the inside of an HTML tag and makes an assoc array of attributes. + * + * @param string $string Inside of tag excluding name. + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array Assoc array of attributes. + */ + public function parseAttributeString($string, $config, $context) + { + $string = (string)$string; // quick typecast + + if ($string == '') { + return array(); + } // no attributes + + $e = false; + if ($config->get('Core.CollectErrors')) { + $e =& $context->get('ErrorCollector'); + } + + // let's see if we can abort as quickly as possible + // one equal sign, no spaces => one attribute + $num_equal = substr_count($string, '='); + $has_space = strpos($string, ' '); + if ($num_equal === 0 && !$has_space) { + // bool attribute + return array($string => $string); + } elseif ($num_equal === 1 && !$has_space) { + // only one attribute + list($key, $quoted_value) = explode('=', $string); + $quoted_value = trim($quoted_value); + if (!$key) { + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing attribute key'); + } + return array(); + } + if (!$quoted_value) { + return array($key => ''); + } + $first_char = @$quoted_value[0]; + $last_char = @$quoted_value[strlen($quoted_value) - 1]; + + $same_quote = ($first_char == $last_char); + $open_quote = ($first_char == '"' || $first_char == "'"); + + if ($same_quote && $open_quote) { + // well behaved + $value = substr($quoted_value, 1, strlen($quoted_value) - 2); + } else { + // not well behaved + if ($open_quote) { + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing end quote'); + } + $value = substr($quoted_value, 1); + } else { + $value = $quoted_value; + } + } + if ($value === false) { + $value = ''; + } + return array($key => $this->parseData($value)); + } + + // setup loop environment + $array = array(); // return assoc array of attributes + $cursor = 0; // current position in string (moves forward) + $size = strlen($string); // size of the string (stays the same) + + // if we have unquoted attributes, the parser expects a terminating + // space, so let's guarantee that there's always a terminating space. + $string .= ' '; + + $old_cursor = -1; + while ($cursor < $size) { + if ($old_cursor >= $cursor) { + throw new Exception("Infinite loop detected"); + } + $old_cursor = $cursor; + + $cursor += ($value = strspn($string, $this->_whitespace, $cursor)); + // grab the key + + $key_begin = $cursor; //we're currently at the start of the key + + // scroll past all characters that are the key (not whitespace or =) + $cursor += strcspn($string, $this->_whitespace . '=', $cursor); + + $key_end = $cursor; // now at the end of the key + + $key = substr($string, $key_begin, $key_end - $key_begin); + + if (!$key) { + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing attribute key'); + } + $cursor += 1 + strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop + continue; // empty key + } + + // scroll past all whitespace + $cursor += strspn($string, $this->_whitespace, $cursor); + + if ($cursor >= $size) { + $array[$key] = $key; + break; + } + + // if the next character is an equal sign, we've got a regular + // pair, otherwise, it's a bool attribute + $first_char = @$string[$cursor]; + + if ($first_char == '=') { + // key="value" + + $cursor++; + $cursor += strspn($string, $this->_whitespace, $cursor); + + if ($cursor === false) { + $array[$key] = ''; + break; + } + + // we might be in front of a quote right now + + $char = @$string[$cursor]; + + if ($char == '"' || $char == "'") { + // it's quoted, end bound is $char + $cursor++; + $value_begin = $cursor; + $cursor = strpos($string, $char, $cursor); + $value_end = $cursor; + } else { + // it's not quoted, end bound is whitespace + $value_begin = $cursor; + $cursor += strcspn($string, $this->_whitespace, $cursor); + $value_end = $cursor; + } + + // we reached a premature end + if ($cursor === false) { + $cursor = $size; + $value_end = $cursor; + } + + $value = substr($string, $value_begin, $value_end - $value_begin); + if ($value === false) { + $value = ''; + } + $array[$key] = $this->parseData($value); + $cursor++; + } else { + // boolattr + if ($key !== '') { + $array[$key] = $key; + } else { + // purely theoretical + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing attribute key'); + } + } + } + } + return $array; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Lexer/PH5P.php b/helpers/htmlpurifier/HTMLPurifier/Lexer/PH5P.php new file mode 100644 index 00000000..ff4fa218 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Lexer/PH5P.php @@ -0,0 +1,4787 @@ +normalize($html, $config, $context); + $new_html = $this->wrapHTML($new_html, $config, $context); + try { + $parser = new HTML5($new_html); + $doc = $parser->save(); + } catch (DOMException $e) { + // Uh oh, it failed. Punt to DirectLex. + $lexer = new HTMLPurifier_Lexer_DirectLex(); + $context->register('PH5PError', $e); // save the error, so we can detect it + return $lexer->tokenizeHTML($html, $config, $context); // use original HTML + } + $tokens = array(); + $this->tokenizeDOM( + $doc->getElementsByTagName('html')->item(0)-> // + getElementsByTagName('body')->item(0) // + , + $tokens + ); + return $tokens; + } +} + +/* + +Copyright 2007 Jeroen van der Meer + +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. + +*/ + +class HTML5 +{ + private $data; + private $char; + private $EOF; + private $state; + private $tree; + private $token; + private $content_model; + private $escape = false; + private $entities = array( + 'AElig;', + 'AElig', + 'AMP;', + 'AMP', + 'Aacute;', + 'Aacute', + 'Acirc;', + 'Acirc', + 'Agrave;', + 'Agrave', + 'Alpha;', + 'Aring;', + 'Aring', + 'Atilde;', + 'Atilde', + 'Auml;', + 'Auml', + 'Beta;', + 'COPY;', + 'COPY', + 'Ccedil;', + 'Ccedil', + 'Chi;', + 'Dagger;', + 'Delta;', + 'ETH;', + 'ETH', + 'Eacute;', + 'Eacute', + 'Ecirc;', + 'Ecirc', + 'Egrave;', + 'Egrave', + 'Epsilon;', + 'Eta;', + 'Euml;', + 'Euml', + 'GT;', + 'GT', + 'Gamma;', + 'Iacute;', + 'Iacute', + 'Icirc;', + 'Icirc', + 'Igrave;', + 'Igrave', + 'Iota;', + 'Iuml;', + 'Iuml', + 'Kappa;', + 'LT;', + 'LT', + 'Lambda;', + 'Mu;', + 'Ntilde;', + 'Ntilde', + 'Nu;', + 'OElig;', + 'Oacute;', + 'Oacute', + 'Ocirc;', + 'Ocirc', + 'Ograve;', + 'Ograve', + 'Omega;', + 'Omicron;', + 'Oslash;', + 'Oslash', + 'Otilde;', + 'Otilde', + 'Ouml;', + 'Ouml', + 'Phi;', + 'Pi;', + 'Prime;', + 'Psi;', + 'QUOT;', + 'QUOT', + 'REG;', + 'REG', + 'Rho;', + 'Scaron;', + 'Sigma;', + 'THORN;', + 'THORN', + 'TRADE;', + 'Tau;', + 'Theta;', + 'Uacute;', + 'Uacute', + 'Ucirc;', + 'Ucirc', + 'Ugrave;', + 'Ugrave', + 'Upsilon;', + 'Uuml;', + 'Uuml', + 'Xi;', + 'Yacute;', + 'Yacute', + 'Yuml;', + 'Zeta;', + 'aacute;', + 'aacute', + 'acirc;', + 'acirc', + 'acute;', + 'acute', + 'aelig;', + 'aelig', + 'agrave;', + 'agrave', + 'alefsym;', + 'alpha;', + 'amp;', + 'amp', + 'and;', + 'ang;', + 'apos;', + 'aring;', + 'aring', + 'asymp;', + 'atilde;', + 'atilde', + 'auml;', + 'auml', + 'bdquo;', + 'beta;', + 'brvbar;', + 'brvbar', + 'bull;', + 'cap;', + 'ccedil;', + 'ccedil', + 'cedil;', + 'cedil', + 'cent;', + 'cent', + 'chi;', + 'circ;', + 'clubs;', + 'cong;', + 'copy;', + 'copy', + 'crarr;', + 'cup;', + 'curren;', + 'curren', + 'dArr;', + 'dagger;', + 'darr;', + 'deg;', + 'deg', + 'delta;', + 'diams;', + 'divide;', + 'divide', + 'eacute;', + 'eacute', + 'ecirc;', + 'ecirc', + 'egrave;', + 'egrave', + 'empty;', + 'emsp;', + 'ensp;', + 'epsilon;', + 'equiv;', + 'eta;', + 'eth;', + 'eth', + 'euml;', + 'euml', + 'euro;', + 'exist;', + 'fnof;', + 'forall;', + 'frac12;', + 'frac12', + 'frac14;', + 'frac14', + 'frac34;', + 'frac34', + 'frasl;', + 'gamma;', + 'ge;', + 'gt;', + 'gt', + 'hArr;', + 'harr;', + 'hearts;', + 'hellip;', + 'iacute;', + 'iacute', + 'icirc;', + 'icirc', + 'iexcl;', + 'iexcl', + 'igrave;', + 'igrave', + 'image;', + 'infin;', + 'int;', + 'iota;', + 'iquest;', + 'iquest', + 'isin;', + 'iuml;', + 'iuml', + 'kappa;', + 'lArr;', + 'lambda;', + 'lang;', + 'laquo;', + 'laquo', + 'larr;', + 'lceil;', + 'ldquo;', + 'le;', + 'lfloor;', + 'lowast;', + 'loz;', + 'lrm;', + 'lsaquo;', + 'lsquo;', + 'lt;', + 'lt', + 'macr;', + 'macr', + 'mdash;', + 'micro;', + 'micro', + 'middot;', + 'middot', + 'minus;', + 'mu;', + 'nabla;', + 'nbsp;', + 'nbsp', + 'ndash;', + 'ne;', + 'ni;', + 'not;', + 'not', + 'notin;', + 'nsub;', + 'ntilde;', + 'ntilde', + 'nu;', + 'oacute;', + 'oacute', + 'ocirc;', + 'ocirc', + 'oelig;', + 'ograve;', + 'ograve', + 'oline;', + 'omega;', + 'omicron;', + 'oplus;', + 'or;', + 'ordf;', + 'ordf', + 'ordm;', + 'ordm', + 'oslash;', + 'oslash', + 'otilde;', + 'otilde', + 'otimes;', + 'ouml;', + 'ouml', + 'para;', + 'para', + 'part;', + 'permil;', + 'perp;', + 'phi;', + 'pi;', + 'piv;', + 'plusmn;', + 'plusmn', + 'pound;', + 'pound', + 'prime;', + 'prod;', + 'prop;', + 'psi;', + 'quot;', + 'quot', + 'rArr;', + 'radic;', + 'rang;', + 'raquo;', + 'raquo', + 'rarr;', + 'rceil;', + 'rdquo;', + 'real;', + 'reg;', + 'reg', + 'rfloor;', + 'rho;', + 'rlm;', + 'rsaquo;', + 'rsquo;', + 'sbquo;', + 'scaron;', + 'sdot;', + 'sect;', + 'sect', + 'shy;', + 'shy', + 'sigma;', + 'sigmaf;', + 'sim;', + 'spades;', + 'sub;', + 'sube;', + 'sum;', + 'sup1;', + 'sup1', + 'sup2;', + 'sup2', + 'sup3;', + 'sup3', + 'sup;', + 'supe;', + 'szlig;', + 'szlig', + 'tau;', + 'there4;', + 'theta;', + 'thetasym;', + 'thinsp;', + 'thorn;', + 'thorn', + 'tilde;', + 'times;', + 'times', + 'trade;', + 'uArr;', + 'uacute;', + 'uacute', + 'uarr;', + 'ucirc;', + 'ucirc', + 'ugrave;', + 'ugrave', + 'uml;', + 'uml', + 'upsih;', + 'upsilon;', + 'uuml;', + 'uuml', + 'weierp;', + 'xi;', + 'yacute;', + 'yacute', + 'yen;', + 'yen', + 'yuml;', + 'yuml', + 'zeta;', + 'zwj;', + 'zwnj;' + ); + + const PCDATA = 0; + const RCDATA = 1; + const CDATA = 2; + const PLAINTEXT = 3; + + const DOCTYPE = 0; + const STARTTAG = 1; + const ENDTAG = 2; + const COMMENT = 3; + const CHARACTR = 4; + const EOF = 5; + + public function __construct($data) + { + $this->data = $data; + $this->char = -1; + $this->EOF = strlen($data); + $this->tree = new HTML5TreeConstructer; + $this->content_model = self::PCDATA; + + $this->state = 'data'; + + while ($this->state !== null) { + $this->{$this->state . 'State'}(); + } + } + + public function save() + { + return $this->tree->save(); + } + + private function char() + { + return ($this->char < $this->EOF) + ? $this->data[$this->char] + : false; + } + + private function character($s, $l = 0) + { + if ($s + $l < $this->EOF) { + if ($l === 0) { + return $this->data[$s]; + } else { + return substr($this->data, $s, $l); + } + } + } + + private function characters($char_class, $start) + { + return preg_replace('#^([' . $char_class . ']+).*#s', '\\1', substr($this->data, $start)); + } + + private function dataState() + { + // Consume the next input character + $this->char++; + $char = $this->char(); + + if ($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) { + /* U+0026 AMPERSAND (&) + When the content model flag is set to one of the PCDATA or RCDATA + states: switch to the entity data state. Otherwise: treat it as per + the "anything else" entry below. */ + $this->state = 'entityData'; + + } elseif ($char === '-') { + /* If the content model flag is set to either the RCDATA state or + the CDATA state, and the escape flag is false, and there are at + least three characters before this one in the input stream, and the + last four characters in the input stream, including this one, are + U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS, + and U+002D HYPHEN-MINUS (""), + set the escape flag to false. */ + if (($this->content_model === self::RCDATA || + $this->content_model === self::CDATA) && $this->escape === true && + $this->character($this->char, 3) === '-->' + ) { + $this->escape = false; + } + + /* In any case, emit the input character as a character token. + Stay in the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => $char + ) + ); + + } elseif ($this->char === $this->EOF) { + /* EOF + Emit an end-of-file token. */ + $this->EOF(); + + } elseif ($this->content_model === self::PLAINTEXT) { + /* When the content model flag is set to the PLAINTEXT state + THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of + the text and emit it as a character token. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => substr($this->data, $this->char) + ) + ); + + $this->EOF(); + + } else { + /* Anything else + THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that + otherwise would also be treated as a character token and emit it + as a single character token. Stay in the data state. */ + $len = strcspn($this->data, '<&', $this->char); + $char = substr($this->data, $this->char, $len); + $this->char += $len - 1; + + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => $char + ) + ); + + $this->state = 'data'; + } + } + + private function entityDataState() + { + // Attempt to consume an entity. + $entity = $this->entity(); + + // If nothing is returned, emit a U+0026 AMPERSAND character token. + // Otherwise, emit the character token that was returned. + $char = (!$entity) ? '&' : $entity; + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => $char + ) + ); + + // Finally, switch to the data state. + $this->state = 'data'; + } + + private function tagOpenState() + { + switch ($this->content_model) { + case self::RCDATA: + case self::CDATA: + /* If the next input character is a U+002F SOLIDUS (/) character, + consume it and switch to the close tag open state. If the next + input character is not a U+002F SOLIDUS (/) character, emit a + U+003C LESS-THAN SIGN character token and switch to the data + state to process the next input character. */ + if ($this->character($this->char + 1) === '/') { + $this->char++; + $this->state = 'closeTagOpen'; + + } else { + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => '<' + ) + ); + + $this->state = 'data'; + } + break; + + case self::PCDATA: + // If the content model flag is set to the PCDATA state + // Consume the next input character: + $this->char++; + $char = $this->char(); + + if ($char === '!') { + /* U+0021 EXCLAMATION MARK (!) + Switch to the markup declaration open state. */ + $this->state = 'markupDeclarationOpen'; + + } elseif ($char === '/') { + /* U+002F SOLIDUS (/) + Switch to the close tag open state. */ + $this->state = 'closeTagOpen'; + + } elseif (preg_match('/^[A-Za-z]$/', $char)) { + /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z + Create a new start tag token, set its tag name to the lowercase + version of the input character (add 0x0020 to the character's code + point), then switch to the tag name state. (Don't emit the token + yet; further details will be filled in before it is emitted.) */ + $this->token = array( + 'name' => strtolower($char), + 'type' => self::STARTTAG, + 'attr' => array() + ); + + $this->state = 'tagName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Emit a U+003C LESS-THAN SIGN character token and a + U+003E GREATER-THAN SIGN character token. Switch to the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => '<>' + ) + ); + + $this->state = 'data'; + + } elseif ($char === '?') { + /* U+003F QUESTION MARK (?) + Parse error. Switch to the bogus comment state. */ + $this->state = 'bogusComment'; + + } else { + /* Anything else + Parse error. Emit a U+003C LESS-THAN SIGN character token and + reconsume the current input character in the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => '<' + ) + ); + + $this->char--; + $this->state = 'data'; + } + break; + } + } + + private function closeTagOpenState() + { + $next_node = strtolower($this->characters('A-Za-z', $this->char + 1)); + $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName; + + if (($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && + (!$the_same || ($the_same && (!preg_match( + '/[\t\n\x0b\x0c >\/]/', + $this->character($this->char + 1 + strlen($next_node)) + ) || $this->EOF === $this->char))) + ) { + /* If the content model flag is set to the RCDATA or CDATA states then + examine the next few characters. If they do not match the tag name of + the last start tag token emitted (case insensitively), or if they do but + they are not immediately followed by one of the following characters: + * U+0009 CHARACTER TABULATION + * U+000A LINE FEED (LF) + * U+000B LINE TABULATION + * U+000C FORM FEED (FF) + * U+0020 SPACE + * U+003E GREATER-THAN SIGN (>) + * U+002F SOLIDUS (/) + * EOF + ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character + token, a U+002F SOLIDUS character token, and switch to the data state + to process the next input character. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => 'state = 'data'; + + } else { + /* Otherwise, if the content model flag is set to the PCDATA state, + or if the next few characters do match that tag name, consume the + next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[A-Za-z]$/', $char)) { + /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z + Create a new end tag token, set its tag name to the lowercase version + of the input character (add 0x0020 to the character's code point), then + switch to the tag name state. (Don't emit the token yet; further details + will be filled in before it is emitted.) */ + $this->token = array( + 'name' => strtolower($char), + 'type' => self::ENDTAG + ); + + $this->state = 'tagName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Switch to the data state. */ + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F + SOLIDUS character token. Reconsume the EOF character in the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => 'char--; + $this->state = 'data'; + + } else { + /* Parse error. Switch to the bogus comment state. */ + $this->state = 'bogusComment'; + } + } + } + + private function tagNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } elseif ($char === '/') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } else { + /* Anything else + Append the current input character to the current tag token's tag name. + Stay in the tag name state. */ + $this->token['name'] .= strtolower($char); + $this->state = 'tagName'; + } + } + + private function beforeAttributeNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '/') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Stay in the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Start a new attribute in the current tag token. Set that attribute's + name to the current input character, and its value to the empty string. + Switch to the attribute name state. */ + $this->token['attr'][] = array( + 'name' => strtolower($char), + 'value' => null + ); + + $this->state = 'attributeName'; + } + } + + private function attributeNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute name state. */ + $this->state = 'afterAttributeName'; + + } elseif ($char === '=') { + /* U+003D EQUALS SIGN (=) + Switch to the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '/' && $this->character($this->char + 1) !== '>') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's name. + Stay in the attribute name state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['name'] .= strtolower($char); + + $this->state = 'attributeName'; + } + } + + private function afterAttributeNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the after attribute name state. */ + $this->state = 'afterAttributeName'; + + } elseif ($char === '=') { + /* U+003D EQUALS SIGN (=) + Switch to the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '/' && $this->character($this->char + 1) !== '>') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the + before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Start a new attribute in the current tag token. Set that attribute's + name to the current input character, and its value to the empty string. + Switch to the attribute name state. */ + $this->token['attr'][] = array( + 'name' => strtolower($char), + 'value' => null + ); + + $this->state = 'attributeName'; + } + } + + private function beforeAttributeValueState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif ($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the attribute value (double-quoted) state. */ + $this->state = 'attributeValueDoubleQuoted'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the attribute value (unquoted) state and reconsume + this input character. */ + $this->char--; + $this->state = 'attributeValueUnquoted'; + + } elseif ($char === '\'') { + /* U+0027 APOSTROPHE (') + Switch to the attribute value (single-quoted) state. */ + $this->state = 'attributeValueSingleQuoted'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Switch to the attribute value (unquoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueUnquoted'; + } + } + + private function attributeValueDoubleQuotedState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if ($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState('double'); + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the character + in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (double-quoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueDoubleQuoted'; + } + } + + private function attributeValueSingleQuotedState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if ($char === '\'') { + /* U+0022 QUOTATION MARK (') + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState('single'); + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the character + in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (single-quoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueSingleQuoted'; + } + } + + private function attributeValueUnquotedState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState(); + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (unquoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueUnquoted'; + } + } + + private function entityInAttributeValueState() + { + // Attempt to consume an entity. + $entity = $this->entity(); + + // If nothing is returned, append a U+0026 AMPERSAND character to the + // current attribute's value. Otherwise, emit the character token that + // was returned. + $char = (!$entity) + ? '&' + : $entity; + + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + } + + private function bogusCommentState() + { + /* Consume every character up to the first U+003E GREATER-THAN SIGN + character (>) or the end of the file (EOF), whichever comes first. Emit + a comment token whose data is the concatenation of all the characters + starting from and including the character that caused the state machine + to switch into the bogus comment state, up to and including the last + consumed character before the U+003E character, if any, or up to the + end of the file otherwise. (If the comment was started by the end of + the file (EOF), the token is empty.) */ + $data = $this->characters('^>', $this->char); + $this->emitToken( + array( + 'data' => $data, + 'type' => self::COMMENT + ) + ); + + $this->char += strlen($data); + + /* Switch to the data state. */ + $this->state = 'data'; + + /* If the end of the file was reached, reconsume the EOF character. */ + if ($this->char === $this->EOF) { + $this->char = $this->EOF - 1; + } + } + + private function markupDeclarationOpenState() + { + /* If the next two characters are both U+002D HYPHEN-MINUS (-) + characters, consume those two characters, create a comment token whose + data is the empty string, and switch to the comment state. */ + if ($this->character($this->char + 1, 2) === '--') { + $this->char += 2; + $this->state = 'comment'; + $this->token = array( + 'data' => null, + 'type' => self::COMMENT + ); + + /* Otherwise if the next seven chacacters are a case-insensitive match + for the word "DOCTYPE", then consume those characters and switch to the + DOCTYPE state. */ + } elseif (strtolower($this->character($this->char + 1, 7)) === 'doctype') { + $this->char += 7; + $this->state = 'doctype'; + + /* Otherwise, is is a parse error. Switch to the bogus comment state. + The next character that is consumed, if any, is the first character + that will be in the comment. */ + } else { + $this->char++; + $this->state = 'bogusComment'; + } + } + + private function commentState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + /* U+002D HYPHEN-MINUS (-) */ + if ($char === '-') { + /* Switch to the comment dash state */ + $this->state = 'commentDash'; + + /* EOF */ + } elseif ($this->char === $this->EOF) { + /* Parse error. Emit the comment token. Reconsume the EOF character + in the data state. */ + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + /* Anything else */ + } else { + /* Append the input character to the comment token's data. Stay in + the comment state. */ + $this->token['data'] .= $char; + } + } + + private function commentDashState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + /* U+002D HYPHEN-MINUS (-) */ + if ($char === '-') { + /* Switch to the comment end state */ + $this->state = 'commentEnd'; + + /* EOF */ + } elseif ($this->char === $this->EOF) { + /* Parse error. Emit the comment token. Reconsume the EOF character + in the data state. */ + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + /* Anything else */ + } else { + /* Append a U+002D HYPHEN-MINUS (-) character and the input + character to the comment token's data. Switch to the comment state. */ + $this->token['data'] .= '-' . $char; + $this->state = 'comment'; + } + } + + private function commentEndState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '-') { + $this->token['data'] .= '-'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['data'] .= '--' . $char; + $this->state = 'comment'; + } + } + + private function doctypeState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + $this->state = 'beforeDoctypeName'; + + } else { + $this->char--; + $this->state = 'beforeDoctypeName'; + } + } + + private function beforeDoctypeNameState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + // Stay in the before DOCTYPE name state. + + } elseif (preg_match('/^[a-z]$/', $char)) { + $this->token = array( + 'name' => strtoupper($char), + 'type' => self::DOCTYPE, + 'error' => true + ); + + $this->state = 'doctypeName'; + + } elseif ($char === '>') { + $this->emitToken( + array( + 'name' => null, + 'type' => self::DOCTYPE, + 'error' => true + ) + ); + + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken( + array( + 'name' => null, + 'type' => self::DOCTYPE, + 'error' => true + ) + ); + + $this->char--; + $this->state = 'data'; + + } else { + $this->token = array( + 'name' => $char, + 'type' => self::DOCTYPE, + 'error' => true + ); + + $this->state = 'doctypeName'; + } + } + + private function doctypeNameState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + $this->state = 'AfterDoctypeName'; + + } elseif ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif (preg_match('/^[a-z]$/', $char)) { + $this->token['name'] .= strtoupper($char); + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['name'] .= $char; + } + + $this->token['error'] = ($this->token['name'] === 'HTML') + ? false + : true; + } + + private function afterDoctypeNameState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + // Stay in the DOCTYPE name state. + + } elseif ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['error'] = true; + $this->state = 'bogusDoctype'; + } + } + + private function bogusDoctypeState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + // Stay in the bogus DOCTYPE state. + } + } + + private function entity() + { + $start = $this->char; + + // This section defines how to consume an entity. This definition is + // used when parsing entities in text and in attributes. + + // The behaviour depends on the identity of the next character (the + // one immediately after the U+0026 AMPERSAND character): + + switch ($this->character($this->char + 1)) { + // U+0023 NUMBER SIGN (#) + case '#': + + // The behaviour further depends on the character after the + // U+0023 NUMBER SIGN: + switch ($this->character($this->char + 1)) { + // U+0078 LATIN SMALL LETTER X + // U+0058 LATIN CAPITAL LETTER X + case 'x': + case 'X': + // Follow the steps below, but using the range of + // characters U+0030 DIGIT ZERO through to U+0039 DIGIT + // NINE, U+0061 LATIN SMALL LETTER A through to U+0066 + // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER + // A, through to U+0046 LATIN CAPITAL LETTER F (in other + // words, 0-9, A-F, a-f). + $char = 1; + $char_class = '0-9A-Fa-f'; + break; + + // Anything else + default: + // Follow the steps below, but using the range of + // characters U+0030 DIGIT ZERO through to U+0039 DIGIT + // NINE (i.e. just 0-9). + $char = 0; + $char_class = '0-9'; + break; + } + + // Consume as many characters as match the range of characters + // given above. + $this->char++; + $e_name = $this->characters($char_class, $this->char + $char + 1); + $entity = $this->character($start, $this->char); + $cond = strlen($e_name) > 0; + + // The rest of the parsing happens bellow. + break; + + // Anything else + default: + // Consume the maximum number of characters possible, with the + // consumed characters case-sensitively matching one of the + // identifiers in the first column of the entities table. + $e_name = $this->characters('0-9A-Za-z;', $this->char + 1); + $len = strlen($e_name); + + for ($c = 1; $c <= $len; $c++) { + $id = substr($e_name, 0, $c); + $this->char++; + + if (in_array($id, $this->entities)) { + if ($e_name[$c - 1] !== ';') { + if ($c < $len && $e_name[$c] == ';') { + $this->char++; // consume extra semicolon + } + } + $entity = $id; + break; + } + } + + $cond = isset($entity); + // The rest of the parsing happens bellow. + break; + } + + if (!$cond) { + // If no match can be made, then this is a parse error. No + // characters are consumed, and nothing is returned. + $this->char = $start; + return false; + } + + // Return a character token for the character corresponding to the + // entity name (as given by the second column of the entities table). + return html_entity_decode('&' . $entity . ';', ENT_QUOTES, 'UTF-8'); + } + + private function emitToken($token) + { + $emit = $this->tree->emitToken($token); + + if (is_int($emit)) { + $this->content_model = $emit; + + } elseif ($token['type'] === self::ENDTAG) { + $this->content_model = self::PCDATA; + } + } + + private function EOF() + { + $this->state = null; + $this->tree->emitToken( + array( + 'type' => self::EOF + ) + ); + } +} + +class HTML5TreeConstructer +{ + public $stack = array(); + + private $phase; + private $mode; + private $dom; + private $foster_parent = null; + private $a_formatting = array(); + + private $head_pointer = null; + private $form_pointer = null; + + private $scoping = array('button', 'caption', 'html', 'marquee', 'object', 'table', 'td', 'th'); + private $formatting = array( + 'a', + 'b', + 'big', + 'em', + 'font', + 'i', + 'nobr', + 's', + 'small', + 'strike', + 'strong', + 'tt', + 'u' + ); + private $special = array( + 'address', + 'area', + 'base', + 'basefont', + 'bgsound', + 'blockquote', + 'body', + 'br', + 'center', + 'col', + 'colgroup', + 'dd', + 'dir', + 'div', + 'dl', + 'dt', + 'embed', + 'fieldset', + 'form', + 'frame', + 'frameset', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'hr', + 'iframe', + 'image', + 'img', + 'input', + 'isindex', + 'li', + 'link', + 'listing', + 'menu', + 'meta', + 'noembed', + 'noframes', + 'noscript', + 'ol', + 'optgroup', + 'option', + 'p', + 'param', + 'plaintext', + 'pre', + 'script', + 'select', + 'spacer', + 'style', + 'tbody', + 'textarea', + 'tfoot', + 'thead', + 'title', + 'tr', + 'ul', + 'wbr' + ); + + // The different phases. + const INIT_PHASE = 0; + const ROOT_PHASE = 1; + const MAIN_PHASE = 2; + const END_PHASE = 3; + + // The different insertion modes for the main phase. + const BEFOR_HEAD = 0; + const IN_HEAD = 1; + const AFTER_HEAD = 2; + const IN_BODY = 3; + const IN_TABLE = 4; + const IN_CAPTION = 5; + const IN_CGROUP = 6; + const IN_TBODY = 7; + const IN_ROW = 8; + const IN_CELL = 9; + const IN_SELECT = 10; + const AFTER_BODY = 11; + const IN_FRAME = 12; + const AFTR_FRAME = 13; + + // The different types of elements. + const SPECIAL = 0; + const SCOPING = 1; + const FORMATTING = 2; + const PHRASING = 3; + + const MARKER = 0; + + public function __construct() + { + $this->phase = self::INIT_PHASE; + $this->mode = self::BEFOR_HEAD; + $this->dom = new DOMDocument; + + $this->dom->encoding = 'UTF-8'; + $this->dom->preserveWhiteSpace = true; + $this->dom->substituteEntities = true; + $this->dom->strictErrorChecking = false; + } + + // Process tag tokens + public function emitToken($token) + { + switch ($this->phase) { + case self::INIT_PHASE: + return $this->initPhase($token); + break; + case self::ROOT_PHASE: + return $this->rootElementPhase($token); + break; + case self::MAIN_PHASE: + return $this->mainPhase($token); + break; + case self::END_PHASE : + return $this->trailingEndPhase($token); + break; + } + } + + private function initPhase($token) + { + /* Initially, the tree construction stage must handle each token + emitted from the tokenisation stage as follows: */ + + /* A DOCTYPE token that is marked as being in error + A comment token + A start tag token + An end tag token + A character token that is not one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE + An end-of-file token */ + if ((isset($token['error']) && $token['error']) || + $token['type'] === HTML5::COMMENT || + $token['type'] === HTML5::STARTTAG || + $token['type'] === HTML5::ENDTAG || + $token['type'] === HTML5::EOF || + ($token['type'] === HTML5::CHARACTR && isset($token['data']) && + !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) + ) { + /* This specification does not define how to handle this case. In + particular, user agents may ignore the entirety of this specification + altogether for such documents, and instead invoke special parse modes + with a greater emphasis on backwards compatibility. */ + + $this->phase = self::ROOT_PHASE; + return $this->rootElementPhase($token); + + /* A DOCTYPE token marked as being correct */ + } elseif (isset($token['error']) && !$token['error']) { + /* Append a DocumentType node to the Document node, with the name + attribute set to the name given in the DOCTYPE token (which will be + "HTML"), and the other attributes specific to DocumentType objects + set to null, empty lists, or the empty string as appropriate. */ + $doctype = new DOMDocumentType(null, null, 'HTML'); + + /* Then, switch to the root element phase of the tree construction + stage. */ + $this->phase = self::ROOT_PHASE; + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif (isset($token['data']) && preg_match( + '/^[\t\n\x0b\x0c ]+$/', + $token['data'] + ) + ) { + /* Append that character to the Document node. */ + $text = $this->dom->createTextNode($token['data']); + $this->dom->appendChild($text); + } + } + + private function rootElementPhase($token) + { + /* After the initial phase, as each token is emitted from the tokenisation + stage, it must be processed as described in this section. */ + + /* A DOCTYPE token */ + if ($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the Document object with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->dom->appendChild($comment); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append that character to the Document node. */ + $text = $this->dom->createTextNode($token['data']); + $this->dom->appendChild($text); + + /* A character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED + (FF), or U+0020 SPACE + A start tag token + An end tag token + An end-of-file token */ + } elseif (($token['type'] === HTML5::CHARACTR && + !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || + $token['type'] === HTML5::STARTTAG || + $token['type'] === HTML5::ENDTAG || + $token['type'] === HTML5::EOF + ) { + /* Create an HTMLElement node with the tag name html, in the HTML + namespace. Append it to the Document object. Switch to the main + phase and reprocess the current token. */ + $html = $this->dom->createElement('html'); + $this->dom->appendChild($html); + $this->stack[] = $html; + + $this->phase = self::MAIN_PHASE; + return $this->mainPhase($token); + } + } + + private function mainPhase($token) + { + /* Tokens in the main phase must be handled as follows: */ + + /* A DOCTYPE token */ + if ($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A start tag token with the tag name "html" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') { + /* If this start tag token was not the first start tag token, then + it is a parse error. */ + + /* For each attribute on the token, check to see if the attribute + is already present on the top element of the stack of open elements. + If it is not, add the attribute and its corresponding value to that + element. */ + foreach ($token['attr'] as $attr) { + if (!$this->stack[0]->hasAttribute($attr['name'])) { + $this->stack[0]->setAttribute($attr['name'], $attr['value']); + } + } + + /* An end-of-file token */ + } elseif ($token['type'] === HTML5::EOF) { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Anything else. */ + } else { + /* Depends on the insertion mode: */ + switch ($this->mode) { + case self::BEFOR_HEAD: + return $this->beforeHead($token); + break; + case self::IN_HEAD: + return $this->inHead($token); + break; + case self::AFTER_HEAD: + return $this->afterHead($token); + break; + case self::IN_BODY: + return $this->inBody($token); + break; + case self::IN_TABLE: + return $this->inTable($token); + break; + case self::IN_CAPTION: + return $this->inCaption($token); + break; + case self::IN_CGROUP: + return $this->inColumnGroup($token); + break; + case self::IN_TBODY: + return $this->inTableBody($token); + break; + case self::IN_ROW: + return $this->inRow($token); + break; + case self::IN_CELL: + return $this->inCell($token); + break; + case self::IN_SELECT: + return $this->inSelect($token); + break; + case self::AFTER_BODY: + return $this->afterBody($token); + break; + case self::IN_FRAME: + return $this->inFrameset($token); + break; + case self::AFTR_FRAME: + return $this->afterFrameset($token); + break; + case self::END_PHASE: + return $this->trailingEndPhase($token); + break; + } + } + } + + private function beforeHead($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token with the tag name "head" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') { + /* Create an element for the token, append the new element to the + current node and push it onto the stack of open elements. */ + $element = $this->insertElement($token); + + /* Set the head element pointer to this new element node. */ + $this->head_pointer = $element; + + /* Change the insertion mode to "in head". */ + $this->mode = self::IN_HEAD; + + /* A start tag token whose tag name is one of: "base", "link", "meta", + "script", "style", "title". Or an end tag with the tag name "html". + Or a character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. Or any other start tag token */ + } elseif ($token['type'] === HTML5::STARTTAG || + ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') || + ($token['type'] === HTML5::CHARACTR && !preg_match( + '/^[\t\n\x0b\x0c ]$/', + $token['data'] + )) + ) { + /* Act as if a start tag token with the tag name "head" and no + attributes had been seen, then reprocess the current token. */ + $this->beforeHead( + array( + 'name' => 'head', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inHead($token); + + /* Any other end tag */ + } elseif ($token['type'] === HTML5::ENDTAG) { + /* Parse error. Ignore the token. */ + } + } + + private function inHead($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. + + THIS DIFFERS FROM THE SPEC: If the current node is either a title, style + or script element, append the character to the current node regardless + of its content. */ + if (($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || ( + $token['type'] === HTML5::CHARACTR && in_array( + end($this->stack)->nodeName, + array('title', 'style', 'script') + )) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + } elseif ($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('title', 'style', 'script')) + ) { + array_pop($this->stack); + return HTML5::PCDATA; + + /* A start tag with the tag name "title" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if ($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + } else { + $element = $this->insertElement($token); + } + + /* Switch the tokeniser's content model flag to the RCDATA state. */ + return HTML5::RCDATA; + + /* A start tag with the tag name "style" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if ($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + } else { + $this->insertElement($token); + } + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + + /* A start tag with the tag name "script" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') { + /* Create an element for the token. */ + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + + /* A start tag with the tag name "base", "link", or "meta" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('base', 'link', 'meta') + ) + ) { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if ($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + array_pop($this->stack); + + } else { + $this->insertElement($token); + } + + /* An end tag with the tag name "head" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') { + /* If the current node is a head element, pop the current node off + the stack of open elements. */ + if ($this->head_pointer->isSameNode(end($this->stack))) { + array_pop($this->stack); + + /* Otherwise, this is a parse error. */ + } else { + // k + } + + /* Change the insertion mode to "after head". */ + $this->mode = self::AFTER_HEAD; + + /* A start tag with the tag name "head" or an end tag except "html". */ + } elseif (($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') || + ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html') + ) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* If the current node is a head element, act as if an end tag + token with the tag name "head" had been seen. */ + if ($this->head_pointer->isSameNode(end($this->stack))) { + $this->inHead( + array( + 'name' => 'head', + 'type' => HTML5::ENDTAG + ) + ); + + /* Otherwise, change the insertion mode to "after head". */ + } else { + $this->mode = self::AFTER_HEAD; + } + + /* Then, reprocess the current token. */ + return $this->afterHead($token); + } + } + + private function afterHead($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token with the tag name "body" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') { + /* Insert a body element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in body". */ + $this->mode = self::IN_BODY; + + /* A start tag token with the tag name "frameset" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') { + /* Insert a frameset element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in frameset". */ + $this->mode = self::IN_FRAME; + + /* A start tag token whose tag name is one of: "base", "link", "meta", + "script", "style", "title" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('base', 'link', 'meta', 'script', 'style', 'title') + ) + ) { + /* Parse error. Switch the insertion mode back to "in head" and + reprocess the token. */ + $this->mode = self::IN_HEAD; + return $this->inHead($token); + + /* Anything else */ + } else { + /* Act as if a start tag token with the tag name "body" and no + attributes had been seen, and then reprocess the current token. */ + $this->afterHead( + array( + 'name' => 'body', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inBody($token); + } + } + + private function inBody($token) + { + /* Handle the token as follows: */ + + switch ($token['type']) { + /* A character token */ + case HTML5::CHARACTR: + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Append the token's character to the current node. */ + $this->insertText($token['data']); + break; + + /* A comment token */ + case HTML5::COMMENT: + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + break; + + case HTML5::STARTTAG: + switch ($token['name']) { + /* A start tag token whose tag name is one of: "script", + "style" */ + case 'script': + case 'style': + /* Process the token as if the insertion mode had been "in + head". */ + return $this->inHead($token); + break; + + /* A start tag token whose tag name is one of: "base", "link", + "meta", "title" */ + case 'base': + case 'link': + case 'meta': + case 'title': + /* Parse error. Process the token as if the insertion mode + had been "in head". */ + return $this->inHead($token); + break; + + /* A start tag token with the tag name "body" */ + case 'body': + /* Parse error. If the second element on the stack of open + elements is not a body element, or, if the stack of open + elements has only one node on it, then ignore the token. + (innerHTML case) */ + if (count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') { + // Ignore + + /* Otherwise, for each attribute on the token, check to see + if the attribute is already present on the body element (the + second element) on the stack of open elements. If it is not, + add the attribute and its corresponding value to that + element. */ + } else { + foreach ($token['attr'] as $attr) { + if (!$this->stack[1]->hasAttribute($attr['name'])) { + $this->stack[1]->setAttribute($attr['name'], $attr['value']); + } + } + } + break; + + /* A start tag whose tag name is one of: "address", + "blockquote", "center", "dir", "div", "dl", "fieldset", + "listing", "menu", "ol", "p", "ul" */ + case 'address': + case 'blockquote': + case 'center': + case 'dir': + case 'div': + case 'dl': + case 'fieldset': + case 'listing': + case 'menu': + case 'ol': + case 'p': + case 'ul': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + break; + + /* A start tag whose tag name is "form" */ + case 'form': + /* If the form element pointer is not null, ignore the + token with a parse error. */ + if ($this->form_pointer !== null) { + // Ignore. + + /* Otherwise: */ + } else { + /* If the stack of open elements has a p element in + scope, then act as if an end tag with the tag name p + had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token, and set the + form element pointer to point to the element created. */ + $element = $this->insertElement($token); + $this->form_pointer = $element; + } + break; + + /* A start tag whose tag name is "li", "dd" or "dt" */ + case 'li': + case 'dd': + case 'dt': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + $stack_length = count($this->stack) - 1; + + for ($n = $stack_length; 0 <= $n; $n--) { + /* 1. Initialise node to be the current node (the + bottommost node of the stack). */ + $stop = false; + $node = $this->stack[$n]; + $cat = $this->getElementCategory($node->tagName); + + /* 2. If node is an li, dd or dt element, then pop all + the nodes from the current node up to node, including + node, then stop this algorithm. */ + if ($token['name'] === $node->tagName || ($token['name'] !== 'li' + && ($node->tagName === 'dd' || $node->tagName === 'dt')) + ) { + for ($x = $stack_length; $x >= $n; $x--) { + array_pop($this->stack); + } + + break; + } + + /* 3. If node is not in the formatting category, and is + not in the phrasing category, and is not an address or + div element, then stop this algorithm. */ + if ($cat !== self::FORMATTING && $cat !== self::PHRASING && + $node->tagName !== 'address' && $node->tagName !== 'div' + ) { + break; + } + } + + /* Finally, insert an HTML element with the same tag + name as the token's. */ + $this->insertElement($token); + break; + + /* A start tag token whose tag name is "plaintext" */ + case 'plaintext': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + return HTML5::PLAINTEXT; + break; + + /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4", + "h5", "h6" */ + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* If the stack of open elements has in scope an element whose + tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then + this is a parse error; pop elements from the stack until an + element with one of those tag names has been popped from the + stack. */ + while ($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) { + array_pop($this->stack); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + break; + + /* A start tag whose tag name is "a" */ + case 'a': + /* If the list of active formatting elements contains + an element whose tag name is "a" between the end of the + list and the last marker on the list (or the start of + the list if there is no marker on the list), then this + is a parse error; act as if an end tag with the tag name + "a" had been seen, then remove that element from the list + of active formatting elements and the stack of open + elements if the end tag didn't already remove it (it + might not have if the element is not in table scope). */ + $leng = count($this->a_formatting); + + for ($n = $leng - 1; $n >= 0; $n--) { + if ($this->a_formatting[$n] === self::MARKER) { + break; + + } elseif ($this->a_formatting[$n]->nodeName === 'a') { + $this->emitToken( + array( + 'name' => 'a', + 'type' => HTML5::ENDTAG + ) + ); + break; + } + } + + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $el = $this->insertElement($token); + + /* Add that element to the list of active formatting + elements. */ + $this->a_formatting[] = $el; + break; + + /* A start tag whose tag name is one of: "b", "big", "em", "font", + "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ + case 'b': + case 'big': + case 'em': + case 'font': + case 'i': + case 'nobr': + case 's': + case 'small': + case 'strike': + case 'strong': + case 'tt': + case 'u': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $el = $this->insertElement($token); + + /* Add that element to the list of active formatting + elements. */ + $this->a_formatting[] = $el; + break; + + /* A start tag token whose tag name is "button" */ + case 'button': + /* If the stack of open elements has a button element in scope, + then this is a parse error; act as if an end tag with the tag + name "button" had been seen, then reprocess the token. (We don't + do that. Unnecessary.) */ + if ($this->elementInScope('button')) { + $this->inBody( + array( + 'name' => 'button', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + break; + + /* A start tag token whose tag name is one of: "marquee", "object" */ + case 'marquee': + case 'object': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + break; + + /* A start tag token whose tag name is "xmp" */ + case 'xmp': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Switch the content model flag to the CDATA state. */ + return HTML5::CDATA; + break; + + /* A start tag whose tag name is "table" */ + case 'table': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in table". */ + $this->mode = self::IN_TABLE; + break; + + /* A start tag whose tag name is one of: "area", "basefont", + "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */ + case 'area': + case 'basefont': + case 'bgsound': + case 'br': + case 'embed': + case 'img': + case 'param': + case 'spacer': + case 'wbr': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "hr" */ + case 'hr': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "image" */ + case 'image': + /* Parse error. Change the token's tag name to "img" and + reprocess it. (Don't ask.) */ + $token['name'] = 'img'; + return $this->inBody($token); + break; + + /* A start tag whose tag name is "input" */ + case 'input': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an input element for the token. */ + $element = $this->insertElement($token, false); + + /* If the form element pointer is not null, then associate the + input element with the form element pointed to by the form + element pointer. */ + $this->form_pointer !== null + ? $this->form_pointer->appendChild($element) + : end($this->stack)->appendChild($element); + + /* Pop that input element off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "isindex" */ + case 'isindex': + /* Parse error. */ + // w/e + + /* If the form element pointer is not null, + then ignore the token. */ + if ($this->form_pointer === null) { + /* Act as if a start tag token with the tag name "form" had + been seen. */ + $this->inBody( + array( + 'name' => 'body', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a start tag token with the tag name "hr" had + been seen. */ + $this->inBody( + array( + 'name' => 'hr', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a start tag token with the tag name "p" had + been seen. */ + $this->inBody( + array( + 'name' => 'p', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a start tag token with the tag name "label" + had been seen. */ + $this->inBody( + array( + 'name' => 'label', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a stream of character tokens had been seen. */ + $this->insertText( + 'This is a searchable index. ' . + 'Insert your search keywords here: ' + ); + + /* Act as if a start tag token with the tag name "input" + had been seen, with all the attributes from the "isindex" + token, except with the "name" attribute set to the value + "isindex" (ignoring any explicit "name" attribute). */ + $attr = $token['attr']; + $attr[] = array('name' => 'name', 'value' => 'isindex'); + + $this->inBody( + array( + 'name' => 'input', + 'type' => HTML5::STARTTAG, + 'attr' => $attr + ) + ); + + /* Act as if a stream of character tokens had been seen + (see below for what they should say). */ + $this->insertText( + 'This is a searchable index. ' . + 'Insert your search keywords here: ' + ); + + /* Act as if an end tag token with the tag name "label" + had been seen. */ + $this->inBody( + array( + 'name' => 'label', + 'type' => HTML5::ENDTAG + ) + ); + + /* Act as if an end tag token with the tag name "p" had + been seen. */ + $this->inBody( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + + /* Act as if a start tag token with the tag name "hr" had + been seen. */ + $this->inBody( + array( + 'name' => 'hr', + 'type' => HTML5::ENDTAG + ) + ); + + /* Act as if an end tag token with the tag name "form" had + been seen. */ + $this->inBody( + array( + 'name' => 'form', + 'type' => HTML5::ENDTAG + ) + ); + } + break; + + /* A start tag whose tag name is "textarea" */ + case 'textarea': + $this->insertElement($token); + + /* Switch the tokeniser's content model flag to the + RCDATA state. */ + return HTML5::RCDATA; + break; + + /* A start tag whose tag name is one of: "iframe", "noembed", + "noframes" */ + case 'iframe': + case 'noembed': + case 'noframes': + $this->insertElement($token); + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + break; + + /* A start tag whose tag name is "select" */ + case 'select': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in select". */ + $this->mode = self::IN_SELECT; + break; + + /* A start or end tag whose tag name is one of: "caption", "col", + "colgroup", "frame", "frameset", "head", "option", "optgroup", + "tbody", "td", "tfoot", "th", "thead", "tr". */ + case 'caption': + case 'col': + case 'colgroup': + case 'frame': + case 'frameset': + case 'head': + case 'option': + case 'optgroup': + case 'tbody': + case 'td': + case 'tfoot': + case 'th': + case 'thead': + case 'tr': + // Parse error. Ignore the token. + break; + + /* A start or end tag whose tag name is one of: "event-source", + "section", "nav", "article", "aside", "header", "footer", + "datagrid", "command" */ + case 'event-source': + case 'section': + case 'nav': + case 'article': + case 'aside': + case 'header': + case 'footer': + case 'datagrid': + case 'command': + // Work in progress! + break; + + /* A start tag token not covered by the previous entries */ + default: + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + $this->insertElement($token, true, true); + break; + } + break; + + case HTML5::ENDTAG: + switch ($token['name']) { + /* An end tag with the tag name "body" */ + case 'body': + /* If the second element in the stack of open elements is + not a body element, this is a parse error. Ignore the token. + (innerHTML case) */ + if (count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') { + // Ignore. + + /* If the current node is not the body element, then this + is a parse error. */ + } elseif (end($this->stack)->nodeName !== 'body') { + // Parse error. + } + + /* Change the insertion mode to "after body". */ + $this->mode = self::AFTER_BODY; + break; + + /* An end tag with the tag name "html" */ + case 'html': + /* Act as if an end tag with tag name "body" had been seen, + then, if that token wasn't ignored, reprocess the current + token. */ + $this->inBody( + array( + 'name' => 'body', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->afterBody($token); + break; + + /* An end tag whose tag name is one of: "address", "blockquote", + "center", "dir", "div", "dl", "fieldset", "listing", "menu", + "ol", "pre", "ul" */ + case 'address': + case 'blockquote': + case 'center': + case 'dir': + case 'div': + case 'dl': + case 'fieldset': + case 'listing': + case 'menu': + case 'ol': + case 'pre': + case 'ul': + /* If the stack of open elements has an element in scope + with the same tag name as that of the token, then generate + implied end tags. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with + the same tag name as that of the token, then this + is a parse error. */ + // w/e + + /* If the stack of open elements has an element in + scope with the same tag name as that of the token, + then pop elements from this stack until an element + with that tag name has been popped from the stack. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is "form" */ + case 'form': + /* If the stack of open elements has an element in scope + with the same tag name as that of the token, then generate + implied end tags. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + } + + if (end($this->stack)->nodeName !== $token['name']) { + /* Now, if the current node is not an element with the + same tag name as that of the token, then this is a parse + error. */ + // w/e + + } else { + /* Otherwise, if the current node is an element with + the same tag name as that of the token pop that element + from the stack. */ + array_pop($this->stack); + } + + /* In any case, set the form element pointer to null. */ + $this->form_pointer = null; + break; + + /* An end tag whose tag name is "p" */ + case 'p': + /* If the stack of open elements has a p element in scope, + then generate implied end tags, except for p elements. */ + if ($this->elementInScope('p')) { + $this->generateImpliedEndTags(array('p')); + + /* If the current node is not a p element, then this is + a parse error. */ + // k + + /* If the stack of open elements has a p element in + scope, then pop elements from this stack until the stack + no longer has a p element in scope. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->elementInScope('p')) { + array_pop($this->stack); + + } else { + break; + } + } + } + break; + + /* An end tag whose tag name is "dd", "dt", or "li" */ + case 'dd': + case 'dt': + case 'li': + /* If the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then + generate implied end tags, except for elements with the + same tag name as the token. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(array($token['name'])); + + /* If the current node is not an element with the same + tag name as the token, then this is a parse error. */ + // w/e + + /* If the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then + pop elements from this stack until an element with that + tag name has been popped from the stack. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4", + "h5", "h6" */ + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'); + + /* If the stack of open elements has in scope an element whose + tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then + generate implied end tags. */ + if ($this->elementInScope($elements)) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with the same + tag name as that of the token, then this is a parse error. */ + // w/e + + /* If the stack of open elements has in scope an element + whose tag name is one of "h1", "h2", "h3", "h4", "h5", or + "h6", then pop elements from the stack until an element + with one of those tag names has been popped from the stack. */ + while ($this->elementInScope($elements)) { + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is one of: "a", "b", "big", "em", + "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ + case 'a': + case 'b': + case 'big': + case 'em': + case 'font': + case 'i': + case 'nobr': + case 's': + case 'small': + case 'strike': + case 'strong': + case 'tt': + case 'u': + /* 1. Let the formatting element be the last element in + the list of active formatting elements that: + * is between the end of the list and the last scope + marker in the list, if any, or the start of the list + otherwise, and + * has the same tag name as the token. + */ + while (true) { + for ($a = count($this->a_formatting) - 1; $a >= 0; $a--) { + if ($this->a_formatting[$a] === self::MARKER) { + break; + + } elseif ($this->a_formatting[$a]->tagName === $token['name']) { + $formatting_element = $this->a_formatting[$a]; + $in_stack = in_array($formatting_element, $this->stack, true); + $fe_af_pos = $a; + break; + } + } + + /* If there is no such node, or, if that node is + also in the stack of open elements but the element + is not in scope, then this is a parse error. Abort + these steps. The token is ignored. */ + if (!isset($formatting_element) || ($in_stack && + !$this->elementInScope($token['name'])) + ) { + break; + + /* Otherwise, if there is such a node, but that node + is not in the stack of open elements, then this is a + parse error; remove the element from the list, and + abort these steps. */ + } elseif (isset($formatting_element) && !$in_stack) { + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + break; + } + + /* 2. Let the furthest block be the topmost node in the + stack of open elements that is lower in the stack + than the formatting element, and is not an element in + the phrasing or formatting categories. There might + not be one. */ + $fe_s_pos = array_search($formatting_element, $this->stack, true); + $length = count($this->stack); + + for ($s = $fe_s_pos + 1; $s < $length; $s++) { + $category = $this->getElementCategory($this->stack[$s]->nodeName); + + if ($category !== self::PHRASING && $category !== self::FORMATTING) { + $furthest_block = $this->stack[$s]; + } + } + + /* 3. If there is no furthest block, then the UA must + skip the subsequent steps and instead just pop all + the nodes from the bottom of the stack of open + elements, from the current node up to the formatting + element, and remove the formatting element from the + list of active formatting elements. */ + if (!isset($furthest_block)) { + for ($n = $length - 1; $n >= $fe_s_pos; $n--) { + array_pop($this->stack); + } + + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + break; + } + + /* 4. Let the common ancestor be the element + immediately above the formatting element in the stack + of open elements. */ + $common_ancestor = $this->stack[$fe_s_pos - 1]; + + /* 5. If the furthest block has a parent node, then + remove the furthest block from its parent node. */ + if ($furthest_block->parentNode !== null) { + $furthest_block->parentNode->removeChild($furthest_block); + } + + /* 6. Let a bookmark note the position of the + formatting element in the list of active formatting + elements relative to the elements on either side + of it in the list. */ + $bookmark = $fe_af_pos; + + /* 7. Let node and last node be the furthest block. + Follow these steps: */ + $node = $furthest_block; + $last_node = $furthest_block; + + while (true) { + for ($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) { + /* 7.1 Let node be the element immediately + prior to node in the stack of open elements. */ + $node = $this->stack[$n]; + + /* 7.2 If node is not in the list of active + formatting elements, then remove node from + the stack of open elements and then go back + to step 1. */ + if (!in_array($node, $this->a_formatting, true)) { + unset($this->stack[$n]); + $this->stack = array_merge($this->stack); + + } else { + break; + } + } + + /* 7.3 Otherwise, if node is the formatting + element, then go to the next step in the overall + algorithm. */ + if ($node === $formatting_element) { + break; + + /* 7.4 Otherwise, if last node is the furthest + block, then move the aforementioned bookmark to + be immediately after the node in the list of + active formatting elements. */ + } elseif ($last_node === $furthest_block) { + $bookmark = array_search($node, $this->a_formatting, true) + 1; + } + + /* 7.5 If node has any children, perform a + shallow clone of node, replace the entry for + node in the list of active formatting elements + with an entry for the clone, replace the entry + for node in the stack of open elements with an + entry for the clone, and let node be the clone. */ + if ($node->hasChildNodes()) { + $clone = $node->cloneNode(); + $s_pos = array_search($node, $this->stack, true); + $a_pos = array_search($node, $this->a_formatting, true); + + $this->stack[$s_pos] = $clone; + $this->a_formatting[$a_pos] = $clone; + $node = $clone; + } + + /* 7.6 Insert last node into node, first removing + it from its previous parent node if any. */ + if ($last_node->parentNode !== null) { + $last_node->parentNode->removeChild($last_node); + } + + $node->appendChild($last_node); + + /* 7.7 Let last node be node. */ + $last_node = $node; + } + + /* 8. Insert whatever last node ended up being in + the previous step into the common ancestor node, + first removing it from its previous parent node if + any. */ + if ($last_node->parentNode !== null) { + $last_node->parentNode->removeChild($last_node); + } + + $common_ancestor->appendChild($last_node); + + /* 9. Perform a shallow clone of the formatting + element. */ + $clone = $formatting_element->cloneNode(); + + /* 10. Take all of the child nodes of the furthest + block and append them to the clone created in the + last step. */ + while ($furthest_block->hasChildNodes()) { + $child = $furthest_block->firstChild; + $furthest_block->removeChild($child); + $clone->appendChild($child); + } + + /* 11. Append that clone to the furthest block. */ + $furthest_block->appendChild($clone); + + /* 12. Remove the formatting element from the list + of active formatting elements, and insert the clone + into the list of active formatting elements at the + position of the aforementioned bookmark. */ + $fe_af_pos = array_search($formatting_element, $this->a_formatting, true); + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + + $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1); + $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting)); + $this->a_formatting = array_merge($af_part1, array($clone), $af_part2); + + /* 13. Remove the formatting element from the stack + of open elements, and insert the clone into the stack + of open elements immediately after (i.e. in a more + deeply nested position than) the position of the + furthest block in that stack. */ + $fe_s_pos = array_search($formatting_element, $this->stack, true); + $fb_s_pos = array_search($furthest_block, $this->stack, true); + unset($this->stack[$fe_s_pos]); + + $s_part1 = array_slice($this->stack, 0, $fb_s_pos); + $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack)); + $this->stack = array_merge($s_part1, array($clone), $s_part2); + + /* 14. Jump back to step 1 in this series of steps. */ + unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block); + } + break; + + /* An end tag token whose tag name is one of: "button", + "marquee", "object" */ + case 'button': + case 'marquee': + case 'object': + /* If the stack of open elements has an element in scope whose + tag name matches the tag name of the token, then generate implied + tags. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with the same + tag name as the token, then this is a parse error. */ + // k + + /* Now, if the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then pop + elements from the stack until that element has been popped from + the stack, and clear the list of active formatting elements up + to the last marker. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + + $marker = end(array_keys($this->a_formatting, self::MARKER, true)); + + for ($n = count($this->a_formatting) - 1; $n > $marker; $n--) { + array_pop($this->a_formatting); + } + } + break; + + /* Or an end tag whose tag name is one of: "area", "basefont", + "bgsound", "br", "embed", "hr", "iframe", "image", "img", + "input", "isindex", "noembed", "noframes", "param", "select", + "spacer", "table", "textarea", "wbr" */ + case 'area': + case 'basefont': + case 'bgsound': + case 'br': + case 'embed': + case 'hr': + case 'iframe': + case 'image': + case 'img': + case 'input': + case 'isindex': + case 'noembed': + case 'noframes': + case 'param': + case 'select': + case 'spacer': + case 'table': + case 'textarea': + case 'wbr': + // Parse error. Ignore the token. + break; + + /* An end tag token not covered by the previous entries */ + default: + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + /* Initialise node to be the current node (the bottommost + node of the stack). */ + $node = end($this->stack); + + /* If node has the same tag name as the end tag token, + then: */ + if ($token['name'] === $node->nodeName) { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* If the tag name of the end tag token does not + match the tag name of the current node, this is a + parse error. */ + // k + + /* Pop all the nodes from the current node up to + node, including node, then stop this algorithm. */ + for ($x = count($this->stack) - $n; $x >= $n; $x--) { + array_pop($this->stack); + } + + } else { + $category = $this->getElementCategory($node); + + if ($category !== self::SPECIAL && $category !== self::SCOPING) { + /* Otherwise, if node is in neither the formatting + category nor the phrasing category, then this is a + parse error. Stop this algorithm. The end tag token + is ignored. */ + return false; + } + } + } + break; + } + break; + } + } + + private function inTable($token) + { + $clear = array('html', 'table'); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $text = $this->dom->createTextNode($token['data']); + end($this->stack)->appendChild($text); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + end($this->stack)->appendChild($comment); + + /* A start tag whose tag name is "caption" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'caption' + ) { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + + /* Insert an HTML element for the token, then switch the + insertion mode to "in caption". */ + $this->insertElement($token); + $this->mode = self::IN_CAPTION; + + /* A start tag whose tag name is "colgroup" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'colgroup' + ) { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the + insertion mode to "in column group". */ + $this->insertElement($token); + $this->mode = self::IN_CGROUP; + + /* A start tag whose tag name is "col" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'col' + ) { + $this->inTable( + array( + 'name' => 'colgroup', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + $this->inColumnGroup($token); + + /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('tbody', 'tfoot', 'thead') + ) + ) { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the insertion + mode to "in table body". */ + $this->insertElement($token); + $this->mode = self::IN_TBODY; + + /* A start tag whose tag name is one of: "td", "th", "tr" */ + } elseif ($token['type'] === HTML5::STARTTAG && + in_array($token['name'], array('td', 'th', 'tr')) + ) { + /* Act as if a start tag token with the tag name "tbody" had been + seen, then reprocess the current token. */ + $this->inTable( + array( + 'name' => 'tbody', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inTableBody($token); + + /* A start tag whose tag name is "table" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'table' + ) { + /* Parse error. Act as if an end tag token with the tag name "table" + had been seen, then, if that token wasn't ignored, reprocess the + current token. */ + $this->inTable( + array( + 'name' => 'table', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->mainPhase($token); + + /* An end tag whose tag name is "table" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'table' + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + return false; + + /* Otherwise: */ + } else { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Now, if the current node is not a table element, then this + is a parse error. */ + // w/e + + /* Pop elements from this stack until a table element has been + popped from the stack. */ + while (true) { + $current = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($current === 'table') { + break; + } + } + + /* Reset the insertion mode appropriately. */ + $this->resetInsertionMode(); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array( + 'body', + 'caption', + 'col', + 'colgroup', + 'html', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* Parse error. Process the token as if the insertion mode was "in + body", with the following exception: */ + + /* If the current node is a table, tbody, tfoot, thead, or tr + element, then, whenever a node would be inserted into the current + node, it must instead be inserted into the foster parent element. */ + if (in_array( + end($this->stack)->nodeName, + array('table', 'tbody', 'tfoot', 'thead', 'tr') + ) + ) { + /* The foster parent element is the parent element of the last + table element in the stack of open elements, if there is a + table element and it has such a parent element. If there is no + table element in the stack of open elements (innerHTML case), + then the foster parent element is the first element in the + stack of open elements (the html element). Otherwise, if there + is a table element in the stack of open elements, but the last + table element in the stack of open elements has no parent, or + its parent node is not an element, then the foster parent + element is the element before the last table element in the + stack of open elements. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === 'table') { + $table = $this->stack[$n]; + break; + } + } + + if (isset($table) && $table->parentNode !== null) { + $this->foster_parent = $table->parentNode; + + } elseif (!isset($table)) { + $this->foster_parent = $this->stack[0]; + + } elseif (isset($table) && ($table->parentNode === null || + $table->parentNode->nodeType !== XML_ELEMENT_NODE) + ) { + $this->foster_parent = $this->stack[$n - 1]; + } + } + + $this->inBody($token); + } + } + + private function inCaption($token) + { + /* An end tag whose tag name is "caption" */ + if ($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore + + /* Otherwise: */ + } else { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Now, if the current node is not a caption element, then this + is a parse error. */ + // w/e + + /* Pop elements from this stack until a caption element has + been popped from the stack. */ + while (true) { + $node = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($node === 'caption') { + break; + } + } + + /* Clear the list of active formatting elements up to the last + marker. */ + $this->clearTheActiveFormattingElementsUpToTheLastMarker(); + + /* Switch the insertion mode to "in table". */ + $this->mode = self::IN_TABLE; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag + name is "table" */ + } elseif (($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array( + 'caption', + 'col', + 'colgroup', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + )) || ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'table') + ) { + /* Parse error. Act as if an end tag with the tag name "caption" + had been seen, then, if that token wasn't ignored, reprocess the + current token. */ + $this->inCaption( + array( + 'name' => 'caption', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inTable($token); + + /* An end tag whose tag name is one of: "body", "col", "colgroup", + "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array( + 'body', + 'col', + 'colgroup', + 'html', + 'tbody', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in body". */ + $this->inBody($token); + } + } + + private function inColumnGroup($token) + { + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $text = $this->dom->createTextNode($token['data']); + end($this->stack)->appendChild($text); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + end($this->stack)->appendChild($comment); + + /* A start tag whose tag name is "col" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') { + /* Insert a col element for the token. Immediately pop the current + node off the stack of open elements. */ + $this->insertElement($token); + array_pop($this->stack); + + /* An end tag whose tag name is "colgroup" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'colgroup' + ) { + /* If the current node is the root html element, then this is a + parse error, ignore the token. (innerHTML case) */ + if (end($this->stack)->nodeName === 'html') { + // Ignore + + /* Otherwise, pop the current node (which will be a colgroup + element) from the stack of open elements. Switch the insertion + mode to "in table". */ + } else { + array_pop($this->stack); + $this->mode = self::IN_TABLE; + } + + /* An end tag whose tag name is "col" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Act as if an end tag with the tag name "colgroup" had been seen, + and then, if that token wasn't ignored, reprocess the current token. */ + $this->inColumnGroup( + array( + 'name' => 'colgroup', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inTable($token); + } + } + + private function inTableBody($token) + { + $clear = array('tbody', 'tfoot', 'thead', 'html'); + + /* A start tag whose tag name is "tr" */ + if ($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Insert a tr element for the token, then switch the insertion + mode to "in row". */ + $this->insertElement($token); + $this->mode = self::IN_ROW; + + /* A start tag whose tag name is one of: "th", "td" */ + } elseif ($token['type'] === HTML5::STARTTAG && + ($token['name'] === 'th' || $token['name'] === 'td') + ) { + /* Parse error. Act as if a start tag with the tag name "tr" had + been seen, then reprocess the current token. */ + $this->inTableBody( + array( + 'name' => 'tr', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inRow($token); + + /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif ($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('tbody', 'tfoot', 'thead')) + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore + + /* Otherwise: */ + } else { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Pop the current node from the stack of open elements. Switch + the insertion mode to "in table". */ + array_pop($this->stack); + $this->mode = self::IN_TABLE; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */ + } elseif (($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead') + )) || + ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table') + ) { + /* If the stack of open elements does not have a tbody, thead, or + tfoot element in table scope, this is a parse error. Ignore the + token. (innerHTML case) */ + if (!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Act as if an end tag with the same tag name as the current + node ("tbody", "tfoot", or "thead") had been seen, then + reprocess the current token. */ + $this->inTableBody( + array( + 'name' => end($this->stack)->nodeName, + 'type' => HTML5::ENDTAG + ) + ); + + return $this->mainPhase($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "td", "th", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr') + ) + ) { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in table". */ + $this->inTable($token); + } + } + + private function inRow($token) + { + $clear = array('tr', 'html'); + + /* A start tag whose tag name is one of: "th", "td" */ + if ($token['type'] === HTML5::STARTTAG && + ($token['name'] === 'th' || $token['name'] === 'td') + ) { + /* Clear the stack back to a table row context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the insertion + mode to "in cell". */ + $this->insertElement($token); + $this->mode = self::IN_CELL; + + /* Insert a marker at the end of the list of active formatting + elements. */ + $this->a_formatting[] = self::MARKER; + + /* An end tag whose tag name is "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Clear the stack back to a table row context. */ + $this->clearStackToTableContext($clear); + + /* Pop the current node (which will be a tr element) from the + stack of open elements. Switch the insertion mode to "in table + body". */ + array_pop($this->stack); + $this->mode = self::IN_TBODY; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr') + ) + ) { + /* Act as if an end tag with the tag name "tr" had been seen, then, + if that token wasn't ignored, reprocess the current token. */ + $this->inRow( + array( + 'name' => 'tr', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inCell($token); + + /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif ($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('tbody', 'tfoot', 'thead')) + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Otherwise, act as if an end tag with the tag name "tr" had + been seen, then reprocess the current token. */ + $this->inRow( + array( + 'name' => 'tr', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inCell($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "td", "th" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr') + ) + ) { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in table". */ + $this->inTable($token); + } + } + + private function inCell($token) + { + /* An end tag whose tag name is one of: "td", "th" */ + if ($token['type'] === HTML5::ENDTAG && + ($token['name'] === 'td' || $token['name'] === 'th') + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as that of the token, then this is a + parse error and the token must be ignored. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Generate implied end tags, except for elements with the same + tag name as the token. */ + $this->generateImpliedEndTags(array($token['name'])); + + /* Now, if the current node is not an element with the same tag + name as the token, then this is a parse error. */ + // k + + /* Pop elements from this stack until an element with the same + tag name as the token has been popped from the stack. */ + while (true) { + $node = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($node === $token['name']) { + break; + } + } + + /* Clear the list of active formatting elements up to the last + marker. */ + $this->clearTheActiveFormattingElementsUpToTheLastMarker(); + + /* Switch the insertion mode to "in row". (The current node + will be a tr element at this point.) */ + $this->mode = self::IN_ROW; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array( + 'caption', + 'col', + 'colgroup', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + /* If the stack of open elements does not have a td or th element + in table scope, then this is a parse error; ignore the token. + (innerHTML case) */ + if (!$this->elementInScope(array('td', 'th'), true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array( + 'caption', + 'col', + 'colgroup', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + /* If the stack of open elements does not have a td or th element + in table scope, then this is a parse error; ignore the token. + (innerHTML case) */ + if (!$this->elementInScope(array('td', 'th'), true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('body', 'caption', 'col', 'colgroup', 'html') + ) + ) { + /* Parse error. Ignore the token. */ + + /* An end tag whose tag name is one of: "table", "tbody", "tfoot", + "thead", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('table', 'tbody', 'tfoot', 'thead', 'tr') + ) + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as that of the token (which can only + happen for "tbody", "tfoot" and "thead", or, in the innerHTML case), + then this is a parse error and the token must be ignored. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in body". */ + $this->inBody($token); + } + } + + private function inSelect($token) + { + /* Handle the token as follows: */ + + /* A character token */ + if ($token['type'] === HTML5::CHARACTR) { + /* Append the token's character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token whose tag name is "option" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'option' + ) { + /* If the current node is an option element, act as if an end tag + with the tag name "option" had been seen. */ + if (end($this->stack)->nodeName === 'option') { + $this->inSelect( + array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* A start tag token whose tag name is "optgroup" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'optgroup' + ) { + /* If the current node is an option element, act as if an end tag + with the tag name "option" had been seen. */ + if (end($this->stack)->nodeName === 'option') { + $this->inSelect( + array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* If the current node is an optgroup element, act as if an end tag + with the tag name "optgroup" had been seen. */ + if (end($this->stack)->nodeName === 'optgroup') { + $this->inSelect( + array( + 'name' => 'optgroup', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* An end tag token whose tag name is "optgroup" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'optgroup' + ) { + /* First, if the current node is an option element, and the node + immediately before it in the stack of open elements is an optgroup + element, then act as if an end tag with the tag name "option" had + been seen. */ + $elements_in_stack = count($this->stack); + + if ($this->stack[$elements_in_stack - 1]->nodeName === 'option' && + $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup' + ) { + $this->inSelect( + array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* If the current node is an optgroup element, then pop that node + from the stack of open elements. Otherwise, this is a parse error, + ignore the token. */ + if ($this->stack[$elements_in_stack - 1] === 'optgroup') { + array_pop($this->stack); + } + + /* An end tag token whose tag name is "option" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'option' + ) { + /* If the current node is an option element, then pop that node + from the stack of open elements. Otherwise, this is a parse error, + ignore the token. */ + if (end($this->stack)->nodeName === 'option') { + array_pop($this->stack); + } + + /* An end tag whose tag name is "select" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'select' + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + // w/e + + /* Otherwise: */ + } else { + /* Pop elements from the stack of open elements until a select + element has been popped from the stack. */ + while (true) { + $current = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($current === 'select') { + break; + } + } + + /* Reset the insertion mode appropriately. */ + $this->resetInsertionMode(); + } + + /* A start tag whose tag name is "select" */ + } elseif ($token['name'] === 'select' && + $token['type'] === HTML5::STARTTAG + ) { + /* Parse error. Act as if the token had been an end tag with the + tag name "select" instead. */ + $this->inSelect( + array( + 'name' => 'select', + 'type' => HTML5::ENDTAG + ) + ); + + /* An end tag whose tag name is one of: "caption", "table", "tbody", + "tfoot", "thead", "tr", "td", "th" */ + } elseif (in_array( + $token['name'], + array( + 'caption', + 'table', + 'tbody', + 'tfoot', + 'thead', + 'tr', + 'td', + 'th' + ) + ) && $token['type'] === HTML5::ENDTAG + ) { + /* Parse error. */ + // w/e + + /* If the stack of open elements has an element in table scope with + the same tag name as that of the token, then act as if an end tag + with the tag name "select" had been seen, and reprocess the token. + Otherwise, ignore the token. */ + if ($this->elementInScope($token['name'], true)) { + $this->inSelect( + array( + 'name' => 'select', + 'type' => HTML5::ENDTAG + ) + ); + + $this->mainPhase($token); + } + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function afterBody($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Process the token as it would be processed if the insertion mode + was "in body". */ + $this->inBody($token); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the first element in the stack of open + elements (the html element), with the data attribute set to the + data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->stack[0]->appendChild($comment); + + /* An end tag with the tag name "html" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') { + /* If the parser was originally created in order to handle the + setting of an element's innerHTML attribute, this is a parse error; + ignore the token. (The element will be an html element in this + case.) (innerHTML case) */ + + /* Otherwise, switch to the trailing end phase. */ + $this->phase = self::END_PHASE; + + /* Anything else */ + } else { + /* Parse error. Set the insertion mode to "in body" and reprocess + the token. */ + $this->mode = self::IN_BODY; + return $this->inBody($token); + } + } + + private function inFrameset($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag with the tag name "frameset" */ + } elseif ($token['name'] === 'frameset' && + $token['type'] === HTML5::STARTTAG + ) { + $this->insertElement($token); + + /* An end tag with the tag name "frameset" */ + } elseif ($token['name'] === 'frameset' && + $token['type'] === HTML5::ENDTAG + ) { + /* If the current node is the root html element, then this is a + parse error; ignore the token. (innerHTML case) */ + if (end($this->stack)->nodeName === 'html') { + // Ignore + + } else { + /* Otherwise, pop the current node from the stack of open + elements. */ + array_pop($this->stack); + + /* If the parser was not originally created in order to handle + the setting of an element's innerHTML attribute (innerHTML case), + and the current node is no longer a frameset element, then change + the insertion mode to "after frameset". */ + $this->mode = self::AFTR_FRAME; + } + + /* A start tag with the tag name "frame" */ + } elseif ($token['name'] === 'frame' && + $token['type'] === HTML5::STARTTAG + ) { + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + + /* A start tag with the tag name "noframes" */ + } elseif ($token['name'] === 'noframes' && + $token['type'] === HTML5::STARTTAG + ) { + /* Process the token as if the insertion mode had been "in body". */ + $this->inBody($token); + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function afterFrameset($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* An end tag with the tag name "html" */ + } elseif ($token['name'] === 'html' && + $token['type'] === HTML5::ENDTAG + ) { + /* Switch to the trailing end phase. */ + $this->phase = self::END_PHASE; + + /* A start tag with the tag name "noframes" */ + } elseif ($token['name'] === 'noframes' && + $token['type'] === HTML5::STARTTAG + ) { + /* Process the token as if the insertion mode had been "in body". */ + $this->inBody($token); + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function trailingEndPhase($token) + { + /* After the main phase, as each token is emitted from the tokenisation + stage, it must be processed as described in this section. */ + + /* A DOCTYPE token */ + if ($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the Document object with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->dom->appendChild($comment); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Process the token as it would be processed in the main phase. */ + $this->mainPhase($token); + + /* A character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. Or a start tag token. Or an end tag token. */ + } elseif (($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || + $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG + ) { + /* Parse error. Switch back to the main phase and reprocess the + token. */ + $this->phase = self::MAIN_PHASE; + return $this->mainPhase($token); + + /* An end-of-file token */ + } elseif ($token['type'] === HTML5::EOF) { + /* OMG DONE!! */ + } + } + + private function insertElement($token, $append = true, $check = false) + { + // Proprietary workaround for libxml2's limitations with tag names + if ($check) { + // Slightly modified HTML5 tag-name modification, + // removing anything that's not an ASCII letter, digit, or hyphen + $token['name'] = preg_replace('/[^a-z0-9-]/i', '', $token['name']); + // Remove leading hyphens and numbers + $token['name'] = ltrim($token['name'], '-0..9'); + // In theory, this should ever be needed, but just in case + if ($token['name'] === '') { + $token['name'] = 'span'; + } // arbitrary generic choice + } + + $el = $this->dom->createElement($token['name']); + + foreach ($token['attr'] as $attr) { + if (!$el->hasAttribute($attr['name'])) { + $el->setAttribute($attr['name'], $attr['value']); + } + } + + $this->appendToRealParent($el); + $this->stack[] = $el; + + return $el; + } + + private function insertText($data) + { + $text = $this->dom->createTextNode($data); + $this->appendToRealParent($text); + } + + private function insertComment($data) + { + $comment = $this->dom->createComment($data); + $this->appendToRealParent($comment); + } + + private function appendToRealParent($node) + { + if ($this->foster_parent === null) { + end($this->stack)->appendChild($node); + + } elseif ($this->foster_parent !== null) { + /* If the foster parent element is the parent element of the + last table element in the stack of open elements, then the new + node must be inserted immediately before the last table element + in the stack of open elements in the foster parent element; + otherwise, the new node must be appended to the foster parent + element. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === 'table' && + $this->stack[$n]->parentNode !== null + ) { + $table = $this->stack[$n]; + break; + } + } + + if (isset($table) && $this->foster_parent->isSameNode($table->parentNode)) { + $this->foster_parent->insertBefore($node, $table); + } else { + $this->foster_parent->appendChild($node); + } + + $this->foster_parent = null; + } + } + + private function elementInScope($el, $table = false) + { + if (is_array($el)) { + foreach ($el as $element) { + if ($this->elementInScope($element, $table)) { + return true; + } + } + + return false; + } + + $leng = count($this->stack); + + for ($n = 0; $n < $leng; $n++) { + /* 1. Initialise node to be the current node (the bottommost node of + the stack). */ + $node = $this->stack[$leng - 1 - $n]; + + if ($node->tagName === $el) { + /* 2. If node is the target node, terminate in a match state. */ + return true; + + } elseif ($node->tagName === 'table') { + /* 3. Otherwise, if node is a table element, terminate in a failure + state. */ + return false; + + } elseif ($table === true && in_array( + $node->tagName, + array( + 'caption', + 'td', + 'th', + 'button', + 'marquee', + 'object' + ) + ) + ) { + /* 4. Otherwise, if the algorithm is the "has an element in scope" + variant (rather than the "has an element in table scope" variant), + and node is one of the following, terminate in a failure state. */ + return false; + + } elseif ($node === $node->ownerDocument->documentElement) { + /* 5. Otherwise, if node is an html element (root element), terminate + in a failure state. (This can only happen if the node is the topmost + node of the stack of open elements, and prevents the next step from + being invoked if there are no more elements in the stack.) */ + return false; + } + + /* Otherwise, set node to the previous entry in the stack of open + elements and return to step 2. (This will never fail, since the loop + will always terminate in the previous step if the top of the stack + is reached.) */ + } + } + + private function reconstructActiveFormattingElements() + { + /* 1. If there are no entries in the list of active formatting elements, + then there is nothing to reconstruct; stop this algorithm. */ + $formatting_elements = count($this->a_formatting); + + if ($formatting_elements === 0) { + return false; + } + + /* 3. Let entry be the last (most recently added) element in the list + of active formatting elements. */ + $entry = end($this->a_formatting); + + /* 2. If the last (most recently added) entry in the list of active + formatting elements is a marker, or if it is an element that is in the + stack of open elements, then there is nothing to reconstruct; stop this + algorithm. */ + if ($entry === self::MARKER || in_array($entry, $this->stack, true)) { + return false; + } + + for ($a = $formatting_elements - 1; $a >= 0; true) { + /* 4. If there are no entries before entry in the list of active + formatting elements, then jump to step 8. */ + if ($a === 0) { + $step_seven = false; + break; + } + + /* 5. Let entry be the entry one earlier than entry in the list of + active formatting elements. */ + $a--; + $entry = $this->a_formatting[$a]; + + /* 6. If entry is neither a marker nor an element that is also in + thetack of open elements, go to step 4. */ + if ($entry === self::MARKER || in_array($entry, $this->stack, true)) { + break; + } + } + + while (true) { + /* 7. Let entry be the element one later than entry in the list of + active formatting elements. */ + if (isset($step_seven) && $step_seven === true) { + $a++; + $entry = $this->a_formatting[$a]; + } + + /* 8. Perform a shallow clone of the element entry to obtain clone. */ + $clone = $entry->cloneNode(); + + /* 9. Append clone to the current node and push it onto the stack + of open elements so that it is the new current node. */ + end($this->stack)->appendChild($clone); + $this->stack[] = $clone; + + /* 10. Replace the entry for entry in the list with an entry for + clone. */ + $this->a_formatting[$a] = $clone; + + /* 11. If the entry for clone in the list of active formatting + elements is not the last entry in the list, return to step 7. */ + if (end($this->a_formatting) !== $clone) { + $step_seven = true; + } else { + break; + } + } + } + + private function clearTheActiveFormattingElementsUpToTheLastMarker() + { + /* When the steps below require the UA to clear the list of active + formatting elements up to the last marker, the UA must perform the + following steps: */ + + while (true) { + /* 1. Let entry be the last (most recently added) entry in the list + of active formatting elements. */ + $entry = end($this->a_formatting); + + /* 2. Remove entry from the list of active formatting elements. */ + array_pop($this->a_formatting); + + /* 3. If entry was a marker, then stop the algorithm at this point. + The list has been cleared up to the last marker. */ + if ($entry === self::MARKER) { + break; + } + } + } + + private function generateImpliedEndTags($exclude = array()) + { + /* When the steps below require the UA to generate implied end tags, + then, if the current node is a dd element, a dt element, an li element, + a p element, a td element, a th element, or a tr element, the UA must + act as if an end tag with the respective tag name had been seen and + then generate implied end tags again. */ + $node = end($this->stack); + $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude); + + while (in_array(end($this->stack)->nodeName, $elements)) { + array_pop($this->stack); + } + } + + private function getElementCategory($node) + { + $name = $node->tagName; + if (in_array($name, $this->special)) { + return self::SPECIAL; + } elseif (in_array($name, $this->scoping)) { + return self::SCOPING; + } elseif (in_array($name, $this->formatting)) { + return self::FORMATTING; + } else { + return self::PHRASING; + } + } + + private function clearStackToTableContext($elements) + { + /* When the steps above require the UA to clear the stack back to a + table context, it means that the UA must, while the current node is not + a table element or an html element, pop elements from the stack of open + elements. If this causes any elements to be popped from the stack, then + this is a parse error. */ + while (true) { + $node = end($this->stack)->nodeName; + + if (in_array($node, $elements)) { + break; + } else { + array_pop($this->stack); + } + } + } + + private function resetInsertionMode() + { + /* 1. Let last be false. */ + $last = false; + $leng = count($this->stack); + + for ($n = $leng - 1; $n >= 0; $n--) { + /* 2. Let node be the last node in the stack of open elements. */ + $node = $this->stack[$n]; + + /* 3. If node is the first node in the stack of open elements, then + set last to true. If the element whose innerHTML attribute is being + set is neither a td element nor a th element, then set node to the + element whose innerHTML attribute is being set. (innerHTML case) */ + if ($this->stack[0]->isSameNode($node)) { + $last = true; + } + + /* 4. If node is a select element, then switch the insertion mode to + "in select" and abort these steps. (innerHTML case) */ + if ($node->nodeName === 'select') { + $this->mode = self::IN_SELECT; + break; + + /* 5. If node is a td or th element, then switch the insertion mode + to "in cell" and abort these steps. */ + } elseif ($node->nodeName === 'td' || $node->nodeName === 'th') { + $this->mode = self::IN_CELL; + break; + + /* 6. If node is a tr element, then switch the insertion mode to + "in row" and abort these steps. */ + } elseif ($node->nodeName === 'tr') { + $this->mode = self::IN_ROW; + break; + + /* 7. If node is a tbody, thead, or tfoot element, then switch the + insertion mode to "in table body" and abort these steps. */ + } elseif (in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) { + $this->mode = self::IN_TBODY; + break; + + /* 8. If node is a caption element, then switch the insertion mode + to "in caption" and abort these steps. */ + } elseif ($node->nodeName === 'caption') { + $this->mode = self::IN_CAPTION; + break; + + /* 9. If node is a colgroup element, then switch the insertion mode + to "in column group" and abort these steps. (innerHTML case) */ + } elseif ($node->nodeName === 'colgroup') { + $this->mode = self::IN_CGROUP; + break; + + /* 10. If node is a table element, then switch the insertion mode + to "in table" and abort these steps. */ + } elseif ($node->nodeName === 'table') { + $this->mode = self::IN_TABLE; + break; + + /* 11. If node is a head element, then switch the insertion mode + to "in body" ("in body"! not "in head"!) and abort these steps. + (innerHTML case) */ + } elseif ($node->nodeName === 'head') { + $this->mode = self::IN_BODY; + break; + + /* 12. If node is a body element, then switch the insertion mode to + "in body" and abort these steps. */ + } elseif ($node->nodeName === 'body') { + $this->mode = self::IN_BODY; + break; + + /* 13. If node is a frameset element, then switch the insertion + mode to "in frameset" and abort these steps. (innerHTML case) */ + } elseif ($node->nodeName === 'frameset') { + $this->mode = self::IN_FRAME; + break; + + /* 14. If node is an html element, then: if the head element + pointer is null, switch the insertion mode to "before head", + otherwise, switch the insertion mode to "after head". In either + case, abort these steps. (innerHTML case) */ + } elseif ($node->nodeName === 'html') { + $this->mode = ($this->head_pointer === null) + ? self::BEFOR_HEAD + : self::AFTER_HEAD; + + break; + + /* 15. If last is true, then set the insertion mode to "in body" + and abort these steps. (innerHTML case) */ + } elseif ($last) { + $this->mode = self::IN_BODY; + break; + } + } + } + + private function closeCell() + { + /* If the stack of open elements has a td or th element in table scope, + then act as if an end tag token with that tag name had been seen. */ + foreach (array('td', 'th') as $cell) { + if ($this->elementInScope($cell, true)) { + $this->inCell( + array( + 'name' => $cell, + 'type' => HTML5::ENDTAG + ) + ); + + break; + } + } + } + + public function save() + { + return $this->dom; + } +} diff --git a/helpers/htmlpurifier/HTMLPurifier/Node.php b/helpers/htmlpurifier/HTMLPurifier/Node.php new file mode 100644 index 00000000..3995fec9 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Node.php @@ -0,0 +1,49 @@ +data = $data; + $this->line = $line; + $this->col = $col; + } + + public function toTokenPair() { + return array(new HTMLPurifier_Token_Comment($this->data, $this->line, $this->col), null); + } +} diff --git a/helpers/htmlpurifier/HTMLPurifier/Node/Element.php b/helpers/htmlpurifier/HTMLPurifier/Node/Element.php new file mode 100644 index 00000000..6cbf56da --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Node/Element.php @@ -0,0 +1,59 @@ + form or the form, i.e. + * is it a pair of start/end tokens or an empty token. + * @bool + */ + public $empty = false; + + public $endCol = null, $endLine = null, $endArmor = array(); + + public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) { + $this->name = $name; + $this->attr = $attr; + $this->line = $line; + $this->col = $col; + $this->armor = $armor; + } + + public function toTokenPair() { + // XXX inefficiency here, normalization is not necessary + if ($this->empty) { + return array(new HTMLPurifier_Token_Empty($this->name, $this->attr, $this->line, $this->col, $this->armor), null); + } else { + $start = new HTMLPurifier_Token_Start($this->name, $this->attr, $this->line, $this->col, $this->armor); + $end = new HTMLPurifier_Token_End($this->name, array(), $this->endLine, $this->endCol, $this->endArmor); + //$end->start = $start; + return array($start, $end); + } + } +} + diff --git a/helpers/htmlpurifier/HTMLPurifier/Node/Text.php b/helpers/htmlpurifier/HTMLPurifier/Node/Text.php new file mode 100644 index 00000000..aec91664 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Node/Text.php @@ -0,0 +1,54 @@ +data = $data; + $this->is_whitespace = $is_whitespace; + $this->line = $line; + $this->col = $col; + } + + public function toTokenPair() { + return array(new HTMLPurifier_Token_Text($this->data, $this->line, $this->col), null); + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/PercentEncoder.php b/helpers/htmlpurifier/HTMLPurifier/PercentEncoder.php new file mode 100644 index 00000000..18c8bbb0 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/PercentEncoder.php @@ -0,0 +1,111 @@ +preserve[$i] = true; + } + for ($i = 65; $i <= 90; $i++) { // upper-case + $this->preserve[$i] = true; + } + for ($i = 97; $i <= 122; $i++) { // lower-case + $this->preserve[$i] = true; + } + $this->preserve[45] = true; // Dash - + $this->preserve[46] = true; // Period . + $this->preserve[95] = true; // Underscore _ + $this->preserve[126]= true; // Tilde ~ + + // extra letters not to escape + if ($preserve !== false) { + for ($i = 0, $c = strlen($preserve); $i < $c; $i++) { + $this->preserve[ord($preserve[$i])] = true; + } + } + } + + /** + * Our replacement for urlencode, it encodes all non-reserved characters, + * as well as any extra characters that were instructed to be preserved. + * @note + * Assumes that the string has already been normalized, making any + * and all percent escape sequences valid. Percents will not be + * re-escaped, regardless of their status in $preserve + * @param string $string String to be encoded + * @return string Encoded string. + */ + public function encode($string) + { + $ret = ''; + for ($i = 0, $c = strlen($string); $i < $c; $i++) { + if ($string[$i] !== '%' && !isset($this->preserve[$int = ord($string[$i])])) { + $ret .= '%' . sprintf('%02X', $int); + } else { + $ret .= $string[$i]; + } + } + return $ret; + } + + /** + * Fix up percent-encoding by decoding unreserved characters and normalizing. + * @warning This function is affected by $preserve, even though the + * usual desired behavior is for this not to preserve those + * characters. Be careful when reusing instances of PercentEncoder! + * @param string $string String to normalize + * @return string + */ + public function normalize($string) + { + if ($string == '') { + return ''; + } + $parts = explode('%', $string); + $ret = array_shift($parts); + foreach ($parts as $part) { + $length = strlen($part); + if ($length < 2) { + $ret .= '%25' . $part; + continue; + } + $encoding = substr($part, 0, 2); + $text = substr($part, 2); + if (!ctype_xdigit($encoding)) { + $ret .= '%25' . $part; + continue; + } + $int = hexdec($encoding); + if (isset($this->preserve[$int])) { + $ret .= chr($int) . $text; + continue; + } + $encoding = strtoupper($encoding); + $ret .= '%' . $encoding . $text; + } + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Printer.php b/helpers/htmlpurifier/HTMLPurifier/Printer.php new file mode 100644 index 00000000..549e4cea --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Printer.php @@ -0,0 +1,218 @@ +getAll(); + $context = new HTMLPurifier_Context(); + $this->generator = new HTMLPurifier_Generator($config, $context); + } + + /** + * Main function that renders object or aspect of that object + * @note Parameters vary depending on printer + */ + // function render() {} + + /** + * Returns a start tag + * @param string $tag Tag name + * @param array $attr Attribute array + * @return string + */ + protected function start($tag, $attr = array()) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Start($tag, $attr ? $attr : array()) + ); + } + + /** + * Returns an end tag + * @param string $tag Tag name + * @return string + */ + protected function end($tag) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_End($tag) + ); + } + + /** + * Prints a complete element with content inside + * @param string $tag Tag name + * @param string $contents Element contents + * @param array $attr Tag attributes + * @param bool $escape whether or not to escape contents + * @return string + */ + protected function element($tag, $contents, $attr = array(), $escape = true) + { + return $this->start($tag, $attr) . + ($escape ? $this->escape($contents) : $contents) . + $this->end($tag); + } + + /** + * @param string $tag + * @param array $attr + * @return string + */ + protected function elementEmpty($tag, $attr = array()) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Empty($tag, $attr) + ); + } + + /** + * @param string $text + * @return string + */ + protected function text($text) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Text($text) + ); + } + + /** + * Prints a simple key/value row in a table. + * @param string $name Key + * @param mixed $value Value + * @return string + */ + protected function row($name, $value) + { + if (is_bool($value)) { + $value = $value ? 'On' : 'Off'; + } + return + $this->start('tr') . "\n" . + $this->element('th', $name) . "\n" . + $this->element('td', $value) . "\n" . + $this->end('tr'); + } + + /** + * Escapes a string for HTML output. + * @param string $string String to escape + * @return string + */ + protected function escape($string) + { + $string = HTMLPurifier_Encoder::cleanUTF8($string); + $string = htmlspecialchars($string, ENT_COMPAT, 'UTF-8'); + return $string; + } + + /** + * Takes a list of strings and turns them into a single list + * @param string[] $array List of strings + * @param bool $polite Bool whether or not to add an end before the last + * @return string + */ + protected function listify($array, $polite = false) + { + if (empty($array)) { + return 'None'; + } + $ret = ''; + $i = count($array); + foreach ($array as $value) { + $i--; + $ret .= $value; + if ($i > 0 && !($polite && $i == 1)) { + $ret .= ', '; + } + if ($polite && $i == 1) { + $ret .= 'and '; + } + } + return $ret; + } + + /** + * Retrieves the class of an object without prefixes, as well as metadata + * @param object $obj Object to determine class of + * @param string $sec_prefix Further prefix to remove + * @return string + */ + protected function getClass($obj, $sec_prefix = '') + { + static $five = null; + if ($five === null) { + $five = version_compare(PHP_VERSION, '5', '>='); + } + $prefix = 'HTMLPurifier_' . $sec_prefix; + if (!$five) { + $prefix = strtolower($prefix); + } + $class = str_replace($prefix, '', get_class($obj)); + $lclass = strtolower($class); + $class .= '('; + switch ($lclass) { + case 'enum': + $values = array(); + foreach ($obj->valid_values as $value => $bool) { + $values[] = $value; + } + $class .= implode(', ', $values); + break; + case 'css_composite': + $values = array(); + foreach ($obj->defs as $def) { + $values[] = $this->getClass($def, $sec_prefix); + } + $class .= implode(', ', $values); + break; + case 'css_multiple': + $class .= $this->getClass($obj->single, $sec_prefix) . ', '; + $class .= $obj->max; + break; + case 'css_denyelementdecorator': + $class .= $this->getClass($obj->def, $sec_prefix) . ', '; + $class .= $obj->element; + break; + case 'css_importantdecorator': + $class .= $this->getClass($obj->def, $sec_prefix); + if ($obj->allow) { + $class .= ', !important'; + } + break; + } + $class .= ')'; + return $class; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Printer/CSSDefinition.php b/helpers/htmlpurifier/HTMLPurifier/Printer/CSSDefinition.php new file mode 100644 index 00000000..29505fe1 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Printer/CSSDefinition.php @@ -0,0 +1,44 @@ +def = $config->getCSSDefinition(); + $ret = ''; + + $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); + $ret .= $this->start('table'); + + $ret .= $this->element('caption', 'Properties ($info)'); + + $ret .= $this->start('thead'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Property', array('class' => 'heavy')); + $ret .= $this->element('th', 'Definition', array('class' => 'heavy', 'style' => 'width:auto;')); + $ret .= $this->end('tr'); + $ret .= $this->end('thead'); + + ksort($this->def->info); + foreach ($this->def->info as $property => $obj) { + $name = $this->getClass($obj, 'AttrDef_'); + $ret .= $this->row($property, $name); + } + + $ret .= $this->end('table'); + $ret .= $this->end('div'); + + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Printer/ConfigForm.css b/helpers/htmlpurifier/HTMLPurifier/Printer/ConfigForm.css new file mode 100644 index 00000000..3ff1a88a --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Printer/ConfigForm.css @@ -0,0 +1,10 @@ + +.hp-config {} + +.hp-config tbody th {text-align:right; padding-right:0.5em;} +.hp-config thead, .hp-config .namespace {background:#3C578C; color:#FFF;} +.hp-config .namespace th {text-align:center;} +.hp-config .verbose {display:none;} +.hp-config .controls {text-align:center;} + +/* vim: et sw=4 sts=4 */ diff --git a/helpers/htmlpurifier/HTMLPurifier/Printer/ConfigForm.js b/helpers/htmlpurifier/HTMLPurifier/Printer/ConfigForm.js new file mode 100644 index 00000000..cba00c9b --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Printer/ConfigForm.js @@ -0,0 +1,5 @@ +function toggleWriteability(id_of_patient, checked) { + document.getElementById(id_of_patient).disabled = checked; +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php b/helpers/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php new file mode 100644 index 00000000..65a77790 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php @@ -0,0 +1,451 @@ +docURL = $doc_url; + $this->name = $name; + $this->compress = $compress; + // initialize sub-printers + $this->fields[0] = new HTMLPurifier_Printer_ConfigForm_default(); + $this->fields[HTMLPurifier_VarParser::BOOL] = new HTMLPurifier_Printer_ConfigForm_bool(); + } + + /** + * Sets default column and row size for textareas in sub-printers + * @param $cols Integer columns of textarea, null to use default + * @param $rows Integer rows of textarea, null to use default + */ + public function setTextareaDimensions($cols = null, $rows = null) + { + if ($cols) { + $this->fields['default']->cols = $cols; + } + if ($rows) { + $this->fields['default']->rows = $rows; + } + } + + /** + * Retrieves styling, in case it is not accessible by webserver + */ + public static function getCSS() + { + return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.css'); + } + + /** + * Retrieves JavaScript, in case it is not accessible by webserver + */ + public static function getJavaScript() + { + return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.js'); + } + + /** + * Returns HTML output for a configuration form + * @param HTMLPurifier_Config|array $config Configuration object of current form state, or an array + * where [0] has an HTML namespace and [1] is being rendered. + * @param array|bool $allowed Optional namespace(s) and directives to restrict form to. + * @param bool $render_controls + * @return string + */ + public function render($config, $allowed = true, $render_controls = true) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + + $this->config = $config; + $this->genConfig = $gen_config; + $this->prepareGenerator($gen_config); + + $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $config->def); + $all = array(); + foreach ($allowed as $key) { + list($ns, $directive) = $key; + $all[$ns][$directive] = $config->get($ns . '.' . $directive); + } + + $ret = ''; + $ret .= $this->start('table', array('class' => 'hp-config')); + $ret .= $this->start('thead'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Directive', array('class' => 'hp-directive')); + $ret .= $this->element('th', 'Value', array('class' => 'hp-value')); + $ret .= $this->end('tr'); + $ret .= $this->end('thead'); + foreach ($all as $ns => $directives) { + $ret .= $this->renderNamespace($ns, $directives); + } + if ($render_controls) { + $ret .= $this->start('tbody'); + $ret .= $this->start('tr'); + $ret .= $this->start('td', array('colspan' => 2, 'class' => 'controls')); + $ret .= $this->elementEmpty('input', array('type' => 'submit', 'value' => 'Submit')); + $ret .= '[Reset]'; + $ret .= $this->end('td'); + $ret .= $this->end('tr'); + $ret .= $this->end('tbody'); + } + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders a single namespace + * @param $ns String namespace name + * @param array $directives array of directives to values + * @return string + */ + protected function renderNamespace($ns, $directives) + { + $ret = ''; + $ret .= $this->start('tbody', array('class' => 'namespace')); + $ret .= $this->start('tr'); + $ret .= $this->element('th', $ns, array('colspan' => 2)); + $ret .= $this->end('tr'); + $ret .= $this->end('tbody'); + $ret .= $this->start('tbody'); + foreach ($directives as $directive => $value) { + $ret .= $this->start('tr'); + $ret .= $this->start('th'); + if ($this->docURL) { + $url = str_replace('%s', urlencode("$ns.$directive"), $this->docURL); + $ret .= $this->start('a', array('href' => $url)); + } + $attr = array('for' => "{$this->name}:$ns.$directive"); + + // crop directive name if it's too long + if (!$this->compress || (strlen($directive) < $this->compress)) { + $directive_disp = $directive; + } else { + $directive_disp = substr($directive, 0, $this->compress - 2) . '...'; + $attr['title'] = $directive; + } + + $ret .= $this->element( + 'label', + $directive_disp, + // component printers must create an element with this id + $attr + ); + if ($this->docURL) { + $ret .= $this->end('a'); + } + $ret .= $this->end('th'); + + $ret .= $this->start('td'); + $def = $this->config->def->info["$ns.$directive"]; + if (is_int($def)) { + $allow_null = $def < 0; + $type = abs($def); + } else { + $type = $def->type; + $allow_null = isset($def->allow_null); + } + if (!isset($this->fields[$type])) { + $type = 0; + } // default + $type_obj = $this->fields[$type]; + if ($allow_null) { + $type_obj = new HTMLPurifier_Printer_ConfigForm_NullDecorator($type_obj); + } + $ret .= $type_obj->render($ns, $directive, $value, $this->name, array($this->genConfig, $this->config)); + $ret .= $this->end('td'); + $ret .= $this->end('tr'); + } + $ret .= $this->end('tbody'); + return $ret; + } + +} + +/** + * Printer decorator for directives that accept null + */ +class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer +{ + /** + * Printer being decorated + * @type HTMLPurifier_Printer + */ + protected $obj; + + /** + * @param HTMLPurifier_Printer $obj Printer to decorate + */ + public function __construct($obj) + { + parent::__construct(); + $this->obj = $obj; + } + + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + public function render($ns, $directive, $value, $name, $config) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + + $ret = ''; + $ret .= $this->start('label', array('for' => "$name:Null_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' Null/Disabled'); + $ret .= $this->end('label'); + $attr = array( + 'type' => 'checkbox', + 'value' => '1', + 'class' => 'null-toggle', + 'name' => "$name" . "[Null_$ns.$directive]", + 'id' => "$name:Null_$ns.$directive", + 'onclick' => "toggleWriteability('$name:$ns.$directive',checked)" // INLINE JAVASCRIPT!!!! + ); + if ($this->obj instanceof HTMLPurifier_Printer_ConfigForm_bool) { + // modify inline javascript slightly + $attr['onclick'] = + "toggleWriteability('$name:Yes_$ns.$directive',checked);" . + "toggleWriteability('$name:No_$ns.$directive',checked)"; + } + if ($value === null) { + $attr['checked'] = 'checked'; + } + $ret .= $this->elementEmpty('input', $attr); + $ret .= $this->text(' or '); + $ret .= $this->elementEmpty('br'); + $ret .= $this->obj->render($ns, $directive, $value, $name, array($gen_config, $config)); + return $ret; + } +} + +/** + * Swiss-army knife configuration form field printer + */ +class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer +{ + /** + * @type int + */ + public $cols = 18; + + /** + * @type int + */ + public $rows = 5; + + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + public function render($ns, $directive, $value, $name, $config) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + // this should probably be split up a little + $ret = ''; + $def = $config->def->info["$ns.$directive"]; + if (is_int($def)) { + $type = abs($def); + } else { + $type = $def->type; + } + if (is_array($value)) { + switch ($type) { + case HTMLPurifier_VarParser::LOOKUP: + $array = $value; + $value = array(); + foreach ($array as $val => $b) { + $value[] = $val; + } + //TODO does this need a break? + case HTMLPurifier_VarParser::ALIST: + $value = implode(PHP_EOL, $value); + break; + case HTMLPurifier_VarParser::HASH: + $nvalue = ''; + foreach ($value as $i => $v) { + if (is_array($v)) { + // HACK + $v = implode(";", $v); + } + $nvalue .= "$i:$v" . PHP_EOL; + } + $value = $nvalue; + break; + default: + $value = ''; + } + } + if ($type === HTMLPurifier_VarParser::MIXED) { + return 'Not supported'; + $value = serialize($value); + } + $attr = array( + 'name' => "$name" . "[$ns.$directive]", + 'id' => "$name:$ns.$directive" + ); + if ($value === null) { + $attr['disabled'] = 'disabled'; + } + if (isset($def->allowed)) { + $ret .= $this->start('select', $attr); + foreach ($def->allowed as $val => $b) { + $attr = array(); + if ($value == $val) { + $attr['selected'] = 'selected'; + } + $ret .= $this->element('option', $val, $attr); + } + $ret .= $this->end('select'); + } elseif ($type === HTMLPurifier_VarParser::TEXT || + $type === HTMLPurifier_VarParser::ITEXT || + $type === HTMLPurifier_VarParser::ALIST || + $type === HTMLPurifier_VarParser::HASH || + $type === HTMLPurifier_VarParser::LOOKUP) { + $attr['cols'] = $this->cols; + $attr['rows'] = $this->rows; + $ret .= $this->start('textarea', $attr); + $ret .= $this->text($value); + $ret .= $this->end('textarea'); + } else { + $attr['value'] = $value; + $attr['type'] = 'text'; + $ret .= $this->elementEmpty('input', $attr); + } + return $ret; + } +} + +/** + * Bool form field printer + */ +class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer +{ + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + public function render($ns, $directive, $value, $name, $config) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + $ret = ''; + $ret .= $this->start('div', array('id' => "$name:$ns.$directive")); + + $ret .= $this->start('label', array('for' => "$name:Yes_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' Yes'); + $ret .= $this->end('label'); + + $attr = array( + 'type' => 'radio', + 'name' => "$name" . "[$ns.$directive]", + 'id' => "$name:Yes_$ns.$directive", + 'value' => '1' + ); + if ($value === true) { + $attr['checked'] = 'checked'; + } + if ($value === null) { + $attr['disabled'] = 'disabled'; + } + $ret .= $this->elementEmpty('input', $attr); + + $ret .= $this->start('label', array('for' => "$name:No_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' No'); + $ret .= $this->end('label'); + + $attr = array( + 'type' => 'radio', + 'name' => "$name" . "[$ns.$directive]", + 'id' => "$name:No_$ns.$directive", + 'value' => '0' + ); + if ($value === false) { + $attr['checked'] = 'checked'; + } + if ($value === null) { + $attr['disabled'] = 'disabled'; + } + $ret .= $this->elementEmpty('input', $attr); + + $ret .= $this->end('div'); + + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Printer/HTMLDefinition.php b/helpers/htmlpurifier/HTMLPurifier/Printer/HTMLDefinition.php new file mode 100644 index 00000000..5f2f2f8a --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Printer/HTMLDefinition.php @@ -0,0 +1,324 @@ +config =& $config; + + $this->def = $config->getHTMLDefinition(); + + $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); + + $ret .= $this->renderDoctype(); + $ret .= $this->renderEnvironment(); + $ret .= $this->renderContentSets(); + $ret .= $this->renderInfo(); + + $ret .= $this->end('div'); + + return $ret; + } + + /** + * Renders the Doctype table + * @return string + */ + protected function renderDoctype() + { + $doctype = $this->def->doctype; + $ret = ''; + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Doctype'); + $ret .= $this->row('Name', $doctype->name); + $ret .= $this->row('XML', $doctype->xml ? 'Yes' : 'No'); + $ret .= $this->row('Default Modules', implode($doctype->modules, ', ')); + $ret .= $this->row('Default Tidy Modules', implode($doctype->tidyModules, ', ')); + $ret .= $this->end('table'); + return $ret; + } + + + /** + * Renders environment table, which is miscellaneous info + * @return string + */ + protected function renderEnvironment() + { + $def = $this->def; + + $ret = ''; + + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Environment'); + + $ret .= $this->row('Parent of fragment', $def->info_parent); + $ret .= $this->renderChildren($def->info_parent_def->child); + $ret .= $this->row('Block wrap name', $def->info_block_wrapper); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Global attributes'); + $ret .= $this->element('td', $this->listifyAttr($def->info_global_attr), null, 0); + $ret .= $this->end('tr'); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Tag transforms'); + $list = array(); + foreach ($def->info_tag_transform as $old => $new) { + $new = $this->getClass($new, 'TagTransform_'); + $list[] = "<$old> with $new"; + } + $ret .= $this->element('td', $this->listify($list)); + $ret .= $this->end('tr'); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Pre-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_pre)); + $ret .= $this->end('tr'); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Post-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_post)); + $ret .= $this->end('tr'); + + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders the Content Sets table + * @return string + */ + protected function renderContentSets() + { + $ret = ''; + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Content Sets'); + foreach ($this->def->info_content_sets as $name => $lookup) { + $ret .= $this->heavyHeader($name); + $ret .= $this->start('tr'); + $ret .= $this->element('td', $this->listifyTagLookup($lookup)); + $ret .= $this->end('tr'); + } + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders the Elements ($info) table + * @return string + */ + protected function renderInfo() + { + $ret = ''; + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Elements ($info)'); + ksort($this->def->info); + $ret .= $this->heavyHeader('Allowed tags', 2); + $ret .= $this->start('tr'); + $ret .= $this->element('td', $this->listifyTagLookup($this->def->info), array('colspan' => 2)); + $ret .= $this->end('tr'); + foreach ($this->def->info as $name => $def) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', "<$name>", array('class' => 'heavy', 'colspan' => 2)); + $ret .= $this->end('tr'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Inline content'); + $ret .= $this->element('td', $def->descendants_are_inline ? 'Yes' : 'No'); + $ret .= $this->end('tr'); + if (!empty($def->excludes)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Excludes'); + $ret .= $this->element('td', $this->listifyTagLookup($def->excludes)); + $ret .= $this->end('tr'); + } + if (!empty($def->attr_transform_pre)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Pre-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_pre)); + $ret .= $this->end('tr'); + } + if (!empty($def->attr_transform_post)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Post-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_post)); + $ret .= $this->end('tr'); + } + if (!empty($def->auto_close)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Auto closed by'); + $ret .= $this->element('td', $this->listifyTagLookup($def->auto_close)); + $ret .= $this->end('tr'); + } + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Allowed attributes'); + $ret .= $this->element('td', $this->listifyAttr($def->attr), array(), 0); + $ret .= $this->end('tr'); + + if (!empty($def->required_attr)) { + $ret .= $this->row('Required attributes', $this->listify($def->required_attr)); + } + + $ret .= $this->renderChildren($def->child); + } + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders a row describing the allowed children of an element + * @param HTMLPurifier_ChildDef $def HTMLPurifier_ChildDef of pertinent element + * @return string + */ + protected function renderChildren($def) + { + $context = new HTMLPurifier_Context(); + $ret = ''; + $ret .= $this->start('tr'); + $elements = array(); + $attr = array(); + if (isset($def->elements)) { + if ($def->type == 'strictblockquote') { + $def->validateChildren(array(), $this->config, $context); + } + $elements = $def->elements; + } + if ($def->type == 'chameleon') { + $attr['rowspan'] = 2; + } elseif ($def->type == 'empty') { + $elements = array(); + } elseif ($def->type == 'table') { + $elements = array_flip( + array( + 'col', + 'caption', + 'colgroup', + 'thead', + 'tfoot', + 'tbody', + 'tr' + ) + ); + } + $ret .= $this->element('th', 'Allowed children', $attr); + + if ($def->type == 'chameleon') { + + $ret .= $this->element( + 'td', + 'Block: ' . + $this->escape($this->listifyTagLookup($def->block->elements)), + null, + 0 + ); + $ret .= $this->end('tr'); + $ret .= $this->start('tr'); + $ret .= $this->element( + 'td', + 'Inline: ' . + $this->escape($this->listifyTagLookup($def->inline->elements)), + null, + 0 + ); + + } elseif ($def->type == 'custom') { + + $ret .= $this->element( + 'td', + '' . ucfirst($def->type) . ': ' . + $def->dtd_regex + ); + + } else { + $ret .= $this->element( + 'td', + '' . ucfirst($def->type) . ': ' . + $this->escape($this->listifyTagLookup($elements)), + null, + 0 + ); + } + $ret .= $this->end('tr'); + return $ret; + } + + /** + * Listifies a tag lookup table. + * @param array $array Tag lookup array in form of array('tagname' => true) + * @return string + */ + protected function listifyTagLookup($array) + { + ksort($array); + $list = array(); + foreach ($array as $name => $discard) { + if ($name !== '#PCDATA' && !isset($this->def->info[$name])) { + continue; + } + $list[] = $name; + } + return $this->listify($list); + } + + /** + * Listifies a list of objects by retrieving class names and internal state + * @param array $array List of objects + * @return string + * @todo Also add information about internal state + */ + protected function listifyObjectList($array) + { + ksort($array); + $list = array(); + foreach ($array as $obj) { + $list[] = $this->getClass($obj, 'AttrTransform_'); + } + return $this->listify($list); + } + + /** + * Listifies a hash of attributes to AttrDef classes + * @param array $array Array hash in form of array('attrname' => HTMLPurifier_AttrDef) + * @return string + */ + protected function listifyAttr($array) + { + ksort($array); + $list = array(); + foreach ($array as $name => $obj) { + if ($obj === false) { + continue; + } + $list[] = "$name = " . $this->getClass($obj, 'AttrDef_') . ''; + } + return $this->listify($list); + } + + /** + * Creates a heavy header row + * @param string $text + * @param int $num + * @return string + */ + protected function heavyHeader($text, $num = 1) + { + $ret = ''; + $ret .= $this->start('tr'); + $ret .= $this->element('th', $text, array('colspan' => $num, 'class' => 'heavy')); + $ret .= $this->end('tr'); + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/PropertyList.php b/helpers/htmlpurifier/HTMLPurifier/PropertyList.php new file mode 100644 index 00000000..189348fd --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/PropertyList.php @@ -0,0 +1,122 @@ +parent = $parent; + } + + /** + * Recursively retrieves the value for a key + * @param string $name + * @throws HTMLPurifier_Exception + */ + public function get($name) + { + if ($this->has($name)) { + return $this->data[$name]; + } + // possible performance bottleneck, convert to iterative if necessary + if ($this->parent) { + return $this->parent->get($name); + } + throw new HTMLPurifier_Exception("Key '$name' not found"); + } + + /** + * Sets the value of a key, for this plist + * @param string $name + * @param mixed $value + */ + public function set($name, $value) + { + $this->data[$name] = $value; + } + + /** + * Returns true if a given key exists + * @param string $name + * @return bool + */ + public function has($name) + { + return array_key_exists($name, $this->data); + } + + /** + * Resets a value to the value of it's parent, usually the default. If + * no value is specified, the entire plist is reset. + * @param string $name + */ + public function reset($name = null) + { + if ($name == null) { + $this->data = array(); + } else { + unset($this->data[$name]); + } + } + + /** + * Squashes this property list and all of its property lists into a single + * array, and returns the array. This value is cached by default. + * @param bool $force If true, ignores the cache and regenerates the array. + * @return array + */ + public function squash($force = false) + { + if ($this->cache !== null && !$force) { + return $this->cache; + } + if ($this->parent) { + return $this->cache = array_merge($this->parent->squash($force), $this->data); + } else { + return $this->cache = $this->data; + } + } + + /** + * Returns the parent plist. + * @return HTMLPurifier_PropertyList + */ + public function getParent() + { + return $this->parent; + } + + /** + * Sets the parent plist. + * @param HTMLPurifier_PropertyList $plist Parent plist + */ + public function setParent($plist) + { + $this->parent = $plist; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/PropertyListIterator.php b/helpers/htmlpurifier/HTMLPurifier/PropertyListIterator.php new file mode 100644 index 00000000..15b330ea --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/PropertyListIterator.php @@ -0,0 +1,42 @@ +l = strlen($filter); + $this->filter = $filter; + } + + /** + * @return bool + */ + public function accept() + { + $key = $this->getInnerIterator()->key(); + if (strncmp($key, $this->filter, $this->l) !== 0) { + return false; + } + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Queue.php b/helpers/htmlpurifier/HTMLPurifier/Queue.php new file mode 100644 index 00000000..f58db904 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Queue.php @@ -0,0 +1,56 @@ +input = $input; + $this->output = array(); + } + + /** + * Shifts an element off the front of the queue. + */ + public function shift() { + if (empty($this->output)) { + $this->output = array_reverse($this->input); + $this->input = array(); + } + if (empty($this->output)) { + return NULL; + } + return array_pop($this->output); + } + + /** + * Pushes an element onto the front of the queue. + */ + public function push($x) { + array_push($this->input, $x); + } + + /** + * Checks if it's empty. + */ + public function isEmpty() { + return empty($this->input) && empty($this->output); + } +} diff --git a/helpers/htmlpurifier/HTMLPurifier/Strategy.php b/helpers/htmlpurifier/HTMLPurifier/Strategy.php new file mode 100644 index 00000000..e1ff3b72 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Strategy.php @@ -0,0 +1,26 @@ +strategies as $strategy) { + $tokens = $strategy->execute($tokens, $config, $context); + } + return $tokens; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Strategy/Core.php b/helpers/htmlpurifier/HTMLPurifier/Strategy/Core.php new file mode 100644 index 00000000..4414c17d --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Strategy/Core.php @@ -0,0 +1,17 @@ +strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements(); + $this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed(); + $this->strategies[] = new HTMLPurifier_Strategy_FixNesting(); + $this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes(); + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Strategy/FixNesting.php b/helpers/htmlpurifier/HTMLPurifier/Strategy/FixNesting.php new file mode 100644 index 00000000..6fa673db --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Strategy/FixNesting.php @@ -0,0 +1,181 @@ +getHTMLDefinition(); + + $excludes_enabled = !$config->get('Core.DisableExcludes'); + + // setup the context variable 'IsInline', for chameleon processing + // is 'false' when we are not inline, 'true' when it must always + // be inline, and an integer when it is inline for a certain + // branch of the document tree + $is_inline = $definition->info_parent_def->descendants_are_inline; + $context->register('IsInline', $is_inline); + + // setup error collector + $e =& $context->get('ErrorCollector', true); + + //####################################################################// + // Loop initialization + + // stack that contains all elements that are excluded + // it is organized by parent elements, similar to $stack, + // but it is only populated when an element with exclusions is + // processed, i.e. there won't be empty exclusions. + $exclude_stack = array($definition->info_parent_def->excludes); + + // variable that contains the start token while we are processing + // nodes. This enables error reporting to do its job + $node = $top_node; + // dummy token + list($token, $d) = $node->toTokenPair(); + $context->register('CurrentNode', $node); + $context->register('CurrentToken', $token); + + //####################################################################// + // Loop + + // We need to implement a post-order traversal iteratively, to + // avoid running into stack space limits. This is pretty tricky + // to reason about, so we just manually stack-ify the recursive + // variant: + // + // function f($node) { + // foreach ($node->children as $child) { + // f($child); + // } + // validate($node); + // } + // + // Thus, we will represent a stack frame as array($node, + // $is_inline, stack of children) + // e.g. array_reverse($node->children) - already processed + // children. + + $parent_def = $definition->info_parent_def; + $stack = array( + array($top_node, + $parent_def->descendants_are_inline, + $parent_def->excludes, // exclusions + 0) + ); + + while (!empty($stack)) { + list($node, $is_inline, $excludes, $ix) = array_pop($stack); + // recursive call + $go = false; + $def = empty($stack) ? $definition->info_parent_def : $definition->info[$node->name]; + while (isset($node->children[$ix])) { + $child = $node->children[$ix++]; + if ($child instanceof HTMLPurifier_Node_Element) { + $go = true; + $stack[] = array($node, $is_inline, $excludes, $ix); + $stack[] = array($child, + // ToDo: I don't think it matters if it's def or + // child_def, but double check this... + $is_inline || $def->descendants_are_inline, + empty($def->excludes) ? $excludes + : array_merge($excludes, $def->excludes), + 0); + break; + } + }; + if ($go) continue; + list($token, $d) = $node->toTokenPair(); + // base case + if ($excludes_enabled && isset($excludes[$node->name])) { + $node->dead = true; + if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded'); + } else { + // XXX I suppose it would be slightly more efficient to + // avoid the allocation here and have children + // strategies handle it + $children = array(); + foreach ($node->children as $child) { + if (!$child->dead) $children[] = $child; + } + $result = $def->child->validateChildren($children, $config, $context); + if ($result === true) { + // nop + $node->children = $children; + } elseif ($result === false) { + $node->dead = true; + if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node removed'); + } else { + $node->children = $result; + if ($e) { + // XXX This will miss mutations of internal nodes. Perhaps defer to the child validators + if (empty($result) && !empty($children)) { + $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed'); + } else if ($result != $children) { + $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized'); + } + } + } + } + } + + //####################################################################// + // Post-processing + + // remove context variables + $context->destroy('IsInline'); + $context->destroy('CurrentNode'); + $context->destroy('CurrentToken'); + + //####################################################################// + // Return + + return HTMLPurifier_Arborize::flatten($node, $config, $context); + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php b/helpers/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php new file mode 100644 index 00000000..e389e001 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php @@ -0,0 +1,600 @@ +getHTMLDefinition(); + + // local variables + $generator = new HTMLPurifier_Generator($config, $context); + $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); + // used for autoclose early abortion + $global_parent_allowed_elements = $definition->info_parent_def->child->getAllowedElements($config); + $e = $context->get('ErrorCollector', true); + $i = false; // injector index + list($zipper, $token) = HTMLPurifier_Zipper::fromArray($tokens); + if ($token === NULL) { + return array(); + } + $reprocess = false; // whether or not to reprocess the same token + $stack = array(); + + // member variables + $this->stack =& $stack; + $this->tokens =& $tokens; + $this->token =& $token; + $this->zipper =& $zipper; + $this->config = $config; + $this->context = $context; + + // context variables + $context->register('CurrentNesting', $stack); + $context->register('InputZipper', $zipper); + $context->register('CurrentToken', $token); + + // -- begin INJECTOR -- + + $this->injectors = array(); + + $injectors = $config->getBatch('AutoFormat'); + $def_injectors = $definition->info_injector; + $custom_injectors = $injectors['Custom']; + unset($injectors['Custom']); // special case + foreach ($injectors as $injector => $b) { + // XXX: Fix with a legitimate lookup table of enabled filters + if (strpos($injector, '.') !== false) { + continue; + } + $injector = "HTMLPurifier_Injector_$injector"; + if (!$b) { + continue; + } + $this->injectors[] = new $injector; + } + foreach ($def_injectors as $injector) { + // assumed to be objects + $this->injectors[] = $injector; + } + foreach ($custom_injectors as $injector) { + if (!$injector) { + continue; + } + if (is_string($injector)) { + $injector = "HTMLPurifier_Injector_$injector"; + $injector = new $injector; + } + $this->injectors[] = $injector; + } + + // give the injectors references to the definition and context + // variables for performance reasons + foreach ($this->injectors as $ix => $injector) { + $error = $injector->prepare($config, $context); + if (!$error) { + continue; + } + array_splice($this->injectors, $ix, 1); // rm the injector + trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING); + } + + // -- end INJECTOR -- + + // a note on reprocessing: + // In order to reduce code duplication, whenever some code needs + // to make HTML changes in order to make things "correct", the + // new HTML gets sent through the purifier, regardless of its + // status. This means that if we add a start token, because it + // was totally necessary, we don't have to update nesting; we just + // punt ($reprocess = true; continue;) and it does that for us. + + // isset is in loop because $tokens size changes during loop exec + for (;; + // only increment if we don't need to reprocess + $reprocess ? $reprocess = false : $token = $zipper->next($token)) { + + // check for a rewind + if (is_int($i)) { + // possibility: disable rewinding if the current token has a + // rewind set on it already. This would offer protection from + // infinite loop, but might hinder some advanced rewinding. + $rewind_offset = $this->injectors[$i]->getRewindOffset(); + if (is_int($rewind_offset)) { + for ($j = 0; $j < $rewind_offset; $j++) { + if (empty($zipper->front)) break; + $token = $zipper->prev($token); + // indicate that other injectors should not process this token, + // but we need to reprocess it + unset($token->skip[$i]); + $token->rewind = $i; + if ($token instanceof HTMLPurifier_Token_Start) { + array_pop($this->stack); + } elseif ($token instanceof HTMLPurifier_Token_End) { + $this->stack[] = $token->start; + } + } + } + $i = false; + } + + // handle case of document end + if ($token === NULL) { + // kill processing if stack is empty + if (empty($this->stack)) { + break; + } + + // peek + $top_nesting = array_pop($this->stack); + $this->stack[] = $top_nesting; + + // send error [TagClosedSuppress] + if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting); + } + + // append, don't splice, since this is the end + $token = new HTMLPurifier_Token_End($top_nesting->name); + + // punt! + $reprocess = true; + continue; + } + + //echo '
'; printZipper($zipper, $token);//printTokens($this->stack); + //flush(); + + // quick-check: if it's not a tag, no need to process + if (empty($token->is_tag)) { + if ($token instanceof HTMLPurifier_Token_Text) { + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) { + continue; + } + if ($token->rewind !== null && $token->rewind !== $i) { + continue; + } + // XXX fuckup + $r = $token; + $injector->handleText($r); + $token = $this->processToken($r, $i); + $reprocess = true; + break; + } + } + // another possibility is a comment + continue; + } + + if (isset($definition->info[$token->name])) { + $type = $definition->info[$token->name]->child->type; + } else { + $type = false; // Type is unknown, treat accordingly + } + + // quick tag checks: anything that's *not* an end tag + $ok = false; + if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) { + // claims to be a start tag but is empty + $token = new HTMLPurifier_Token_Empty( + $token->name, + $token->attr, + $token->line, + $token->col, + $token->armor + ); + $ok = true; + } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) { + // claims to be empty but really is a start tag + // NB: this assignment is required + $old_token = $token; + $token = new HTMLPurifier_Token_End($token->name); + $token = $this->insertBefore( + new HTMLPurifier_Token_Start($old_token->name, $old_token->attr, $old_token->line, $old_token->col, $old_token->armor) + ); + // punt (since we had to modify the input stream in a non-trivial way) + $reprocess = true; + continue; + } elseif ($token instanceof HTMLPurifier_Token_Empty) { + // real empty token + $ok = true; + } elseif ($token instanceof HTMLPurifier_Token_Start) { + // start tag + + // ...unless they also have to close their parent + if (!empty($this->stack)) { + + // Performance note: you might think that it's rather + // inefficient, recalculating the autoclose information + // for every tag that a token closes (since when we + // do an autoclose, we push a new token into the + // stream and then /process/ that, before + // re-processing this token.) But this is + // necessary, because an injector can make an + // arbitrary transformations to the autoclosing + // tokens we introduce, so things may have changed + // in the meantime. Also, doing the inefficient thing is + // "easy" to reason about (for certain perverse definitions + // of "easy") + + $parent = array_pop($this->stack); + $this->stack[] = $parent; + + $parent_def = null; + $parent_elements = null; + $autoclose = false; + if (isset($definition->info[$parent->name])) { + $parent_def = $definition->info[$parent->name]; + $parent_elements = $parent_def->child->getAllowedElements($config); + $autoclose = !isset($parent_elements[$token->name]); + } + + if ($autoclose && $definition->info[$token->name]->wrap) { + // Check if an element can be wrapped by another + // element to make it valid in a context (for + // example,
      needs a
    • in between) + $wrapname = $definition->info[$token->name]->wrap; + $wrapdef = $definition->info[$wrapname]; + $elements = $wrapdef->child->getAllowedElements($config); + if (isset($elements[$token->name]) && isset($parent_elements[$wrapname])) { + $newtoken = new HTMLPurifier_Token_Start($wrapname); + $token = $this->insertBefore($newtoken); + $reprocess = true; + continue; + } + } + + $carryover = false; + if ($autoclose && $parent_def->formatting) { + $carryover = true; + } + + if ($autoclose) { + // check if this autoclose is doomed to fail + // (this rechecks $parent, which his harmless) + $autoclose_ok = isset($global_parent_allowed_elements[$token->name]); + if (!$autoclose_ok) { + foreach ($this->stack as $ancestor) { + $elements = $definition->info[$ancestor->name]->child->getAllowedElements($config); + if (isset($elements[$token->name])) { + $autoclose_ok = true; + break; + } + if ($definition->info[$token->name]->wrap) { + $wrapname = $definition->info[$token->name]->wrap; + $wrapdef = $definition->info[$wrapname]; + $wrap_elements = $wrapdef->child->getAllowedElements($config); + if (isset($wrap_elements[$token->name]) && isset($elements[$wrapname])) { + $autoclose_ok = true; + break; + } + } + } + } + if ($autoclose_ok) { + // errors need to be updated + $new_token = new HTMLPurifier_Token_End($parent->name); + $new_token->start = $parent; + // [TagClosedSuppress] + if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) { + if (!$carryover) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent); + } else { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent); + } + } + if ($carryover) { + $element = clone $parent; + // [TagClosedAuto] + $element->armor['MakeWellFormed_TagClosedError'] = true; + $element->carryover = true; + $token = $this->processToken(array($new_token, $token, $element)); + } else { + $token = $this->insertBefore($new_token); + } + } else { + $token = $this->remove(); + } + $reprocess = true; + continue; + } + + } + $ok = true; + } + + if ($ok) { + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) { + continue; + } + if ($token->rewind !== null && $token->rewind !== $i) { + continue; + } + $r = $token; + $injector->handleElement($r); + $token = $this->processToken($r, $i); + $reprocess = true; + break; + } + if (!$reprocess) { + // ah, nothing interesting happened; do normal processing + if ($token instanceof HTMLPurifier_Token_Start) { + $this->stack[] = $token; + } elseif ($token instanceof HTMLPurifier_Token_End) { + throw new HTMLPurifier_Exception( + 'Improper handling of end tag in start code; possible error in MakeWellFormed' + ); + } + } + continue; + } + + // sanity check: we should be dealing with a closing tag + if (!$token instanceof HTMLPurifier_Token_End) { + throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier'); + } + + // make sure that we have something open + if (empty($this->stack)) { + if ($escape_invalid_tags) { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text'); + } + $token = new HTMLPurifier_Token_Text($generator->generateFromToken($token)); + } else { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed'); + } + $token = $this->remove(); + } + $reprocess = true; + continue; + } + + // first, check for the simplest case: everything closes neatly. + // Eventually, everything passes through here; if there are problems + // we modify the input stream accordingly and then punt, so that + // the tokens get processed again. + $current_parent = array_pop($this->stack); + if ($current_parent->name == $token->name) { + $token->start = $current_parent; + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) { + continue; + } + if ($token->rewind !== null && $token->rewind !== $i) { + continue; + } + $r = $token; + $injector->handleEnd($r); + $token = $this->processToken($r, $i); + $this->stack[] = $current_parent; + $reprocess = true; + break; + } + continue; + } + + // okay, so we're trying to close the wrong tag + + // undo the pop previous pop + $this->stack[] = $current_parent; + + // scroll back the entire nest, trying to find our tag. + // (feature could be to specify how far you'd like to go) + $size = count($this->stack); + // -2 because -1 is the last element, but we already checked that + $skipped_tags = false; + for ($j = $size - 2; $j >= 0; $j--) { + if ($this->stack[$j]->name == $token->name) { + $skipped_tags = array_slice($this->stack, $j); + break; + } + } + + // we didn't find the tag, so remove + if ($skipped_tags === false) { + if ($escape_invalid_tags) { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text'); + } + $token = new HTMLPurifier_Token_Text($generator->generateFromToken($token)); + } else { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed'); + } + $token = $this->remove(); + } + $reprocess = true; + continue; + } + + // do errors, in REVERSE $j order: a,b,c with + $c = count($skipped_tags); + if ($e) { + for ($j = $c - 1; $j > 0; $j--) { + // notice we exclude $j == 0, i.e. the current ending tag, from + // the errors... [TagClosedSuppress] + if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]); + } + } + } + + // insert tags, in FORWARD $j order: c,b,a with + $replace = array($token); + for ($j = 1; $j < $c; $j++) { + // ...as well as from the insertions + $new_token = new HTMLPurifier_Token_End($skipped_tags[$j]->name); + $new_token->start = $skipped_tags[$j]; + array_unshift($replace, $new_token); + if (isset($definition->info[$new_token->name]) && $definition->info[$new_token->name]->formatting) { + // [TagClosedAuto] + $element = clone $skipped_tags[$j]; + $element->carryover = true; + $element->armor['MakeWellFormed_TagClosedError'] = true; + $replace[] = $element; + } + } + $token = $this->processToken($replace); + $reprocess = true; + continue; + } + + $context->destroy('CurrentToken'); + $context->destroy('CurrentNesting'); + $context->destroy('InputZipper'); + + unset($this->injectors, $this->stack, $this->tokens); + return $zipper->toArray($token); + } + + /** + * Processes arbitrary token values for complicated substitution patterns. + * In general: + * + * If $token is an array, it is a list of tokens to substitute for the + * current token. These tokens then get individually processed. If there + * is a leading integer in the list, that integer determines how many + * tokens from the stream should be removed. + * + * If $token is a regular token, it is swapped with the current token. + * + * If $token is false, the current token is deleted. + * + * If $token is an integer, that number of tokens (with the first token + * being the current one) will be deleted. + * + * @param HTMLPurifier_Token|array|int|bool $token Token substitution value + * @param HTMLPurifier_Injector|int $injector Injector that performed the substitution; default is if + * this is not an injector related operation. + * @throws HTMLPurifier_Exception + */ + protected function processToken($token, $injector = -1) + { + // normalize forms of token + if (is_object($token)) { + $token = array(1, $token); + } + if (is_int($token)) { + $token = array($token); + } + if ($token === false) { + $token = array(1); + } + if (!is_array($token)) { + throw new HTMLPurifier_Exception('Invalid token type from injector'); + } + if (!is_int($token[0])) { + array_unshift($token, 1); + } + if ($token[0] === 0) { + throw new HTMLPurifier_Exception('Deleting zero tokens is not valid'); + } + + // $token is now an array with the following form: + // array(number nodes to delete, new node 1, new node 2, ...) + + $delete = array_shift($token); + list($old, $r) = $this->zipper->splice($this->token, $delete, $token); + + if ($injector > -1) { + // determine appropriate skips + $oldskip = isset($old[0]) ? $old[0]->skip : array(); + foreach ($token as $object) { + $object->skip = $oldskip; + $object->skip[$injector] = true; + } + } + + return $r; + + } + + /** + * Inserts a token before the current token. Cursor now points to + * this token. You must reprocess after this. + * @param HTMLPurifier_Token $token + */ + private function insertBefore($token) + { + // NB not $this->zipper->insertBefore(), due to positioning + // differences + $splice = $this->zipper->splice($this->token, 0, array($token)); + + return $splice[1]; + } + + /** + * Removes current token. Cursor now points to new token occupying previously + * occupied space. You must reprocess after this. + */ + private function remove() + { + return $this->zipper->delete(); + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php b/helpers/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php new file mode 100644 index 00000000..1a8149ec --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php @@ -0,0 +1,207 @@ +getHTMLDefinition(); + $generator = new HTMLPurifier_Generator($config, $context); + $result = array(); + + $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); + $remove_invalid_img = $config->get('Core.RemoveInvalidImg'); + + // currently only used to determine if comments should be kept + $trusted = $config->get('HTML.Trusted'); + $comment_lookup = $config->get('HTML.AllowedComments'); + $comment_regexp = $config->get('HTML.AllowedCommentsRegexp'); + $check_comments = $comment_lookup !== array() || $comment_regexp !== null; + + $remove_script_contents = $config->get('Core.RemoveScriptContents'); + $hidden_elements = $config->get('Core.HiddenElements'); + + // remove script contents compatibility + if ($remove_script_contents === true) { + $hidden_elements['script'] = true; + } elseif ($remove_script_contents === false && isset($hidden_elements['script'])) { + unset($hidden_elements['script']); + } + + $attr_validator = new HTMLPurifier_AttrValidator(); + + // removes tokens until it reaches a closing tag with its value + $remove_until = false; + + // converts comments into text tokens when this is equal to a tag name + $textify_comments = false; + + $token = false; + $context->register('CurrentToken', $token); + + $e = false; + if ($config->get('Core.CollectErrors')) { + $e =& $context->get('ErrorCollector'); + } + + foreach ($tokens as $token) { + if ($remove_until) { + if (empty($token->is_tag) || $token->name !== $remove_until) { + continue; + } + } + if (!empty($token->is_tag)) { + // DEFINITION CALL + + // before any processing, try to transform the element + if (isset($definition->info_tag_transform[$token->name])) { + $original_name = $token->name; + // there is a transformation for this tag + // DEFINITION CALL + $token = $definition-> + info_tag_transform[$token->name]->transform($token, $config, $context); + if ($e) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Tag transform', $original_name); + } + } + + if (isset($definition->info[$token->name])) { + // mostly everything's good, but + // we need to make sure required attributes are in order + if (($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) && + $definition->info[$token->name]->required_attr && + ($token->name != 'img' || $remove_invalid_img) // ensure config option still works + ) { + $attr_validator->validateToken($token, $config, $context); + $ok = true; + foreach ($definition->info[$token->name]->required_attr as $name) { + if (!isset($token->attr[$name])) { + $ok = false; + break; + } + } + if (!$ok) { + if ($e) { + $e->send( + E_ERROR, + 'Strategy_RemoveForeignElements: Missing required attribute', + $name + ); + } + continue; + } + $token->armor['ValidateAttributes'] = true; + } + + if (isset($hidden_elements[$token->name]) && $token instanceof HTMLPurifier_Token_Start) { + $textify_comments = $token->name; + } elseif ($token->name === $textify_comments && $token instanceof HTMLPurifier_Token_End) { + $textify_comments = false; + } + + } elseif ($escape_invalid_tags) { + // invalid tag, generate HTML representation and insert in + if ($e) { + $e->send(E_WARNING, 'Strategy_RemoveForeignElements: Foreign element to text'); + } + $token = new HTMLPurifier_Token_Text( + $generator->generateFromToken($token) + ); + } else { + // check if we need to destroy all of the tag's children + // CAN BE GENERICIZED + if (isset($hidden_elements[$token->name])) { + if ($token instanceof HTMLPurifier_Token_Start) { + $remove_until = $token->name; + } elseif ($token instanceof HTMLPurifier_Token_Empty) { + // do nothing: we're still looking + } else { + $remove_until = false; + } + if ($e) { + $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign meta element removed'); + } + } else { + if ($e) { + $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign element removed'); + } + } + continue; + } + } elseif ($token instanceof HTMLPurifier_Token_Comment) { + // textify comments in script tags when they are allowed + if ($textify_comments !== false) { + $data = $token->data; + $token = new HTMLPurifier_Token_Text($data); + } elseif ($trusted || $check_comments) { + // always cleanup comments + $trailing_hyphen = false; + if ($e) { + // perform check whether or not there's a trailing hyphen + if (substr($token->data, -1) == '-') { + $trailing_hyphen = true; + } + } + $token->data = rtrim($token->data, '-'); + $found_double_hyphen = false; + while (strpos($token->data, '--') !== false) { + $found_double_hyphen = true; + $token->data = str_replace('--', '-', $token->data); + } + if ($trusted || !empty($comment_lookup[trim($token->data)]) || + ($comment_regexp !== null && preg_match($comment_regexp, trim($token->data)))) { + // OK good + if ($e) { + if ($trailing_hyphen) { + $e->send( + E_NOTICE, + 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed' + ); + } + if ($found_double_hyphen) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Hyphens in comment collapsed'); + } + } + } else { + if ($e) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed'); + } + continue; + } + } else { + // strip comments + if ($e) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed'); + } + continue; + } + } elseif ($token instanceof HTMLPurifier_Token_Text) { + } else { + continue; + } + $result[] = $token; + } + if ($remove_until && $e) { + // we removed tokens until the end, throw error + $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Token removed to end', $remove_until); + } + $context->destroy('CurrentToken'); + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Strategy/ValidateAttributes.php b/helpers/htmlpurifier/HTMLPurifier/Strategy/ValidateAttributes.php new file mode 100644 index 00000000..fbb3d27c --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Strategy/ValidateAttributes.php @@ -0,0 +1,45 @@ +register('CurrentToken', $token); + + foreach ($tokens as $key => $token) { + + // only process tokens that have attributes, + // namely start and empty tags + if (!$token instanceof HTMLPurifier_Token_Start && !$token instanceof HTMLPurifier_Token_Empty) { + continue; + } + + // skip tokens that are armored + if (!empty($token->armor['ValidateAttributes'])) { + continue; + } + + // note that we have no facilities here for removing tokens + $validator->validateToken($token, $config, $context); + } + $context->destroy('CurrentToken'); + return $tokens; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/StringHash.php b/helpers/htmlpurifier/HTMLPurifier/StringHash.php new file mode 100644 index 00000000..c0737019 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/StringHash.php @@ -0,0 +1,47 @@ +accessed[$index] = true; + return parent::offsetGet($index); + } + + /** + * Returns a lookup array of all array indexes that have been accessed. + * @return array in form array($index => true). + */ + public function getAccessed() + { + return $this->accessed; + } + + /** + * Resets the access array. + */ + public function resetAccessed() + { + $this->accessed = array(); + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/StringHashParser.php b/helpers/htmlpurifier/HTMLPurifier/StringHashParser.php new file mode 100644 index 00000000..7c73f808 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/StringHashParser.php @@ -0,0 +1,136 @@ + 'DefaultKeyValue', + * 'KEY' => 'Value', + * 'KEY2' => 'Value2', + * 'MULTILINE-KEY' => "Multiline\nvalue.\n", + * ) + * + * We use this as an easy to use file-format for configuration schema + * files, but the class itself is usage agnostic. + * + * You can use ---- to forcibly terminate parsing of a single string-hash; + * this marker is used in multi string-hashes to delimit boundaries. + */ +class HTMLPurifier_StringHashParser +{ + + /** + * @type string + */ + public $default = 'ID'; + + /** + * Parses a file that contains a single string-hash. + * @param string $file + * @return array + */ + public function parseFile($file) + { + if (!file_exists($file)) { + return false; + } + $fh = fopen($file, 'r'); + if (!$fh) { + return false; + } + $ret = $this->parseHandle($fh); + fclose($fh); + return $ret; + } + + /** + * Parses a file that contains multiple string-hashes delimited by '----' + * @param string $file + * @return array + */ + public function parseMultiFile($file) + { + if (!file_exists($file)) { + return false; + } + $ret = array(); + $fh = fopen($file, 'r'); + if (!$fh) { + return false; + } + while (!feof($fh)) { + $ret[] = $this->parseHandle($fh); + } + fclose($fh); + return $ret; + } + + /** + * Internal parser that acepts a file handle. + * @note While it's possible to simulate in-memory parsing by using + * custom stream wrappers, if such a use-case arises we should + * factor out the file handle into its own class. + * @param resource $fh File handle with pointer at start of valid string-hash + * block. + * @return array + */ + protected function parseHandle($fh) + { + $state = false; + $single = false; + $ret = array(); + do { + $line = fgets($fh); + if ($line === false) { + break; + } + $line = rtrim($line, "\n\r"); + if (!$state && $line === '') { + continue; + } + if ($line === '----') { + break; + } + if (strncmp('--#', $line, 3) === 0) { + // Comment + continue; + } elseif (strncmp('--', $line, 2) === 0) { + // Multiline declaration + $state = trim($line, '- '); + if (!isset($ret[$state])) { + $ret[$state] = ''; + } + continue; + } elseif (!$state) { + $single = true; + if (strpos($line, ':') !== false) { + // Single-line declaration + list($state, $line) = explode(':', $line, 2); + $line = trim($line); + } else { + // Use default declaration + $state = $this->default; + } + } + if ($single) { + $ret[$state] = $line; + $single = false; + $state = false; + } else { + $ret[$state] .= "$line\n"; + } + } while (!feof($fh)); + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/TagTransform.php b/helpers/htmlpurifier/HTMLPurifier/TagTransform.php new file mode 100644 index 00000000..7b8d8334 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/TagTransform.php @@ -0,0 +1,37 @@ + 'xx-small', + '1' => 'xx-small', + '2' => 'small', + '3' => 'medium', + '4' => 'large', + '5' => 'x-large', + '6' => 'xx-large', + '7' => '300%', + '-1' => 'smaller', + '-2' => '60%', + '+1' => 'larger', + '+2' => '150%', + '+3' => '200%', + '+4' => '300%' + ); + + /** + * @param HTMLPurifier_Token_Tag $tag + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token_End|string + */ + public function transform($tag, $config, $context) + { + if ($tag instanceof HTMLPurifier_Token_End) { + $new_tag = clone $tag; + $new_tag->name = $this->transform_to; + return $new_tag; + } + + $attr = $tag->attr; + $prepend_style = ''; + + // handle color transform + if (isset($attr['color'])) { + $prepend_style .= 'color:' . $attr['color'] . ';'; + unset($attr['color']); + } + + // handle face transform + if (isset($attr['face'])) { + $prepend_style .= 'font-family:' . $attr['face'] . ';'; + unset($attr['face']); + } + + // handle size transform + if (isset($attr['size'])) { + // normalize large numbers + if ($attr['size'] !== '') { + if ($attr['size']{0} == '+' || $attr['size']{0} == '-') { + $size = (int)$attr['size']; + if ($size < -2) { + $attr['size'] = '-2'; + } + if ($size > 4) { + $attr['size'] = '+4'; + } + } else { + $size = (int)$attr['size']; + if ($size > 7) { + $attr['size'] = '7'; + } + } + } + if (isset($this->_size_lookup[$attr['size']])) { + $prepend_style .= 'font-size:' . + $this->_size_lookup[$attr['size']] . ';'; + } + unset($attr['size']); + } + + if ($prepend_style) { + $attr['style'] = isset($attr['style']) ? + $prepend_style . $attr['style'] : + $prepend_style; + } + + $new_tag = clone $tag; + $new_tag->name = $this->transform_to; + $new_tag->attr = $attr; + + return $new_tag; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/TagTransform/Simple.php b/helpers/htmlpurifier/HTMLPurifier/TagTransform/Simple.php new file mode 100644 index 00000000..71bf10b9 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/TagTransform/Simple.php @@ -0,0 +1,44 @@ +transform_to = $transform_to; + $this->style = $style; + } + + /** + * @param HTMLPurifier_Token_Tag $tag + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function transform($tag, $config, $context) + { + $new_tag = clone $tag; + $new_tag->name = $this->transform_to; + if (!is_null($this->style) && + ($new_tag instanceof HTMLPurifier_Token_Start || $new_tag instanceof HTMLPurifier_Token_Empty) + ) { + $this->prependCSS($new_tag->attr, $this->style); + } + return $new_tag; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Token.php b/helpers/htmlpurifier/HTMLPurifier/Token.php new file mode 100644 index 00000000..85b85e07 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Token.php @@ -0,0 +1,100 @@ +line = $l; + $this->col = $c; + } + + /** + * Convenience function for DirectLex settings line/col position. + * @param int $l + * @param int $c + */ + public function rawPosition($l, $c) + { + if ($c === -1) { + $l++; + } + $this->line = $l; + $this->col = $c; + } + + /** + * Converts a token into its corresponding node. + */ + abstract public function toNode(); +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Token/Comment.php b/helpers/htmlpurifier/HTMLPurifier/Token/Comment.php new file mode 100644 index 00000000..23453c70 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Token/Comment.php @@ -0,0 +1,38 @@ +data = $data; + $this->line = $line; + $this->col = $col; + } + + public function toNode() { + return new HTMLPurifier_Node_Comment($this->data, $this->line, $this->col); + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Token/Empty.php b/helpers/htmlpurifier/HTMLPurifier/Token/Empty.php new file mode 100644 index 00000000..78a95f55 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Token/Empty.php @@ -0,0 +1,15 @@ +empty = true; + return $n; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Token/End.php b/helpers/htmlpurifier/HTMLPurifier/Token/End.php new file mode 100644 index 00000000..59b38fdc --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Token/End.php @@ -0,0 +1,24 @@ +toNode not supported!"); + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Token/Start.php b/helpers/htmlpurifier/HTMLPurifier/Token/Start.php new file mode 100644 index 00000000..019f317a --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Token/Start.php @@ -0,0 +1,10 @@ +!empty($obj->is_tag) + * without having to use a function call is_a(). + * @type bool + */ + public $is_tag = true; + + /** + * The lower-case name of the tag, like 'a', 'b' or 'blockquote'. + * + * @note Strictly speaking, XML tags are case sensitive, so we shouldn't + * be lower-casing them, but these tokens cater to HTML tags, which are + * insensitive. + * @type string + */ + public $name; + + /** + * Associative array of the tag's attributes. + * @type array + */ + public $attr = array(); + + /** + * Non-overloaded constructor, which lower-cases passed tag name. + * + * @param string $name String name. + * @param array $attr Associative array of attributes. + * @param int $line + * @param int $col + * @param array $armor + */ + public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) + { + $this->name = ctype_lower($name) ? $name : strtolower($name); + foreach ($attr as $key => $value) { + // normalization only necessary when key is not lowercase + if (!ctype_lower($key)) { + $new_key = strtolower($key); + if (!isset($attr[$new_key])) { + $attr[$new_key] = $attr[$key]; + } + if ($new_key !== $key) { + unset($attr[$key]); + } + } + } + $this->attr = $attr; + $this->line = $line; + $this->col = $col; + $this->armor = $armor; + } + + public function toNode() { + return new HTMLPurifier_Node_Element($this->name, $this->attr, $this->line, $this->col, $this->armor); + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/Token/Text.php b/helpers/htmlpurifier/HTMLPurifier/Token/Text.php new file mode 100644 index 00000000..f26a1c21 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/Token/Text.php @@ -0,0 +1,53 @@ +data = $data; + $this->is_whitespace = ctype_space($data); + $this->line = $line; + $this->col = $col; + } + + public function toNode() { + return new HTMLPurifier_Node_Text($this->data, $this->is_whitespace, $this->line, $this->col); + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/TokenFactory.php b/helpers/htmlpurifier/HTMLPurifier/TokenFactory.php new file mode 100644 index 00000000..dea2446b --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/TokenFactory.php @@ -0,0 +1,118 @@ +p_start = new HTMLPurifier_Token_Start('', array()); + $this->p_end = new HTMLPurifier_Token_End(''); + $this->p_empty = new HTMLPurifier_Token_Empty('', array()); + $this->p_text = new HTMLPurifier_Token_Text(''); + $this->p_comment = new HTMLPurifier_Token_Comment(''); + } + + /** + * Creates a HTMLPurifier_Token_Start. + * @param string $name Tag name + * @param array $attr Associative array of attributes + * @return HTMLPurifier_Token_Start Generated HTMLPurifier_Token_Start + */ + public function createStart($name, $attr = array()) + { + $p = clone $this->p_start; + $p->__construct($name, $attr); + return $p; + } + + /** + * Creates a HTMLPurifier_Token_End. + * @param string $name Tag name + * @return HTMLPurifier_Token_End Generated HTMLPurifier_Token_End + */ + public function createEnd($name) + { + $p = clone $this->p_end; + $p->__construct($name); + return $p; + } + + /** + * Creates a HTMLPurifier_Token_Empty. + * @param string $name Tag name + * @param array $attr Associative array of attributes + * @return HTMLPurifier_Token_Empty Generated HTMLPurifier_Token_Empty + */ + public function createEmpty($name, $attr = array()) + { + $p = clone $this->p_empty; + $p->__construct($name, $attr); + return $p; + } + + /** + * Creates a HTMLPurifier_Token_Text. + * @param string $data Data of text token + * @return HTMLPurifier_Token_Text Generated HTMLPurifier_Token_Text + */ + public function createText($data) + { + $p = clone $this->p_text; + $p->__construct($data); + return $p; + } + + /** + * Creates a HTMLPurifier_Token_Comment. + * @param string $data Data of comment token + * @return HTMLPurifier_Token_Comment Generated HTMLPurifier_Token_Comment + */ + public function createComment($data) + { + $p = clone $this->p_comment; + $p->__construct($data); + return $p; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URI.php b/helpers/htmlpurifier/HTMLPurifier/URI.php new file mode 100644 index 00000000..a5e7ae29 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URI.php @@ -0,0 +1,314 @@ +scheme = is_null($scheme) || ctype_lower($scheme) ? $scheme : strtolower($scheme); + $this->userinfo = $userinfo; + $this->host = $host; + $this->port = is_null($port) ? $port : (int)$port; + $this->path = $path; + $this->query = $query; + $this->fragment = $fragment; + } + + /** + * Retrieves a scheme object corresponding to the URI's scheme/default + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_URIScheme Scheme object appropriate for validating this URI + */ + public function getSchemeObj($config, $context) + { + $registry = HTMLPurifier_URISchemeRegistry::instance(); + if ($this->scheme !== null) { + $scheme_obj = $registry->getScheme($this->scheme, $config, $context); + if (!$scheme_obj) { + return false; + } // invalid scheme, clean it out + } else { + // no scheme: retrieve the default one + $def = $config->getDefinition('URI'); + $scheme_obj = $def->getDefaultScheme($config, $context); + if (!$scheme_obj) { + // something funky happened to the default scheme object + trigger_error( + 'Default scheme object "' . $def->defaultScheme . '" was not readable', + E_USER_WARNING + ); + return false; + } + } + return $scheme_obj; + } + + /** + * Generic validation method applicable for all schemes. May modify + * this URI in order to get it into a compliant form. + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool True if validation/filtering succeeds, false if failure + */ + public function validate($config, $context) + { + // ABNF definitions from RFC 3986 + $chars_sub_delims = '!$&\'()*+,;='; + $chars_gen_delims = ':/?#[]@'; + $chars_pchar = $chars_sub_delims . ':@'; + + // validate host + if (!is_null($this->host)) { + $host_def = new HTMLPurifier_AttrDef_URI_Host(); + $this->host = $host_def->validate($this->host, $config, $context); + if ($this->host === false) { + $this->host = null; + } + } + + // validate scheme + // NOTE: It's not appropriate to check whether or not this + // scheme is in our registry, since a URIFilter may convert a + // URI that we don't allow into one we do. So instead, we just + // check if the scheme can be dropped because there is no host + // and it is our default scheme. + if (!is_null($this->scheme) && is_null($this->host) || $this->host === '') { + // support for relative paths is pretty abysmal when the + // scheme is present, so axe it when possible + $def = $config->getDefinition('URI'); + if ($def->defaultScheme === $this->scheme) { + $this->scheme = null; + } + } + + // validate username + if (!is_null($this->userinfo)) { + $encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . ':'); + $this->userinfo = $encoder->encode($this->userinfo); + } + + // validate port + if (!is_null($this->port)) { + if ($this->port < 1 || $this->port > 65535) { + $this->port = null; + } + } + + // validate path + $segments_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/'); + if (!is_null($this->host)) { // this catches $this->host === '' + // path-abempty (hier and relative) + // http://www.example.com/my/path + // //www.example.com/my/path (looks odd, but works, and + // recognized by most browsers) + // (this set is valid or invalid on a scheme by scheme + // basis, so we'll deal with it later) + // file:///my/path + // ///my/path + $this->path = $segments_encoder->encode($this->path); + } elseif ($this->path !== '') { + if ($this->path[0] === '/') { + // path-absolute (hier and relative) + // http:/my/path + // /my/path + if (strlen($this->path) >= 2 && $this->path[1] === '/') { + // This could happen if both the host gets stripped + // out + // http://my/path + // //my/path + $this->path = ''; + } else { + $this->path = $segments_encoder->encode($this->path); + } + } elseif (!is_null($this->scheme)) { + // path-rootless (hier) + // http:my/path + // Short circuit evaluation means we don't need to check nz + $this->path = $segments_encoder->encode($this->path); + } else { + // path-noscheme (relative) + // my/path + // (once again, not checking nz) + $segment_nc_encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . '@'); + $c = strpos($this->path, '/'); + if ($c !== false) { + $this->path = + $segment_nc_encoder->encode(substr($this->path, 0, $c)) . + $segments_encoder->encode(substr($this->path, $c)); + } else { + $this->path = $segment_nc_encoder->encode($this->path); + } + } + } else { + // path-empty (hier and relative) + $this->path = ''; // just to be safe + } + + // qf = query and fragment + $qf_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/?'); + + if (!is_null($this->query)) { + $this->query = $qf_encoder->encode($this->query); + } + + if (!is_null($this->fragment)) { + $this->fragment = $qf_encoder->encode($this->fragment); + } + return true; + } + + /** + * Convert URI back to string + * @return string URI appropriate for output + */ + public function toString() + { + // reconstruct authority + $authority = null; + // there is a rendering difference between a null authority + // (http:foo-bar) and an empty string authority + // (http:///foo-bar). + if (!is_null($this->host)) { + $authority = ''; + if (!is_null($this->userinfo)) { + $authority .= $this->userinfo . '@'; + } + $authority .= $this->host; + if (!is_null($this->port)) { + $authority .= ':' . $this->port; + } + } + + // Reconstruct the result + // One might wonder about parsing quirks from browsers after + // this reconstruction. Unfortunately, parsing behavior depends + // on what *scheme* was employed (file:///foo is handled *very* + // differently than http:///foo), so unfortunately we have to + // defer to the schemes to do the right thing. + $result = ''; + if (!is_null($this->scheme)) { + $result .= $this->scheme . ':'; + } + if (!is_null($authority)) { + $result .= '//' . $authority; + } + $result .= $this->path; + if (!is_null($this->query)) { + $result .= '?' . $this->query; + } + if (!is_null($this->fragment)) { + $result .= '#' . $this->fragment; + } + + return $result; + } + + /** + * Returns true if this URL might be considered a 'local' URL given + * the current context. This is true when the host is null, or + * when it matches the host supplied to the configuration. + * + * Note that this does not do any scheme checking, so it is mostly + * only appropriate for metadata that doesn't care about protocol + * security. isBenign is probably what you actually want. + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function isLocal($config, $context) + { + if ($this->host === null) { + return true; + } + $uri_def = $config->getDefinition('URI'); + if ($uri_def->host === $this->host) { + return true; + } + return false; + } + + /** + * Returns true if this URL should be considered a 'benign' URL, + * that is: + * + * - It is a local URL (isLocal), and + * - It has a equal or better level of security + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function isBenign($config, $context) + { + if (!$this->isLocal($config, $context)) { + return false; + } + + $scheme_obj = $this->getSchemeObj($config, $context); + if (!$scheme_obj) { + return false; + } // conservative approach + + $current_scheme_obj = $config->getDefinition('URI')->getDefaultScheme($config, $context); + if ($current_scheme_obj->secure) { + if (!$scheme_obj->secure) { + return false; + } + } + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIDefinition.php b/helpers/htmlpurifier/HTMLPurifier/URIDefinition.php new file mode 100644 index 00000000..e0bd8bcc --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIDefinition.php @@ -0,0 +1,112 @@ +registerFilter(new HTMLPurifier_URIFilter_DisableExternal()); + $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternalResources()); + $this->registerFilter(new HTMLPurifier_URIFilter_DisableResources()); + $this->registerFilter(new HTMLPurifier_URIFilter_HostBlacklist()); + $this->registerFilter(new HTMLPurifier_URIFilter_SafeIframe()); + $this->registerFilter(new HTMLPurifier_URIFilter_MakeAbsolute()); + $this->registerFilter(new HTMLPurifier_URIFilter_Munge()); + } + + public function registerFilter($filter) + { + $this->registeredFilters[$filter->name] = $filter; + } + + public function addFilter($filter, $config) + { + $r = $filter->prepare($config); + if ($r === false) return; // null is ok, for backwards compat + if ($filter->post) { + $this->postFilters[$filter->name] = $filter; + } else { + $this->filters[$filter->name] = $filter; + } + } + + protected function doSetup($config) + { + $this->setupMemberVariables($config); + $this->setupFilters($config); + } + + protected function setupFilters($config) + { + foreach ($this->registeredFilters as $name => $filter) { + if ($filter->always_load) { + $this->addFilter($filter, $config); + } else { + $conf = $config->get('URI.' . $name); + if ($conf !== false && $conf !== null) { + $this->addFilter($filter, $config); + } + } + } + unset($this->registeredFilters); + } + + protected function setupMemberVariables($config) + { + $this->host = $config->get('URI.Host'); + $base_uri = $config->get('URI.Base'); + if (!is_null($base_uri)) { + $parser = new HTMLPurifier_URIParser(); + $this->base = $parser->parse($base_uri); + $this->defaultScheme = $this->base->scheme; + if (is_null($this->host)) $this->host = $this->base->host; + } + if (is_null($this->defaultScheme)) $this->defaultScheme = $config->get('URI.DefaultScheme'); + } + + public function getDefaultScheme($config, $context) + { + return HTMLPurifier_URISchemeRegistry::instance()->getScheme($this->defaultScheme, $config, $context); + } + + public function filter(&$uri, $config, $context) + { + foreach ($this->filters as $name => $f) { + $result = $f->filter($uri, $config, $context); + if (!$result) return false; + } + return true; + } + + public function postFilter(&$uri, $config, $context) + { + foreach ($this->postFilters as $name => $f) { + $result = $f->filter($uri, $config, $context); + if (!$result) return false; + } + return true; + } + +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIFilter.php b/helpers/htmlpurifier/HTMLPurifier/URIFilter.php new file mode 100644 index 00000000..09724e9f --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIFilter.php @@ -0,0 +1,74 @@ +getDefinition('URI')->host; + if ($our_host !== null) { + $this->ourHostParts = array_reverse(explode('.', $our_host)); + } + } + + /** + * @param HTMLPurifier_URI $uri Reference + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + if (is_null($uri->host)) { + return true; + } + if ($this->ourHostParts === false) { + return false; + } + $host_parts = array_reverse(explode('.', $uri->host)); + foreach ($this->ourHostParts as $i => $x) { + if (!isset($host_parts[$i])) { + return false; + } + if ($host_parts[$i] != $this->ourHostParts[$i]) { + return false; + } + } + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIFilter/DisableExternalResources.php b/helpers/htmlpurifier/HTMLPurifier/URIFilter/DisableExternalResources.php new file mode 100644 index 00000000..c6562169 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIFilter/DisableExternalResources.php @@ -0,0 +1,25 @@ +get('EmbeddedURI', true)) { + return true; + } + return parent::filter($uri, $config, $context); + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIFilter/DisableResources.php b/helpers/htmlpurifier/HTMLPurifier/URIFilter/DisableResources.php new file mode 100644 index 00000000..d5c412c4 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIFilter/DisableResources.php @@ -0,0 +1,22 @@ +get('EmbeddedURI', true); + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php b/helpers/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php new file mode 100644 index 00000000..a6645c17 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php @@ -0,0 +1,46 @@ +blacklist = $config->get('URI.HostBlacklist'); + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + foreach ($this->blacklist as $blacklisted_host_fragment) { + if (strpos($uri->host, $blacklisted_host_fragment) !== false) { + return false; + } + } + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php b/helpers/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php new file mode 100644 index 00000000..c507bbff --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php @@ -0,0 +1,158 @@ +getDefinition('URI'); + $this->base = $def->base; + if (is_null($this->base)) { + trigger_error( + 'URI.MakeAbsolute is being ignored due to lack of ' . + 'value for URI.Base configuration', + E_USER_WARNING + ); + return false; + } + $this->base->fragment = null; // fragment is invalid for base URI + $stack = explode('/', $this->base->path); + array_pop($stack); // discard last segment + $stack = $this->_collapseStack($stack); // do pre-parsing + $this->basePathStack = $stack; + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + if (is_null($this->base)) { + return true; + } // abort early + if ($uri->path === '' && is_null($uri->scheme) && + is_null($uri->host) && is_null($uri->query) && is_null($uri->fragment)) { + // reference to current document + $uri = clone $this->base; + return true; + } + if (!is_null($uri->scheme)) { + // absolute URI already: don't change + if (!is_null($uri->host)) { + return true; + } + $scheme_obj = $uri->getSchemeObj($config, $context); + if (!$scheme_obj) { + // scheme not recognized + return false; + } + if (!$scheme_obj->hierarchical) { + // non-hierarchal URI with explicit scheme, don't change + return true; + } + // special case: had a scheme but always is hierarchical and had no authority + } + if (!is_null($uri->host)) { + // network path, don't bother + return true; + } + if ($uri->path === '') { + $uri->path = $this->base->path; + } elseif ($uri->path[0] !== '/') { + // relative path, needs more complicated processing + $stack = explode('/', $uri->path); + $new_stack = array_merge($this->basePathStack, $stack); + if ($new_stack[0] !== '' && !is_null($this->base->host)) { + array_unshift($new_stack, ''); + } + $new_stack = $this->_collapseStack($new_stack); + $uri->path = implode('/', $new_stack); + } else { + // absolute path, but still we should collapse + $uri->path = implode('/', $this->_collapseStack(explode('/', $uri->path))); + } + // re-combine + $uri->scheme = $this->base->scheme; + if (is_null($uri->userinfo)) { + $uri->userinfo = $this->base->userinfo; + } + if (is_null($uri->host)) { + $uri->host = $this->base->host; + } + if (is_null($uri->port)) { + $uri->port = $this->base->port; + } + return true; + } + + /** + * Resolve dots and double-dots in a path stack + * @param array $stack + * @return array + */ + private function _collapseStack($stack) + { + $result = array(); + $is_folder = false; + for ($i = 0; isset($stack[$i]); $i++) { + $is_folder = false; + // absorb an internally duplicated slash + if ($stack[$i] == '' && $i && isset($stack[$i + 1])) { + continue; + } + if ($stack[$i] == '..') { + if (!empty($result)) { + $segment = array_pop($result); + if ($segment === '' && empty($result)) { + // error case: attempted to back out too far: + // restore the leading slash + $result[] = ''; + } elseif ($segment === '..') { + $result[] = '..'; // cannot remove .. with .. + } + } else { + // relative path, preserve the double-dots + $result[] = '..'; + } + $is_folder = true; + continue; + } + if ($stack[$i] == '.') { + // silently absorb + $is_folder = true; + continue; + } + $result[] = $stack[$i]; + } + if ($is_folder) { + $result[] = ''; + } + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIFilter/Munge.php b/helpers/htmlpurifier/HTMLPurifier/URIFilter/Munge.php new file mode 100644 index 00000000..6e03315a --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIFilter/Munge.php @@ -0,0 +1,115 @@ +target = $config->get('URI.' . $this->name); + $this->parser = new HTMLPurifier_URIParser(); + $this->doEmbed = $config->get('URI.MungeResources'); + $this->secretKey = $config->get('URI.MungeSecretKey'); + if ($this->secretKey && !function_exists('hash_hmac')) { + throw new Exception("Cannot use %URI.MungeSecretKey without hash_hmac support."); + } + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + if ($context->get('EmbeddedURI', true) && !$this->doEmbed) { + return true; + } + + $scheme_obj = $uri->getSchemeObj($config, $context); + if (!$scheme_obj) { + return true; + } // ignore unknown schemes, maybe another postfilter did it + if (!$scheme_obj->browsable) { + return true; + } // ignore non-browseable schemes, since we can't munge those in a reasonable way + if ($uri->isBenign($config, $context)) { + return true; + } // don't redirect if a benign URL + + $this->makeReplace($uri, $config, $context); + $this->replace = array_map('rawurlencode', $this->replace); + + $new_uri = strtr($this->target, $this->replace); + $new_uri = $this->parser->parse($new_uri); + // don't redirect if the target host is the same as the + // starting host + if ($uri->host === $new_uri->host) { + return true; + } + $uri = $new_uri; // overwrite + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + */ + protected function makeReplace($uri, $config, $context) + { + $string = $uri->toString(); + // always available + $this->replace['%s'] = $string; + $this->replace['%r'] = $context->get('EmbeddedURI', true); + $token = $context->get('CurrentToken', true); + $this->replace['%n'] = $token ? $token->name : null; + $this->replace['%m'] = $context->get('CurrentAttr', true); + $this->replace['%p'] = $context->get('CurrentCSSProperty', true); + // not always available + if ($this->secretKey) { + $this->replace['%t'] = hash_hmac("sha256", $string, $this->secretKey); + } + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIFilter/SafeIframe.php b/helpers/htmlpurifier/HTMLPurifier/URIFilter/SafeIframe.php new file mode 100644 index 00000000..f609c47a --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIFilter/SafeIframe.php @@ -0,0 +1,68 @@ +regexp = $config->get('URI.SafeIframeRegexp'); + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + // check if filter not applicable + if (!$config->get('HTML.SafeIframe')) { + return true; + } + // check if the filter should actually trigger + if (!$context->get('EmbeddedURI', true)) { + return true; + } + $token = $context->get('CurrentToken', true); + if (!($token && $token->name == 'iframe')) { + return true; + } + // check if we actually have some whitelists enabled + if ($this->regexp === null) { + return false; + } + // actually check the whitelists + return preg_match($this->regexp, $uri->toString()); + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIParser.php b/helpers/htmlpurifier/HTMLPurifier/URIParser.php new file mode 100644 index 00000000..0e7381a0 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIParser.php @@ -0,0 +1,71 @@ +percentEncoder = new HTMLPurifier_PercentEncoder(); + } + + /** + * Parses a URI. + * @param $uri string URI to parse + * @return HTMLPurifier_URI representation of URI. This representation has + * not been validated yet and may not conform to RFC. + */ + public function parse($uri) + { + $uri = $this->percentEncoder->normalize($uri); + + // Regexp is as per Appendix B. + // Note that ["<>] are an addition to the RFC's recommended + // characters, because they represent external delimeters. + $r_URI = '!'. + '(([a-zA-Z0-9\.\+\-]+):)?'. // 2. Scheme + '(//([^/?#"<>]*))?'. // 4. Authority + '([^?#"<>]*)'. // 5. Path + '(\?([^#"<>]*))?'. // 7. Query + '(#([^"<>]*))?'. // 8. Fragment + '!'; + + $matches = array(); + $result = preg_match($r_URI, $uri, $matches); + + if (!$result) return false; // *really* invalid URI + + // seperate out parts + $scheme = !empty($matches[1]) ? $matches[2] : null; + $authority = !empty($matches[3]) ? $matches[4] : null; + $path = $matches[5]; // always present, can be empty + $query = !empty($matches[6]) ? $matches[7] : null; + $fragment = !empty($matches[8]) ? $matches[9] : null; + + // further parse authority + if ($authority !== null) { + $r_authority = "/^((.+?)@)?(\[[^\]]+\]|[^:]*)(:(\d*))?/"; + $matches = array(); + preg_match($r_authority, $authority, $matches); + $userinfo = !empty($matches[1]) ? $matches[2] : null; + $host = !empty($matches[3]) ? $matches[3] : ''; + $port = !empty($matches[4]) ? (int) $matches[5] : null; + } else { + $port = $host = $userinfo = null; + } + + return new HTMLPurifier_URI( + $scheme, $userinfo, $host, $port, $path, $query, $fragment); + } + +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIScheme.php b/helpers/htmlpurifier/HTMLPurifier/URIScheme.php new file mode 100644 index 00000000..fe9e82cf --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIScheme.php @@ -0,0 +1,102 @@ +, resolves edge cases + * with making relative URIs absolute + * @type bool + */ + public $hierarchical = false; + + /** + * Whether or not the URI may omit a hostname when the scheme is + * explicitly specified, ala file:///path/to/file. As of writing, + * 'file' is the only scheme that browsers support his properly. + * @type bool + */ + public $may_omit_host = false; + + /** + * Validates the components of a URI for a specific scheme. + * @param HTMLPurifier_URI $uri Reference to a HTMLPurifier_URI object + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool success or failure + */ + abstract public function doValidate(&$uri, $config, $context); + + /** + * Public interface for validating components of a URI. Performs a + * bunch of default actions. Don't overload this method. + * @param HTMLPurifier_URI $uri Reference to a HTMLPurifier_URI object + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool success or failure + */ + public function validate(&$uri, $config, $context) + { + if ($this->default_port == $uri->port) { + $uri->port = null; + } + // kludge: browsers do funny things when the scheme but not the + // authority is set + if (!$this->may_omit_host && + // if the scheme is present, a missing host is always in error + (!is_null($uri->scheme) && ($uri->host === '' || is_null($uri->host))) || + // if the scheme is not present, a *blank* host is in error, + // since this translates into '///path' which most browsers + // interpret as being 'http://path'. + (is_null($uri->scheme) && $uri->host === '') + ) { + do { + if (is_null($uri->scheme)) { + if (substr($uri->path, 0, 2) != '//') { + $uri->host = null; + break; + } + // URI is '////path', so we cannot nullify the + // host to preserve semantics. Try expanding the + // hostname instead (fall through) + } + // first see if we can manually insert a hostname + $host = $config->get('URI.Host'); + if (!is_null($host)) { + $uri->host = $host; + } else { + // we can't do anything sensible, reject the URL. + return false; + } + } while (false); + } + return $this->doValidate($uri, $config, $context); + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIScheme/data.php b/helpers/htmlpurifier/HTMLPurifier/URIScheme/data.php new file mode 100644 index 00000000..41c49d55 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIScheme/data.php @@ -0,0 +1,136 @@ + true, + 'image/gif' => true, + 'image/png' => true, + ); + // this is actually irrelevant since we only write out the path + // component + /** + * @type bool + */ + public $may_omit_host = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $result = explode(',', $uri->path, 2); + $is_base64 = false; + $charset = null; + $content_type = null; + if (count($result) == 2) { + list($metadata, $data) = $result; + // do some legwork on the metadata + $metas = explode(';', $metadata); + while (!empty($metas)) { + $cur = array_shift($metas); + if ($cur == 'base64') { + $is_base64 = true; + break; + } + if (substr($cur, 0, 8) == 'charset=') { + // doesn't match if there are arbitrary spaces, but + // whatever dude + if ($charset !== null) { + continue; + } // garbage + $charset = substr($cur, 8); // not used + } else { + if ($content_type !== null) { + continue; + } // garbage + $content_type = $cur; + } + } + } else { + $data = $result[0]; + } + if ($content_type !== null && empty($this->allowed_types[$content_type])) { + return false; + } + if ($charset !== null) { + // error; we don't allow plaintext stuff + $charset = null; + } + $data = rawurldecode($data); + if ($is_base64) { + $raw_data = base64_decode($data); + } else { + $raw_data = $data; + } + if ( strlen($raw_data) < 12 ) { + // error; exif_imagetype throws exception with small files, + // and this likely indicates a corrupt URI/failed parse anyway + return false; + } + // XXX probably want to refactor this into a general mechanism + // for filtering arbitrary content types + if (function_exists('sys_get_temp_dir')) { + $file = tempnam(sys_get_temp_dir(), ""); + } else { + $file = tempnam("/tmp", ""); + } + file_put_contents($file, $raw_data); + if (function_exists('exif_imagetype')) { + $image_code = exif_imagetype($file); + unlink($file); + } elseif (function_exists('getimagesize')) { + set_error_handler(array($this, 'muteErrorHandler')); + $info = getimagesize($file); + restore_error_handler(); + unlink($file); + if ($info == false) { + return false; + } + $image_code = $info[2]; + } else { + trigger_error("could not find exif_imagetype or getimagesize functions", E_USER_ERROR); + } + $real_content_type = image_type_to_mime_type($image_code); + if ($real_content_type != $content_type) { + // we're nice guys; if the content type is something else we + // support, change it over + if (empty($this->allowed_types[$real_content_type])) { + return false; + } + $content_type = $real_content_type; + } + // ok, it's kosher, rewrite what we need + $uri->userinfo = null; + $uri->host = null; + $uri->port = null; + $uri->fragment = null; + $uri->query = null; + $uri->path = "$content_type;base64," . base64_encode($raw_data); + return true; + } + + /** + * @param int $errno + * @param string $errstr + */ + public function muteErrorHandler($errno, $errstr) + { + } +} diff --git a/helpers/htmlpurifier/HTMLPurifier/URIScheme/file.php b/helpers/htmlpurifier/HTMLPurifier/URIScheme/file.php new file mode 100644 index 00000000..215be4ba --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIScheme/file.php @@ -0,0 +1,44 @@ +userinfo = null; + // file:// makes no provisions for accessing the resource + $uri->port = null; + // While it seems to work on Firefox, the querystring has + // no possible effect and is thus stripped. + $uri->query = null; + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIScheme/ftp.php b/helpers/htmlpurifier/HTMLPurifier/URIScheme/ftp.php new file mode 100644 index 00000000..1eb43ee5 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIScheme/ftp.php @@ -0,0 +1,58 @@ +query = null; + + // typecode check + $semicolon_pos = strrpos($uri->path, ';'); // reverse + if ($semicolon_pos !== false) { + $type = substr($uri->path, $semicolon_pos + 1); // no semicolon + $uri->path = substr($uri->path, 0, $semicolon_pos); + $type_ret = ''; + if (strpos($type, '=') !== false) { + // figure out whether or not the declaration is correct + list($key, $typecode) = explode('=', $type, 2); + if ($key !== 'type') { + // invalid key, tack it back on encoded + $uri->path .= '%3B' . $type; + } elseif ($typecode === 'a' || $typecode === 'i' || $typecode === 'd') { + $type_ret = ";type=$typecode"; + } + } else { + $uri->path .= '%3B' . $type; + } + $uri->path = str_replace(';', '%3B', $uri->path); + $uri->path .= $type_ret; + } + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIScheme/http.php b/helpers/htmlpurifier/HTMLPurifier/URIScheme/http.php new file mode 100644 index 00000000..ce69ec43 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIScheme/http.php @@ -0,0 +1,36 @@ +userinfo = null; + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIScheme/https.php b/helpers/htmlpurifier/HTMLPurifier/URIScheme/https.php new file mode 100644 index 00000000..0e96882d --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIScheme/https.php @@ -0,0 +1,18 @@ +userinfo = null; + $uri->host = null; + $uri->port = null; + // we need to validate path against RFC 2368's addr-spec + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIScheme/news.php b/helpers/htmlpurifier/HTMLPurifier/URIScheme/news.php new file mode 100644 index 00000000..7490927d --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIScheme/news.php @@ -0,0 +1,35 @@ +userinfo = null; + $uri->host = null; + $uri->port = null; + $uri->query = null; + // typecode check needed on path + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIScheme/nntp.php b/helpers/htmlpurifier/HTMLPurifier/URIScheme/nntp.php new file mode 100644 index 00000000..f211d715 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIScheme/nntp.php @@ -0,0 +1,32 @@ +userinfo = null; + $uri->query = null; + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URIScheme/tel.php b/helpers/htmlpurifier/HTMLPurifier/URIScheme/tel.php new file mode 100644 index 00000000..8cd19335 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URIScheme/tel.php @@ -0,0 +1,46 @@ +userinfo = null; + $uri->host = null; + $uri->port = null; + + // Delete all non-numeric characters, non-x characters + // from phone number, EXCEPT for a leading plus sign. + $uri->path = preg_replace('/(?!^\+)[^\dx]/', '', + // Normalize e(x)tension to lower-case + str_replace('X', 'x', $uri->path)); + + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/URISchemeRegistry.php b/helpers/htmlpurifier/HTMLPurifier/URISchemeRegistry.php new file mode 100644 index 00000000..4ac8a0b7 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/URISchemeRegistry.php @@ -0,0 +1,81 @@ +get('URI.AllowedSchemes'); + if (!$config->get('URI.OverrideAllowedSchemes') && + !isset($allowed_schemes[$scheme]) + ) { + return; + } + + if (isset($this->schemes[$scheme])) { + return $this->schemes[$scheme]; + } + if (!isset($allowed_schemes[$scheme])) { + return; + } + + $class = 'HTMLPurifier_URIScheme_' . $scheme; + if (!class_exists($class)) { + return; + } + $this->schemes[$scheme] = new $class(); + return $this->schemes[$scheme]; + } + + /** + * Registers a custom scheme to the cache, bypassing reflection. + * @param string $scheme Scheme name + * @param HTMLPurifier_URIScheme $scheme_obj + */ + public function register($scheme, $scheme_obj) + { + $this->schemes[$scheme] = $scheme_obj; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/UnitConverter.php b/helpers/htmlpurifier/HTMLPurifier/UnitConverter.php new file mode 100644 index 00000000..166f3bf3 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/UnitConverter.php @@ -0,0 +1,307 @@ + array( + 'px' => 3, // This is as per CSS 2.1 and Firefox. Your mileage may vary + 'pt' => 4, + 'pc' => 48, + 'in' => 288, + self::METRIC => array('pt', '0.352777778', 'mm'), + ), + self::METRIC => array( + 'mm' => 1, + 'cm' => 10, + self::ENGLISH => array('mm', '2.83464567', 'pt'), + ), + ); + + /** + * Minimum bcmath precision for output. + * @type int + */ + protected $outputPrecision; + + /** + * Bcmath precision for internal calculations. + * @type int + */ + protected $internalPrecision; + + /** + * Whether or not BCMath is available. + * @type bool + */ + private $bcmath; + + public function __construct($output_precision = 4, $internal_precision = 10, $force_no_bcmath = false) + { + $this->outputPrecision = $output_precision; + $this->internalPrecision = $internal_precision; + $this->bcmath = !$force_no_bcmath && function_exists('bcmul'); + } + + /** + * Converts a length object of one unit into another unit. + * @param HTMLPurifier_Length $length + * Instance of HTMLPurifier_Length to convert. You must validate() + * it before passing it here! + * @param string $to_unit + * Unit to convert to. + * @return HTMLPurifier_Length|bool + * @note + * About precision: This conversion function pays very special + * attention to the incoming precision of values and attempts + * to maintain a number of significant figure. Results are + * fairly accurate up to nine digits. Some caveats: + * - If a number is zero-padded as a result of this significant + * figure tracking, the zeroes will be eliminated. + * - If a number contains less than four sigfigs ($outputPrecision) + * and this causes some decimals to be excluded, those + * decimals will be added on. + */ + public function convert($length, $to_unit) + { + if (!$length->isValid()) { + return false; + } + + $n = $length->getN(); + $unit = $length->getUnit(); + + if ($n === '0' || $unit === false) { + return new HTMLPurifier_Length('0', false); + } + + $state = $dest_state = false; + foreach (self::$units as $k => $x) { + if (isset($x[$unit])) { + $state = $k; + } + if (isset($x[$to_unit])) { + $dest_state = $k; + } + } + if (!$state || !$dest_state) { + return false; + } + + // Some calculations about the initial precision of the number; + // this will be useful when we need to do final rounding. + $sigfigs = $this->getSigFigs($n); + if ($sigfigs < $this->outputPrecision) { + $sigfigs = $this->outputPrecision; + } + + // BCMath's internal precision deals only with decimals. Use + // our default if the initial number has no decimals, or increase + // it by how ever many decimals, thus, the number of guard digits + // will always be greater than or equal to internalPrecision. + $log = (int)floor(log(abs($n), 10)); + $cp = ($log < 0) ? $this->internalPrecision - $log : $this->internalPrecision; // internal precision + + for ($i = 0; $i < 2; $i++) { + + // Determine what unit IN THIS SYSTEM we need to convert to + if ($dest_state === $state) { + // Simple conversion + $dest_unit = $to_unit; + } else { + // Convert to the smallest unit, pending a system shift + $dest_unit = self::$units[$state][$dest_state][0]; + } + + // Do the conversion if necessary + if ($dest_unit !== $unit) { + $factor = $this->div(self::$units[$state][$unit], self::$units[$state][$dest_unit], $cp); + $n = $this->mul($n, $factor, $cp); + $unit = $dest_unit; + } + + // Output was zero, so bail out early. Shouldn't ever happen. + if ($n === '') { + $n = '0'; + $unit = $to_unit; + break; + } + + // It was a simple conversion, so bail out + if ($dest_state === $state) { + break; + } + + if ($i !== 0) { + // Conversion failed! Apparently, the system we forwarded + // to didn't have this unit. This should never happen! + return false; + } + + // Pre-condition: $i == 0 + + // Perform conversion to next system of units + $n = $this->mul($n, self::$units[$state][$dest_state][1], $cp); + $unit = self::$units[$state][$dest_state][2]; + $state = $dest_state; + + // One more loop around to convert the unit in the new system. + + } + + // Post-condition: $unit == $to_unit + if ($unit !== $to_unit) { + return false; + } + + // Useful for debugging: + //echo "
      n";
      +        //echo "$n\nsigfigs = $sigfigs\nnew_log = $new_log\nlog = $log\nrp = $rp\n
      \n"; + + $n = $this->round($n, $sigfigs); + if (strpos($n, '.') !== false) { + $n = rtrim($n, '0'); + } + $n = rtrim($n, '.'); + + return new HTMLPurifier_Length($n, $unit); + } + + /** + * Returns the number of significant figures in a string number. + * @param string $n Decimal number + * @return int number of sigfigs + */ + public function getSigFigs($n) + { + $n = ltrim($n, '0+-'); + $dp = strpos($n, '.'); // decimal position + if ($dp === false) { + $sigfigs = strlen(rtrim($n, '0')); + } else { + $sigfigs = strlen(ltrim($n, '0.')); // eliminate extra decimal character + if ($dp !== 0) { + $sigfigs--; + } + } + return $sigfigs; + } + + /** + * Adds two numbers, using arbitrary precision when available. + * @param string $s1 + * @param string $s2 + * @param int $scale + * @return string + */ + private function add($s1, $s2, $scale) + { + if ($this->bcmath) { + return bcadd($s1, $s2, $scale); + } else { + return $this->scale((float)$s1 + (float)$s2, $scale); + } + } + + /** + * Multiples two numbers, using arbitrary precision when available. + * @param string $s1 + * @param string $s2 + * @param int $scale + * @return string + */ + private function mul($s1, $s2, $scale) + { + if ($this->bcmath) { + return bcmul($s1, $s2, $scale); + } else { + return $this->scale((float)$s1 * (float)$s2, $scale); + } + } + + /** + * Divides two numbers, using arbitrary precision when available. + * @param string $s1 + * @param string $s2 + * @param int $scale + * @return string + */ + private function div($s1, $s2, $scale) + { + if ($this->bcmath) { + return bcdiv($s1, $s2, $scale); + } else { + return $this->scale((float)$s1 / (float)$s2, $scale); + } + } + + /** + * Rounds a number according to the number of sigfigs it should have, + * using arbitrary precision when available. + * @param float $n + * @param int $sigfigs + * @return string + */ + private function round($n, $sigfigs) + { + $new_log = (int)floor(log(abs($n), 10)); // Number of digits left of decimal - 1 + $rp = $sigfigs - $new_log - 1; // Number of decimal places needed + $neg = $n < 0 ? '-' : ''; // Negative sign + if ($this->bcmath) { + if ($rp >= 0) { + $n = bcadd($n, $neg . '0.' . str_repeat('0', $rp) . '5', $rp + 1); + $n = bcdiv($n, '1', $rp); + } else { + // This algorithm partially depends on the standardized + // form of numbers that comes out of bcmath. + $n = bcadd($n, $neg . '5' . str_repeat('0', $new_log - $sigfigs), 0); + $n = substr($n, 0, $sigfigs + strlen($neg)) . str_repeat('0', $new_log - $sigfigs + 1); + } + return $n; + } else { + return $this->scale(round($n, $sigfigs - $new_log - 1), $rp + 1); + } + } + + /** + * Scales a float to $scale digits right of decimal point, like BCMath. + * @param float $r + * @param int $scale + * @return string + */ + private function scale($r, $scale) + { + if ($scale < 0) { + // The f sprintf type doesn't support negative numbers, so we + // need to cludge things manually. First get the string. + $r = sprintf('%.0f', (float)$r); + // Due to floating point precision loss, $r will more than likely + // look something like 4652999999999.9234. We grab one more digit + // than we need to precise from $r and then use that to round + // appropriately. + $precise = (string)round(substr($r, 0, strlen($r) + $scale), -1); + // Now we return it, truncating the zero that was rounded off. + return substr($precise, 0, -1) . str_repeat('0', -$scale + 1); + } + return sprintf('%.' . $scale . 'f', (float)$r); + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/VarParser.php b/helpers/htmlpurifier/HTMLPurifier/VarParser.php new file mode 100644 index 00000000..50cba691 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/VarParser.php @@ -0,0 +1,198 @@ + self::STRING, + 'istring' => self::ISTRING, + 'text' => self::TEXT, + 'itext' => self::ITEXT, + 'int' => self::INT, + 'float' => self::FLOAT, + 'bool' => self::BOOL, + 'lookup' => self::LOOKUP, + 'list' => self::ALIST, + 'hash' => self::HASH, + 'mixed' => self::MIXED + ); + + /** + * Lookup table of types that are string, and can have aliases or + * allowed value lists. + */ + public static $stringTypes = array( + self::STRING => true, + self::ISTRING => true, + self::TEXT => true, + self::ITEXT => true, + ); + + /** + * Validate a variable according to type. + * It may return NULL as a valid type if $allow_null is true. + * + * @param mixed $var Variable to validate + * @param int $type Type of variable, see HTMLPurifier_VarParser->types + * @param bool $allow_null Whether or not to permit null as a value + * @return string Validated and type-coerced variable + * @throws HTMLPurifier_VarParserException + */ + final public function parse($var, $type, $allow_null = false) + { + if (is_string($type)) { + if (!isset(HTMLPurifier_VarParser::$types[$type])) { + throw new HTMLPurifier_VarParserException("Invalid type '$type'"); + } else { + $type = HTMLPurifier_VarParser::$types[$type]; + } + } + $var = $this->parseImplementation($var, $type, $allow_null); + if ($allow_null && $var === null) { + return null; + } + // These are basic checks, to make sure nothing horribly wrong + // happened in our implementations. + switch ($type) { + case (self::STRING): + case (self::ISTRING): + case (self::TEXT): + case (self::ITEXT): + if (!is_string($var)) { + break; + } + if ($type == self::ISTRING || $type == self::ITEXT) { + $var = strtolower($var); + } + return $var; + case (self::INT): + if (!is_int($var)) { + break; + } + return $var; + case (self::FLOAT): + if (!is_float($var)) { + break; + } + return $var; + case (self::BOOL): + if (!is_bool($var)) { + break; + } + return $var; + case (self::LOOKUP): + case (self::ALIST): + case (self::HASH): + if (!is_array($var)) { + break; + } + if ($type === self::LOOKUP) { + foreach ($var as $k) { + if ($k !== true) { + $this->error('Lookup table contains value other than true'); + } + } + } elseif ($type === self::ALIST) { + $keys = array_keys($var); + if (array_keys($keys) !== $keys) { + $this->error('Indices for list are not uniform'); + } + } + return $var; + case (self::MIXED): + return $var; + default: + $this->errorInconsistent(get_class($this), $type); + } + $this->errorGeneric($var, $type); + } + + /** + * Actually implements the parsing. Base implementation does not + * do anything to $var. Subclasses should overload this! + * @param mixed $var + * @param int $type + * @param bool $allow_null + * @return string + */ + protected function parseImplementation($var, $type, $allow_null) + { + return $var; + } + + /** + * Throws an exception. + * @throws HTMLPurifier_VarParserException + */ + protected function error($msg) + { + throw new HTMLPurifier_VarParserException($msg); + } + + /** + * Throws an inconsistency exception. + * @note This should not ever be called. It would be called if we + * extend the allowed values of HTMLPurifier_VarParser without + * updating subclasses. + * @param string $class + * @param int $type + * @throws HTMLPurifier_Exception + */ + protected function errorInconsistent($class, $type) + { + throw new HTMLPurifier_Exception( + "Inconsistency in $class: " . HTMLPurifier_VarParser::getTypeName($type) . + " not implemented" + ); + } + + /** + * Generic error for if a type didn't work. + * @param mixed $var + * @param int $type + */ + protected function errorGeneric($var, $type) + { + $vtype = gettype($var); + $this->error("Expected type " . HTMLPurifier_VarParser::getTypeName($type) . ", got $vtype"); + } + + /** + * @param int $type + * @return string + */ + public static function getTypeName($type) + { + static $lookup; + if (!$lookup) { + // Lazy load the alternative lookup table + $lookup = array_flip(HTMLPurifier_VarParser::$types); + } + if (!isset($lookup[$type])) { + return 'unknown'; + } + return $lookup[$type]; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/VarParser/Flexible.php b/helpers/htmlpurifier/HTMLPurifier/VarParser/Flexible.php new file mode 100644 index 00000000..b15016c5 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/VarParser/Flexible.php @@ -0,0 +1,130 @@ + $j) { + $var[$i] = trim($j); + } + if ($type === self::HASH) { + // key:value,key2:value2 + $nvar = array(); + foreach ($var as $keypair) { + $c = explode(':', $keypair, 2); + if (!isset($c[1])) { + continue; + } + $nvar[trim($c[0])] = trim($c[1]); + } + $var = $nvar; + } + } + if (!is_array($var)) { + break; + } + $keys = array_keys($var); + if ($keys === array_keys($keys)) { + if ($type == self::ALIST) { + return $var; + } elseif ($type == self::LOOKUP) { + $new = array(); + foreach ($var as $key) { + $new[$key] = true; + } + return $new; + } else { + break; + } + } + if ($type === self::ALIST) { + trigger_error("Array list did not have consecutive integer indexes", E_USER_WARNING); + return array_values($var); + } + if ($type === self::LOOKUP) { + foreach ($var as $key => $value) { + if ($value !== true) { + trigger_error( + "Lookup array has non-true value at key '$key'; " . + "maybe your input array was not indexed numerically", + E_USER_WARNING + ); + } + $var[$key] = true; + } + } + return $var; + default: + $this->errorInconsistent(__CLASS__, $type); + } + $this->errorGeneric($var, $type); + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/VarParser/Native.php b/helpers/htmlpurifier/HTMLPurifier/VarParser/Native.php new file mode 100644 index 00000000..f11c318e --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/VarParser/Native.php @@ -0,0 +1,38 @@ +evalExpression($var); + } + + /** + * @param string $expr + * @return mixed + * @throws HTMLPurifier_VarParserException + */ + protected function evalExpression($expr) + { + $var = null; + $result = eval("\$var = $expr;"); + if ($result === false) { + throw new HTMLPurifier_VarParserException("Fatal error in evaluated code"); + } + return $var; + } +} + +// vim: et sw=4 sts=4 diff --git a/helpers/htmlpurifier/HTMLPurifier/VarParserException.php b/helpers/htmlpurifier/HTMLPurifier/VarParserException.php new file mode 100644 index 00000000..5df34149 --- /dev/null +++ b/helpers/htmlpurifier/HTMLPurifier/VarParserException.php @@ -0,0 +1,11 @@ +front = $front; + $this->back = $back; + } + + /** + * Creates a zipper from an array, with a hole in the + * 0-index position. + * @param Array to zipper-ify. + * @return Tuple of zipper and element of first position. + */ + static public function fromArray($array) { + $z = new self(array(), array_reverse($array)); + $t = $z->delete(); // delete the "dummy hole" + return array($z, $t); + } + + /** + * Convert zipper back into a normal array, optionally filling in + * the hole with a value. (Usually you should supply a $t, unless you + * are at the end of the array.) + */ + public function toArray($t = NULL) { + $a = $this->front; + if ($t !== NULL) $a[] = $t; + for ($i = count($this->back)-1; $i >= 0; $i--) { + $a[] = $this->back[$i]; + } + return $a; + } + + /** + * Move hole to the next element. + * @param $t Element to fill hole with + * @return Original contents of new hole. + */ + public function next($t) { + if ($t !== NULL) array_push($this->front, $t); + return empty($this->back) ? NULL : array_pop($this->back); + } + + /** + * Iterated hole advancement. + * @param $t Element to fill hole with + * @param $i How many forward to advance hole + * @return Original contents of new hole, i away + */ + public function advance($t, $n) { + for ($i = 0; $i < $n; $i++) { + $t = $this->next($t); + } + return $t; + } + + /** + * Move hole to the previous element + * @param $t Element to fill hole with + * @return Original contents of new hole. + */ + public function prev($t) { + if ($t !== NULL) array_push($this->back, $t); + return empty($this->front) ? NULL : array_pop($this->front); + } + + /** + * Delete contents of current hole, shifting hole to + * next element. + * @return Original contents of new hole. + */ + public function delete() { + return empty($this->back) ? NULL : array_pop($this->back); + } + + /** + * Returns true if we are at the end of the list. + * @return bool + */ + public function done() { + return empty($this->back); + } + + /** + * Insert element before hole. + * @param Element to insert + */ + public function insertBefore($t) { + if ($t !== NULL) array_push($this->front, $t); + } + + /** + * Insert element after hole. + * @param Element to insert + */ + public function insertAfter($t) { + if ($t !== NULL) array_push($this->back, $t); + } + + /** + * Splice in multiple elements at hole. Functional specification + * in terms of array_splice: + * + * $arr1 = $arr; + * $old1 = array_splice($arr1, $i, $delete, $replacement); + * + * list($z, $t) = HTMLPurifier_Zipper::fromArray($arr); + * $t = $z->advance($t, $i); + * list($old2, $t) = $z->splice($t, $delete, $replacement); + * $arr2 = $z->toArray($t); + * + * assert($old1 === $old2); + * assert($arr1 === $arr2); + * + * NB: the absolute index location after this operation is + * *unchanged!* + * + * @param Current contents of hole. + */ + public function splice($t, $delete, $replacement) { + // delete + $old = array(); + $r = $t; + for ($i = $delete; $i > 0; $i--) { + $old[] = $r; + $r = $this->delete(); + } + // insert + for ($i = count($replacement)-1; $i >= 0; $i--) { + $this->insertAfter($r); + $r = $replacement[$i]; + } + return array($old, $r); + } +} diff --git a/helpers/htmlpurifier/helper.json b/helpers/htmlpurifier/helper.json new file mode 100644 index 00000000..7951089e --- /dev/null +++ b/helpers/htmlpurifier/helper.json @@ -0,0 +1,5 @@ +{ + "file": "HTMLPurifier.includes.php", + "class": "HTMLPurifier", + "params": [] +} diff --git a/system/WCore/WRequest.php b/system/WCore/WRequest.php index 2007610d..bbc2df4f 100644 --- a/system/WCore/WRequest.php +++ b/system/WCore/WRequest.php @@ -234,27 +234,8 @@ public static function filter($variable) { $variable[$key] = self::filter($val); } } else { - // Prevent XSS abuse - $variable = preg_replace_callback('##i', function($matches) { - $tag = strtolower($matches[1]); - - // Allowed tags - if (in_array($tag, array( - 'b', 'strong', 'small', 'i', 'em', 'u', 's', 'sub', 'sup', 'a', 'button', 'img', 'br', - 'font', 'span', 'blockquote', 'q', 'abbr', 'address', 'code', 'hr', - 'audio', 'video', 'source', 'iframe', - 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'ul', 'ol', 'li', 'dl', 'dt', 'dd', - 'div', 'p', 'var', - 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'colgroup', 'col', - 'section', 'article', 'aside'))) { - return $matches[0]; - } else if (in_array($tag, array('script', 'link'))) { - return ''; - } else { - return htmlentities($matches[0]); - } - }, $variable); + $htmlPurifier = WHelper::load('htmlpurifier'); + $variable = $htmlPurifier->purify($variable); } return $variable; From 7c3b69733a47ec3007f988adc5169c5a36356197 Mon Sep 17 00:00:00 2001 From: Johan Dufau Date: Mon, 13 Nov 2017 16:30:47 +0100 Subject: [PATCH 07/10] Updates submodule WTemplate --- system/WTemplate | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/WTemplate b/system/WTemplate index 7f223570..3b008d94 160000 --- a/system/WTemplate +++ b/system/WTemplate @@ -1 +1 @@ -Subproject commit 7f22357014140642195474ffc6103160b66c59da +Subproject commit 3b008d94f33618a2e7110f63dc1d8423b0a362f3 From 1d4d6443648256a055e4e1a51cbbe41973cb616c Mon Sep 17 00:00:00 2001 From: Johan Dufau Date: Mon, 4 Jun 2018 09:44:07 +0200 Subject: [PATCH 08/10] Upgrades WTemplate to v1.0.0 --- system/WTemplate | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/WTemplate b/system/WTemplate index 3b008d94..c4e703ff 160000 --- a/system/WTemplate +++ b/system/WTemplate @@ -1 +1 @@ -Subproject commit 3b008d94f33618a2e7110f63dc1d8423b0a362f3 +Subproject commit c4e703ff27a72d2777d8bb1698d6a7b36ec68f2f From 45dad2a0fc3bfad997bc5498933b424d053c901c Mon Sep 17 00:00:00 2001 From: Johan Dufau Date: Mon, 4 Jun 2018 09:44:22 +0200 Subject: [PATCH 09/10] Updates LICENSE file --- LICENSE.md | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 4723dfe6..4123e08b 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,22 +1,9 @@ -## License +MIT License -The MIT License (MIT) +Copyright (c) 2018 Creatiwity -Copyright (c) 2013 - Julien Blatecky and Johan Dufau +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: -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 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. +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. From 4da60d609d21b83a4534b74cf6b4253e246a1787 Mon Sep 17 00:00:00 2001 From: Johan Dufau Date: Mon, 4 Jun 2018 09:48:48 +0200 Subject: [PATCH 10/10] Upgrades to v0.6.2 --- README.md | 2 +- apps/contact/admin/main.php | 2 +- apps/contact/admin/model.php | 2 +- apps/contact/admin/view.php | 2 +- apps/contact/front/main.php | 2 +- apps/contact/front/model.php | 2 +- apps/contact/front/view.php | 2 +- apps/contact/js/main.js | 2 +- apps/contact/manifest.php | 2 +- apps/lang/front/main.php | 2 +- apps/lang/manifest.php | 2 +- apps/mail/front/main.php | 2 +- apps/mail/front/model.php | 2 +- apps/mail/manifest.php | 2 +- apps/news/admin/main.php | 2 +- apps/news/admin/model.php | 2 +- apps/news/admin/view.php | 2 +- apps/news/front/main.php | 2 +- apps/news/front/model.php | 2 +- apps/news/front/view.php | 2 +- apps/news/manifest.php | 2 +- apps/newsletter/admin/main.php | 2 +- apps/newsletter/admin/model.php | 2 +- apps/newsletter/admin/view.php | 2 +- apps/newsletter/front/main.php | 2 +- apps/newsletter/front/model.php | 2 +- apps/newsletter/front/view.php | 2 +- apps/newsletter/manifest.php | 2 +- apps/page/admin/main.php | 2 +- apps/page/admin/model.php | 2 +- apps/page/admin/view.php | 2 +- apps/page/front/main.php | 2 +- apps/page/front/model.php | 2 +- apps/page/front/view.php | 2 +- apps/page/manifest.php | 2 +- apps/search/front/main.php | 2 +- apps/search/front/view.php | 2 +- apps/search/manifest.php | 2 +- apps/settings/admin/main.php | 2 +- apps/settings/admin/model.php | 2 +- apps/settings/admin/view.php | 2 +- apps/settings/manifest.php | 2 +- apps/slideshow/admin/main.php | 2 +- apps/slideshow/admin/model.php | 2 +- apps/slideshow/admin/view.php | 2 +- apps/slideshow/front/main.php | 2 +- apps/slideshow/front/model.php | 2 +- apps/slideshow/front/view.php | 2 +- apps/slideshow/manifest.php | 2 +- apps/team/admin/main.php | 2 +- apps/team/admin/model.php | 2 +- apps/team/admin/view.php | 2 +- apps/team/front/main.php | 2 +- apps/team/front/model.php | 2 +- apps/team/front/view.php | 2 +- apps/team/manifest.php | 2 +- apps/user/admin/main.php | 2 +- apps/user/admin/model.php | 2 +- apps/user/admin/view.php | 2 +- apps/user/front/main.php | 2 +- apps/user/front/model.php | 2 +- apps/user/front/view.php | 2 +- apps/user/manifest.php | 2 +- index.php | 4 ++-- installer/bdd/witycms.sql | 2 +- installer/installer.php | 2 +- installer/js/form.js | 2 +- installer/js/installer.js | 2 +- installer/view.php | 2 +- paths.php | 2 +- system/WCore/WConfig.php | 2 +- system/WCore/WController.php | 2 +- system/WCore/WDatabase.php | 2 +- system/WCore/WDate.php | 2 +- system/WCore/WExport.php | 2 +- system/WCore/WHelper.php | 2 +- system/WCore/WLang.php | 2 +- system/WCore/WMain.php | 2 +- system/WCore/WNote.php | 2 +- system/WCore/WRequest.php | 2 +- system/WCore/WResponse.php | 2 +- system/WCore/WRetriever.php | 2 +- system/WCore/WRoute.php | 2 +- system/WCore/WSession.php | 2 +- system/WCore/WSystem.php | 2 +- system/WCore/WTools.php | 2 +- system/WCore/WView.php | 2 +- themes/admin-bootstrap/templates/index.html | 4 ++-- 88 files changed, 90 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index d68789fa..7bf8ef3b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This CMS uses its own templating system, named [WTemplate](https://github.com/Cr * An **Apache server** with PHP 5.3+ or PHP7, *mod_rewrite* enabled and .htaccess files allowed; * A **SQL server**, like *MySQL* or *MariaDB*, with a database available; * A **FTP client**, like [FileZilla](https://filezilla-project.org/), to upload the files; -* Download the latest version of **wityCMS**: [zip](https://github.com/Creatiwity/wityCMS/releases/download/0.6.1/wityCMS-0.6.1.zip). +* Download the latest version of **wityCMS**: [zip](https://github.com/Creatiwity/wityCMS/releases/download/0.6.2/wityCMS-0.6.2.zip). ### Let's go diff --git a/apps/contact/admin/main.php b/apps/contact/admin/main.php index 03cb8baa..0770b97f 100644 --- a/apps/contact/admin/main.php +++ b/apps/contact/admin/main.php @@ -11,7 +11,7 @@ * @package Apps\Contact\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class ContactAdminController extends WController { diff --git a/apps/contact/admin/model.php b/apps/contact/admin/model.php index cb7c6a1a..cece267a 100644 --- a/apps/contact/admin/model.php +++ b/apps/contact/admin/model.php @@ -14,7 +14,7 @@ * @package Apps\Contact\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class ContactAdminModel extends ContactModel { diff --git a/apps/contact/admin/view.php b/apps/contact/admin/view.php index b71ea24d..973a46e4 100644 --- a/apps/contact/admin/view.php +++ b/apps/contact/admin/view.php @@ -11,7 +11,7 @@ * @package Apps\Contact\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class ContactAdminView extends WView { diff --git a/apps/contact/front/main.php b/apps/contact/front/main.php index c76b27ed..782c6c28 100644 --- a/apps/contact/front/main.php +++ b/apps/contact/front/main.php @@ -11,7 +11,7 @@ * @package Apps\Contact\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class ContactController extends WController { diff --git a/apps/contact/front/model.php b/apps/contact/front/model.php index 82ce3e3b..9996b156 100644 --- a/apps/contact/front/model.php +++ b/apps/contact/front/model.php @@ -11,7 +11,7 @@ * @package Apps\Contact\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class ContactModel { diff --git a/apps/contact/front/view.php b/apps/contact/front/view.php index 07914fca..21abfc4c 100644 --- a/apps/contact/front/view.php +++ b/apps/contact/front/view.php @@ -11,7 +11,7 @@ * @package Apps\Contact\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class ContactView extends WView { diff --git a/apps/contact/js/main.js b/apps/contact/js/main.js index 9c08d97f..a2143860 100644 --- a/apps/contact/js/main.js +++ b/apps/contact/js/main.js @@ -2,7 +2,7 @@ * Contacts management script allowing ajax usage. * * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ require(['jquery'], function($) { $(document).ready(function() { diff --git a/apps/contact/manifest.php b/apps/contact/manifest.php index 06fcac47..5332f8bf 100644 --- a/apps/contact/manifest.php +++ b/apps/contact/manifest.php @@ -4,7 +4,7 @@ Contact - 0.6.1 + 0.6.2 02-10-2013 diff --git a/apps/lang/front/main.php b/apps/lang/front/main.php index 8bec90ca..6e7c08f6 100644 --- a/apps/lang/front/main.php +++ b/apps/lang/front/main.php @@ -11,7 +11,7 @@ * @package Apps\Lang\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class LangController extends WController { protected function select($params) { diff --git a/apps/lang/manifest.php b/apps/lang/manifest.php index c6474809..5827487f 100644 --- a/apps/lang/manifest.php +++ b/apps/lang/manifest.php @@ -4,7 +4,7 @@ Lang - 0.6.1 + 0.6.2 14-06-2015 diff --git a/apps/mail/front/main.php b/apps/mail/front/main.php index 69107c66..24f2b230 100644 --- a/apps/mail/front/main.php +++ b/apps/mail/front/main.php @@ -11,7 +11,7 @@ * @package Apps\Mail\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class MailController extends WController { diff --git a/apps/mail/front/model.php b/apps/mail/front/model.php index cf9f17dd..e2e72aaa 100644 --- a/apps/mail/front/model.php +++ b/apps/mail/front/model.php @@ -11,7 +11,7 @@ * @package Apps\Mail\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class MailModel { /** diff --git a/apps/mail/manifest.php b/apps/mail/manifest.php index 6fe090e8..3da1abb2 100644 --- a/apps/mail/manifest.php +++ b/apps/mail/manifest.php @@ -4,7 +4,7 @@ Mail - 0.6.1 + 0.6.2 25-11-2013 diff --git a/apps/news/admin/main.php b/apps/news/admin/main.php index 0e1308b8..cab75497 100644 --- a/apps/news/admin/main.php +++ b/apps/news/admin/main.php @@ -11,7 +11,7 @@ * @package Apps\News\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class NewsAdminController extends WController { private $upload_dir; diff --git a/apps/news/admin/model.php b/apps/news/admin/model.php index df0b136f..f9c3c628 100644 --- a/apps/news/admin/model.php +++ b/apps/news/admin/model.php @@ -16,7 +16,7 @@ * @package Apps\News\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class NewsAdminModel extends NewsModel { public function __construct() { diff --git a/apps/news/admin/view.php b/apps/news/admin/view.php index 7248d205..fc77c9e0 100644 --- a/apps/news/admin/view.php +++ b/apps/news/admin/view.php @@ -11,7 +11,7 @@ * @package Apps\News\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class NewsAdminView extends WView { public function __construct() { diff --git a/apps/news/front/main.php b/apps/news/front/main.php index eb99af36..bf35f378 100644 --- a/apps/news/front/main.php +++ b/apps/news/front/main.php @@ -11,7 +11,7 @@ * @package Apps\News\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class NewsController extends WController { protected function listing($params) { diff --git a/apps/news/front/model.php b/apps/news/front/model.php index f8a0d45d..2ad0617c 100644 --- a/apps/news/front/model.php +++ b/apps/news/front/model.php @@ -11,7 +11,7 @@ * @package Apps\News\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class NewsModel { /** diff --git a/apps/news/front/view.php b/apps/news/front/view.php index 7cbeee07..96595481 100644 --- a/apps/news/front/view.php +++ b/apps/news/front/view.php @@ -11,7 +11,7 @@ * @package Apps\News\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class NewsView extends WView { public function listing($data) { diff --git a/apps/news/manifest.php b/apps/news/manifest.php index 7ce8db29..803043ef 100644 --- a/apps/news/manifest.php +++ b/apps/news/manifest.php @@ -4,7 +4,7 @@ News - 0.6.1 + 0.6.2 02-05-2015 diff --git a/apps/newsletter/admin/main.php b/apps/newsletter/admin/main.php index 1e423faa..b7f9536a 100644 --- a/apps/newsletter/admin/main.php +++ b/apps/newsletter/admin/main.php @@ -10,7 +10,7 @@ * * @package Apps\Newsletter\Admin * @author Thibault Vlacich - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class NewsletterAdminController extends WController { /** diff --git a/apps/newsletter/admin/model.php b/apps/newsletter/admin/model.php index 98b28c44..97814c39 100644 --- a/apps/newsletter/admin/model.php +++ b/apps/newsletter/admin/model.php @@ -10,7 +10,7 @@ * * @package Apps\Newsletter\Admin * @author Thibault Vlacich - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class NewsletterAdminModel { /** diff --git a/apps/newsletter/admin/view.php b/apps/newsletter/admin/view.php index 3eafdbc0..fd1685c2 100644 --- a/apps/newsletter/admin/view.php +++ b/apps/newsletter/admin/view.php @@ -11,7 +11,7 @@ * @package Apps\Newsletter\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class NewsletterAdminView extends WView { public function listing($model) { diff --git a/apps/newsletter/front/main.php b/apps/newsletter/front/main.php index 87ac6a03..c9a9243f 100644 --- a/apps/newsletter/front/main.php +++ b/apps/newsletter/front/main.php @@ -10,7 +10,7 @@ * * @package Apps\Newsletter\Front * @author Thibault Vlacich - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class NewsletterController extends WController { public function add(array $params) { diff --git a/apps/newsletter/front/model.php b/apps/newsletter/front/model.php index 4d4f3fc5..c04c5b61 100644 --- a/apps/newsletter/front/model.php +++ b/apps/newsletter/front/model.php @@ -10,7 +10,7 @@ * * @package Apps\Newsletter\Front * @author Thibault Vlacich - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class NewsletterModel { /** diff --git a/apps/newsletter/front/view.php b/apps/newsletter/front/view.php index bb9fafcb..b7041f26 100644 --- a/apps/newsletter/front/view.php +++ b/apps/newsletter/front/view.php @@ -10,7 +10,7 @@ * * @package Apps\Newsletter\Front * @author Thibault Vlacich - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class NewsletterView extends WView { public function add($model) { diff --git a/apps/newsletter/manifest.php b/apps/newsletter/manifest.php index 84f29dae..2eaaf72a 100644 --- a/apps/newsletter/manifest.php +++ b/apps/newsletter/manifest.php @@ -4,7 +4,7 @@ Newsletter - 0.6.1 + 0.6.2 01-03-2015 diff --git a/apps/page/admin/main.php b/apps/page/admin/main.php index 0b349a47..d24bfc95 100644 --- a/apps/page/admin/main.php +++ b/apps/page/admin/main.php @@ -11,7 +11,7 @@ * @package Apps\Page\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class PageAdminController extends WController { private $upload_dir; diff --git a/apps/page/admin/model.php b/apps/page/admin/model.php index 1d520ed5..5a10db89 100644 --- a/apps/page/admin/model.php +++ b/apps/page/admin/model.php @@ -16,7 +16,7 @@ * @package Apps\Page\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class PageAdminModel extends PageModel { public function __construct() { diff --git a/apps/page/admin/view.php b/apps/page/admin/view.php index a3ed3877..419baee0 100644 --- a/apps/page/admin/view.php +++ b/apps/page/admin/view.php @@ -11,7 +11,7 @@ * @package Apps\Page\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class PageAdminView extends WView { public function pages($model) { diff --git a/apps/page/front/main.php b/apps/page/front/main.php index bee70c69..645ba629 100644 --- a/apps/page/front/main.php +++ b/apps/page/front/main.php @@ -11,7 +11,7 @@ * @package Apps\Page\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class PageController extends WController { protected function display(array $params) { diff --git a/apps/page/front/model.php b/apps/page/front/model.php index b12ec998..23e7f055 100644 --- a/apps/page/front/model.php +++ b/apps/page/front/model.php @@ -11,7 +11,7 @@ * @package Apps\Page\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class PageModel { /** diff --git a/apps/page/front/view.php b/apps/page/front/view.php index 995b67cb..0a22496d 100644 --- a/apps/page/front/view.php +++ b/apps/page/front/view.php @@ -11,7 +11,7 @@ * @package Apps\Page\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class PageView extends WView { public function display(array $model) { diff --git a/apps/page/manifest.php b/apps/page/manifest.php index e94c368c..62b9faca 100644 --- a/apps/page/manifest.php +++ b/apps/page/manifest.php @@ -4,7 +4,7 @@ Page - 0.6.1 + 0.6.2 23-03-2015 diff --git a/apps/search/front/main.php b/apps/search/front/main.php index 505ac5e7..9ec6acf2 100644 --- a/apps/search/front/main.php +++ b/apps/search/front/main.php @@ -11,7 +11,7 @@ * @package Apps\Search\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class SearchController extends WController { protected function form(array $params) { diff --git a/apps/search/front/view.php b/apps/search/front/view.php index fb32b767..2e95cbb1 100644 --- a/apps/search/front/view.php +++ b/apps/search/front/view.php @@ -11,7 +11,7 @@ * @package Apps\Search\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class SearchView extends WView { public function form($data) { diff --git a/apps/search/manifest.php b/apps/search/manifest.php index dd45d16d..da33ecb9 100644 --- a/apps/search/manifest.php +++ b/apps/search/manifest.php @@ -4,7 +4,7 @@ Search - 0.6.1 + 0.6.2 23-03-2015 diff --git a/apps/settings/admin/main.php b/apps/settings/admin/main.php index 40e43ba6..3b3d6a42 100644 --- a/apps/settings/admin/main.php +++ b/apps/settings/admin/main.php @@ -11,7 +11,7 @@ * @package Apps\Settings\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class SettingsAdminController extends WController { private $upload_dir; diff --git a/apps/settings/admin/model.php b/apps/settings/admin/model.php index 3489e76d..90ea78a0 100644 --- a/apps/settings/admin/model.php +++ b/apps/settings/admin/model.php @@ -11,7 +11,7 @@ * @package Apps\Settings\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class SettingsAdminModel { /** diff --git a/apps/settings/admin/view.php b/apps/settings/admin/view.php index f95e4c01..a45ea619 100644 --- a/apps/settings/admin/view.php +++ b/apps/settings/admin/view.php @@ -11,7 +11,7 @@ * @package Apps\Settings\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class SettingsAdminView extends WView { /** diff --git a/apps/settings/manifest.php b/apps/settings/manifest.php index 57f50fab..44aea3be 100644 --- a/apps/settings/manifest.php +++ b/apps/settings/manifest.php @@ -4,7 +4,7 @@ Settings - 0.6.1 + 0.6.2 20-11-2016 diff --git a/apps/slideshow/admin/main.php b/apps/slideshow/admin/main.php index 81356bcc..2bad4a9f 100644 --- a/apps/slideshow/admin/main.php +++ b/apps/slideshow/admin/main.php @@ -11,7 +11,7 @@ * @package Apps\Slideshow\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class SlideshowAdminController extends WController { private $upload_dir; diff --git a/apps/slideshow/admin/model.php b/apps/slideshow/admin/model.php index e8712a26..0482c9cb 100644 --- a/apps/slideshow/admin/model.php +++ b/apps/slideshow/admin/model.php @@ -13,7 +13,7 @@ * @package Apps\Slideshow\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class SlideshowAdminModel extends SlideshowModel { public function getSlide($id_slide) { diff --git a/apps/slideshow/admin/view.php b/apps/slideshow/admin/view.php index f1d217d1..6a7a1483 100644 --- a/apps/slideshow/admin/view.php +++ b/apps/slideshow/admin/view.php @@ -11,7 +11,7 @@ * @package Apps\Slideshow\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class SlideshowAdminView extends WView { public function __construct() { diff --git a/apps/slideshow/front/main.php b/apps/slideshow/front/main.php index e90b34ef..b5556506 100644 --- a/apps/slideshow/front/main.php +++ b/apps/slideshow/front/main.php @@ -11,7 +11,7 @@ * @package Apps\Slideshow\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class SlideshowController extends WController { protected function block(array $params) { diff --git a/apps/slideshow/front/model.php b/apps/slideshow/front/model.php index ea2166e0..c2b1d04b 100644 --- a/apps/slideshow/front/model.php +++ b/apps/slideshow/front/model.php @@ -11,7 +11,7 @@ * @package Apps\Slideshow\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class SlideshowModel { /** diff --git a/apps/slideshow/front/view.php b/apps/slideshow/front/view.php index cb5bf676..2cdb14fe 100644 --- a/apps/slideshow/front/view.php +++ b/apps/slideshow/front/view.php @@ -11,7 +11,7 @@ * @package Apps\Slideshow\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class SlideshowView extends WView { public function block($model) { diff --git a/apps/slideshow/manifest.php b/apps/slideshow/manifest.php index 649f4a4c..6b3d2656 100644 --- a/apps/slideshow/manifest.php +++ b/apps/slideshow/manifest.php @@ -4,7 +4,7 @@ Slideshow - 0.6.1 + 0.6.2 07-02-2015 diff --git a/apps/team/admin/main.php b/apps/team/admin/main.php index ae485583..bb5693e9 100644 --- a/apps/team/admin/main.php +++ b/apps/team/admin/main.php @@ -11,7 +11,7 @@ * @package Apps\Team\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class TeamAdminController extends WController { private $upload_dir; diff --git a/apps/team/admin/model.php b/apps/team/admin/model.php index ef61da10..0d6ca057 100644 --- a/apps/team/admin/model.php +++ b/apps/team/admin/model.php @@ -16,7 +16,7 @@ * @package Apps\Team\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class TeamAdminModel extends TeamModel { public function __construct() { diff --git a/apps/team/admin/view.php b/apps/team/admin/view.php index 2ccd78f8..d6ae5ed5 100644 --- a/apps/team/admin/view.php +++ b/apps/team/admin/view.php @@ -11,7 +11,7 @@ * @package Apps\Team\Admin * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class TeamAdminView extends WView { public function members(array $model) { diff --git a/apps/team/front/main.php b/apps/team/front/main.php index fd192297..8d6f9554 100644 --- a/apps/team/front/main.php +++ b/apps/team/front/main.php @@ -11,7 +11,7 @@ * @package Apps\Team\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class TeamController extends WController { protected function members(array $params) { diff --git a/apps/team/front/model.php b/apps/team/front/model.php index c7f00d6f..2a9949af 100644 --- a/apps/team/front/model.php +++ b/apps/team/front/model.php @@ -11,7 +11,7 @@ * @package Apps\Team\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class TeamModel { /** diff --git a/apps/team/front/view.php b/apps/team/front/view.php index d93a3028..267865bd 100644 --- a/apps/team/front/view.php +++ b/apps/team/front/view.php @@ -11,7 +11,7 @@ * @package Apps\Team\Front * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class TeamView extends WView { public function members(array $model) { diff --git a/apps/team/manifest.php b/apps/team/manifest.php index 62c8be24..58aefbca 100644 --- a/apps/team/manifest.php +++ b/apps/team/manifest.php @@ -4,7 +4,7 @@ Team - 0.6.1 + 0.6.2 07-06-2015 diff --git a/apps/user/admin/main.php b/apps/user/admin/main.php index c2558cee..bb2715ae 100644 --- a/apps/user/admin/main.php +++ b/apps/user/admin/main.php @@ -10,7 +10,7 @@ * * @package Apps\User\Admin * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class UserAdminController extends WController { /** diff --git a/apps/user/admin/model.php b/apps/user/admin/model.php index c1d87e4c..c439eadf 100644 --- a/apps/user/admin/model.php +++ b/apps/user/admin/model.php @@ -15,7 +15,7 @@ * * @package Apps\User\Admin * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class UserAdminModel extends UserModel { public function __construct() { diff --git a/apps/user/admin/view.php b/apps/user/admin/view.php index 9e2069c5..6ee3e994 100644 --- a/apps/user/admin/view.php +++ b/apps/user/admin/view.php @@ -10,7 +10,7 @@ * * @package Apps\User\Admin * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class UserAdminView extends WView { public function __construct() { diff --git a/apps/user/front/main.php b/apps/user/front/main.php index d609135f..511b01e0 100644 --- a/apps/user/front/main.php +++ b/apps/user/front/main.php @@ -10,7 +10,7 @@ * * @package Apps\User\Front * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class UserController extends WController { /* diff --git a/apps/user/front/model.php b/apps/user/front/model.php index 11797108..2a109817 100644 --- a/apps/user/front/model.php +++ b/apps/user/front/model.php @@ -10,7 +10,7 @@ * * @package Apps\User\Front * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class UserModel { protected $db; diff --git a/apps/user/front/view.php b/apps/user/front/view.php index 67f9eb06..55ecee8b 100644 --- a/apps/user/front/view.php +++ b/apps/user/front/view.php @@ -10,7 +10,7 @@ * * @package Apps\User\Front * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class UserView extends WView { public function __construct() { diff --git a/apps/user/manifest.php b/apps/user/manifest.php index 734c73b6..59f45751 100644 --- a/apps/user/manifest.php +++ b/apps/user/manifest.php @@ -4,7 +4,7 @@ User - 0.6.1 + 0.6.2 09-05-2015 diff --git a/index.php b/index.php index 50abc75e..6754b19e 100644 --- a/index.php +++ b/index.php @@ -6,13 +6,13 @@ * * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ /** * wityCMS version number */ -define('WITYCMS_VERSION', '0.6.1'); +define('WITYCMS_VERSION', '0.6.2'); /** * Error reporting level = MAXIMUM diff --git a/installer/bdd/witycms.sql b/installer/bdd/witycms.sql index c29acab7..f086338a 100644 --- a/installer/bdd/witycms.sql +++ b/installer/bdd/witycms.sql @@ -1,5 +1,5 @@ -- wityCMS Database Dump --- Version: 0.6.1-03-09-2016 +-- Version: 0.6.2-04-06-2018 SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; SET time_zone = "+00:00"; diff --git a/installer/installer.php b/installer/installer.php index f0fbc3ca..2f5cf6f1 100644 --- a/installer/installer.php +++ b/installer/installer.php @@ -12,7 +12,7 @@ * * @package Installer * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class Installer { private $THEMES_DIR; diff --git a/installer/js/form.js b/installer/js/form.js index 16b79d20..5b8e6339 100644 --- a/installer/js/form.js +++ b/installer/js/form.js @@ -2,7 +2,7 @@ * Form generator * * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ var Form, FormNode; diff --git a/installer/js/installer.js b/installer/js/installer.js index 2fd00b24..adafbcc4 100644 --- a/installer/js/installer.js +++ b/installer/js/installer.js @@ -2,7 +2,7 @@ * Installer form description * * @package Installer - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ $(window).ready(function() { diff --git a/installer/view.php b/installer/view.php index 8aa3a64a..0e44a775 100644 --- a/installer/view.php +++ b/installer/view.php @@ -10,7 +10,7 @@ * * @package Installer * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class View { /** diff --git a/paths.php b/paths.php index 7105f02a..6413ea90 100644 --- a/paths.php +++ b/paths.php @@ -4,7 +4,7 @@ * * Content Management System for everyone. * - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ defined('WITYCMS_VERSION') or die('Access denied'); diff --git a/system/WCore/WConfig.php b/system/WCore/WConfig.php index 753bdf51..d4c11f73 100644 --- a/system/WCore/WConfig.php +++ b/system/WCore/WConfig.php @@ -11,7 +11,7 @@ * @package System\WCore * @author xpLosIve * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WConfig { /** diff --git a/system/WCore/WController.php b/system/WCore/WController.php index dd209b14..593b2154 100644 --- a/system/WCore/WController.php +++ b/system/WCore/WController.php @@ -11,7 +11,7 @@ * @package System\WCore * @author Johan Dufau * @author Julien Blatecky - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ abstract class WController { /** diff --git a/system/WCore/WDatabase.php b/system/WCore/WDatabase.php index 8c16908f..f12ad5f4 100644 --- a/system/WCore/WDatabase.php +++ b/system/WCore/WDatabase.php @@ -11,7 +11,7 @@ * @package System\WCore * @author Johan Dufau * @author Matthieu Raymond - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WDatabase extends PDO { /** diff --git a/system/WCore/WDate.php b/system/WCore/WDate.php index da5421ac..ff65ee5d 100644 --- a/system/WCore/WDate.php +++ b/system/WCore/WDate.php @@ -10,7 +10,7 @@ * * @package System\WCore * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WDate extends DateTime implements JsonSerializable { /** diff --git a/system/WCore/WExport.php b/system/WCore/WExport.php index eb860383..2243609b 100644 --- a/system/WCore/WExport.php +++ b/system/WCore/WExport.php @@ -11,7 +11,7 @@ * @package System\WCore * @author Johan Dufau * @author Thibault Vlacich - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WExport { public static function toCSVNamed($filename_prefix, array &$data) { diff --git a/system/WCore/WHelper.php b/system/WCore/WHelper.php index 37237773..14a66ab2 100644 --- a/system/WCore/WHelper.php +++ b/system/WCore/WHelper.php @@ -10,7 +10,7 @@ * * @package System\WCore * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WHelper { /** diff --git a/system/WCore/WLang.php b/system/WCore/WLang.php index 4d7daf6b..a14e5afa 100644 --- a/system/WCore/WLang.php +++ b/system/WCore/WLang.php @@ -15,7 +15,7 @@ * @package System\WCore * @author xpLosIve * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WLang { /** diff --git a/system/WCore/WMain.php b/system/WCore/WMain.php index 63ebc6c9..3512e139 100644 --- a/system/WCore/WMain.php +++ b/system/WCore/WMain.php @@ -13,7 +13,7 @@ * * @package System\WCore * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WMain { /** diff --git a/system/WCore/WNote.php b/system/WCore/WNote.php index f77babc2..4418da8d 100644 --- a/system/WCore/WNote.php +++ b/system/WCore/WNote.php @@ -10,7 +10,7 @@ * * @package System\WCore * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WNote { /** diff --git a/system/WCore/WRequest.php b/system/WCore/WRequest.php index bbc2df4f..ba6bab9c 100644 --- a/system/WCore/WRequest.php +++ b/system/WCore/WRequest.php @@ -10,7 +10,7 @@ * * @package System\WCore * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WRequest { /** diff --git a/system/WCore/WResponse.php b/system/WCore/WResponse.php index cb202220..6df4526c 100644 --- a/system/WCore/WResponse.php +++ b/system/WCore/WResponse.php @@ -10,7 +10,7 @@ * * @package System\WCore * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WResponse { /** diff --git a/system/WCore/WRetriever.php b/system/WCore/WRetriever.php index b0f3b011..bdc23e46 100644 --- a/system/WCore/WRetriever.php +++ b/system/WCore/WRetriever.php @@ -10,7 +10,7 @@ * * @package System\WCore * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WRetriever { /** diff --git a/system/WCore/WRoute.php b/system/WCore/WRoute.php index 18c7dbfe..a3839174 100644 --- a/system/WCore/WRoute.php +++ b/system/WCore/WRoute.php @@ -31,7 +31,7 @@ * * @package System\WCore * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WRoute { /** diff --git a/system/WCore/WSession.php b/system/WCore/WSession.php index dff51b89..138a62ed 100644 --- a/system/WCore/WSession.php +++ b/system/WCore/WSession.php @@ -10,7 +10,7 @@ * * @package System\WCore * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WSession { /* diff --git a/system/WCore/WSystem.php b/system/WCore/WSystem.php index 50fb6a9c..af570e8a 100644 --- a/system/WCore/WSystem.php +++ b/system/WCore/WSystem.php @@ -13,7 +13,7 @@ * * @package System\WCore * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WSystem { /** diff --git a/system/WCore/WTools.php b/system/WCore/WTools.php index 2c298a65..dc96023a 100644 --- a/system/WCore/WTools.php +++ b/system/WCore/WTools.php @@ -10,7 +10,7 @@ * * @package System\WCore * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WTools { /** diff --git a/system/WCore/WView.php b/system/WCore/WView.php index ea0a1845..b013fa4d 100644 --- a/system/WCore/WView.php +++ b/system/WCore/WView.php @@ -10,7 +10,7 @@ * * @package System\WCore * @author Johan Dufau - * @version 0.6.1-23-11-2016 + * @version 0.6.2-04-06-2018 */ class WView { /** diff --git a/themes/admin-bootstrap/templates/index.html b/themes/admin-bootstrap/templates/index.html index fb0b51cd..6692098b 100644 --- a/themes/admin-bootstrap/templates/index.html +++ b/themes/admin-bootstrap/templates/index.html @@ -4,7 +4,7 @@ {$wity_page_title} - + {if !empty({$wity_site_favicon})}{/if} {if !empty({$wity_site_icon})}{/if} @@ -84,7 +84,7 @@