From aef34de42e7fdf1f0dd29358cdef73bb38e5e39d Mon Sep 17 00:00:00 2001 From: pvincent Date: Sun, 20 Sep 2020 18:13:25 +0400 Subject: [PATCH] sugoi added --- LICENSE | 340 +++++++++++ haxelib.json | 14 + poko.txt | 28 + src/sugoi/BaseApp.hx | 425 +++++++++++++ src/sugoi/BaseController.hx | 95 +++ src/sugoi/BaseView.hx | 163 +++++ src/sugoi/Config.hx | 63 ++ src/sugoi/ControllerAction.hx | 7 + src/sugoi/Web.hx | 12 + src/sugoi/apis/facebook/server/FB.hx | 127 ++++ src/sugoi/apis/google/GeoCode.hx | 134 ++++ src/sugoi/apis/google/Recaptcha.hx | 26 + src/sugoi/apis/linux/Curl.hx | 109 ++++ src/sugoi/apis/mailchimp/Mailchimp.hx | 72 +++ src/sugoi/apis/morning/MorningUp.hx | 101 +++ src/sugoi/db/BufferedMail.hx | 132 ++++ src/sugoi/db/Cache.hx | 65 ++ src/sugoi/db/Error.hx | 16 + src/sugoi/db/File.hx | 77 +++ src/sugoi/db/Session.hx | 135 ++++ src/sugoi/db/Variable.hx | 48 ++ src/sugoi/form/FieldSet.hx | 29 + src/sugoi/form/Form.hx | 578 ++++++++++++++++++ src/sugoi/form/FormElement.hx | 242 ++++++++ src/sugoi/form/FormMethod.hx | 5 + src/sugoi/form/Formatter.hx | 6 + src/sugoi/form/ListData.hx | 127 ++++ src/sugoi/form/Rules.hx | 26 + src/sugoi/form/elements/Button.hx | 63 ++ src/sugoi/form/elements/CSRFProtection.hx | 44 ++ src/sugoi/form/elements/Checkbox.hx | 45 ++ src/sugoi/form/elements/CheckboxGroup.hx | 104 ++++ src/sugoi/form/elements/DateDropdowns.hx | 142 +++++ src/sugoi/form/elements/DateInput.hx | 72 +++ src/sugoi/form/elements/DatePicker.hx | 118 ++++ .../form/elements/EmbeddedVideoOptions.hx | 130 ++++ src/sugoi/form/elements/Enum.hx | 128 ++++ src/sugoi/form/elements/FileUpload.hx | 87 +++ src/sugoi/form/elements/Flags.hx | 175 ++++++ src/sugoi/form/elements/FloatInput.hx | 24 + src/sugoi/form/elements/FloatSelect.hx | 19 + src/sugoi/form/elements/HourDropDowns.hx | 92 +++ src/sugoi/form/elements/Html.hx | 30 + src/sugoi/form/elements/ImageUpload.hx | 76 +++ src/sugoi/form/elements/Input.hx | 99 +++ src/sugoi/form/elements/IntInput.hx | 39 ++ src/sugoi/form/elements/IntSelect.hx | 21 + src/sugoi/form/elements/KeyVal.hx | 11 + src/sugoi/form/elements/Label.hx | 22 + src/sugoi/form/elements/RadioGroup.hx | 63 ++ src/sugoi/form/elements/Readonly.hx | 43 ++ src/sugoi/form/elements/Richtext.hx | 105 ++++ src/sugoi/form/elements/RichtextWym.hx | 92 +++ src/sugoi/form/elements/Selectbox.hx | 50 ++ src/sugoi/form/elements/StringInput.hx | 20 + src/sugoi/form/elements/StringSelect.hx | 19 + src/sugoi/form/elements/Submit.hx | 36 ++ src/sugoi/form/elements/TextArea.hx | 39 ++ src/sugoi/form/filters/Filter.hx | 9 + src/sugoi/form/filters/FloatFilter.hx | 28 + src/sugoi/form/filters/IFilter.hx | 10 + src/sugoi/form/validators/BoolValidator.hx | 29 + src/sugoi/form/validators/CustomValidator.hx | 58 ++ .../form/validators/DateTimeValidator.hx | 162 +++++ src/sugoi/form/validators/DateValidator.hx | 147 +++++ src/sugoi/form/validators/EmailValidator.hx | 40 ++ src/sugoi/form/validators/ListValidator.hx | 105 ++++ src/sugoi/form/validators/NumberValidator.hx | 113 ++++ src/sugoi/form/validators/RegexValidator.hx | 62 ++ src/sugoi/form/validators/StringValidator.hx | 139 +++++ src/sugoi/form/validators/Validator.hx | 23 + src/sugoi/helper/BreadCrumb.hx | 102 ++++ src/sugoi/helper/Helper.hx | 21 + src/sugoi/helper/Ordinal.hx | 113 ++++ src/sugoi/helper/READ ME.txt | 8 + src/sugoi/helper/Table.hx | 166 +++++ src/sugoi/i18n/GetText.hx | 558 +++++++++++++++++ src/sugoi/i18n/Locale.hx | 75 +++ src/sugoi/i18n/TemplateTranslator.hx | 133 ++++ src/sugoi/i18n/translator/GetText.hx | 178 ++++++ src/sugoi/i18n/translator/ITranslator.hx | 21 + src/sugoi/i18n/translator/TMap.hx | 45 ++ src/sugoi/i18n/translator/TSimple.hx | 178 ++++++ src/sugoi/i18n/translator/TXml.hx | 63 ++ src/sugoi/mail/BufferedMailer.hx | 61 ++ src/sugoi/mail/DebugMailer.hx | 45 ++ src/sugoi/mail/IMail.hx | 23 + src/sugoi/mail/IMailer.hx | 41 ++ src/sugoi/mail/Mail.hx | 182 ++++++ src/sugoi/mail/MandrillMailer.hx | 132 ++++ src/sugoi/mail/SmtpMailer.hx | 68 +++ src/sugoi/plugin/IPlugIn.hx | 13 + src/sugoi/plugin/PlugIn.hx | 135 ++++ src/sugoi/tools/Csv.hx | 272 +++++++++ src/sugoi/tools/DebugConnection.hx | 120 ++++ src/sugoi/tools/Macros.hx | 76 +++ src/sugoi/tools/ResultsBrowser.hx | 54 ++ src/sugoi/tools/UploadedImage.hx | 38 ++ src/sugoi/tools/Utils.hx | 51 ++ src/thx/csv/DCsv.hx | 31 + 100 files changed, 9138 insertions(+) create mode 100644 LICENSE create mode 100644 haxelib.json create mode 100644 poko.txt create mode 100644 src/sugoi/BaseApp.hx create mode 100644 src/sugoi/BaseController.hx create mode 100644 src/sugoi/BaseView.hx create mode 100644 src/sugoi/Config.hx create mode 100644 src/sugoi/ControllerAction.hx create mode 100644 src/sugoi/Web.hx create mode 100644 src/sugoi/apis/facebook/server/FB.hx create mode 100644 src/sugoi/apis/google/GeoCode.hx create mode 100644 src/sugoi/apis/google/Recaptcha.hx create mode 100644 src/sugoi/apis/linux/Curl.hx create mode 100644 src/sugoi/apis/mailchimp/Mailchimp.hx create mode 100644 src/sugoi/apis/morning/MorningUp.hx create mode 100644 src/sugoi/db/BufferedMail.hx create mode 100644 src/sugoi/db/Cache.hx create mode 100644 src/sugoi/db/Error.hx create mode 100644 src/sugoi/db/File.hx create mode 100644 src/sugoi/db/Session.hx create mode 100644 src/sugoi/db/Variable.hx create mode 100644 src/sugoi/form/FieldSet.hx create mode 100644 src/sugoi/form/Form.hx create mode 100644 src/sugoi/form/FormElement.hx create mode 100644 src/sugoi/form/FormMethod.hx create mode 100644 src/sugoi/form/Formatter.hx create mode 100644 src/sugoi/form/ListData.hx create mode 100644 src/sugoi/form/Rules.hx create mode 100644 src/sugoi/form/elements/Button.hx create mode 100644 src/sugoi/form/elements/CSRFProtection.hx create mode 100644 src/sugoi/form/elements/Checkbox.hx create mode 100644 src/sugoi/form/elements/CheckboxGroup.hx create mode 100644 src/sugoi/form/elements/DateDropdowns.hx create mode 100644 src/sugoi/form/elements/DateInput.hx create mode 100644 src/sugoi/form/elements/DatePicker.hx create mode 100644 src/sugoi/form/elements/EmbeddedVideoOptions.hx create mode 100644 src/sugoi/form/elements/Enum.hx create mode 100644 src/sugoi/form/elements/FileUpload.hx create mode 100644 src/sugoi/form/elements/Flags.hx create mode 100644 src/sugoi/form/elements/FloatInput.hx create mode 100644 src/sugoi/form/elements/FloatSelect.hx create mode 100644 src/sugoi/form/elements/HourDropDowns.hx create mode 100644 src/sugoi/form/elements/Html.hx create mode 100644 src/sugoi/form/elements/ImageUpload.hx create mode 100644 src/sugoi/form/elements/Input.hx create mode 100644 src/sugoi/form/elements/IntInput.hx create mode 100644 src/sugoi/form/elements/IntSelect.hx create mode 100644 src/sugoi/form/elements/KeyVal.hx create mode 100644 src/sugoi/form/elements/Label.hx create mode 100644 src/sugoi/form/elements/RadioGroup.hx create mode 100644 src/sugoi/form/elements/Readonly.hx create mode 100644 src/sugoi/form/elements/Richtext.hx create mode 100644 src/sugoi/form/elements/RichtextWym.hx create mode 100644 src/sugoi/form/elements/Selectbox.hx create mode 100644 src/sugoi/form/elements/StringInput.hx create mode 100644 src/sugoi/form/elements/StringSelect.hx create mode 100644 src/sugoi/form/elements/Submit.hx create mode 100644 src/sugoi/form/elements/TextArea.hx create mode 100644 src/sugoi/form/filters/Filter.hx create mode 100644 src/sugoi/form/filters/FloatFilter.hx create mode 100644 src/sugoi/form/filters/IFilter.hx create mode 100644 src/sugoi/form/validators/BoolValidator.hx create mode 100644 src/sugoi/form/validators/CustomValidator.hx create mode 100644 src/sugoi/form/validators/DateTimeValidator.hx create mode 100644 src/sugoi/form/validators/DateValidator.hx create mode 100644 src/sugoi/form/validators/EmailValidator.hx create mode 100644 src/sugoi/form/validators/ListValidator.hx create mode 100644 src/sugoi/form/validators/NumberValidator.hx create mode 100644 src/sugoi/form/validators/RegexValidator.hx create mode 100644 src/sugoi/form/validators/StringValidator.hx create mode 100644 src/sugoi/form/validators/Validator.hx create mode 100644 src/sugoi/helper/BreadCrumb.hx create mode 100644 src/sugoi/helper/Helper.hx create mode 100644 src/sugoi/helper/Ordinal.hx create mode 100644 src/sugoi/helper/READ ME.txt create mode 100644 src/sugoi/helper/Table.hx create mode 100644 src/sugoi/i18n/GetText.hx create mode 100644 src/sugoi/i18n/Locale.hx create mode 100644 src/sugoi/i18n/TemplateTranslator.hx create mode 100644 src/sugoi/i18n/translator/GetText.hx create mode 100644 src/sugoi/i18n/translator/ITranslator.hx create mode 100644 src/sugoi/i18n/translator/TMap.hx create mode 100644 src/sugoi/i18n/translator/TSimple.hx create mode 100644 src/sugoi/i18n/translator/TXml.hx create mode 100644 src/sugoi/mail/BufferedMailer.hx create mode 100644 src/sugoi/mail/DebugMailer.hx create mode 100644 src/sugoi/mail/IMail.hx create mode 100644 src/sugoi/mail/IMailer.hx create mode 100644 src/sugoi/mail/Mail.hx create mode 100644 src/sugoi/mail/MandrillMailer.hx create mode 100644 src/sugoi/mail/SmtpMailer.hx create mode 100644 src/sugoi/plugin/IPlugIn.hx create mode 100644 src/sugoi/plugin/PlugIn.hx create mode 100644 src/sugoi/tools/Csv.hx create mode 100644 src/sugoi/tools/DebugConnection.hx create mode 100644 src/sugoi/tools/Macros.hx create mode 100644 src/sugoi/tools/ResultsBrowser.hx create mode 100644 src/sugoi/tools/UploadedImage.hx create mode 100644 src/sugoi/tools/Utils.hx create mode 100644 src/thx/csv/DCsv.hx diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d6a9326 --- /dev/null +++ b/LICENSE @@ -0,0 +1,340 @@ +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. + diff --git a/haxelib.json b/haxelib.json new file mode 100644 index 0000000..8ce2dc1 --- /dev/null +++ b/haxelib.json @@ -0,0 +1,14 @@ +{ + "name": "sugoi", + "url" : "https://github.com/bablukid/sugoi/", + "license": "GPL", + "classPath": "src", + "tags": ["web", "neko","mysql","templo"], + "description": "A simple MVC web framework for Haxe and Neko", + "version": "1.0.0", + "releasenote": "", + "contributors": ["bablukid","ncannasse","tarwin","tonypee"], + "dependencies": { + + } +} \ No newline at end of file diff --git a/poko.txt b/poko.txt new file mode 100644 index 0000000..f96b036 --- /dev/null +++ b/poko.txt @@ -0,0 +1,28 @@ +/* + * POKO + * https://code.google.com/p/poko/ + * Copyright (c) 2008, TouchMyPixel & contributors + * Original author : Tony Polinelli + * Contributers: Tarwin Stroh-Spijer + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE TOUCH MY PIXEL & CONTRIBUTERS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE TOUCH MY PIXEL & CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ diff --git a/src/sugoi/BaseApp.hx b/src/sugoi/BaseApp.hx new file mode 100644 index 0000000..6ea6d57 --- /dev/null +++ b/src/sugoi/BaseApp.hx @@ -0,0 +1,425 @@ +package sugoi; +import sugoi.i18n.TemplateTranslator; +import sugoi.Web; + +class BaseApp { + + public var cnx : sys.db.Connection; + public var template : templo.Loader; + public var maintain : Bool; + public var session : sugoi.db.Session; + public var view : View; + public var user : db.User; + public var params : Map; + public var cookieName : String; + public var cookieDomain : String; + public var uri : String; + + public static var config: Config; + //public static var classPathes = sugoi.tools.Macros.getClassPathes(); + + public function new() { + + if (config == null) { + loadConfig(); + } + + cookieName = "sid"; + cookieDomain = "." + App.config.HOST; + + #if plugins + if( false ) sugoi.plugin.PlugIn.copyTpl(); + #end + + // This macro generates translated templates for each langage + #if i18n_generation + if( false ) TemplateTranslator.parse("lang/master"); + #end + } + + public function loadConfig() { + App.config = BaseApp.config = new sugoi.Config(); + } + + public function loadTemplate( t : String ) { + templo.Loader.OPTIMIZED = App.config.DEBUG == false; + templo.Loader.BASE_DIR = App.config.TPL; + templo.Loader.TMP_DIR = App.config.TPL_TMP; + if ( t == null ) return null; + #if neko + return new templo.Loader(t, App.config.getBool("cachetpl")); + #else + return new templo.Loader(t); + #end + } + + public function setTemplate( t : String ) { + template = t == null ? null : loadTemplate(t); + } + + public function initLang( lang : String ) { + + if (lang == null || lang == "") lang = config.LANG; + + //Define template path + var path; + if (!App.config.DEBUG){ + path = Web.getCwd() + "../lang/" + lang + "/"; + }else{ + path = Web.getCwd() + "../lang/master/"; + } + App.config.TPL = path + "tpl/"; + App.config.TPL_TMP = path + "tmp/"; + + //init system locale + if ( !Sys.setTimeLocale("en_US.UTF-8") ) { + Sys.setTimeLocale("en"); + } + + //init gettext translator + sugoi.i18n.Locale.init(lang); + + return true; + } + + function saveAndClose() { + + if( cnx == null ) return; + if( session.sid != null ) + session.update(); + + cnx.commit(); + cnx.close(); + untyped cnx.close = function() {} + untyped cnx.request = function(s) return null; + } + + function executeTemplate( ?save ) { + view.init(); + var result = template.execute(view); + if ( save ) saveAndClose(); + #if php + //strange bug with templo in PHP + if (result.substr(0, 4) == "null") result = result.substr(4); + #end + Sys.print(result); + } + + function onMeta( m : String, args : Array ) { + switch( m ) { + case "tpl": + setTemplate(args[0]); + case "logged": + if ( user == null ) + throw sugoi.ControllerAction.RedirectAction("/?__redirect="+Web.getURI()); + case "admin": + if( user == null || !user.isAdmin() ) + throw sugoi.ControllerAction.RedirectAction("/"); + default: + } + } + + /** + * Detect lang from HTTP headers + */ + function detectLang() { + var l = Web.getClientHeader("Accept-Language"); + if( l != null ) + for( l in l.split(",") ) { + l = l.split(";")[0]; + l = l.split("-")[0]; + l = StringTools.trim(l); + for( a in App.config.LANGS ) + if( a == l ) + return a; + } + + return App.config.LANG; + } + + + /** + * Setup current app language + */ + function setupLang() { + + //this app is monolingual and doesn't manage i18n + if (App.config.LANG == "master") return; + + //lang is taken from user object or from HTTP headers + if ( session.lang == null || !Lambda.has(App.config.LANGS, session.lang) ){ + session.lang = (user == null) ? detectLang() : user.lang; + } + + //override if param is given + var lang = params.get("lang"); + if ( lang != null && Lambda.has(App.config.LANGS, lang) ){ + session.lang = lang; + + } + + //init lang + initLang(session.lang); + } + + /** + * Get current application langage (2 letters lowercase) + */ + public function getLang(){ + return (session != null && session.lang != null && session.lang != "") ? session.lang : App.config.LANG; + } + + public function rollback() { + if( cnx != null ) cnx.rollback(); + sys.db.Manager.cleanup(); + if( user != null && session != null ) + user = session.user; + // does not reset session + } + + public function setCookie( oldCookie : String ){ + if( session != null && session.sid != null && session.sid != oldCookie ) { + Web.setHeader("Set-Cookie", cookieName+"=" + session.sid + "; path=/;"); + } + } + + function mainLoop() { + params = Web.getParams(); + + //Get session + var sids = []; + var cookieSid = Web.getCookies().get(cookieName); + if( params.exists("sid") ) sids.push(params.get("sid")); + if( cookieSid != null ) sids.push(cookieSid); + session = sugoi.db.Session.init(sids); + + //Check for maintenance + maintain = sugoi.db.Variable.getInt("maintain") != 0; + user = session.user; + + //setup langage + setupLang(); + + + if( maintain && ((user != null && user.isAdmin()) ) ) + maintain = false; + + setCookie(cookieSid); + + if( maintain ) { + setTemplate("maintain.mtt"); + executeTemplate(); + return; + } + + //dispatching + try { + + uri = Web.getURI(); + if ( StringTools.endsWith(uri, "/index.n") ) uri = uri.substr(0, -8); + + //"before dispatch" callback + beforeDispatch(); + + var d = new haxe.web.Dispatch(uri, params); + d.onMeta = onMeta; + d.dispatch(new controller.Main()); + + } catch ( e : haxe.web.Dispatch.DispatchError ) { + + //dispatch / routing error + if ( App.config.DEBUG ) { + #if neko + neko.Lib.rethrow(e); + #else + php.Lib.rethrow(e); + #end + } + cnx.rollback(); + Web.redirect("/"); + return; + + } catch ( e : sugoi.ControllerAction) { + + switch( e ) { + case RedirectAction(url): + Web.redirect(url); + template = null; + case ErrorAction(url, text), OkAction(url,text): + if( text == null ) { + text = url; + url = Web.getURI(); + } + Web.redirect(url); + var error = switch(e) { case ErrorAction(_): true; default: false; }; + if( error ) rollback(); + if ( error ) { + session.addMessage(text,true); + }else { + session.addMessage(text); + } + template = null; + } + } + + //Render template + if ( template == null ) { + saveAndClose(); + } else { + executeTemplate(true); // will saveAndClose + } + } + + /** + * Override this function if you want + * to insert some actions + */ + public function beforeDispatch() { + + } + + public function logError( e : Dynamic, ?stack : String ) { + var stack = if( stack != null ) stack else haxe.CallStack.toString(haxe.CallStack.exceptionStack()); + var message = new StringBuf(); + message.add(Std.string(e)); + message.add("\n"); + message.add(stack); + message.add("\n"); + var e = new sugoi.db.Error(); + e.url = Web.getURI(); + e.ip = Web.getClientIP(); + e.user = if( user != null ) user else null; + e.date = Date.now(); + e.userAgent = Web.getClientHeader("User-Agent"); + e.error = message.toString(); + e.insert(); + } + + function errorHandler( e:Dynamic ) { + try { + var stack = haxe.CallStack.toString(haxe.CallStack.exceptionStack()); + // ROLLBACK and LOG + if ( cnx != null ) { + cnx.rollback(); + logError(e,stack); + } + + //log also in a file, in case we don't have a valid connexion to DB + Web.logMessage(e+"\n" + stack); + + maintain = true; + view = new View(); + view.message = Std.string(e); + if ( App.config.DEBUG || (user != null && user.isAdmin()) ) { + view.stack = stack; + } + + setTemplate("error.mtt"); + executeTemplate(false); + + } catch( e : Dynamic ) { + Sys.print("
");
+			Sys.println("Error : "+try Std.string(e) catch( e : Dynamic ) "???");
+			Sys.println(haxe.CallStack.toString(haxe.CallStack.exceptionStack()));
+			try {
+				if( cnx != null )
+					sugoi.db.Error.manager.get(0,false);
+			} catch( e : Dynamic ) {
+				Sys.println("Initializing Database...");
+				sys.db.Admin.initializeDatabase();
+				Sys.println("Done");
+			}
+			Sys.print("
"); + } + } + + /** + * init template engine + * and db connexion + */ + function init() { + maintain = App.config.getBool("maintain"); + if( maintain ) { + view = new View(); + setTemplate("maintain.mtt"); + executeTemplate(false); + return false; + } + try { + var dbstr = App.config.get("database"); + var dbreg = ~/([^:]+):\/\/([^:]+):([^@]*?)@([^:]+)(:[0-9]+)?\/(.*?)$/; + if( !dbreg.match(dbstr) ) + throw "Configuration requires a valid database attribute, format is : mysql://user:password@host:port/dbname"; + var port = dbreg.matched(5); + var dbparams = { + user:dbreg.matched(2), + pass:dbreg.matched(3), + host:dbreg.matched(4), + port:port == null ? 3306 : Std.parseInt(port.substr(1)), + database:dbreg.matched(6), + socket:null + }; + cnx = sys.db.Mysql.connect(dbparams); + } catch( e : Dynamic ) { + errorHandler(e); + return false; + } + if( App.config.SQL_LOG ) + cnx = new sugoi.tools.DebugConnection(cnx); + return true; + } + + function cloneApp() { + // ensure that we have no variable initialized in app loop + var app = new App(); + var bapp : BaseApp = app; + bapp.cnx = cnx; + bapp.view = new View(); + App.current = app; + bapp.mainLoop(); + } + + function run() { + + // Will close the connection + sys.db.Transaction.main(cnx, cloneApp, function(e) { var b : BaseApp = App.current; b.errorHandler(e); }); + App.current = null; + } + + function sendHeaders(){ + Web.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); + Web.setHeader("Pragma", "no-cache"); + Web.setHeader("Expires", "-1"); + Web.setHeader("P3P", "CP=\"ALL DSP COR NID CURa OUR STP PUR\""); + Web.setHeader("Content-Type", "text/html; Charset=UTF-8"); + Web.setHeader("Expires", "Mon, 26 Jul 1997 05:00:00 GMT"); + } + + static function main() { + + /** + * this macro will parse the code and generate the allTexts.pot file + * which will be used as a template for translation files (*.po and *.mo) + */ + #if i18n_parsing + if( false ) sugoi.i18n.GetText.parse(["src", "lang/master","js","common"], "www/lang/allTexts.pot"); + #end + + App.current = new App(); + var a : BaseApp = App.current; + + a.sendHeaders(); + + if( !a.init() ) { + a = null; + return; + } + a.run(); + a = null; + #if neko + if ( App.config.getInt("cache", 0) == 1 ) { + neko.Web.cacheModule(App.main); + } + #end + } +} diff --git a/src/sugoi/BaseController.hx b/src/sugoi/BaseController.hx new file mode 100644 index 0000000..f9d6b47 --- /dev/null +++ b/src/sugoi/BaseController.hx @@ -0,0 +1,95 @@ +package sugoi; +import sugoi.db.File; +import sugoi.Web; +import sugoi.ControllerAction; + +@:autoBuild(sugoi.tools.Macros.buildController()) +class BaseController { + + var app : App; + var view : View; + + public function new() { + app = App.current; + view = app.view; + } + + function getParam( v : String ) { + return app.params.get(v); + } + + function checkToken():Bool { + var token = haxe.crypto.Md5.encode(app.session.sid + App.config.KEY.substr(0,6)); + view.token = token; + return app.params.get("token") == token; + } + + function isAdmin() { + return app.user != null && app.user.isAdmin(); + } + + public function Redirect( url : String ) { + return RedirectAction(url); + } + + public function Error( url : String, ?text : String ) { + return ErrorAction(url, text); + } + + public function Ok( url : String, ?text : String ) { + return OkAction(url, text); + } + + /** + * User uploaded images are stored in the db.File table. + * When there is an attempt to display an image like /file/***.jpg + * the .htaccess in /file/ redirects to this handler to generate the file from the DB + * @param fname + */ + function doFile( fname : String ) { + + //get the file from DB + var fid = Std.parseInt(fname); + var f = File.manager.get(fid, false); + var ext = fname.substr( fname.lastIndexOf(".") );//.png + if( f == null ) { + Sys.print("404 - File not found '"+StringTools.htmlEscape(fname)+"' id #"+fid); + return; + } + if ( fname != File.makeSign(fid) + ext ){ + Sys.print("404 - File signature do not match '"+fname+"' != '"+File.makeSign(fid)+ ext+"'"); + return; + } + var path; + var ch; + try { + path = Web.getCwd()+"/file/"+File.makeSign(f.id)+ext; + ch = sys.io.File.write(path,true); + } catch( e : Dynamic ) { + Sys.sleep(0.1); // wait for another process to write ? + Web.redirect(Web.getURI()+"?retry=1"); + return; + } + ch.write(f.data); + ch.close(); + + try { + // get mtime of current index.n + #if neko + var s = sys.FileSystem.stat(Web.getCwd() + "index.n"); + #else + var s = sys.FileSystem.stat(Web.getCwd() + "index.php"); + #end + var mtime = s.mtime.toString(); + + // set mtime of new file + var p = new sys.io.Process("touch",["-m","-d",mtime,path]); + p.exitCode(); + }catch( e : Dynamic ){ + } + + Web.redirect(Web.getURI()+"?reload=1"); + } + + +} \ No newline at end of file diff --git a/src/sugoi/BaseView.hx b/src/sugoi/BaseView.hx new file mode 100644 index 0000000..6c63dc3 --- /dev/null +++ b/src/sugoi/BaseView.hx @@ -0,0 +1,163 @@ +package sugoi; + +import sugoi.db.Variable; +import sugoi.db.File; + +class BaseView implements Dynamic { + + var _vcache : Map; + + public function new() { + _vcache = new Map(); + } + + public function init() { + var app = App.current; + + this.user = app.user; + this.session = app.session; + this.LANG = App.config.LANG; + this.HOST = App.config.HOST; + this.DATA_HOST = App.config.DATA_HOST; + this.DEBUG = App.config.DEBUG; + this.NAME = App.config.NAME; + this.isAdmin = app.user != null && app.user.isAdmin(); + + //Access basic functions in views + this.Std = Std; + this.Math = Math; + + + if ( App.config.SQL_LOG ) { + this.sqlLog = untyped app.cnx == null ? null : app.cnx.log; + } + } + + function getMessage() { + var session = App.current.session; + if ( session == null ) return null; + if (session.messages == null) return null; + var n = session.messages.pop(); + if( n == null ) return null; + return { text : n.text, error : n.error, next : session.messages.length > 0 }; + } + + + function getMessages() { + var out = []; + var session = App.current.session; + if ( session == null ) return []; + if (session.messages == null) return []; + + for ( m in session.messages) { + if( m == null ) continue; + out.push( { text : m.text, error : m.error } ); + } + session.messages = []; + return out; + } + + + function urlEncode(str:String) { + return StringTools.urlEncode(str); + } + + /** + * To safely print a string in javascript + * @param str + */ + public function escapeJS( str : String ) { + if (str == null) return ""; + return str.split("\\").join("\\\\").split("'").join("\\'").split("\r").join("\\r").split("\n").join("\\n"); + } + + /** + * Get a value from the Variable table + * @param file + */ + function getVariable( file : String ) { + + var v = _vcache.get(file); + if( v != null ) + return v; + if( App.current.maintain ) + return ""; + v = Variable.get(file); + if( v == null ) v = ""; + _vcache.set(file,v); + return v; + } + + function getParam( p : String ) { + return App.current.params.get(p); + } + + /** + * Return the url of a db.File record + */ + public function file( file : sugoi.db.File) { + if (file == null) throw "file is null"; + return "/file/"+sugoi.db.File.makeSign(file.id)+"."+file.getExtension(); + } + + /** + * Try to print an HTML table from any kind of object + * @param data + */ + function table(data:Dynamic) { + return new sugoi.helper.Table("table table-bordered").toString(data); + } + + /** + * newline to
+ * @param txt + */ + public function nl2br(txt:String):String { + if (txt == null) return ""; + return txt.split("\n").join("
"); + } + + + public function _(str:String):String { + if (sugoi.i18n.Locale.texts != null) { + return sugoi.i18n.Locale.texts.get(str); + } else { + return str; + } + } + + //same function with params ( templo doesnt manage optionnal params in functions ) + public function __(str:String, params:Dynamic){ + return sugoi.i18n.Locale.texts.get(str, params); + } +/* +#else + public function _(str:String):String { + neko.Web.logMessage("_call "+str); + return StringTools.rtrim( str.split("||")[0] ); + } + + //same function with params ( templo doesnt manage optionnal params in functions ) + public function __(str:String, params:Dynamic):String { + neko.Web.logMessage("_call "+str); + str = StringTools.rtrim( str.split("||")[0] ); + var list = str.split("::"); + if(params != null) { + for (k in Reflect.fields(params)) { + str = StringTools.replace(str, "::" + k + "::", Reflect.field(params, k)); + } + } + return str; + } +#end +*/ + + public function loopList(start:Int,end:Int):List { + var list = new List(); + for (i in start...end) { + list.add(i); + } + return list; + } + +} diff --git a/src/sugoi/Config.hx b/src/sugoi/Config.hx new file mode 100644 index 0000000..3e19938 --- /dev/null +++ b/src/sugoi/Config.hx @@ -0,0 +1,63 @@ +package sugoi; + +import sugoi.Web; + +class Config { + + public var PATH :String; + //public var json : Dynamic; + public var xml : Xml; + public var LANG :String; + public var LANGS :Array; + public var TPL :String; + public var TPL_TMP :String; + public var DEBUG :Bool; + public var HOST :String; + public var NAME :String; + public var KEY :String; + public var DATA_HOST :String; + public var SQL_LOG :Bool; + + public function new(?path:String) { + PATH = (path != null) ? path : Web.getCwd() + "../"; + //json = haxe.Json.parse(sys.io.File.getContent(PATH + "config.json")); + xml = Xml.parse(sys.io.File.getContent(PATH + "config.xml")).firstElement(); + + LANG = get("lang"); + LANGS = get("langs").split(";"); + TPL = PATH + "lang/" + LANG + "/tpl/"; + TPL_TMP = PATH + "lang/" + LANG + "/tmp/"; + DEBUG = get("debug","0") == "1"; + HOST = get("host"); + NAME = get("name"); + KEY = get("key"); + DATA_HOST = get("dataHost","data."+HOST); + SQL_LOG = getBool("sqllog", false); + } + + public function defined( val : String ) { + //return Reflect.field(xml,val) != null; + return xml.get(val) != null; + } + + public function getBool( val : String, ?def ) { + var v = get(val); + if( v == null ) return def; + return( v == "1" || v == "true" ); + } + + public function get( val : String, ?def : String ) : String { + //var v = Reflect.field(xml,val); + var v = xml.get(val); + if( v == null ) + v = def; + if( v == null ) + throw "Missing config attribute : '"+val+"'"; + return v; + } + + public function getInt( val : String, ?def : Int ) : Int { + return Std.parseInt(get(val,Std.string(def))); + } + +} diff --git a/src/sugoi/ControllerAction.hx b/src/sugoi/ControllerAction.hx new file mode 100644 index 0000000..726b881 --- /dev/null +++ b/src/sugoi/ControllerAction.hx @@ -0,0 +1,7 @@ +package sugoi; + +enum ControllerAction { + RedirectAction( url : String ); + ErrorAction( url : String, ?text : String ); + OkAction( url : String, ?text : String ); +} \ No newline at end of file diff --git a/src/sugoi/Web.hx b/src/sugoi/Web.hx new file mode 100644 index 0000000..22c2ee2 --- /dev/null +++ b/src/sugoi/Web.hx @@ -0,0 +1,12 @@ +package sugoi; + +/** + * Shortcut to system class + * + * @author fbarbut + */ +#if php +typedef Web = php.Web; +#else +typedef Web = neko.Web; +#end \ No newline at end of file diff --git a/src/sugoi/apis/facebook/server/FB.hx b/src/sugoi/apis/facebook/server/FB.hx new file mode 100644 index 0000000..ecd3a95 --- /dev/null +++ b/src/sugoi/apis/facebook/server/FB.hx @@ -0,0 +1,127 @@ +package sugoi.apis.facebook.server; + +/** + * + * Call Facebook services thru the graph API + * + * @author fbarbut + */ +class FB +{ + + var app_id : String; + var app_secret : String; + var token : String; + + public function new(fbToken, ?app_id, ?app_secret) { + + this.token = fbToken; + this.app_id = app_id; + this.app_secret = app_secret; + + } + + public static function init(fbToken, ?app_id, ?app_secret) { + return new FB(fbToken, app_id, app_secret); + } + + /** + * Publish a photo in the current user galery + * @param imgUrl + * @param text + * @return a json object + */ + public function publishPhoto(imgUrl:String,text:String):Dynamic { + + //var c = new sugoi.apis.linux.Curl(); + //c.setPostData("access_token", token); + //c.setPostData("url", imgUrl ); + //var r = c.call("POST", "https://graph.facebook.com/v2.5/me/photos"); + + //var c = new sys.io.Process("curl", [ + //"-X POST", + //"-d url="+imgUrl, + //"-d access_token=" + token, + //"https://graph.facebook.com/v2.5/me/photos" + //]); + + + var text = StringTools.urlEncode(text); + return call('curl -X POST -d url=$imgUrl -d access_token=$token -d caption="$text" https://graph.facebook.com/v2.5/me/photos'); + + } + + /** + * Publish a story + * @doc https://developers.facebook.com/docs/sharing/opengraph + * + * @param action Like 'game.achieves', see https://developers.facebook.com/docs/reference/opengraph/action-type/games.achieves/ + * @param ogObjectUrl An opengraph object representing the subject the action in done to + */ + public function publishStory(action:String, objectType:String,ogObjectUrl:String,?message:String) { + + ogObjectUrl = StringTools.urlEncode(ogObjectUrl); + + return call('curl -X POST -d access_token=$token -d "$objectType=$ogObjectUrl" -d fb:explicitly_shared=true '+(message!=null ? ' -d message="'+StringTools.urlDecode(message)+'"' : "") +' https://graph.facebook.com/v2.5/me/$action'); + + } + + /** + * Get a long lived facebook token from a short lived one + * @doc https://developers.facebook.com/docs/facebook-login/access-tokens/expiration-and-extension + * @return token + */ + public function getLongLivedToken():String { + + //WTF the result is not JSON but like "access_token=XXX&expires=5179521" + var req = 'curl -X GET "https://graph.facebook.com/oauth/access_token?grant_type=fb_exchange_token&client_id=$app_id&client_secret=$app_secret&fb_exchange_token=$token&redirect_uri=https://' + App.config.HOST + '"'; + var r = call(req, false); + + if (r.substr(0, 13) != "access_token=") throw r; + + var r2 = r.split("&")[0].split("=")[1]; + + if (r == null) throw 'error while parsing $r'; + + return r2; + + } + + + /** + * call via cURL + */ + function call(cmd:String,?isJson=true):Dynamic { + var c = new sys.io.Process(cmd,[]); + #if neko + var r = neko.Lib.stringReference(p.stdout.readAll()); + #else + var r = c.stdout.readAll().toString(); + #end + //error ? + if (r == null) { + #if neko + var r = neko.Lib.stringReference(c.stderr.readAll()); + #else + var r = c.stderr.readAll().toString(); + #end + } + c.exitCode(); + + if (r == null) throw "cUrl answer is null"; + + if (!isJson) return r; + + var json:Dynamic = haxe.Json.parse(r); + if ( json == null ) throw "json result is null"; + + if (json.error==1 || json.error || json.error=="1") { + throw r; + } + + return json; + } + + + +} \ No newline at end of file diff --git a/src/sugoi/apis/google/GeoCode.hx b/src/sugoi/apis/google/GeoCode.hx new file mode 100644 index 0000000..5b2bd5c --- /dev/null +++ b/src/sugoi/apis/google/GeoCode.hx @@ -0,0 +1,134 @@ +package sugoi.apis.google; + +/** + * Geocoding via Google maps API + * + * @doc https://developers.google.com/maps/documentation/geocoding/ + * @author fbarbut + */ + + + /** + * { "results" : [ + * { "address_components" : [ + * { "long_name" : "1", "short_name" : "1", "types" : [ "street_number" ] }, + * { "long_name" : "Place Saint Bénigne", "short_name" : "Pl. Saint Bénigne", "types" : [ "route" ] }, + * { "long_name" : "Dijon", "short_name" : "Dijon", "types" : [ "locality", "political" ] }, + * { "long_name" : "Côte-d'Or", "short_name" : "Côte-d'Or", "types" : [ "administrative_area_level_2", "political" ] }, + * { "long_name" : "Bourgogne", "short_name" : "Bourgogne", "types" : [ "administrative_area_level_1", "political" ] }, + * { "long_name" : "France", "short_name" : "FR", "types" : [ "country", "political" ] }, + * { "long_name" : "21000", "short_name" : "21000", "types" : [ "postal_code" ] } ], + * "formatted_address" : "1 Pl. Saint Bénigne, 21000 Dijon, France", + * "geometry" : { "bounds" : { "northeast" : { "lat" : 47.32183939999999, "lng" : 5.0339407 }, "southwest" : { "lat" : 47.3218299, "lng" : 5.0339282 } }, "location" : { "lat" : 47.32183939999999, "lng" : 5.0339282 }, "location_type" : "RANGE_INTERPOLATED", "viewport" : { "northeast" : { "lat" : 47.3231836302915, "lng" : 5.035283430291502 }, "southwest" : { "lat" : 47.3204856697085, "lng" : 5.032585469708497 } } }, "partial_match" : true, "place_id" : "EikxIFBsLiBTYWludCBCw6luaWduZSwgMjEwMDAgRGlqb24sIEZyYW5jZQ", "types" : [ "street_address" ] } ], "status" : "OK" } + */ + typedef GeoCodingData = Array<{ + address_components : Array<{long_name:String,short_name:String,types:Array}>, + formatted_address : String, //Marché de Lerme, Pl. de Lerme, 33000 Bordeaux, France + geometry : {location:{lat:Float,lng:Float}} + }> + + + + +class GeoCode +{ + + //API KEY : https://developers.google.com/maps/documentation/geocoding/?hl=FR#api_key + public static var KEY = ""; + public static var USE_CURL = true; + + public function new(api_key) { + KEY = api_key; + } + + /** + * address -> lat/lng + * + * @doc components filtering : https://developers.google.com/maps/documentation/geocoding/intro#ComponentFiltering + */ + public function geocode(address:String,?components:String):GeoCodingData { + var url = "https://maps.googleapis.com/maps/api/geocode/json?address=" + StringTools.urlEncode(address) + "&key=" + KEY; + if (components != null) url += "&components=" + StringTools.urlEncode(components); + + var d = curlRequest("GET", url, null, null); + if (d == "" || d == null) throw "curl response is empty"; + return onData(d); + } + + + /** + * Reverse geocoding : latitude/longitude -> address + * @param lat + * @param lng + */ + public function reverse(lat:Float, lng:Float) { + + //clermont-ferrand : 45.783 3.083 + + var url = "https://maps.googleapis.com/maps/api/geocode/json?latlng=" + lat + "," + lng + "&key="+KEY; + + //if(USE_CURL) { + //NEEDS CURL + var d = curlRequest("GET", url, null, null); + if(d == "" || d == null) throw "curl response is empty"; + return onReverseData(d); + + //}else { + ////NEEDS HXSSL + //var req = new haxe.Http(url); + //req.onError = function(err:String) trace("error : " + err); + //req.onData = onData; + //req.request(false); + //} + + } + + public function onData(s:String) { + //trace("
"+s+"
"); + var json = cast haxe.Json.parse(s); + if (json.status != "OK") throw "Google geocoding API Error : " + s; + var r : GeoCodingData = cast json.results; + return r; + } + + + + public function onReverseData(s:String) { + + //var out : GeoCodingData = [{address_components:[],formatted_address:null,}]; +// + //var json = haxe.Json.parse(s); + //if(json.status != "OK") throw "Google geocoding API Error : " + s; + //var arr : Array = cast json.results; + //var addrs : Array = cast arr[0].address_components; +// + //for ( a in addrs) { + //var types : Array = cast a.types ; + //var o = { long_name:Std.string(a.long_name), short_name:Std.string(a.short_name), types:types}; + //out[0].address_components.push(o); + //} +// + //return out; + } + + public function curlRequest( method: String, url : String, ?headers : Dynamic, postData : String ) : Dynamic { + var cParams = ["-X"+method,"--max-time","5"]; + for( k in Reflect.fields(headers) ){ + cParams.push("-H"); + cParams.push(k+": "+Reflect.field(headers,k)); + } + cParams.push(url); + if( postData != null ){ + cParams.push("-d"); + cParams.push(postData); + } + + var p = new sys.io.Process("curl",cParams); + var str = neko.Lib.stringReference(p.stdout.readAll()); + p.exitCode(); + + return str; + } + + +} \ No newline at end of file diff --git a/src/sugoi/apis/google/Recaptcha.hx b/src/sugoi/apis/google/Recaptcha.hx new file mode 100644 index 0000000..b075a81 --- /dev/null +++ b/src/sugoi/apis/google/Recaptcha.hx @@ -0,0 +1,26 @@ +package sugoi.apis.google; +import sugoi.apis.linux.Curl; + + +/** + * Google ReCaptcha + * @doc https://www.google.com/recaptcha/ + * @author fbarbut + */ +class Recaptcha +{ + + /** + * Calls the service and returns a JSON response (not parsed) + */ + public static function call(secret:String, token:String, ip:String):String { + + var c = new Curl(); + c.setPostData("secret", secret); + c.setPostData("response", token); + c.setPostData("remoteip", ip ); + return c.call("POST", "https://www.google.com/recaptcha/api/siteverify", { } ); + + } + +} \ No newline at end of file diff --git a/src/sugoi/apis/linux/Curl.hx b/src/sugoi/apis/linux/Curl.hx new file mode 100644 index 0000000..8898ed6 --- /dev/null +++ b/src/sugoi/apis/linux/Curl.hx @@ -0,0 +1,109 @@ +package sugoi.apis.linux; + +/** + * cURL + * + * @doc https://en.wikipedia.org/wiki/CURL + * + * Call cURL as an external process. + * It's an easy way to call HTTPS services from neko and php. + * Be sure to have cURL installed on your system + * + * @author fbarbut + */ +class Curl +{ + public var postData : Map; //POST params + public var params : Array; //curl CLI params + public var debugCommand : String; //store here the generated command + + public function new() + { + postData = new Map(); + params = []; + } + + public static function get() { + return new Curl(); + } + + + public function setPostData(key:String, value:String) { + postData.set(key, value); + + } + + /** + * Execute CURL request + * + * @param method POST, GET or PUT + * @param url + * @param headers + * @param post A string sent as raw POST i.e a json request object + */ + public function call( method:String, url : String, ?headers : Map,?post:String) : String { + + //method GET or POST + if (post != null || Lambda.count(postData) > 0) + params.push("-X" + method); + + //time out + params.push("--max-time"); + params.push("20"); + + //headers + if (headers != null){ + for( k in headers.keys() ){ + params.push("-H"); + //params.push("\""+haxe.Utf8.encode(k+": "+headers.get(k))+"\""); + params.push( k+": "+headers.get(k) ); + } + + } + + params.push(url); + + //POST params (key-values) + if( postData!=null && Lambda.count(postData) > 0 ){ + params.push("-d"); + var d = []; + for (k in postData.keys()) { + d.push( k + "=" + StringTools.urlEncode(postData.get(k)) ); + //d.push( k + "=" + postData.get(k) ); + } + params.push("\""+d.join("&")+"\""); + } + + //if there is a POST payload ( i.e a JSON formatted request ) + if (post != null) { + params.push("-d"); + params.push(post); + //params.push("\""+StringTools.urlEncode(post)+"\""); + } + + //params = params.map(function(s) return haxe.Utf8.encode(s)); + + + debugCommand = "curl " + params.join(" "); + var p = new sys.io.Process("curl", params); + + #if neko + var str = neko.Lib.stringReference(p.stdout.readAll()); + #else + var str = p.stdout.readAll().toString(); + #end + + //error ? + if (str == null) { + #if neko + str = "Error : " + neko.Lib.stringReference(p.stderr.readAll()); + #else + str = "Error : " + p.stderr.readAll().toString(); + #end + } + + p.exitCode(); + return str; + } + +} \ No newline at end of file diff --git a/src/sugoi/apis/mailchimp/Mailchimp.hx b/src/sugoi/apis/mailchimp/Mailchimp.hx new file mode 100644 index 0000000..8fa9c82 --- /dev/null +++ b/src/sugoi/apis/mailchimp/Mailchimp.hx @@ -0,0 +1,72 @@ +package sugoi.apis.mailchimp; +import sugoi.apis.linux.Curl; + + +class Mailchimp +{ + var lastError:String; + var dataCenter:String; + var apiKey:String; + var listId:String; + var serviceUrl : String; + + public function new(apiKey:String, listId:String, ?dataCenter:String = null) + { + this.apiKey = apiKey; + this.listId = listId; + this.dataCenter = (dataCenter == null) ? apiKey.split("-")[1] : dataCenter; + serviceUrl = "https://"+dataCenter+".api.mailchimp.com/2.0/"; + } + + /** + * Access up to the previous 180 days of daily detailed aggregated activity stats for a given list. + * Does not include AutoResponder activity. + * + * @return [ + * {"user_id":13422379,"day":"2013-07-25","emails_sent":0,"unique_opens":0,"recipient_clicks":0,"hard_bounce":0,"soft_bounce":0,"abuse_reports":0,"subs":1,"unsubs":0,"other_adds":0,"other_removes":0}, + * {"user_id":13422379,"day":"2013-10-24","emails_sent":0,"unique_opens":0,"recipient_clicks":0,"hard_bounce":0,"soft_bounce":0,"abuse_reports":0,"subs":1,"unsubs":0,"other_adds":0,"other_removes":0}, + * {"user_id":13422379,"day":"2013-11-24","emails_sent":0,"unique_opens":0,"recipient_clicks":0,"hard_bounce":0,"soft_bounce":0,"abuse_reports":0,"subs":1,"unsubs":0,"other_adds":0,"other_removes":0} + * ] + */ + public function getActivity() { + + var curl = Curl.get(); + return haxe.Json.parse( curl.call("POST", serviceUrl + "lists/activity/", { }, haxe.Json.stringify( { apikey:apiKey, id:listId } ) ) ); + + } + + /** + * Subscribe (or update) a member to a list + * + * @see https://apidocs.mailchimp.com/api/2.0/lists/subscribe.php + * @return { euid => a519e7b675, leid => 59171769, email => francois.barbut@gmail.com } + */ + public function subscribe(listId:String, email: { email:String }, merge_vars: { FNAME:String, LNAME:String, mc_language:String},custom_tags:Dynamic, double_option:Bool, update_existing:Bool, send_welcome:Bool ) { + //merge custom tags with merge_vars + if (custom_tags != null) { + for (f in Reflect.fields(custom_tags)) { + Reflect.setField(merge_vars, f, Reflect.getProperty(custom_tags, f)); + } + } + + var data = { + apikey:this.apiKey, + id:listId, + email:email, + merge_vars:merge_vars, + double_option:double_option, + update_existing:update_existing, + send_welcome:send_welcome + }; + + var curl = Curl.get(); + var res = curl.call("POST", serviceUrl + "lists/subscribe/", { }, haxe.Json.stringify(data)); + //Sys.println(curl.params); + return haxe.Json.parse(res); + } + + + //public function batchSuscribe(listId:String, + + +} \ No newline at end of file diff --git a/src/sugoi/apis/morning/MorningUp.hx b/src/sugoi/apis/morning/MorningUp.hx new file mode 100644 index 0000000..f7bd0e4 --- /dev/null +++ b/src/sugoi/apis/morning/MorningUp.hx @@ -0,0 +1,101 @@ +package sugoi.apis.morning; + +/** + * Morning Up Payment Service Connector + * + * @author fbarbut + * @date 2016-10-05 + * @doc https://up.morning.com + */ +class MorningUp +{ + + var token : String; + + public function new(token:String) + { + this.token = token; + } + + + public function createPayment(amount:Float,title:String,?type=null,order_id:String,back_url:String) { + + var amount = Std.string(amount); + title = title.substr(0, 255); + + var args = [ + '-X', + 'POST', + '-d', + 'token=$token', + '-d', + 'amount=$amount', + '-d', + 'title=$title', + '-d', + 'order_id=$order_id', + '-d', + 'back_url=$back_url', + 'https://up.morning.com/api/creer-un-paiement' + ]; + + return call("curl", args); + } + + public function paymentInfo(hash:String){ + + var args = [ + /*'-X', + 'GET', + '-d', + 'token=$token', + '-d', + 'hash=$hash', */ + 'https://up.morning.com/api/paiement-information?hash=$hash&token=$token' + ]; + + return call("curl", args); + } + + /** + * call via cURL + */ + function call(cmd:String,args:Array,?isJson=true):Dynamic { + var p = new sys.io.Process(cmd, args); + var r:String = null; + #if neko + r = neko.Lib.stringReference(p.stdout.readAll()); + #else + r = p.stdout.readAll().toString(); + #end + //error ? + if (r == null || r == "") { + #if neko + r = neko.Lib.stringReference(p.stderr.readAll()); + #else + r = p.stderr.readAll().toString(); + #end + } + p.exitCode(); + + if (r == null) throw "cUrl answer is null"; + if (r == "") throw "cUrl answer is an empty string"; + + if (!isJson) return r; + + var json:Dynamic = null; + try{ + json = haxe.Json.parse(r); + }catch (e:Dynamic){ + throw 'Error while parsing JSON "$r" : "$e"'; + } + if ( json == null ) throw "JSON result is null"; + + if (json.error==1 || json.error || json.error=="1") { + throw r; + } + + return json; + } + +} \ No newline at end of file diff --git a/src/sugoi/db/BufferedMail.hx b/src/sugoi/db/BufferedMail.hx new file mode 100644 index 0000000..173c1cd --- /dev/null +++ b/src/sugoi/db/BufferedMail.hx @@ -0,0 +1,132 @@ +package sugoi.db; +import sys.db.Types; +import sugoi.mail.IMail; +import sugoi.mail.IMailer; + +/** + * DB Buffer for emails + */ +@:index(remoteId,sdate,cdate) +class BufferedMail extends sys.db.Object +{ + public var id : SId; + + //email content + public var title : SString<256>; + public var htmlBody : SNull; + public var textBody : SNull; + public var headers : SData>; + public var sender : SData<{name:String,email:String,?userId:Int}>; + public var recipients : SData>; + + //utility fields + public var mailerType : SString<32>; //mailer used when sending for real + public var tries : SInt; //number of times we tried to send the mail + public var cdate : SDateTime; //creation date + public var sdate : SNull; //sent date + public var rawStatus : SNull; //raw return from the smtp server or mandrill API + public var status : SNull>; //map of emails with api/smtp results + + //custom datas + public var data : SNull>;//custom datas + public var remoteId : SNull; //custom remote Id (userId, groupId ...) + + + public function getMailerResultMessage(k:String):{failure:String, success:String}{ + var t = sugoi.i18n.Locale.texts; + var out = {failure:null, success:null}; + switch(status.get(k)){ + case tink.core.Outcome.Failure(f): + out.failure = switch(f){ + case GenericError(e): t._("Generic error: ") + e.toString(); + case HardBounce : t._("Mailbox does not exist"); + case SoftBounce : t._("Mailbox full or blocked"); + case Spam: t._("Message considered as spam"); + case Unsub: t._("This user unsubscribed"); + case Unsigned: t._("Sender incorrect (Unsigned)"); + + }; + case tink.core.Outcome.Success(s): + out.success = switch(s){ + case Sent : t._("Sent"); + case Queued : t._("Queued"); + }; + } + return out; + + } + + + public function new(){ + super(); + cdate = Date.now(); + tries = 0; + } + + public function isSent(){ + return sdate!=null; + } + + /** + * Finally really send the message + */ + public function finallySend():Void{ + + if(isSent()) throw "already sent"; + + var conf = { + smtp_host:sugoi.db.Variable.get("smtp_host"), + smtp_port:sugoi.db.Variable.getInt("smtp_port"), + smtp_user:sugoi.db.Variable.get("smtp_user"), + smtp_pass:sugoi.db.Variable.get("smtp_pass") + }; + + var mailer : sugoi.mail.IMailer = switch(this.mailerType){ + case "mandrill": + new sugoi.mail.MandrillMailer().init(conf); + case "smtp": + new sugoi.mail.SmtpMailer().init(conf); + case "debug": + new sugoi.mail.DebugMailer(); + default : + throw "Unknown mailer type : "+this.mailerType; + }; + + + var m = new sugoi.mail.Mail(); + for( k in this.headers.keys() ) m.setHeader( k,headers[k] ); + m.setSubject(this.title); + for( r in recipients) m.setRecipient(r.email,r.name,r.userId); + m.setSender(sender.email,sender.name,sender.userId); + m.setHtmlBody(this.htmlBody); + m.setTextBody(this.textBody); + + + this.lock(); + this.tries++; + + try{ + mailer.send(m,null,afterSendCb); + this.sdate = Date.now(); + this.rawStatus = null; + }catch(e:Dynamic){ + this.sdate = null; + this.rawStatus = Std.string(e); + // App.current.logError( Std.string(e) ); + } + + this.update(); + + } + + + function afterSendCb(status:MailerResult){ + //App.current.logError(status); + this.status = status; + this.update(); + } + + + + +} \ No newline at end of file diff --git a/src/sugoi/db/Cache.hx b/src/sugoi/db/Cache.hx new file mode 100644 index 0000000..56a2ef1 --- /dev/null +++ b/src/sugoi/db/Cache.hx @@ -0,0 +1,65 @@ +package sugoi.db; +import sys.db.Types; + +/** + * Key-Value temporary storage in a Memcached style + */ +@:id(name) +class Cache extends sys.db.Object +{ + public var name : SString<128>; + public var value : SText; + public var expire : SDateTime; + public var cdate : SNull; + + public function new(){ + super(); + cdate = Date.now(); + } + + /** + * Read the value for key $id + */ + public static function get(id:String):Dynamic { + if (Std.random(1000) == 0) Cache.manager.delete($expire < Date.now()); + + var c = manager.get(id, true); + if (c == null) return null; + if (c.expire.getTime() < Date.now().getTime()) { + c.delete(); + return null; + } + return haxe.Unserializer.run(c.value); + } + + /** + * Set a value for key $id + * and optionnaly a lifetime in seconds + */ + public static function set(id:String, value:Dynamic,expireInSeconds:Float) { + var c = manager.get(id, true); + var niou = false; + if (c == null) { + niou = true; + c = new Cache(); + c.name = id; + } + c.value = haxe.Serializer.run(value); + c.expire = DateTools.delta(Date.now(), expireInSeconds*1000.0); + if (niou) { + c.insert(); + }else { + c.update(); + } + } + + /** + * Delete a value for key $id + */ + public static function destroy(id:String) { + var c = manager.get(id, true); + if (c != null) c.delete(); + } + + +} \ No newline at end of file diff --git a/src/sugoi/db/Error.hx b/src/sugoi/db/Error.hx new file mode 100644 index 0000000..ed16737 --- /dev/null +++ b/src/sugoi/db/Error.hx @@ -0,0 +1,16 @@ +package sugoi.db; +import sys.db.Types; + +class Error extends sys.db.Object { + + public var id : SId; + public var date : SDateTime; + public var error : SText; + + @:relation(uid) public var user : SNull; + + public var ip : SNull>; + public var userAgent : SNull>; + public var url : SNull; + +} diff --git a/src/sugoi/db/File.hx b/src/sugoi/db/File.hx new file mode 100644 index 0000000..73e1999 --- /dev/null +++ b/src/sugoi/db/File.hx @@ -0,0 +1,77 @@ +package sugoi.db; +import sys.db.Manager; +import sys.db.Types; + +/** + * Store files in DB + */ + +class File extends sys.db.Object { + + public var id : SId; + public var name : STinyText; //filename + public var cdate : SNull; //creation datetime + public var data : SBinary; + + @:skip + static var CACHE = []; + + override public function new(){ + super(); + cdate = Date.now(); + } + + /** + * Get the file name related to this File record. + * Usually files should be generated in /file/ + */ + public static function makeSign( id : Int ) { + if( id == null ) + return ""; + var s = CACHE[id]; + if( s != null ) return s; + s = id+"_"+haxe.crypto.Md5.encode(id + App.config.get('key')); + CACHE[id] = s; + return s; + } + + public override function toString() { + return "#" + id + " " + name; + } + + /** + * Creates a File record + * from string data (typically sent from a form) and a file name + */ + public static function create(stringData:String, ?fileName=""):File { + + var bytes = new haxe.io.StringInput(stringData).readAll(); + return createFromBytes(bytes, fileName); + + } + + + public static function createFromBytes(b:haxe.io.Bytes, ?fileName=""):File { + + #if neko + var f = new File(); + f.name = fileName; + f.data = b; + f.insert(); + return f; + #else + //there is a bug in PHP, we do it manually + var hexa = b.toHex(); + Manager.cnx.request("INSERT INTO File (name,data) VALUES ('"+fileName+"', 0x"+hexa+")"); + return File.manager.select($name==fileName); + #end + + } + + public function getExtension():String { + if (name == null || name=="") return "jpg"; + + return name.split(".")[1]; + } + +} diff --git a/src/sugoi/db/Session.hx b/src/sugoi/db/Session.hx new file mode 100644 index 0000000..df83081 --- /dev/null +++ b/src/sugoi/db/Session.hx @@ -0,0 +1,135 @@ +package sugoi.db; +import sys.db.Types; +import db.User; +#if neko +import neko.Web; +#else +import php.Web; +#end + +@:id(sid) +@:index(uid,unique) +class Session extends sys.db.Object { + + public var sid : SString<32>; + public var ip : SString<15>; + public var lang : SString<2>; + public var messages : SData>; + public var lastTime : SDateTime; + public var createTime : SDateTime; + + #if neko + public var sdata : SNekoSerialized; + #else + public var sdata : SText; + #end + + @:skip public var data : Dynamic; + + //public var uid : SNull; + @:relation(uid) public var user : SNull; + + + + public function new() { + super(); + messages = []; + data = {}; + + } + + /** + * Stores a message in session + */ + public function addMessage( text : String, ?error=false ) { + messages.push({ error : error, text : text }); + } + + + public function setUser( u : User ):Void { + + //remove any previous session for this user + manager.delete($uid==u.id); + + lang = u.lang; + user = u; + update(); + + App.current.user = u; + } + + public override function update() { + #if neko + sdata = neko.Lib.serialize(data); + #else + sdata = haxe.Serializer.run(data); + #end + lastTime = Date.now(); + super.update(); + } + + private static function get( sid:String ):Session { + if ( sid == null ) return null; + + var s = manager.get(sid,true); + if ( s == null ) return null; + try { + #if neko + s.data = neko.Lib.localUnserialize(s.sdata); + #else + s.data = haxe.Unserializer.run(s.sdata); + #end + }catch (e:Dynamic) { + s.data = null; + } + + return s; + } + + + public static function init( sids : Array ) { + for( sid in sids ) { + var s = get(sid); + if( s != null ) return s; + } + var ip = Web.getClientIP(); + var s = new Session(); + s.ip = ip; + s.createTime = Date.now(); + s.lastTime = Date.now(); + + s.sid = generateId(); + var count = 20; + while( try { s.insert(); false; } catch( e : Dynamic ) true ) { + s.sid = generateId(); + // prevent infinite loop in SQL error + if( count-- == 0 ) { + s.insert(); + break; + } + } + + return s; + } + + /** + * Generate a random 32 chars string + */ + public static var S = "abcdefjhijklmnopqrstuvwxyABCDEFJHIJKLMNOPQRSTUVWXYZ0123456789"; + public static function generateId():String { + + var id = ""; + for ( x in 0...32 ) { + id += S.substr(Std.random(S.length),1); + } + return id; + } + + /** + * Delete sessions older than 1 month + */ + public static function clean() { + manager.delete($lastTime < DateTools.delta(Date.now(),-1000.0*60*60*24*30)); + } + +} diff --git a/src/sugoi/db/Variable.hx b/src/sugoi/db/Variable.hx new file mode 100644 index 0000000..7e499cb --- /dev/null +++ b/src/sugoi/db/Variable.hx @@ -0,0 +1,48 @@ +package sugoi.db; +import sys.db.Types; + +@:id(name) +class Variable extends sys.db.Object { + + public var name : SString<50>; + public var value : SString<50>; + + public static function get( name ) { + var v = manager.get(name,false); + return v == null ? null : v.value; + } + + public static function set(name, val:Dynamic) { + var v = Variable.manager.get(name,true); + if (v==null) { + v = new Variable(); + v.name = name; + v.value = Std.string(val); + v.insert(); + } + else { + v.value = Std.string(val); + v.update(); + } + } + + public static function increment(name, ?inc=1) { + var v = Variable.manager.get(name,true); + if (v==null) { + v = new Variable(); + v.name = name; + v.value = Std.string(inc); + v.insert(); + } + else { + v.value = Std.string(Std.parseInt(v.value)+1); + v.update(); + } + } + + public static function getInt( name ) { + var v = manager.get(name,false); + return v == null ? 0 : Std.parseInt(v.value); + } + +} diff --git a/src/sugoi/form/FieldSet.hx b/src/sugoi/form/FieldSet.hx new file mode 100644 index 0000000..32254a8 --- /dev/null +++ b/src/sugoi/form/FieldSet.hx @@ -0,0 +1,29 @@ +package sugoi.form; + +class FieldSet +{ + public var name:String; + public var form:Form; + public var label:String; + public var visible:Bool; + public var elements:Array>; + + public function new(?name:String = "", ?label:String = "", ?visible:Bool = true) + { + this.name = name; + this.label = label; + this.visible = visible; + + elements = []; + } + + public function getOpenTag() + { + return "
" + label + ""; + } + + public function getCloseTag() + { + return "
"; + } +} diff --git a/src/sugoi/form/Form.hx b/src/sugoi/form/Form.hx new file mode 100644 index 0000000..8eee9e1 --- /dev/null +++ b/src/sugoi/form/Form.hx @@ -0,0 +1,578 @@ +package sugoi.form; + +import haxe.crypto.Md5; +import sugoi.form.elements.Input; +import sugoi.i18n.translator.ITranslator; +import sugoi.form.elements.*; +import sugoi.Web; +import sys.db.Types; +import sys.db.Object; +import sys.db.Manager; +import sys.db.TableInfos; + +enum FormMethod +{ + GET; + POST; +} + +class Form +{ + public var id:String; + public var name:String; + public var action:String; + public var method:FormMethod; + public var elements:Array>; + public var fieldsets:Map; + public var forcePopulate:Bool; //the form is populated by web params if isValid() is called + public var submitButton:FormElement; + private var extraErrors:List; + public var requiredClass:String; + public var requiredErrorClass:String; + public var invalidErrorClass:String; + public var labelRequiredIndicator:String; + public var defaultClass : String; + public var multipart:Bool; + + public static var translator : ITranslator; + + //submit button + public var submitButtonLabel:String; + public var autoGenSubmitButton:Bool; //add a submit button automatically + + //conf + public static var USE_TWITTER_BOOTSTRAP = true; + public static var USE_DATEPICKER = true; //http://eonasdan.github.io/bootstrap-datetimepicker/ + + public var toString : Void->String; //you can change the way the form is rendered + + public function new(name:String, ?action:String, ?method:FormMethod) + { + requiredClass = "formRequired"; + requiredErrorClass = "formRequiredError"; + invalidErrorClass = "formInvalidError"; + labelRequiredIndicator = " *"; + defaultClass = Form.USE_TWITTER_BOOTSTRAP ? "form-horizontal":""; + + forcePopulate = true; + multipart = false; + autoGenSubmitButton = true; + + this.id = name; + this.name = name; + + if (action == null) { + this.action = Web.getURI(); + }else { + this.action = action; + } + + this.method = (method == null) ? FormMethod.POST : method; + + elements = new Array(); + extraErrors = new List(); + fieldsets = new Map(); + addFieldset("__default", new FieldSet("__default", "Default", false)); + + addElement(new CSRFProtection()); + + toString = render; + } + + /** + * Adds a form element to the form + * @param element + * @param ?fieldSetKey Add it to a specific fieldset + * @param ?index which index do u want to push it + * @return + */ + public function addElement(element:FormElement,?index:Int, ?fieldSetKey:String = "__default"):FormElement + { + element.parentForm = this; + if (index != null) { + var out = elements.slice(0, index); + out = out.concat([element]); + out = out.concat(elements.slice(index)); + elements = out; + }else { + elements.push(element); + } + + // add it to a group if requested + if (fieldSetKey != null){ + if (!fieldsets.exists(fieldSetKey)) throw "No fieldset '" + fieldSetKey + "' exists in '" + name + "' form."; + fieldsets.get(fieldSetKey).elements.push(element); + } + + //if ( Std.is(element, RichtextWym) ) + //wymEditorCount++; + + return element; + } + + public function removeElement(element:FormElement):Bool + { + if ( elements.remove(element) ) + { + element.parentForm= null; + for ( fs in fieldsets ) + { + fs.elements.remove(element); + } + + //if ( Std.is(element, RichtextWym) ) + //wymEditorCount--; + return true; + } + return false; + } + + public function setSubmitButton(el:FormElement):FormElement + { + submitButton = el; + submitButton.parentForm = this; + return el; + } + + public function addFieldset(fieldSetKey:String, fieldSet:FieldSet) + { + fieldSet.form = this; + fieldsets.set(fieldSetKey, fieldSet); + } + + public function getFieldsets():Map + { + return fieldsets; + } + + public function getLabel( elementName : String ) : String + { + return getElement( elementName ).getLabel(); + } + + public function getElement(name:String):FormElement { + if (name == null || name=='') throw "Element name is null"; + for (element in elements){ + if (element.name == name) return element; + } + return null; + } + + public function removeElementByName(name:String) { + var e = getElement(name); + if (e != null) removeElement(e); + } + + /** + * Get the typed value of a form element. + * The value can be of any type ! + * + * @param elementName + * @return + */ + public function getValueOf(elementName:String):Dynamic { + return getElement(elementName).value; + } + + public function getElementTyped(name:String, type:Class):T{ + var o:T = cast(getElement(name)); + return o; + } + + /** + * return datas contained in current form elements + * @return + */ + public function getData():Map + { + var data = new Map(); + for (element in getElements()) + { + if (element.name == null) throw "Element has no name : "+element.toString(); + data.set( element.name,element.getValue() ); + } + return data; + } + + /** + * return datas in an anonymous object + * @return + */ + public function getDatasAsObject():Dynamic { + + var data = { }; + for ( el in elements) { + Reflect.setField(data, el.name, el.value); + } + return data; + + } + + /** + * populate Form from anonymous object or if null from web params. + * @param custom + */ + public function populate(?custom:Dynamic){ + if (custom != null) { + //from object + for (element in getElements()) { + var n = element.name; + var v = Reflect.field(custom, n); + if (v != null) + element.value = v; + } + } else { + for (element in getElements()) { + //populate from web params + element.populate(); + } + } + } + + /** + * update a spod object from the content of the form + * @param data + * @param obj + */ + public function toSpod(obj:sys.db.Object) { + if (!isValid()) throw "submitted form should be valid"; + var data = getData(); + + //if not new object, lock it + var id = Std.parseInt(data.get("id")); + if (id == 0) id = null; + if (id != null) { + obj.lock(); + } + + for (f in data.keys()) { + + //check if field was in the original form + if (this.getElement(f) == null) throw "field '"+f+"' was not in the original form"; + var v = data.get(f); + if (f == "id") continue; + + //Values are already cleaned by each form elements when populated + /*if (Std.is(v, String)) { + v = StringTools.trim(v); + if (v == "") v = null; + }*/ + + //Debug : trace(f + " -> " + v+"
"); + try{ + Reflect.setProperty(obj, f, v); + }catch (e:Dynamic){ + throw "Error '" + e+"' while setting value " + v + " to " + f; + } + } + } + + /** + * Generate a form from any object + * @param obj + */ + public static function fromObject(obj:Dynamic) { + var form = new Form('fromObj'); + for (f in Reflect.fields(obj)) { + var val = Reflect.field(obj, f); + if (val == "") val = null; + form.addElement(new sugoi.form.elements.StringInput(f, f, val)); + } + return form; + } + + /* + * Generate a form from a spod object + */ + public static function fromSpod(obj:sys.db.Object) { + + //generate a form name + var cl = Type.getClass(obj); + var name = Type.getClassName(cl); + + var form = new Form("form"+Md5.encode(name)); + var ti = new TableInfos(Type.getClassName(Type.getClass(obj))); + + //translator + //var t = Form.translator; + var t = new Map(); + if (Reflect.hasField(cl, "getLabels")){ + t = Reflect.callMethod(cl, Reflect.getProperty(cl,"getLabels"),[]); + } + var label = function(s) return if (t.get(s) == null) s else t.get(s); + + //get metas of this object + var metas = haxe.rtti.Meta.getFields(Type.getClass(obj)); + + //loop on db object fields to create form elements + for (f in ti.fields) { + + var e : FormElement; + //field value + var v :Dynamic = Reflect.field(obj, f.name); + //trace( "field " + f.name+" of " + obj + " is " + v+"
"); + + //meta of this field + var meta :Dynamic = Reflect.field(metas, f.name); + //trace(f.name+"=>" + meta + "
"); + + //hide this field in forms + if (meta!=null && Reflect.hasField(meta,'hideInForms')) { + continue; + } + + //check if its a foreign key + var rl = Lambda.filter(ti.relations, function(r) return r.key == f.name ); + var isNull = ti.nulls.get(f.name); + + //foreign keys + if (rl.length > 0 ) { + + var r = rl.first(); + //trace(f.name + ' is a key for ' + r.key + "/"+r.prop); + var objects = new List(); + + meta = Reflect.field(metas, r.prop); + if (meta != null) { + //trace(r.prop+"=>" + meta + "
"); + if (meta.formPopulate != null) { + //If @formPopulate() meta is set, use this function to populate select box. + objects = Reflect.callMethod(obj, Reflect.field(obj,Std.string(meta.formPopulate[0])) , []); + } + + //if @hideInForms meta is set, hide the fields in the form + if (meta!=null && Reflect.hasField(meta,'hideInForms')) { + continue; + } + + }else { + //get all available values + objects = r.manager.all(false).map(function(d) { + return { + label : d.toString(), + value : Reflect.field(d,r.manager.table_keys[0]) + }; + }); + } + + e = new IntSelect(f.name, label(r.prop), Lambda.array(objects),v, !isNull); + + }else { + //not foreign key + + switch (f.type) { + case DId, DUId: + e = new IntInput(f.name, "id", v, false); + untyped e.inputType = ITHidden; + + case DEncoded: + e = new StringInput(f.name, label(f.name), v); + + case DFlags(fl, auto): + e = new Flags(f.name,label(f.name), Lambda.array(fl), Std.parseInt(v)); + + case DTinyInt, DUInt, DSingle, DInt: + e = new IntInput(f.name, label(f.name) , v , !isNull); + + case DFloat: + e = new FloatInput(f.name, label(f.name), v, !isNull ); + + case DBool : + e = new Checkbox(f.name, label(f.name), Std.string(v) == 'true'); + + case DString(n): + e = new StringInput(f.name,label(f.name), v, !isNull ,null,"maxlength="+n); + + case DTinyText, DSmallText, DText, DSerialized: + e = new TextArea(f.name, label(f.name), v,!isNull); + + case DTimeStamp, DDateTime: + + if (USE_DATEPICKER) { + + //WTF bugfix : the type is correct (Date) but is null when traced in DatePicker + var d :Date = cast v; + e = new DatePicker(f.name, label(f.name), d); + untyped e.format = "LLLL"; + }else { + e = new DateInput(f.name, label(f.name), v); + } + + case DDate : + + if (USE_DATEPICKER) { + //trace(f.name+" => " + v); + //trace(Type.getClassName(Type.getClass(v))); + + //WTF bugfix : the type is correct (Date) but is null when traced in DatePicker + var d :Date = cast v; + e = new DatePicker(f.name, label(f.name), d); + untyped e.format = "LL"; + }else { + e = new DateDropdowns(f.name, label(f.name), v); + } + + + case DEnum(name): + e = new sugoi.form.elements.Enum(f.name, label(f.name), name, Std.parseInt(v), !isNull); + + default : + e = new StringInput(f.name, label(f.name) , "unknown field type : "+f.type+", value : "+v); + } + } + + form.addElement(e); + } + return form; + } + + + public function clearData() + { + for (element in getElements()){ + element.value = null; + } + } + + /** + * Prints form open tag
+ */ + public function getOpenTag():String + { + //if there is a file input in the form, make it multipart + for ( e in elements) { + if (Type.getClass(e) == sugoi.form.elements.FileUpload || Type.getClass(e) == sugoi.form.elements.ImageUpload){ + multipart = true; + break; + } + } + return ''; + } + + /** + * Prints form close tag ...
+ */ + public function getCloseTag():String + { + var s = new StringBuf(); + s.add('
 
'); + s.add(''); + return s.toString(); + } + + public function isValid():Bool + { + if (!isSubmitted()) return false; + + populate(); + + var valid = true; + + for (element in getElements()){ + //trace(element.name+" -> "+element.value+" : "+element.isValid()+"
"); + element.filter(); + if (!element.isValid()) valid = false; + } + if (extraErrors.length > 0) valid = false; + return valid; + } + + public function checkToken() { + return isValid(); + } + + public function addError(error:String) + { + extraErrors.add(error); + } + + public function getErrorsList():List + { + isValid(); + + var errors:List = new List(); + + for(e in extraErrors) + errors.add(e); + + for (element in getElements()) + for (error in element.getErrors()) + errors.add(error); + + return errors; + } + + public function getElements():Array> + { + return elements; + } + + public function isSubmitted():Bool + { + //if (multipart){ + //var req = sugoi.tools.Utils.getMultipart(1024 * 1024 * 12); + //for ( r in req.keys() ) App.current.params.set(r, req.get(r)); + //} + + return App.current.params.get(name + "_formSubmitted") == "true"; + } + + public function getSubmittedValue():String + { + return App.current.params.get(name + "_formSubmitted"); + } + + public function getErrors():String + { + if (!isSubmitted()) + return ""; + + var s:StringBuf = new StringBuf(); + var errors = getErrorsList(); + + if (errors.length > 0) + { + if (USE_TWITTER_BOOTSTRAP) s.add('
'); + s.add("
    "); + for (error in errors) + { + s.add("
  • "+error+"
  • "); + } + s.add("
"); + if (USE_TWITTER_BOOTSTRAP) s.add('
'); + } + return s.toString(); + } + + /** + * Render form's HTML + */ + public function render() + { + + var s:StringBuf = new StringBuf(); + s.add(getOpenTag()); + + //errors + if (isSubmitted()) + s.add(getErrors()); + + for (element in getElements()) + if (element != submitButton && element.internal == false) + s.add("\t"+element.getFullRow()+"\n"); + + //submit button + if (submitButton != null) { + submitButton.parentForm = this; + }else if(autoGenSubmitButton){ + submitButton = new Submit('submit', submitButtonLabel != null ? submitButtonLabel : 'OK'); + submitButton.parentForm = this; + } + if(submitButton!=null) s.add(submitButton.getFullRow()); + + s.add(getCloseTag()); + + return s.toString(); + } + +} \ No newline at end of file diff --git a/src/sugoi/form/FormElement.hx b/src/sugoi/form/FormElement.hx new file mode 100644 index 0000000..cbd76a7 --- /dev/null +++ b/src/sugoi/form/FormElement.hx @@ -0,0 +1,242 @@ +package sugoi.form; + +import sugoi.form.filters.IFilter; +import sugoi.form.validators.Validator; +using StringTools; + +class FormElement +{ + public var parentForm:Form; + public var name:String; + public var label:String; + public var description:String; + + //value can be any type : Int, Float, Enum... + public var value:T; + + public var required:Bool; + public var errors:List; + public var attributes:String; + public var active:Bool; + + public var cssClass:String; + public var inited:Bool; + public var internal:Bool; + + public var validators:List>; + public var filters:List>; + + public function new() + { + active = true; + errors = new List(); + validators = new List(); + filters = new List(); + inited = false; + internal = false; + } + + /** + * apply all linked filter to the data + */ + public function filter() { + for ( f in filters) { + value = f.filter(value); + } + return value; + } + + /** + * Checks if the current value of the elements is valid + */ + public function isValid():Bool + { + errors.clear(); + + if (!active) return true; + + if ( value == null && required ) { + //required field is empty + errors.add("\"" + ((label != null && label != "") ? label : name) + "\" ne doit pas être vide."); + return false; + } + + if (value!=null) { + //check validity + if (!validators.isEmpty()){ + for (validator in validators) + { + if (!validator.isValid(value)) return false; + } + + } + + return true; + } + return true; + } + + public function init(){ + inited = true; + } + + public function addValidator(validator:Validator){ + validators.add(validator); + } + + public function addFilter(filter:IFilter) { + filters.add(filter); + } + + /** + * Fill the element with a value taken from the web params + */ + public function populate():Void + { + if (!inited) init(); + + var n = parentForm.name + "_" + name; + var v = App.current.params.get(n); + value = getTypedValue(v); + + //Debug + //trace("value of " + name +"("+n+") is " + v + ", typed :"+ value+"
"); + } + + /** + * From string (web param) to typed value. + * This method is in charge of cleaning the input which may be unsafe ( triming, escaping ...) + */ + public function getTypedValue(str:String):T{ + throw "getTypedValue() function not implemented in \""+name+"\""; + } + + public function getErrors():List + { + isValid(); + + for (val in validators) + for(err in val.errors) + errors.add("" + label + " : " + err); + + return errors; + } + + /** + * Render the element in HTML + */ + public function render():String + { + if (!inited) init(); + return Std.string(value); + } + + public function remove():Bool + { + if ( parentForm!= null ){ + return parentForm.removeElement(this); + } + return false; + } + + /** + * renders the element with label+tr+td... + */ + public function getFullRow():String { + var s = new StringBuf(); + if(Form.USE_TWITTER_BOOTSTRAP) s.add('
\n'); + s.add(getLabel()); + s.add("
" + this.render() + "
"); + if (Form.USE_TWITTER_BOOTSTRAP) s.add('
\n'); + return s.toString(); + } + + public function getType():String + { + return Std.string(Type.getClass(this)); + } + + /** + * Get CSS classes for the form element label + */ + public function getLabelClasses() : String + { + var css = ""; + if (Form.USE_TWITTER_BOOTSTRAP) css = "col-sm-4 control-label"; + + var requiredSet = false; + if (required) { + css += " "+parentForm.requiredClass; + if (parentForm.isSubmitted() && required && value == null) { + css += " "+parentForm.requiredErrorClass; + requiredSet = true; + } + } + if(!requiredSet && parentForm.isSubmitted() && !isValid()){ + css += " "+parentForm.invalidErrorClass; + } + + //if ( cssClass != null ) + //css += ( css == "" ) ? cssClass : " " + cssClass; + + return css; + } + + public function getLabel():String + { + var n = parentForm.name + "_" + name; + return ""; + } + + /** + * Return CSS classes of the element + */ + public function getClasses() : String + { + var css = ( cssClass != null ) ? cssClass : parentForm.defaultClass; + + if ( required && parentForm.isSubmitted() ) + { + if ( value == null ) + css += " " + parentForm.requiredErrorClass; + if ( !isValid() ) + css += " " + parentForm.invalidErrorClass; + } + if(css == null) css = ""; + return css.trim(); + } + + public function getErrorClasses() + { + var css = ""; + + if ( required && parentForm.isSubmitted() ) + { + if ( value == null ) + css += " " + parentForm.requiredErrorClass; + if ( !isValid() ) + css += " " + parentForm.invalidErrorClass; + } + + return css.trim(); + } + + private inline function safeString(s:Dynamic) { + return s == null ? "" : Std.string(s).htmlEscape().split('"').join("""); + } + + /** + * Renders the element in HTML + */ + public function toString() :String + { + return render(); + } + + /** + * get element value with the correct type + */ + public function getValue():T{ + return value; + } +} diff --git a/src/sugoi/form/FormMethod.hx b/src/sugoi/form/FormMethod.hx new file mode 100644 index 0000000..239a65d --- /dev/null +++ b/src/sugoi/form/FormMethod.hx @@ -0,0 +1,5 @@ +enum FormMethod +{ + GET; + POST; +} \ No newline at end of file diff --git a/src/sugoi/form/Formatter.hx b/src/sugoi/form/Formatter.hx new file mode 100644 index 0000000..620cdc9 --- /dev/null +++ b/src/sugoi/form/Formatter.hx @@ -0,0 +1,6 @@ +package sugoi.form; + +interface Formatter +{ + function format(data:Dynamic):String; +} \ No newline at end of file diff --git a/src/sugoi/form/ListData.hx b/src/sugoi/form/ListData.hx new file mode 100644 index 0000000..699a5a4 --- /dev/null +++ b/src/sugoi/form/ListData.hx @@ -0,0 +1,127 @@ +package sugoi.form; + +typedef FormData = Array<{label:String,value:T}>; + +class ListData +{ + public static function getDateElement( low : Int, high : Int, ?labels : Array ) : FormData + { + var data = []; + if ( labels != null ){ + for ( i in low ... high + 1 ) + data.push( { label:labels[i-1], value:i } ); + }else{ + for ( i in low ... high + 1 ){ + var n = Std.string(i); + data.push( { label:((i < 10) ? "0" + n : n), value: i } ); + } + } + return data; + } + + public static function getMinutes():FormData { + var data = []; + for ( i in 0...12) { + var x = i * 5; + data.push( {label: (x<10) ? "0"+Std.string(x) : Std.string(x) ,value: x } ); + } + return data; + } + + public static function fromArray(arr:Array) { + var data = []; + for (a in arr) { + data.push( {key:Std.string(a),value:Std.string(a) } ); + } + return data; + } + + public static function getDays(?reverse = true):Array<{label:String,value:Int}> + { + var data= []; + for (i in 1...31+1) { + data.push( { label:Std.string(i), value:i } ); + } + return(data); + } + + //public static var months_short = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + public static var months_short = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + //public static var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + public static var months = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Aout", "Septembre", "Octobre", "Novembre", "Décembre"]; + + /** + * Get months list + */ + public inline static function getMonths(?short = false):Array<{label:String,value:Int}> + { + var input = short ? months_short : months; + var out = []; + var c = 1; + for ( i in input) { + out.push( { label:i, value:c } ); + c++; + } + return out; + } + + /** + * get years list + */ + public static function getYears(from:Int, to:Int, ?reverse = true):Array<{label:String,value:Int}> + { + var data = []; + + if (reverse){ + for (i in 0...(to-from+1)) { + var n = to - i; + data.push( { label:Std.string(n), value:n } ); + } + }else { + for (i in 0...(to-from+1)) { + var n = from + i; + data.push( { label:Std.string(n), value:n } ); + } + } + return(data); + } + + /*public static function getLetters(uppercase=false){ + if (uppercase) return(array(a=>"A", b=>"B", c=>"C", d=>"D", e=>"E", f=>"F", g=>"G", h=>"H", i=>"I", j=>"J", k=>"K", l=>"L", m=>"M", n=>"N", o=>"O", p=>"P", q=>"Q", r=>"R", s=>"S", t=>"T", u=>"U", v=>"V", w=>"W", x=>"X", y=>"Y", z=>"Z")); + return(array(a=>"a", b=>"b", c=>"c", d=>"d", e=>"e", f=>"f", g=>"g", h=>"h", i=>"i", j=>"j", k=>"k", l=>"l", m=>"m", n=>"n", o=>"o", p=>"p", q=>"q", r=>"r", s=>"s", t=>"t", u=>"u", v=>"v", w=>"w", x=>"x", y=>"y", z=>"z")); + }*/ + + public static function hashToList(hash:Map, ?startCounter:Int=0):List + { + var data:List = new List(); + + for (key in hash.keys()) + { + data.add( { key:key, value:hash.get(key) } ); + } + return data; + } + + public static function arrayToList(array:Array, ?startCounter:Int=0):List + { + var data:List = new List(); + + var c = startCounter; + for (v in array) + { + data.add( { key:c, value:v } ); + c++; + } + return data; + } + + public static function flatArraytoList(array:Array):List + { + var data:List = new List(); + + for (i in array) data.add( { key:i, value:i } ); + + return data; + } + +} \ No newline at end of file diff --git a/src/sugoi/form/Rules.hx b/src/sugoi/form/Rules.hx new file mode 100644 index 0000000..6b8ee00 --- /dev/null +++ b/src/sugoi/form/Rules.hx @@ -0,0 +1,26 @@ +package sugoi.form; + +class Rules +{ + + public function new() + { + + } + + public static function isNumber(element:FormElement) + { + + } + + public static function greaterThan(element:FormElement, value) + { + + } + + public static function lessThan(element:FormElement, value) + { + + } + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/Button.hx b/src/sugoi/form/elements/Button.hx new file mode 100644 index 0000000..cb75c67 --- /dev/null +++ b/src/sugoi/form/elements/Button.hx @@ -0,0 +1,63 @@ +package sugoi.form.elements; + +import form.Form; +import form.FormElement; + + +class Button extends FormElement +{ + public var type:ButtonType; + + //public function new(name:String, label:String, ?value:String = "Submit", ?type:ButtonType = null) + public function new(name:String, label:String, ?value:String = null, ?type:ButtonType = null) + { + super(); + this.name = name; + this.label = label; + this.value = value; + this.type = (type == null) ? ButtonType.SUBMIT : type; + } + + override public function isValid():Bool + { + return true; + } + + override public function render() :String + { + return ""; + + } + + public function toString() :String + { + return render(); + } + + override public function getLabel():String + { + var n = parentForm.name + "_" + name; + + return ""; + } + + override public function getPreview():String + { + return "" + this.render() + ""; + } + + override public function populate():Void + { + super.populate(); + var n = parentForm.name + "_" + name; + if ( App.current.params.exists(n) ) + parentForm.submittedButtonName = name; + } +} + +enum ButtonType +{ + SUBMIT; + BUTTON; + RESET; +} \ No newline at end of file diff --git a/src/sugoi/form/elements/CSRFProtection.hx b/src/sugoi/form/elements/CSRFProtection.hx new file mode 100644 index 0000000..5e6b52d --- /dev/null +++ b/src/sugoi/form/elements/CSRFProtection.hx @@ -0,0 +1,44 @@ +package sugoi.form.elements; +import sugoi.form.FormElement; +import sugoi.form.elements.Input; +#if neko +import neko.Web; +#else +import php.Web; +#end + +/** + * creates a hidden token in forms to avoid CSRF + */ +class CSRFProtection extends StringInput +{ + + public function new() + { + + value = haxe.crypto.Md5.encode(App.current.session.sid + App.config.KEY.substr(0, 5)); + super("token","", value, true); + inputType = ITHidden; + } + + override public function isValid() { + if (value == null) throw "empty token"; + var valid = Web.getParams().get(parentForm.name + "_" + name) == value; + + if (!valid) { + errors.add("Bad token"); + } + return valid; + } + + + override public function getFullRow() { + return render(); + } + + override public function render() { + + return ""; + + } +} \ No newline at end of file diff --git a/src/sugoi/form/elements/Checkbox.hx b/src/sugoi/form/elements/Checkbox.hx new file mode 100644 index 0000000..41aa3d3 --- /dev/null +++ b/src/sugoi/form/elements/Checkbox.hx @@ -0,0 +1,45 @@ +package sugoi.form.elements; + +import sugoi.form.Form; +import sugoi.form.FormElement; + +class Checkbox extends FormElement +{ + + public function new(name:String, label:String, ?checked:Bool=false, ?required:Bool=false, ?attibutes:String="") + { + super(); + + this.name = name; + this.label = label; + this.value = checked; + this.required = required; + this.attributes = attibutes; + } + + override public function render():String + { + var n = parentForm.name + "_" +name; + + var checkedStr = value ? "checked" : ""; + + return ""; + } + + + override public function getTypedValue(str:String):Bool + { + return str == "1" || str == "true"; + } + + override public function isValid():Bool + { + errors.clear(); + if ( required && value == null ) + { + errors.add("Please check '" + ((label != null && label != "") ? label : name) + "'"); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/sugoi/form/elements/CheckboxGroup.hx b/src/sugoi/form/elements/CheckboxGroup.hx new file mode 100644 index 0000000..f9500ab --- /dev/null +++ b/src/sugoi/form/elements/CheckboxGroup.hx @@ -0,0 +1,104 @@ +package sugoi.form.elements; +import sugoi.form.Form; +import sugoi.form.FormElement; +import sugoi.form.Formatter; +import sugoi.form.ListData; + +/** + * Manage an array of string with a checkbox group + */ +class CheckboxGroup extends FormElement> +{ + public var data:Array; + public var selectMessage:String; + public var labelLeft:Bool; + public var verticle:Bool; + public var labelRight:Bool; + public var formatter:Formatter; + public var columns:Int; + + public function new(name:String, label:String,data:FormData, ?selected:Array, ?verticle:Bool=true, ?labelRight:Bool=true) + { + super(); + this.name = name; + this.label = label; + this.data = data; + this.value = selected != null ? selected : new Array(); + this.verticle = verticle; + this.labelRight = labelRight; + + columns = 1; + } + + override public function populate() + { + + var v = Web.getParamValues(parentForm.name + "_" + name); + + if (parentForm.isSubmitted()) + { + value = (v != null) ? v : []; + } else { + if (v != null) value = v; + } + } + + override public function render():String + { + var s = ""; + var n = parentForm.name + "_" +name; + + var tagCss = getClasses(); + var labelCss = getLabelClasses(); + + var c = 0; + var datas = Lambda.array(data); + if (datas != null) + { + var rowsPerColumn = Math.ceil(datas.length / columns); + s = ""; + for (i in 0...columns) + { + s += ""; + } + s += "
\n"; + s += "\n"; + + for (j in 0...rowsPerColumn) + { + if (c >= datas.length) break; + + s += ""; + + var row:Dynamic = datas[c]; + + var checkbox = "\n"; + var label; + + if (formatter != null){ + label = "\n"; + //}else if(Form.translator!=null){ + //label = "\n"; + }else { + label = "\n"; + } + + if (labelRight) + { + s += "\n"; + s += "\n"; + } else { + s += "\n"; + s += "\n"; + } + s += ""; + c++; + } + s += "
" + checkbox + " " + label + "" + label + " " + checkbox + "
"; + s += "
\n"; + } + + return s; + } + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/DateDropdowns.hx b/src/sugoi/form/elements/DateDropdowns.hx new file mode 100644 index 0000000..35b8a28 --- /dev/null +++ b/src/sugoi/form/elements/DateDropdowns.hx @@ -0,0 +1,142 @@ +package sugoi.form.elements; + +import sugoi.Web; +import sugoi.form.FormElement; +import sugoi.form.validators.Validator; +import sugoi.form.ListData; + +/** + * A list of selectBox for day + month + year + */ +class DateDropdowns extends FormElement +{ + public var maxOffset:Int; + public var minOffset:Int; + + //public var date : Date; //valeur typée à la place de value:Dynamic + + public var yearMin:Int; + public var yearMax:Int; + + private var daySelector:Selectbox; + private var monthSelector:Selectbox; + private var yearSelector:Selectbox; + + public function new(name:String, label:String, ?_value:Date, ?required:Bool=false, yearMin:Int=1950, yearMax:Int=null, ?validators:Array>, ?attibutes:String="") + { + super(); + this.name = name; + this.label = label; + + if (_value == null) { + value = Date.now(); + }else { + value = _value; + } + + this.required = required; + this.attributes = attibutes; + this.yearMin = yearMin; + this.yearMax = yearMax; + + maxOffset = null; + minOffset = null; + + var day :Int = null; + var month :Int = null; + var year :Int = null; + + if (value != null) + { + day = value.getDate(); + month = (value.getMonth()+1); + year = value.getFullYear(); + } + + var t = sugoi.form.Form.translator; + daySelector = new IntSelect(name+"_day", t._("day"),ListData.getDays(),day,true); + monthSelector = new IntSelect(name+"_month", t._("month"),ListData.getMonths(),month,true); + yearSelector = new IntSelect(name+"_year", t._("year"), ListData.getYears(Date.now().getFullYear()-3, Date.now().getFullYear()+3, true), year, true); + + daySelector.internal = monthSelector.internal = yearSelector.internal = true; + + //if (Form.USE_TWITTER_BOOTSTRAP) { + //daySelector.cssClass = "input-mini"; + //} + //trace("date : " + date); + } + public function shortLabels() + { + daySelector.nullMessage = "-D-"; + monthSelector.nullMessage = "-M-"; + yearSelector.nullMessage = "-Y-"; + monthSelector.data = ListData.getMonths(true); + } + + override public function init() + { + super.init(); + + parentForm.addElement(daySelector); + parentForm.addElement(monthSelector); + parentForm.addElement(yearSelector); + } + + override public function populate() + { + + var day = Std.parseInt(App.current.params.get(parentForm.name + "_" + daySelector.name)); + var month = Std.parseInt(App.current.params.get(parentForm.name + "_" + monthSelector.name)); + var year = Std.parseInt(App.current.params.get(parentForm.name + "_" + yearSelector.name)); + + value = (day != null && month != null && year != null ) ? new Date(year, month - 1, day, 0, 0, 0) : null; + } + + override public function isValid():Bool + { + /*var valid = super.isValid(); + + if ( required && valid ) + { + var n = form.name + "_" + name; + var day = Std.parseInt(App.current.params.get(n)); + var month = Std.parseInt(App.current.params.get(n)); + var year = Std.parseInt(App.current.params.get(n)); + + if (day == null || month == null || year == null ) + { + errors.add("" + ((label != null && label != "") ? label : name) + " is an invalid date."); + return false; + } + return true; + } + + return valid;*/ + return true; + } + + + + override public function render():String + { + super.render(); + + if (value != null) + { + try{ + var v:Date = cast value; + daySelector.value = v.getDate(); + monthSelector.value = v.getMonth()+1; + yearSelector.value = v.getFullYear(); + }catch(e:Dynamic){} + } + + return '
+
'+daySelector.render()+'
+
'+monthSelector.render()+'
+
'+yearSelector.render()+'
+
'; + } + + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/DateInput.hx b/src/sugoi/form/elements/DateInput.hx new file mode 100644 index 0000000..0b716b5 --- /dev/null +++ b/src/sugoi/form/elements/DateInput.hx @@ -0,0 +1,72 @@ +package sugoi.form.elements; +import sugoi.form.FormElement; +import sugoi.form.validators.Validator; +import sugoi.form.ListData; + +/** + * date selectBox : day month year hour minutes + */ +class DateInput extends DateDropdowns +{ + private var hourSelector:Selectbox; + private var minuteSelector:Selectbox; + + public function new(name:String, label:String, ?value:Date, ?required:Bool=false, yearMin:Int=1950, yearMax:Int=null, ?validators:Array>, ?attibutes:String="") + { + super(name, label, value, required, yearMin, yearMax, validators, attibutes); + var t = sugoi.form.Form.translator; + hourSelector = new Selectbox(name+"_hour" , t._("hour") , ListData.getDateElement(0,23), value.getHours(),true,"-",'title="Hour"'); + minuteSelector = new Selectbox(name+"_minute", t._("minute"), ListData.getDateElement(0, 59), value.getMinutes(), true, "-", 'title="Minute"'); + + if (Form.USE_TWITTER_BOOTSTRAP) { + hourSelector.cssClass = "form-control"; + minuteSelector.cssClass = "form-control"; + } + + } + + override public function isValid() { + return true; + } + + + override public function render():String + { + hourSelector.parentForm = this.parentForm; + minuteSelector.parentForm = this.parentForm; + + var s = super.render() + " : "; + + if (value != null){ + var v:Date = cast value; + hourSelector.value = v.getHours(); + minuteSelector.value = v.getMinutes(); + } + s += hourSelector.render() + " h "; + s += minuteSelector.render() + " m "; + return s; + } + + override public function populate() + { + //super.populate(); + var n = parentForm.name + "_" + hourSelector.name; + var v = App.current.params.get(n); + var params = App.current.params; + + if (v != null) + { + var minute = Std.parseInt(params.get(parentForm.name + "_" + minuteSelector.name)); + var hour = Std.parseInt(params.get(parentForm.name + "_" + hourSelector.name)); + var day = Std.parseInt(params.get(parentForm.name + "_" + daySelector.name)); + var month = Std.parseInt(params.get(parentForm.name + "_" + monthSelector.name)); + var year = Std.parseInt(params.get(parentForm.name + "_" + yearSelector.name)); + + value = new Date(year, month - 1, day, hour, minute, 0); + + } + + + + } +} diff --git a/src/sugoi/form/elements/DatePicker.hx b/src/sugoi/form/elements/DatePicker.hx new file mode 100644 index 0000000..efcd51d --- /dev/null +++ b/src/sugoi/form/elements/DatePicker.hx @@ -0,0 +1,118 @@ +package sugoi.form.elements; + +import sugoi.Web; +import sugoi.form.FormElement; +import sugoi.form.validators.Validator; +import sugoi.form.ListData; + +/** + * DatePicker for Bootstrap 3 + * + * You'll need to install some additionnal js librairies (moment.js, jquery) + * more info at : http://eonasdan.github.io/bootstrap-datetimepicker/ + */ +class DatePicker extends FormElement +{ + public var maxOffset:Int; + public var minOffset:Int; + + public var yearMin:Int; + public var yearMax:Int; + + private var daySelector:Selectbox; + private var monthSelector:Selectbox; + private var yearSelector:Selectbox; + + public var format : String; //moment.js format + + public function new(name:String, label:String, ?v:Date, ?required:Bool=false, yearMin:Int=1950, yearMax:Int=null, ?validators:Array>, ?attibutes:String="") + { + + super(); + this.name = name; + this.label = label; + format = 'LLLL'; + + if (v == null) { + this.value = Date.now(); + }else { + this.value = v; + } + + //trace(value); + + this.required = required; + this.attributes = attibutes; + this.yearMin = yearMin; + this.yearMax = yearMax; + + maxOffset = null; + minOffset = null; + + var day = ""; + var month = ""; + var year = ""; + + if (value != null) + { + day = ""+value.getDate(); + month = ""+(value.getMonth()+1); + year = ""+value.getFullYear(); + } + + } + + override public function populate() + { + //data is stored as float in the html form element + var d = App.current.params.get(parentForm.name + "_" + name); + //trace(parentForm.name + "_" + name+"="+d); + //value = Date.fromTime(Std.parseFloat(d)); + value = Date.fromString(d); + } + + override public function isValid():Bool + { + return true; + } + + override public function render():String + { + + //component init date + //var d = value.getFullYear() +"-" + (value.getMonth() + 1) + "-" + value.getDate() + " " + value.getHours() + ":" + value.getMinutes()+":00"; + var d = value.toString(); + var defaultDate = 'moment("' + d + '", "YYYY-MM-DD HH:mm:ss")'; + + return " +
+ + + + +
+ + + "; + + } + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/EmbeddedVideoOptions.hx b/src/sugoi/form/elements/EmbeddedVideoOptions.hx new file mode 100644 index 0000000..40ef3f0 --- /dev/null +++ b/src/sugoi/form/elements/EmbeddedVideoOptions.hx @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2008, TouchMyPixel & contributors + * Original author : Matt Benton + * Contributers: Tarwin Stroh-Spijer + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE TOUCH MY PIXEL & CONTRIBUTERS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE TOUCH MY PIXEL & CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + + +package form.elements; + +import form.Form; +import form.FormElement; +import poko.Poko; + +enum EmbeddedVideoService +{ + youtube; + vimeo; +} + +typedef EmbeddedVideoConfig = +{ + var videoID:String; + var width:Int; + var height:Int; +} + +typedef VimeoConfig = +{ > EmbeddedVideoConfig, + var color:String; + var showPortrait:Bool; + var showTitle:Bool; + var showByline:Bool; +} + +class EmbeddedVideoOptions extends FormElement +{ + // Type of video service + public var service:EmbeddedVideoService; + /** + * Common options + */ + //public var videoID:String; + //public var width:Int; + //public var height:Int; + /** + * Vimeo options + */ + // Can be Blue, Orange, Lime, Fuschia, White or #RRGGBB + //public var color:String; + //public var showPortrait:Bool; + //public var showTitle:Bool; + //public var showByline:Bool; + + public var vimeo (default, null) : VimeoConfig; + + public function new(name:String, label:String, service:EmbeddedVideoService) + { + super(); + + this.name = name; + this.label = label; + this.service = service; + } + + override public function render():String + { + var n = form.name + "_" + name; + if ( service == EmbeddedVideoService.vimeo ) + { + var color = new Input(n + "Color", "Color", "Blue"); + + } + return null; + } + + public function toString() :String + { + return render(); + } + + override public function populate():Void + { + } +} + + + +/*var date:Date = cast value; + var year = date.getFullYear(); + var month = date.getMonth(); + var day = date.getDate(); + + var l = new List(); + var s = ""; + + var elYear = new Selectbox(form, "1", ListData.getYears(1990, 2000, true), Std.string(year), false, ""); + var elMonth = new Selectbox(form, "2", ListData.getMonths(), Std.string(year), false); + var elDay = new Selectbox(form, "3", ListData.getDays() , Std.string(year), false); + + form.addElement(name + "[]", elYear); + form.addElement(name + "[]", elMonth); + form.addElement(name + "[]", elDay); + + s += elYear.toString(); + s += elMonth.toString(); + s += elDay.toString(); + + form.initElements(); + */ \ No newline at end of file diff --git a/src/sugoi/form/elements/Enum.hx b/src/sugoi/form/elements/Enum.hx new file mode 100644 index 0000000..3ea574a --- /dev/null +++ b/src/sugoi/form/elements/Enum.hx @@ -0,0 +1,128 @@ +package sugoi.form.elements; +import sugoi.form.Form; +import sugoi.form.FormElement; +import sugoi.form.Formatter; +import sugoi.form.elements.Flags; + +class Enum extends FormElement +{ + public var enumName:String; + public var selectMessage:String; + public var labelLeft:Bool; + public var verticle:Bool; + public var labelRight:Bool; + var checked : Array; + + + public var columns:Int; + + /** + * + * @param name + * @param label + * @param data list of enums + * @param value int (enum index) + * @param ?verticle + * @param ?labelRight + */ + public function new(name:String, label:String, enumName:String, value:Int, ?required=false, ?verticle:Bool=false, ?labelRight:Bool=true) + { + super(); + this.name = name; + this.label = label; + this.enumName = enumName; + + this.verticle = verticle; + this.labelRight = labelRight; + + //trace("value = " + value); + + if (required && value == null){ + this.value = /*Type.resolveEnum(enumName).createByIndex(0)*/0; + }else{ + this.value = value; + } + + columns = 1; + } + + override function getTypedValue(str:String):Int { + if (str == null) return null; + + str = StringTools.trim(str); + if (str == "") { + return null; + }else{ + return Std.parseInt(str); + } + } + + override function getValue(){ + if (value == null) return null; + return Type.resolveEnum(enumName).createByIndex(value); + } + + override public function render():String + { + var s = ""; + var n = parentForm.name + "_" +name; + + var tagCss = getClasses(); + //no label css otherwise the style col-sm-4 will be added, and we dont want that + // as its for the left column labels + //var labelCss = getLabelClasses(); + + var c = 0; + + var array = Type.allEnums(Type.resolveEnum(enumName)); + + var rowsPerColumn = Math.ceil(array.length / columns); + s = ""; + for (i in 0...columns) + { + s += ""; + } + s += "
\n"; + s += "\n"; + + for (j in 0...rowsPerColumn) + { + if (c >= array.length) break; + + s += ""; + + var row:Dynamic = array[c]; + var checked = value == Type.enumIndex(row); + var checkbox = "\n"; + var label; + + var t = Form.translator; + if (t == null){ + label = "\n"; + }else{ + label = "\n"; + } + + + if (labelRight) + { + s += " \n"; + s += "\n"; + } else { + s += " \n"; + s += "\n"; + } + + s += ""; + c++; + } + s += "
" + checkbox + "" + label + "" + label + "" + checkbox + "
"; + s += "
\n"; + + + + return s; + } + + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/FileUpload.hx b/src/sugoi/form/elements/FileUpload.hx new file mode 100644 index 0000000..21ec5fd --- /dev/null +++ b/src/sugoi/form/elements/FileUpload.hx @@ -0,0 +1,87 @@ +package sugoi.form.elements; +import haxe.crypto.Md5; +import haxe.Timer; +import sugoi.form.Form; +import sys.io.File; + +/** + * Manage an element. + */ +class FileUpload extends FormElement +{ + public var fileName: String; + public var maxSize : Int; //Max file size in Mb + + public function new(name:String, label:String, ?value:haxe.io.Bytes, ?required:Bool=false, toFolder:String=null, ?keepFullFileName:Bool=true ) + { + super(); + this.name = name; + this.label = label; + this.value = value; + this.required = required; + fileName = null; + maxSize = 6; + } + + override public function getTypedValue(s:String):haxe.io.Bytes + { + var request = sugoi.tools.Utils.getMultipart(1024 * 1024 * maxSize); + + //trace(request.toString()); + + var strData = request.get(parentForm.name + "_" + name); + fileName = request.get(parentForm.name + "_" + name+"_filename"); + + return new haxe.io.StringInput(strData).readAll(); + + + } + + override public function render():String + { + var n = parentForm.name + "_" +name; + //var path = toFolder.substr((Sys.getCwd() + "tmp/").length); + //var path = toFolder; + + var str:String = ""; + + //str += ''+getOriginalFileName()+'
'; + str += ''; + //if (!required && value != '' && value != null) str += '[ remove ]'; + //str += ''; + //str += ''; + + return str; + } + + /** + * Contains MD5 and original filename. + */ + //public function getFileName() + //{ + //if (keepFullFileName) + //{ + //var s = Std.string(value); + //return s.substr(s.lastIndexOf("/") + 1); + //} else { + //return value; + //} + //} + + /** + * Orginal filename. + */ + //public function getOriginalFileName() + //{ + //if (keepFullFileName) + //{ + //var s = Std.string(value); + //return s.substr(s.lastIndexOf("/") + 33); + //} else { + //return Std.string(value).substr(33); + //} + //} + + + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/Flags.hx b/src/sugoi/form/elements/Flags.hx new file mode 100644 index 0000000..47b6046 --- /dev/null +++ b/src/sugoi/form/elements/Flags.hx @@ -0,0 +1,175 @@ +package sugoi.form.elements; +import sugoi.form.Form; +import sugoi.form.FormElement; +import sugoi.form.Formatter; +#if php +import php.Web; +#else +import neko.Web; +#end + + +enum FakeFlag { + Flag1; + Flag2; + Flag3; + Flag4; + Flag5; + Flag6; + Flag7; + Flag8; + Flag9; + Flag10; + Flag11; + Flag12; + Flag13; + Flag14; + Flag15; + Flag16; + Flag17; + Flag18; + Flag19; + Flag20; + Flag21; + Flag22; + Flag23; + Flag24; + Flag25; + Flag26; + Flag27; + Flag28; + Flag29; + Flag30; + Flag31; + Flag32; +} + +/** + * Manage flags stored in an Int , various flags are defined by an Enum + */ +class Flags extends FormElement +{ + public var data:Array; + public var selectMessage:String; + public var labelLeft:Bool; + public var verticle:Bool; + public var labelRight:Bool; + var checked : Array; + + public var columns:Int; + + /** + * + * @param name + * @param label + * @param data list of enums + * @param value int + * @param ?verticle + * @param ?labelRight + */ + public function new(name:String, label:String, data:Array, value:Int, ?verticle:Bool=true, ?labelRight:Bool=true) + { + super(); + this.name = name; + this.label = label; + this.data = data; + this.value = value; + this.verticle = verticle; + this.labelRight = labelRight; + if (value == null) value = 0; + + checked = []; + var i = 0; + for( f in data) { + checked.push( value & (1 << i) != 0 ); + i++; + } + + columns = 1; + } + + override public function populate() + { + + var v = Web.getParamValues(parentForm.name + "_" + name); + value = 0; + + if (v != null) { + //App.log("flags populate : " + v ); + var val = new haxe.EnumFlags(); + //var i = 0; + for (vv in v) { + val.set( FakeFlag.createByIndex(Std.parseInt(vv)) ); + //i++; + } + + value = val.toInt(); + } + + + //if (form.isSubmitted()){ + //value = (v != null) ? v : new Array(); + //} else { + //if (v != null) value = v; + //} + } + + override public function render():String + { + var s = ""; + var n = parentForm.name + "_" +name; + + var tagCss = getClasses(); + var labelCss = getLabelClasses(); + + var c = 0; + var array = Lambda.array(data); + if (array != null) + { + //trace("L" + array.length); + var rowsPerColumn = Math.ceil(array.length / columns); + s = ""; + for (i in 0...columns) + { + s += ""; + } + s += "
\n"; + s += "\n"; + + for (j in 0...rowsPerColumn) + { + if (c >= array.length) break; + + s += ""; + + var row:Dynamic = array[c]; + + var checkbox = "\n"; + var label; + + var t = Form.translator; + + label = "\n"; + + + if (labelRight) + { + s += "\n"; + s += "\n"; + } else { + s += "\n"; + s += "\n"; + } + s += ""; + + c++; + } + s += "
" + checkbox + "" + label + "" + label + "" + checkbox + "
"; + s += "
\n"; + + } + + return s; + } + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/FloatInput.hx b/src/sugoi/form/elements/FloatInput.hx new file mode 100644 index 0000000..801ecea --- /dev/null +++ b/src/sugoi/form/elements/FloatInput.hx @@ -0,0 +1,24 @@ +package sugoi.form.elements; +import sugoi.form.filters.FloatFilter; + +class FloatInput extends Input +{ + + public function new(name, label, value, ?required=false){ + super(name, label, value, required); + } + + override public function getTypedValue(str:String):Float{ + + var f = new FloatFilter(); + var n = f.filterString(str); + + if (n==null && this.required){ + return 0.0; + }else{ + return n; + } + + } + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/FloatSelect.hx b/src/sugoi/form/elements/FloatSelect.hx new file mode 100644 index 0000000..38e5954 --- /dev/null +++ b/src/sugoi/form/elements/FloatSelect.hx @@ -0,0 +1,19 @@ +package sugoi.form.elements; + +/** + * ... + * @author fbarbut + */ +class FloatSelect extends Selectbox +{ + + override function getTypedValue(str:String):Float{ + str = StringTools.trim(str); + if (str == "" || str==null) { + return null; + }else{ + return Std.parseFloat(str); + } + } + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/HourDropDowns.hx b/src/sugoi/form/elements/HourDropDowns.hx new file mode 100644 index 0000000..e61d9ee --- /dev/null +++ b/src/sugoi/form/elements/HourDropDowns.hx @@ -0,0 +1,92 @@ +package sugoi.form.elements; + +import sugoi.Web; +import sugoi.form.FormElement; +import sugoi.form.validators.Validator; +import sugoi.form.ListData; + +class HourDropDowns extends FormElement +{ + var hourSelector:Selectbox; + var minuteSelector:Selectbox; + + public function new(name:String, label:String, ?_value:Date, ?required:Bool=false,?attributes="") + { + super(); + this.name = name; + this.label = label; + + if (_value == null) { + value = Date.now(); + }else { + value = _value; + } + + this.required = required; + this.attributes = attributes; + + var hours = 0; + var minutes = 0; + + if (value != null) + { + hours = value.getHours(); + minutes = value.getMinutes(); + } + + var t = sugoi.form.Form.translator; + + hourSelector = new IntSelect(name+"_hour", t._("hour"), ListData.getDateElement(0, 23), value.getHours(), true, "-", 'title="Hour"'); + minuteSelector = new IntSelect(name+"_minute", t._("minute"), ListData.getMinutes(), value.getMinutes(), true, "-", 'title="Minute"'); + + hourSelector.internal = minuteSelector.internal = true; + + if (Form.USE_TWITTER_BOOTSTRAP) { + minuteSelector.cssClass = "form-control"; + hourSelector.cssClass = "form-control"; + } + + } + + + override public function init() + { + super.init(); + + parentForm.addElement(hourSelector); + parentForm.addElement(minuteSelector); + } + + override public function populate() + { + var hour = Std.parseInt(App.current.params.get(parentForm.name + "_" + hourSelector.name)); + var minute = Std.parseInt(App.current.params.get(parentForm.name + "_" + minuteSelector.name)); + var now = Date.now(); + value = (hour!= null && minute != null) ? new Date(now.getFullYear(),now.getMonth(), now.getDay(), hour, minute, 0) : null; + } + + override public function isValid():Bool + { + return super.isValid(); + } + + override public function render():String{ + super.render(); + var s = ""; + if (value != null){ + try{ + var v:Date = cast value; + hourSelector.value = v.getHours(); + minuteSelector.value = v.getMinutes(); + }catch(e:Dynamic){} + } + + s += hourSelector.render(); + s += " : "; + s += minuteSelector.render(); + + return s+""; + } + + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/Html.hx b/src/sugoi/form/elements/Html.hx new file mode 100644 index 0000000..a920978 --- /dev/null +++ b/src/sugoi/form/elements/Html.hx @@ -0,0 +1,30 @@ +package sugoi.form.elements; + +/** + * Use this to fill some custom HTML between form elements + * + * @author fbarbut + */ +class Html extends sugoi.form.FormElement +{ + var html : String; + + public function new(name:String,html:String,?label="") + { + this.name = name; + this.html = html; + this.label = label; + super(); + } + + override public function render() + { + return html; + } + + override public function getTypedValue(str:String):String + { + return null; + } + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/ImageUpload.hx b/src/sugoi/form/elements/ImageUpload.hx new file mode 100644 index 0000000..3b5086a --- /dev/null +++ b/src/sugoi/form/elements/ImageUpload.hx @@ -0,0 +1,76 @@ +package sugoi.form.elements; +import sugoi.form.Form; +import sys.io.File; + +/** + * Manage an element for uploading images. + */ +class ImageUpload extends FormElement +{ + public var fileName: String; + public var maxSize : Int; // Max file size in Mb + public var previewMaxSize : Int; // Image preview max size in pixels + public var url : String; + public var text : String; + + /** + * @param name + * @param label + * @param url file URL for previewing the image + * @param required + */ + public function new(name:String, label:String, ?url:String, ?required:Bool=false ) + { + super(); + + this.name = "upload_"+name; + this.label = label; + + this.url = url; + this.required = required; + fileName = null; + maxSize = 6; + previewMaxSize = 200; + } + + override public function getTypedValue(s:String):haxe.io.Bytes + { + var request = sugoi.tools.Utils.getMultipart(1024 * 1024 * maxSize); + + var strData = request.get(parentForm.name + "_" + name + "_data"); + if (strData != null && strData != ""){ + fileName = request.get(parentForm.name + "_" + name + "_data_filename"); + return new haxe.io.StringInput(strData).readAll(); + } + + return null; + } + + public function hasDeleteAction():Bool{ + var n = parentForm.name + "_" +name; + return (App.current.params.get(n + "_delete") == "1"); + } + + /** + * Renders an input + image preview + delete btn + */ + override public function render():String + { + var n = parentForm.name + "_" +name; + var str = new StringBuf(); + str.add('
'); + if (url!= null)str.add(''); + str.add(''); + if (text != null) str.add('

$text

'); + if (!required && url!= null){ + str.add(''); + str.add(''); + str.add(''); + } + str.add(''); + str.add('
'); + + return str.toString(); + } + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/Input.hx b/src/sugoi/form/elements/Input.hx new file mode 100644 index 0000000..aeaaa14 --- /dev/null +++ b/src/sugoi/form/elements/Input.hx @@ -0,0 +1,99 @@ +package sugoi.form.elements; + +import sugoi.form.Form; +import sugoi.form.FormElement; +import sugoi.form.validators.*; +import sugoi.form.Formatter; + +using StringTools; + +enum InputType{ + ITText; + ITPassword; + ITHidden; + ITColor; //http://caniuse.com/#feat=input-color +} + +class Input extends FormElement +{ + public var password(get,set):Bool; + public var disabled:Bool; + public var showLabelAsDefaultValue:Bool; + public var printRequired:Bool; + + public var formatter:Formatter; + public var inputType : InputType; + + public function new(name:String, label:String, ?value:T, ?required=false, ?validators:Array>, ?attributes="") + { + super(); + this.name = name; + this.label = label; + this.value = value; + this.required = required; + this.attributes = attributes; + + if (validators != null) + { + for (i in validators) + { + this.validators.add(i); + } + } + + this.password = false; + this.disabled = false; + inputType = ITText; + + printRequired = false; + if(Form.USE_TWITTER_BOOTSTRAP) cssClass = "form-control"; + } + + public function get_password(){ + return inputType == ITPassword; + } + + public function set_password(v:Bool){ + if (v){ + inputType = ITPassword; + }else{ + inputType = ITText; + } + return v; + } + + override public function render():String + { + var n = parentForm.name + "_" +name; + var tType = switch(inputType){ + case ITHidden: "hidden"; + case ITPassword : "password" ; + case ITText : "text" ; + case ITColor : "color"; + } + + return "" + ((required && parentForm.isSubmitted() && printRequired)?" required":"") ; + } + + override public function getTypedValue(str:String):T{ + + if (str == "" || str==null) { + return null; + } + return cast StringTools.trim(str); + + } + + /** + * render label + field + */ + override public function getFullRow():String { + if (this.inputType == ITHidden){ + return this.render(); + }else{ + return super.getFullRow(); + } + + } + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/IntInput.hx b/src/sugoi/form/elements/IntInput.hx new file mode 100644 index 0000000..de0fbeb --- /dev/null +++ b/src/sugoi/form/elements/IntInput.hx @@ -0,0 +1,39 @@ +package sugoi.form.elements; + + + +class IntInput extends Input +{ + + public function new(name, label, value, ?required=false){ + super(name, label, value, required); + } + + override public function getTypedValue(str:String):Int{ + if(str!=null) str = StringTools.trim(str); + + if (str == "" || str==null) { + + if (this.required){ + return 0; + }else{ + return null; + } + + + }else{ + var v = Std.parseInt(str); + + if (v == null){ + if (this.required){ + return 0; + }else{ + return null; + } + }else{ + return v; + } + } + } + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/IntSelect.hx b/src/sugoi/form/elements/IntSelect.hx new file mode 100644 index 0000000..8e81b43 --- /dev/null +++ b/src/sugoi/form/elements/IntSelect.hx @@ -0,0 +1,21 @@ +package sugoi.form.elements; + +/** + * ... + * @author fbarbut + */ +class IntSelect extends Selectbox +{ + + override function getTypedValue(str:String):Int{ + + if (str != null) str = StringTools.trim(str); + + if (str==null || str=="") { + return null; + }else{ + return Std.parseInt(str); + } + } + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/KeyVal.hx b/src/sugoi/form/elements/KeyVal.hx new file mode 100644 index 0000000..c7ff6d2 --- /dev/null +++ b/src/sugoi/form/elements/KeyVal.hx @@ -0,0 +1,11 @@ +/** + * ... + * @author Tonypee + */ + +package form.elements; + +typedef KeyVal = { + var key:String; + var value:Dynamic; +} \ No newline at end of file diff --git a/src/sugoi/form/elements/Label.hx b/src/sugoi/form/elements/Label.hx new file mode 100644 index 0000000..60b077f --- /dev/null +++ b/src/sugoi/form/elements/Label.hx @@ -0,0 +1,22 @@ +package form; + +class Label +{ + public var forElement:FormElement; + public var value:String; + + public function new() + { + + } + + override public function render():String + { + + } + + public function populate(data:String) + { + value = data; + } +} \ No newline at end of file diff --git a/src/sugoi/form/elements/RadioGroup.hx b/src/sugoi/form/elements/RadioGroup.hx new file mode 100644 index 0000000..a3e46de --- /dev/null +++ b/src/sugoi/form/elements/RadioGroup.hx @@ -0,0 +1,63 @@ +package sugoi.form.elements; +import sugoi.form.Form; +import sugoi.form.FormElement; + +class RadioGroup extends FormElement +{ + public var data:ListData.FormData; + public var selectMessage:String; + public var labelLeft:Bool; + public var labelRight:Bool; + public var vertical:Bool; + + public function new(name:String, label:String, ?data:Array<{label:String,value:String}>, ?selected:String, ?defaultValue:String, ?vertical:Bool=true, ?labelRight:Bool=true,?required=false) + { + super(); + this.name = name; + this.label = label; + this.data = data != null ? data : []; + this.value = selected != null ? selected : defaultValue; + this.vertical = vertical; + this.labelRight = labelRight; + this.required = required; + } + + public function addOption(label:String, value:String) + { + data.push( { label:label, value:value } ); + } + + override public function render():String + { + var s = ""; + var n = parentForm.name + "_" +name; + + var c = 0; + if (data != null) + { + for (row in data) + { + var vClass = vertical ? " radioItemVertical" : " radioItemHorizontal"; + s += '
'; + var radio = "\n"; + var label = ""; + + s += labelRight ? radio + " "+label+" ": label+" "+radio+" "; + s += '
'; + //if (verticle) s += "
"; + c++; + } + } + + return s; + } + + override function getTypedValue(str:String){ + if(str==null) return null; + str = StringTools.trim(str); + return (str == "") ? return null : str; + + + } + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/Readonly.hx b/src/sugoi/form/elements/Readonly.hx new file mode 100644 index 0000000..8a7e596 --- /dev/null +++ b/src/sugoi/form/elements/Readonly.hx @@ -0,0 +1,43 @@ +package sugoi.form.elements; + +import sugoi.form.Form; +import sugoi.form.FormElement; + +class Readonly extends FormElement +{ + public var display:Bool; + + public function new(name:String, label:String, ?value:T, ?required:Bool = false, ?display:Bool = false, ?attributes:String = "") + { + super(); + this.name = name; + this.label = label; + this.value = value; + this.required = required; + this.display = display; + this.attributes = attributes; + } + + override public function render():String + { + var n = parentForm.name + "_" + name; + + var str:StringBuf = new StringBuf(); + + str.add(""); + if (display) { + str.add(value); + } + + return str.toString(); + } + + override public function getTypedValue(str:String):T + { + if (str == "" || str==null) { + return null; + } + + return cast StringTools.trim(str); + } +} diff --git a/src/sugoi/form/elements/Richtext.hx b/src/sugoi/form/elements/Richtext.hx new file mode 100644 index 0000000..6f4f87e --- /dev/null +++ b/src/sugoi/form/elements/Richtext.hx @@ -0,0 +1,105 @@ +package form.elements; +/* + * TinyMCE rich text editor + * + */ + +import form.Form; +import form.FormElement; + +class Richtext extends FormElement +{ + public var width:Float; + public var height:Float; + public var content_css:String; + public var mode:RichtextMode; + + public function new(name:String, label:String, ?value:String, ?required:Bool=false, ?attibutes:String="") + { + super(); + this.name = name; + this.label = label; + this.value = value; + this.required = required; + this.attributes = attibutes; + + width = 300; + height = 300; + content_css = "css/cms/richtext_default.css"; + + mode = RichtextMode.SIMPLE; + } + + override public function render():String + { + var n = form.name + "_" +name; + + if(content_css != "") content_css += "?" + Date.now().getTime(); + + var str:StringBuf = new StringBuf(); + str.add("\n "); + str.add("\n \n "); + + if (!isValid()) str.add(" required"); + + return str.toString(); + } + + public function toString() :String + { + return render(); + } +} + +enum RichtextMode +{ + SIMPLE; + FORMAT; + SIMPLE_TABLES; + ADVANCED; +} \ No newline at end of file diff --git a/src/sugoi/form/elements/RichtextWym.hx b/src/sugoi/form/elements/RichtextWym.hx new file mode 100644 index 0000000..57e07eb --- /dev/null +++ b/src/sugoi/form/elements/RichtextWym.hx @@ -0,0 +1,92 @@ +/* + * WYMEditor ( XHTML wysiwig editor ) + * http://files.wymeditor.org/wymeditor-1.0.0b2/examples/ + */ +package form.elements; + +import form.Form; +import form.FormElement; + +class RichtextWym extends FormElement +{ + public var width:Float; + public var height:Float; + public var allowImages:Bool; + public var allowTables:Bool; + public var editorStyles:String; + public var containersItems:String; + public var classesItems:String; + + public function new(name:String, label:String, ?value:String, ?required:Bool=false, ?attibutes:String="") + { + super(); + this.name = name; + this.label = label; + this.value = value; + this.required = required; + this.attributes = attibutes; + + width = 500; + height = 300; + + allowImages = true; + allowTables = false; + editorStyles = ""; + containersItems = ""; + classesItems = ""; + } + + override public function render():String + { + var n = form.name + "_" +name; + + editorStyles = StringTools.replace(editorStyles, "\n", " "); + editorStyles = StringTools.replace(editorStyles, "\r", " "); + + var str:StringBuf = new StringBuf(); + str.add("\n "); + str.add(""); + + if (!isValid()) str.add(" required"); + + return str.toString(); + } + + public function toString() :String + { + return render(); + } +} \ No newline at end of file diff --git a/src/sugoi/form/elements/Selectbox.hx b/src/sugoi/form/elements/Selectbox.hx new file mode 100644 index 0000000..54076b3 --- /dev/null +++ b/src/sugoi/form/elements/Selectbox.hx @@ -0,0 +1,50 @@ +package sugoi.form.elements; +import sugoi.form.Form; +import sugoi.form.FormElement; + +class Selectbox extends FormElement +{ + public var data:Array<{label:String,value:T}>; + public var nullMessage:String; + public var onChange:String; + public var size:Int; + public var multiple:Bool; + + public function new(name:String, label:String, ?data:Array<{label:String,value:T}>, ?selected:T, required:Bool=false, ?nullMessage="-", ?attributes="") + { + super(); + this.name = name; + this.label = label; + this.data = data != null ? data: new Array(); + this.value = selected; + this.required = required; + this.nullMessage = nullMessage; + this.attributes = attributes; + size = 1; + multiple = false; + onChange = ""; + if(Form.USE_TWITTER_BOOTSTRAP) cssClass = "form-control"; + } + + override public function render():String + { + var s = ""; + var n = parentForm.name; + n += "_" +name; + + s += '\n"; + + return s; + } + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/StringInput.hx b/src/sugoi/form/elements/StringInput.hx new file mode 100644 index 0000000..d4a705f --- /dev/null +++ b/src/sugoi/form/elements/StringInput.hx @@ -0,0 +1,20 @@ +package sugoi.form.elements; + + + +class StringInput extends Input +{ + + override public function getTypedValue(str:String):String{ + + if (str != null) + str = StringTools.trim(str); + + if (str == "" || str==null) { + return null; + }else{ + return str; + } + } + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/StringSelect.hx b/src/sugoi/form/elements/StringSelect.hx new file mode 100644 index 0000000..de7927e --- /dev/null +++ b/src/sugoi/form/elements/StringSelect.hx @@ -0,0 +1,19 @@ +package sugoi.form.elements; + +/** + * ... + * @author fbarbut + */ +class StringSelect extends Selectbox +{ + + override function getTypedValue(str:String){ + str = StringTools.trim(str); + if (str == "" || str==null) { + return null; + }else{ + return str; + } + } + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/Submit.hx b/src/sugoi/form/elements/Submit.hx new file mode 100644 index 0000000..baf42da --- /dev/null +++ b/src/sugoi/form/elements/Submit.hx @@ -0,0 +1,36 @@ +package sugoi.form.elements; +import sugoi.form.Form; +import sugoi.form.FormElement; + + +class Submit extends FormElement +{ + public function new(name:String, value:String) + { + super(); + this.name = name; + this.value = value; + + } + + override public function isValid():Bool + { + return true; + } + + override public function render() :String + { + if (Form.USE_TWITTER_BOOTSTRAP) cssClass = "btn btn-primary"; + + var s = ""; + return s; + } + + + override public function getFullRow():String + { + return "
" + this.render() + "
"; + } + + +} \ No newline at end of file diff --git a/src/sugoi/form/elements/TextArea.hx b/src/sugoi/form/elements/TextArea.hx new file mode 100644 index 0000000..e632be4 --- /dev/null +++ b/src/sugoi/form/elements/TextArea.hx @@ -0,0 +1,39 @@ + +package sugoi.form.elements; + +import sugoi.form.elements.Input; +import sugoi.form.Form; +import sugoi.form.validators.*; + +class TextArea extends StringInput +{ + public var height:Int; + + public function new(name:String, label:String, ?value:String, ?required:Bool=false, ?validators:Array>, ?attributes:String) + { + super(name, label, value, required, validators, attributes); + + } + + override public function render():String + { + var n = parentForm.name + "_" +name; + + //if (showLabelAsDefaultValue && value == label){ + //addValidator(new BoolValidator(false, "Not valid")); + //} + + if ((value == null || value == "") && showLabelAsDefaultValue) { + value = label; + } + + var s = ""; + if (required && parentForm.isSubmitted() && printRequired) s += "required
"; + + s += ""; + + return s; + } + + +} \ No newline at end of file diff --git a/src/sugoi/form/filters/Filter.hx b/src/sugoi/form/filters/Filter.hx new file mode 100644 index 0000000..37cfccd --- /dev/null +++ b/src/sugoi/form/filters/Filter.hx @@ -0,0 +1,9 @@ +package sugoi.form.filters; + +class Filter +{ + public function new() { + + } + +} \ No newline at end of file diff --git a/src/sugoi/form/filters/FloatFilter.hx b/src/sugoi/form/filters/FloatFilter.hx new file mode 100644 index 0000000..e35ed77 --- /dev/null +++ b/src/sugoi/form/filters/FloatFilter.hx @@ -0,0 +1,28 @@ +package sugoi.form.filters; + +/** + * Converts a String to a Float + */ +class FloatFilter extends Filter implements IFilter +{ + + public function new() + { + super(); + } + + public function filter(f:Float):Float{ + return f; + } + + public function filterString(n:String):Float { + + if (n == null || n=="") return null; + n = StringTools.trim(n); + n = StringTools.replace(n, ",", "."); + var f = Std.parseFloat(n); + if( Math.isNaN(f) ) f = null; + return f; + } + +} \ No newline at end of file diff --git a/src/sugoi/form/filters/IFilter.hx b/src/sugoi/form/filters/IFilter.hx new file mode 100644 index 0000000..f45901d --- /dev/null +++ b/src/sugoi/form/filters/IFilter.hx @@ -0,0 +1,10 @@ +package sugoi.form.filters; + + +interface IFilter +{ + + public function filter(data:T):T; + + public function filterString(data:String):T; +} \ No newline at end of file diff --git a/src/sugoi/form/validators/BoolValidator.hx b/src/sugoi/form/validators/BoolValidator.hx new file mode 100644 index 0000000..e2f7c5b --- /dev/null +++ b/src/sugoi/form/validators/BoolValidator.hx @@ -0,0 +1,29 @@ + +package sugoi.form.validators; +import sugoi.form.validators.Validator; + +class BoolValidator extends Validator +{ + public var errorNotValid:String; + public var valid:Bool; + + public function new(valid:Bool, ?error:String) + { + super(); + + this.valid = valid; + + if (error != null) { + errorNotValid = error; + }else { + errorNotValid = "Not valid."; + } + } + + override public function isValid(value):Bool + { + if (!valid) + errors.push(errorNotValid); + return valid; + } +} \ No newline at end of file diff --git a/src/sugoi/form/validators/CustomValidator.hx b/src/sugoi/form/validators/CustomValidator.hx new file mode 100644 index 0000000..7b07422 --- /dev/null +++ b/src/sugoi/form/validators/CustomValidator.hx @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2008, TouchMyPixel & contributors + * Original author : Tony Polinelli + * Contributers: Tarwin Stroh-Spijer + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE TOUCH MY PIXEL & CONTRIBUTERS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE TOUCH MY PIXEL & CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +package sugoi.form.validators; +import sugoi.form.Form; +import sugoi.form.FormElement; +import sugoi.form.Validator; + +class CustomValidator extends Validator +{ + public var validationFunction : Dynamic->Bool; + public var errorNotValid:String; + + public function new( validationFunction : Dynamic->Bool, ?errorMessage:String = null ) + { + super(); + this.validationFunction = validationFunction; + this.errorNotValid = errorMessage; + } + + override public function isValid( value : Dynamic ) : Bool + { + super.isValid( value ); + + var valid = false; + if ( validationFunction != null ) + valid = validationFunction( value); + + if (!valid) + errors.add(errorNotValid); + + return valid; + } +} \ No newline at end of file diff --git a/src/sugoi/form/validators/DateTimeValidator.hx b/src/sugoi/form/validators/DateTimeValidator.hx new file mode 100644 index 0000000..ecfe861 --- /dev/null +++ b/src/sugoi/form/validators/DateTimeValidator.hx @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2008, TouchMyPixel & contributors + * Original author : Tony Polinelli + * Contributers: Tarwin Stroh-Spijer + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE TOUCH MY PIXEL & CONTRIBUTERS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE TOUCH MY PIXEL & CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +package sugoi.form.validators; +//import sugoi.form.Validator; +//import poko.utils.StringTools2; +//import site.cms.common.DateTimeMode; + +class DateTimeValidator extends Validator +{ + //public static var EMAIL_REGEX : EReg = "([0-9]{4})[-\.[:space:]]([0-9]{2})[-\.[:space:]]([0-9]{2})"; + public var format:EReg; + + public var minDate:Date; + public var maxDate:Date; + + public var errorDateOutOfRange:String; + public var errorDateNotValid:String; + public var errorDateNotExist:String; + + public function new( ?mode : DateTimeMode = null, ?minDate:Date, ?maxDate:Date ) + { + super(); + + if ( mode == DateTimeMode.date ) + { + format = new EReg("[0-9]{4}-[0-9]{2}-[0-9]{2}", null); + errorDateNotValid = "Is not in the correct format. YYYY-MM-DD is required."; + } + else if ( mode == DateTimeMode.time ) + { + format = new EReg("[0-9]{2}:[0-9]{2}:[0-9]{2}", null); + errorDateNotValid = "Is not in the correct format. HH:MM:SS is required."; + } + else + { + format = new EReg("[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}", null); + errorDateNotValid = "Is not in the correct format. YYYY-MM-DD HH:MM:SS is required."; + } + + errorDateOutOfRange = "Must be between %s and %s"; + + + if (minDate != null) this.minDate = minDate; + if (maxDate != null) this.maxDate = maxDate; + } + + override public function isValid(value:Dynamic):Bool + { + var valid = true; + var d:Date; + + // value is date + if (Type.getClass(value) == Date){ + d = value; + // value conforms to format, convert to date + }else if (Type.getClass(value) == String && format.match(value)) { + d = Date.fromString(value); + }else { + errors.add(errorDateNotValid); + return false; + } + + // check date range + if (minDate != null){ + if (d.getTime() < minDate.getTime()) + valid = false; + } + + if (maxDate != null){ + if (d.getTime() > maxDate.getTime()) + valid = false; + } + + // date must be out of range if invalid at this point + if (!valid) + errors.add(StringTools2.printf(errorDateOutOfRange, [dateOnly(minDate), dateOnly(maxDate)])); + + return valid; + } + + private function dateOnly(d:Date):String + { + return StringTools.lpad(Std.string(d.getFullYear()), "0", 4) + "-" + StringTools.lpad(Std.string(d.getMonth()), "0", 2) + "-" + StringTools.lpad(Std.string(d.getDate()), "0", 2); + } +} + +/* + var v:DateValidator = new DateValidator(Date.fromString("1981-12-09"), Date.fromString("2030-11-11")); + + // ------------------------------------------------- + // check input + trace("

INPUT
"); + + v.reset(); + trace(true); + trace(v.isValid("1981-12-09")); + trace(v.errors); + + v.reset(); + trace(false); + trace(v.isValid("x981-12-09")); + trace(v.errors); + + v.reset(); + trace(false); + trace(v.isValid("1985-90-90")); + trace(v.errors); + + // ------------------------------------------------- + // check ranges + trace("

RANGES
"); + + v.reset(); + trace(true); + trace(v.isValid(Date.fromString("1981-12-09"))); + trace(v.errors); + + v.reset(); + trace(false); + trace(v.isValid(Date.fromString("1981-12-08"))); + trace(v.errors); + + v.reset(); + trace(true); + trace(v.isValid(Date.now())); + trace(v.errors); + + v.reset(); + trace(true); + trace(v.isValid(Date.fromString("2030-11-11"))); + trace(v.errors); + + v.reset(); + trace(false); + trace(v.isValid(Date.fromString("2030-11-12"))); + trace(v.errors); +*/ \ No newline at end of file diff --git a/src/sugoi/form/validators/DateValidator.hx b/src/sugoi/form/validators/DateValidator.hx new file mode 100644 index 0000000..02f73a6 --- /dev/null +++ b/src/sugoi/form/validators/DateValidator.hx @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2008, TouchMyPixel & contributors + * Original author : Tony Polinelli + * Contributers: Tarwin Stroh-Spijer + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE TOUCH MY PIXEL & CONTRIBUTERS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE TOUCH MY PIXEL & CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +package sugoi.form.validators; +import sugoi.form.Validator; +import poko.utils.StringTools2; + +class DateValidator extends Validator +{ + //public static var EMAIL_REGEX : EReg = "([0-9]{4})[-\.[:space:]]([0-9]{2})[-\.[:space:]]([0-9]{2})"; + public var format:EReg; + + public var minDate:Date; + public var maxDate:Date; + + public var errorDateOutOfRange:String; + public var errorDateNotValid:String; + public var errorDateNotExist:String; + + public function new(?minDate:Date, ?maxDate:Date) + { + super(); + + format = new EReg("[0-9]{4}-[0-9]{2}-[0-9]{2}", null); + + errorDateOutOfRange = "Must be between %s and %s"; + errorDateNotValid = "Is not in the correct format. YYYY-MM-DD is required."; + + if (minDate != null) this.minDate = minDate; + if (maxDate != null) this.maxDate = maxDate; + } + + override public function isValid(value:Dynamic):Bool + { + var valid = true; + var d:Date; + + // value is date + if (Type.getClass(value) == Date){ + d = value; + // value conforms to format, convert to date + }else if (Type.getClass(value) == String && format.match(value)) { + d = Date.fromString(value); + }else { + errors.add(errorDateNotValid); + return false; + } + + // check date range + if (minDate != null){ + if (d.getTime() < minDate.getTime()) + valid = false; + } + + if (maxDate != null){ + if (d.getTime() > maxDate.getTime()) + valid = false; + } + + // date must be out of range if invalid at this point + if (!valid) + errors.add(StringTools2.printf(errorDateOutOfRange, [dateOnly(minDate), dateOnly(maxDate)])); + + return valid; + } + + private function dateOnly(d:Date):String + { + return StringTools.lpad(Std.string(d.getFullYear()), "0", 4) + "-" + StringTools.lpad(Std.string(d.getMonth()), "0", 2) + "-" + StringTools.lpad(Std.string(d.getDate()), "0", 2); + } +} + +/* + var v:DateValidator = new DateValidator(Date.fromString("1981-12-09"), Date.fromString("2030-11-11")); + + // ------------------------------------------------- + // check input + trace("

INPUT
"); + + v.reset(); + trace(true); + trace(v.isValid("1981-12-09")); + trace(v.errors); + + v.reset(); + trace(false); + trace(v.isValid("x981-12-09")); + trace(v.errors); + + v.reset(); + trace(false); + trace(v.isValid("1985-90-90")); + trace(v.errors); + + // ------------------------------------------------- + // check ranges + trace("

RANGES
"); + + v.reset(); + trace(true); + trace(v.isValid(Date.fromString("1981-12-09"))); + trace(v.errors); + + v.reset(); + trace(false); + trace(v.isValid(Date.fromString("1981-12-08"))); + trace(v.errors); + + v.reset(); + trace(true); + trace(v.isValid(Date.now())); + trace(v.errors); + + v.reset(); + trace(true); + trace(v.isValid(Date.fromString("2030-11-11"))); + trace(v.errors); + + v.reset(); + trace(false); + trace(v.isValid(Date.fromString("2030-11-12"))); + trace(v.errors); +*/ \ No newline at end of file diff --git a/src/sugoi/form/validators/EmailValidator.hx b/src/sugoi/form/validators/EmailValidator.hx new file mode 100644 index 0000000..fde769e --- /dev/null +++ b/src/sugoi/form/validators/EmailValidator.hx @@ -0,0 +1,40 @@ +package sugoi.form.validators; +import sugoi.form.validators.Validator; + +class EmailValidator extends Validator +{ + public var errorNotValid:String; + public static var emailRegex = ~/^[^()<>@,;:\\"\[\]\s[:cntrl:]]+@[A-Z0-9][A-Z0-9-]*(\.[A-Z0-9][A-Z0-9-]*)*\.(xn--[A-Z0-9]+|[A-Z]{2,8})$/i; + + public function new() + { + super(); + #if js + errorNotValid = "Not a valid email address"; + #else + errorNotValid = switch(App.current.getLang()){ + case "fr" : "Adresse email invalide"; + default : "Not a valid email address"; + }; + #end + } + + override public function isValid(value:Dynamic):Bool + { + super.isValid(value); + + var valid = emailRegex.match(Std.string(value)); + if (!valid) + errors.add(errorNotValid); + + return valid; + } + + public inline static function check(value:String):Bool + { + var val = new EmailValidator(); + return val.isValid(value); + } + + +} \ No newline at end of file diff --git a/src/sugoi/form/validators/ListValidator.hx b/src/sugoi/form/validators/ListValidator.hx new file mode 100644 index 0000000..69dcf67 --- /dev/null +++ b/src/sugoi/form/validators/ListValidator.hx @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2008, TouchMyPixel & contributors + * Original author : Tony Polinelli + * Contributers: Tarwin Stroh-Spijer + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE TOUCH MY PIXEL & CONTRIBUTERS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE TOUCH MY PIXEL & CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + + +package sugoi.form.validators; + +import sugoi.form.Validator; +import poko.utils.StringTools2; + +class ListValidator extends Validator +{ + public var list:Array; + public var mode:ListValidatorMode; + + public var errorAllow:String; + public var errorDeny:String; + + public function new(?mode:ListValidatorMode) + { + super(); + + errorAllow = "Only the values %s are allowed."; + // this is used for the complete list of denied values + //errorDeny = "The values %s are not allowed."; + errorDeny = "The value '%s' is not allowed."; + + this.mode = mode != null ? mode : ListValidatorMode.ALLOW; + } + + override public function isValid(value:Dynamic):Bool + { + super.isValid(value); + + var valueExists = Lambda.has(list, value); + var valid = (mode == ListValidatorMode.ALLOW) ? valueExists : !valueExists; + if (!valid) { + // this one returns a list of denied values, which is nice, but though thought it might be a security risk somehow? + //errors.push(StringTools2.printf(mode == ListValidatorMode.ALLOW ? errorAllow : errorDeny, [joinAsSentence(list, "'")])); + if (mode == ListValidatorMode.ALLOW) { + errors.push(StringTools2.printf(errorAllow, [joinAsSentence(list, "'")])); + }else { + errors.push(StringTools2.printf(errorDeny, [value])); + } + } + return valid; + } + + private function joinAsSentence(a:Array, ?wrapWith:String):String + { + if (wrapWith != null) { + for(i in 0...a.length) + a[i] = wrapWith + a[i] + wrapWith; + } + var e = a.pop(); + var s = a.join(", ") + " and " + e; + return s; + } +} + +enum ListValidatorMode +{ + ALLOW; + DENY; +} + +/* + var v:ListValidator = new ListValidator(ListValidatorMode.ALLOW); + var a:Array = ["good", "bad", "stupid"]; + v.list = a; + trace(v.isValid("good")); + trace(v.errors); + v.reset(); + v.list = a; + trace(v.isValid("bad")); + trace(v.errors); + v.reset(); + v.list = a; + trace(v.isValid("ugly")); + trace(v.errors); + v.reset(); +*/ \ No newline at end of file diff --git a/src/sugoi/form/validators/NumberValidator.hx b/src/sugoi/form/validators/NumberValidator.hx new file mode 100644 index 0000000..3eb74b5 --- /dev/null +++ b/src/sugoi/form/validators/NumberValidator.hx @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2008, TouchMyPixel & contributors + * Original author : Tony Polinelli + * Contributers: Tarwin Stroh-Spijer + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE TOUCH MY PIXEL & CONTRIBUTERS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE TOUCH MY PIXEL & CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + + +package sugoi.form.validators; + +import sugoi.form.Validator; +import poko.utils.StringTools2; + +class NumberValidator extends Validator +{ + public var isInt:Bool; + public var min:Float; + public var max:Float; + + public var errorNumber:String; + public var errorInt:String; + public var errorMin:String; + public var errorMax:String; + + public function new(min:Float=0, max:Float=999999999999, isInt:Bool=false) + { + super(); + + this.min = min; + this.max = max; + this.isInt = isInt; + + errorNumber = "Must be a number"; + errorInt = "Must be an integer"; + errorMin = "Minimum number %s"; + errorMax = "Maximum number %s"; + } + + override public function isValid(value:Dynamic):Bool + { + super.isValid(value); + + var valid = true; + var f = Std.parseFloat(Std.string(value)); + var i = Std.int(f); + + if (Math.isNaN(f)) + { + errors.add(errorNumber); + valid = false; + }else{ + + if (isInt && i != f) { + errors.add(errorInt); + valid = false; + } + + var n:Float = isInt ? i : f; + + if (n < min) { + errors.add(StringTools2.printf(errorMin, [min])); + valid = false; + }else if (n > max) { + errors.add(StringTools2.printf(errorMax, [max])); + valid = false; + } + } + + return valid; + } + +} + +/* + + var v:NumberValidator = new NumberValidator(); + v.isInt = false; + v.min = -5; + v.max = 10.55; + trace(v.validate(5)); + trace(v.errors); + v.reset(); + trace(v.validate( -6)); + trace(v.errors); + v.reset(); + trace(v.validate( -3)); + trace(v.errors); + v.reset(); + trace(v.validate(11.5)); + trace(v.errors); + v.reset(); + +*/ \ No newline at end of file diff --git a/src/sugoi/form/validators/RegexValidator.hx b/src/sugoi/form/validators/RegexValidator.hx new file mode 100644 index 0000000..f4aed27 --- /dev/null +++ b/src/sugoi/form/validators/RegexValidator.hx @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2008, TouchMyPixel & contributors + * Original author : Tony Polinelli + * Contributers: Tarwin Stroh-Spijer + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE TOUCH MY PIXEL & CONTRIBUTERS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE TOUCH MY PIXEL & CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + + +package sugoi.form.validators; +import sugoi.form.Form; +import sugoi.form.FormElement; +import sugoi.form.Validator; +import poko.utils.StringTools2; + +class RegexValidator extends Validator +{ + public var regex:EReg; + public var regexOptions:String; + public var errorRegex:String; + + public function new(regex:EReg, ?errorMessage:String=null ) + { + super(); + this.regex = regex; + errorRegex = (errorMessage != null) ? errorMessage : "Regex Failed"; + } + + override public function isValid(value:Dynamic):Bool + { + super.isValid(value); + + var valid:Bool = true; + + if (!regex.match(Std.string(value))) + { + errors.add(StringTools2.printf(errorRegex, [regex])); + valid = false; + } + + return valid; + } +} \ No newline at end of file diff --git a/src/sugoi/form/validators/StringValidator.hx b/src/sugoi/form/validators/StringValidator.hx new file mode 100644 index 0000000..c67e49c --- /dev/null +++ b/src/sugoi/form/validators/StringValidator.hx @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2008, TouchMyPixel & contributors + * Original author : Tony Polinelli + * Contributers: Tarwin Stroh-Spijer + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE TOUCH MY PIXEL & CONTRIBUTERS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE TOUCH MY PIXEL & CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + + +package sugoi.form.validators; +import sugoi.form.Validator; +import poko.utils.StringTools2; +import EReg; + +class StringValidator extends Validator +{ + public var minChars:Int; + public var maxChars:Int; + public var charList:String; + public var mode:StringValidatorMode; + + public var regex:EReg; + public var regexError:String; + + public var errorMinChars:String; + public var errorMaxChars:String; + public var errorDenyChars:String; + public var errorAllowChars:String; + + public function new(?minChars:Int=0, ?maxChars:Int=999999, ?charList:String="", ?mode:StringValidatorMode, ?regex:EReg = null, ?regexError:String) + { + super(); + + errorMinChars = "Must be at least %s characters long"; + errorMaxChars = "Must be less than %s characters long"; + errorDenyChars = "Cannot contain the characters '%s'"; + errorAllowChars = "Must contain only the characers '%s'"; + + this.minChars = minChars; + this.maxChars = maxChars; + this.charList = charList; + this.mode = mode; + if (this.mode == null) this.mode = StringValidatorMode.ALLOW; + + this.regex = regex; + this.regexError = regexError != null ? regexError : "Doesn't match required input."; + + errors = new List(); + } + + override public function isValid(value:Dynamic):Bool + { + super.isValid(value); + + var valid = true; + var s = Std.string(value); + + if (minChars != null && minChars > 0 && s.length < minChars) + { + valid = false; + errors.add(StringTools2.printf(errorMinChars, [ minChars])); + } + + if (maxChars != null && maxChars > 0 && s.length > maxChars) + { + valid = false; + errors.add(StringTools2.printf(errorMaxChars, [maxChars])); + } + + if (charList.length > 0) + { + switch(mode) + { + case StringValidatorMode.ALLOW: + for (i in 0...s.length) + { + var letter = s.charAt(i); + if (charList.indexOf(letter) == -1) + { + valid = false; + // errors.add(StringTools2.printf(errorAllowChars, [StringTools2.toSentenceList(charList)])); + errors.add(StringTools2.printf(errorAllowChars, [charList])); + break; + } + } + case StringValidatorMode.DENY: + for (i in 0...s.length) + { + var letter = s.charAt(i); + if (charList.indexOf(letter) != -1) + { + valid = false; + //errors.add(StringTools2.printf(errorDenyChars, [StringTools2.toSentenceList(charList)])); + errors.add(StringTools2.printf(errorDenyChars, [charList])); + break; + } + } + } + } + + if (regex != null) + { + if (!regex.match(s)) + { + valid = false; + errors.add(regexError); + } + } + + return valid; + } + +} + + +enum StringValidatorMode +{ + ALLOW; + DENY; +} \ No newline at end of file diff --git a/src/sugoi/form/validators/Validator.hx b/src/sugoi/form/validators/Validator.hx new file mode 100644 index 0000000..03c5f56 --- /dev/null +++ b/src/sugoi/form/validators/Validator.hx @@ -0,0 +1,23 @@ +package sugoi.form.validators; + +class Validator +{ + public var errors:List; + + public function new() + { + errors = new List(); + } + + public function isValid(value:T):Bool + { + errors.clear(); + + return true; + } + + public function reset() + { + errors.clear(); + } +} \ No newline at end of file diff --git a/src/sugoi/helper/BreadCrumb.hx b/src/sugoi/helper/BreadCrumb.hx new file mode 100644 index 0000000..da0216e --- /dev/null +++ b/src/sugoi/helper/BreadCrumb.hx @@ -0,0 +1,102 @@ +package mt.net.helper; +import mt.Compat; +/** + * BreadCrumb (fil d'ariane) + * + * For breadcrumb translation, + * create "breadcrumb_*" entries in the translation file for each section. + * + * @author fbarbut + **/ +class BreadCrumb{ + + public var lang : String; + public var sections : Array; /* raw sections like "game/tetris/highscores" */ + public var translation : Hash; /* translated sections */ + + public function new(uri:String,?translator:String->String) { + sections = []; + translation = new Hash(); + + //remove GET params + uri = uri.split('?')[0]; + + if( uri.substr(0,1) == "/" ) + uri = uri.substr(1); + var uri = uri.split('/'); + + /*modify*/ + for (i in 0...uri.length) { + var u = uri[i]; + if(u == null) + continue; + + if(Std.parseInt(u) != null) { + uri[i] = null; + continue; + } + + if(u == 'index.n' || u == '') { + u = uri[i] = "home"; + continue; + } + } + + /*remove nulls*/ + uri = Lambda.array(Lambda.filter(uri,function(e) return e!=null)); + + sections = uri; + + /*translation*/ + if(translator != null) { + for(s in sections) { + var name = translator("breadcrumb_" + s); + if(name != "#breadcrumb_" + s + "#") { + translation.set(s, name); + }else { + //no translation + translation.set(s, s); + } + } + } + + + } + + + public function toString(?mode=1, ?format:String):Dynamic { + switch(mode) { + case 1 : + /* raw sections */ + return sections; + case 2 : + /* translated sections */ + return getTranslatedSections(); + + } + return null; + + //if(format != null) { + //format = StringTools.replace(format, "::rank::", Std.string(rank)); + //format = StringTools.replace(format, "::suffix::", Std.string(suffix)); + //return format; + //}else { + //return rank + suffix; + //} + + + + } + + public function getTranslatedSections() { + var out = []; + for(s in sections) { + out.push(translation.get(s)); + } + return out; + } + + + +} + diff --git a/src/sugoi/helper/Helper.hx b/src/sugoi/helper/Helper.hx new file mode 100644 index 0000000..3fd9e85 --- /dev/null +++ b/src/sugoi/helper/Helper.hx @@ -0,0 +1,21 @@ +package mt.net.helper; + +class Helper { + + /** + * Init all helpers in applicatio view. + */ + public static function init(context:Dynamic, viewHelpers:Array) { + /* gros paté (c) ncannasse */ + for(vh in viewHelpers) { + var methodName = Type.getClassName(Type.getClass(vh)).split(".").pop().toLowerCase(); + var toString = Reflect.field(vh, "toString"); + var nargs : Int = untyped $nargs(toString); + Reflect.setField(context, methodName , Reflect.makeVarArgs(function(p:Array) { + while( p.length < nargs ) p.push(null); + return Reflect.callMethod(vh, toString, p); + })); + } + } + +} \ No newline at end of file diff --git a/src/sugoi/helper/Ordinal.hx b/src/sugoi/helper/Ordinal.hx new file mode 100644 index 0000000..ab3e57d --- /dev/null +++ b/src/sugoi/helper/Ordinal.hx @@ -0,0 +1,113 @@ +package mt.net.helper; + +/** + * Ordinal number helper + * + * @author fbarbut + **/ +class Ordinal /*implements IHelper*/{ + + public var lang : String; + public static var AVAILABLE_LANGS = ['fr','en','es']; + + public function new(_lang:String) { + + lang = _lang.toLowerCase(); + if(!Lambda.has(AVAILABLE_LANGS, lang)) lang = 'en'; + } + + + + /** + * Get output, like "1st" or "2nd" + * @param rank number/rank starting from 1 + * @param ?sex 1:male,2:female, 3: neutral (german) + * @param ?format text format like "::rank::::suffix::" + */ + public function toString(rank:Int, ?sex:Int, ?format:String):String { + var suffix = ""; + if(sex == null) sex = 1; + + + switch(lang) { + case "fr": + if(rank == 1) { + suffix = (sex==1)?"er":"ère"; + }else { + suffix = "ème"; + } + + case "es": + //http://reglasdeortografia.com/numerales.html + suffix = (sex == 1)?"o":"a"; + + case "de": + switch(sex) { + case 1 : + suffix = "ter"; + case 2 : + suffix = "te"; + default : + suffix = "tes"; + } + + default: + //"en": + if(rank == 1) { + suffix = "st"; + }else if(rank == 2) { + suffix = "nd"; + }else if(rank == 3) { + suffix = "rd"; + }else { + suffix = "th"; + } + + /*DE + * + * Karsten : + Hihi. Un peu compliqué pour l'Allemand comme les suffix/mots dépendend de nombre exacte. ...en plus il y un troisième sexe. + Je propose tu utilise ce modèle qui fonctionne toujours: > X : X. + + Voici le modèle complète : + + > masculin: + > X = 1 : Erster + > X = 2 : Zweiter + > X = 3 : Dritter + > X = 4-19 : Xter + > X = 20-1000 : Xster + ... + : Les derniers 3 chiffres décident de nouveau (comme indiqué anvant) + p.ex. 232456 = Y[232]X[456] + + > neutre : + > X = 1 : Erstes + > X = 2 : Zweites + > X = 3 : Drittes + > X = 4-19 : Xtes + > X = 20-1000 : Xstes + ... + : Les derniers 3 chiffres décident de nouveau (comme indiqué anvant) + p.ex. 232456 = Y[232]X[456] + + > feminin : + > X = 1 : Erste + > X = 2 : Zweite + > X = 3 : Dritte + > X = 4-19 : Xte + > X = 20-1000 : Xste + ... + : Les derniers 3 chiffres décident de nouveau (comme indiqué anvant) + p.ex. 232456 = Y[232]X[456]> + * */ + + } + + if(format != null) { + format = StringTools.replace(format, "::rank::", Std.string(rank)); + format = StringTools.replace(format, "::suffix::", Std.string(suffix)); + return format; + }else { + return rank + suffix; + } + } +} + diff --git a/src/sugoi/helper/READ ME.txt b/src/sugoi/helper/READ ME.txt new file mode 100644 index 0000000..79cd8c8 --- /dev/null +++ b/src/sugoi/helper/READ ME.txt @@ -0,0 +1,8 @@ +C'est quoi un helper ( ou view helper ? ) + +C'est un petit composant qui permet de rendre en html des éléments complexes, difficiles à rendre en macro templo. + +Utiliser Helper.init() avec le Context d'une application et une liste de helpers pour les rendres disponible dans le Context de l'application web. + + +Inspiré de http://framework.zend.com/manual/1.12/en/zend.view.helpers.html \ No newline at end of file diff --git a/src/sugoi/helper/Table.hx b/src/sugoi/helper/Table.hx new file mode 100644 index 0000000..ca55222 --- /dev/null +++ b/src/sugoi/helper/Table.hx @@ -0,0 +1,166 @@ +package sugoi.helper; + +/** + * Simple class to print a HTML table + * + * @author fbarbut + * + * It should be able to print a lot of structures : + * - a spod request result + * - arrays, lists + * - anonymous objects + * + * usage : + * var t = new Table(); + * t.title = "the table"; + * t.setContent( [{toto:1,tata:2},{toto:4,tata:7}] ); + * neko.Lib.print( t.toString(); ); + **/ +class Table{ + + //table content + public var title : String; + public var head : Array; + public var content : Array>; + + //table CSS class + public var tableCSSClass : String; + + + public function new(?defaultCss:String) { + + if(defaultCss != null) tableCSSClass = defaultCss; + + } + + public function toString(?param:Dynamic):String { + if(param != null) { + setContent(param); + return go(); + }else { + return "null"; + } + + + } + + public function go():String { + var output = ""; + output += ""; + + if (title != null) { + var length = 0; + for(row in content) { + for(cell in row) { + length++; + } + break; + } + + //alors là je comprends pas du tout du tout du tout pourquoi content[0].length ne marche pas... + //output += content[0]+ " "+Type.getClass(content[0])+" "+content[0].length ; + output += ""; + } + + + if (head != null) { + output += ""; + for ( cell in head ) { + output += ""; + } + output += ""; + } + + for (row in content) { + output += ""; + for (cell in row) { + output += ""; + } + output += ""; + } + output += "
" + title + "
" + cell + "
"+cell+"
"; + return output; + } + + + /** + * Iterable -> Array + */ + function fromIterableToArray(iterable) { + var out = []; + var iterable : Iterable = cast iterable; + for (el in iterable) { + out.push(el); + } + return out; + } + + /** + * Reflectable -> Array + */ + function fromReflectableToArray(reflectable) { + var out = []; + //get fields + for (field in Reflect.fields(reflectable) ) { + if(!Reflect.isFunction(Reflect.field(reflectable,field)) && field!="__cache__" && field!="__lock") + out.push(Reflect.field(reflectable,field)); + } + return out; + + } + + public function toArray(c:Dynamic) { + if (c.iterator!=null) { //Reflect.hasField(c, "iterator") + //trace("fromIterableToArray "+c); + return fromIterableToArray(c); + }else { + //trace("fromReflectableToArray "+c); + return fromReflectableToArray(c); + } + } + + public function setContent(c:Dynamic) { + //header + head = []; + + //content + content = []; + var row = []; + + //on essaye de voir si il y a une deuxieme dimension au tableau (ex : liste d'objets ) + try{ + for (obj in toArray(c) ) { + row = []; + for (prop in toArray(obj)) { + //get header + if(head.length==0){ + for (prop in Reflect.fields(obj)) { + //if the field is not a function, lets add it : + if(!Reflect.isFunction(Reflect.field(obj,prop)) && prop!="__cache__") + head.push(prop); + } + } + row.push(prop); + } + content.push(row); + } + }catch(e :Dynamic) { + content = []; + head = ["field","value"]; + //a priori c'est un objet simple pas une liste , donc on liste ses propriétés + for (field in Reflect.fields(c) ) { + var row = []; + if(!Reflect.isFunction(Reflect.field(c, field)) && field != "__cache__" && field != "__lock") { + row.push(field); + row.push(Reflect.field(c, field)); + } + content.push(row); + } + + } + return content; + } + + +} + diff --git a/src/sugoi/i18n/GetText.hx b/src/sugoi/i18n/GetText.hx new file mode 100644 index 0000000..8070b50 --- /dev/null +++ b/src/sugoi/i18n/GetText.hx @@ -0,0 +1,558 @@ +package sugoi.i18n; + +import haxe.macro.Expr; +import haxe.macro.Context; + +/** + * @author tpfeiffer + */ +#if (potools || macro) +typedef POData = Array; + +typedef POEntry = { + ?msgid: String, + ?msgstr: String, + + id: Null, + str: Null, + + ?cTranslator: String, + ?cExtracted: String, + ?cRef: String, + ?cFlags: String, + ?cPrevious : String, + ?cComment: String, +} +#end + +typedef LocaleString = String; + +class GetText { + + public var texts : Map; + + public function new() {} + + public function toString() { + return "GetText"; + } + + public function untranslated(str:Dynamic) : LocaleString { + return cast Std.string(str); + } + + public macro function _(ethis:Expr, estr:ExprOf, ?params:ExprOf) { + var str = switch(estr.expr) { + case EConst(CString(s)) : s; + default : Context.error("Constant string expected here!", estr.pos); + } + + var odd = false; + var hasVars = false; + var strVars = []; + for(s in str.split("::")) { + odd = !odd; + if( !odd ) { + hasVars = true; + strVars.push(s); + } + } + + switch( params.expr ) { + case EObjectDecl(fields) : + var vmap = new Map(); + for(f in fields) { + if( str.indexOf("::"+f.field+"::")<0 ) + Context.error("Variable "+f.field+" not found in the string!", f.expr.pos); + vmap.set(f.field, true); + //useless when there's no obfuscation + //f.field = "_"+f.field; + } + + for(k in strVars) + if( !vmap.exists(k) ) + Context.error("String requires field "+k, params.pos); + + params = { expr:EObjectDecl(fields), pos:params.pos }; + + case EConst(CIdent("null")) : + if( hasVars ) + Context.error("Missing params: "+strVars.join(", "), params.pos); + + default : + Context.error("Anonymous object expected here!", params.pos); + } + + + return macro $ethis.get($estr,$params); + } + + @:noCompletion public function get(str:String, ?params:Dynamic) : LocaleString { + + if(texts == null){ + //fail silently + texts = new Map(); + } + + str = StringTools.rtrim( str.split("||")[0] ); + + if(texts.exists(str)) { + str = texts.get(str); + } + + var list = str.split("::"); + if (params != null) { + + for (k in Reflect.fields(params)) { + str = StringTools.replace(str, "::" + k + "::", Reflect.field(params, k)); + } + } + return new LocaleString(str); + } + + public function readMo(data:haxe.io.Bytes) { + var r = new MoReader(data); + texts = r.parse(); + r = null; + } + + public function emptyDictionary() { + texts = new Map(); + } + + /** + * Parses all the project to generate the POT file + */ + macro public static function parse(codePath:Array, potFilePath:String, ?refPoFilePath:String) { + Sys.println("[GetText] Parsing source code..."); + var data : POData = []; + data.push(POTools.mkHeaders([ + "MIME-Version" => "1.0", + "Content-Type" => "text/plain; charset=UTF-8", + "Content-Transfer-Encoding" => "8bit" + ])); + var strMap : Map = new Map(); + + for( path in codePath ) + explore(path, data, strMap); + + Sys.println("[GetText] Saving POT file..." + potFilePath); + POTools.exportFile( potFilePath, data ); + + if( refPoFilePath != null ) { + if( !sys.FileSystem.exists(refPoFilePath) ) { + Sys.println("[GetText] Warning: File not found: "+refPoFilePath ); + } else { + Sys.println("[GetText] Saving Translated-POT file..."); + POTools.exportTranslatedFile(potFilePath,refPoFilePath,data); + } + } + + Sys.println("[GetText] Done."); + return macro {} + } + + #if macro + static function explore(folder:String, data:POData, strMap:Map) { + for ( f in sys.FileSystem.readDirectory(folder) ) { + // Parse sub folders + if( sys.FileSystem.isDirectory(folder+"/"+f) ) { + explore(folder+"/"+f, data, strMap); + continue; + } + + // Ignore non-sourcecode + var isHaxeFile = f.substr(f.length - 3) == ".hx"; + var isTemplateFile = f.substr(f.length - 4) == ".mtt"; + + if( !(isHaxeFile || isTemplateFile) ) + continue; + + Sys.println('explore $folder/$f'); + + // Read lines + var c = sys.io.File.getContent(folder + "/" + f); + + // Test it: http://regexr.com/ + //var strReg = ~/_\([ ]*"((?:[^"\\]+|\\.)*)"[ ]*\)/igm; + var strReg = ~/_\([ ]*"((?:[^"\\]+|\\.)*)"[ ]*(?:,[ ]*{[()\[\].,:\w\s]*})?\)/igm; + var out = strReg.map(c, function(e) { + var fullStr = e.matched(0); + var str = e.matched(1); + //Sys.println("str matched:"+str); + // Ignore commented strings + var i = str.indexOf("//"); + var matchPos = strReg.matchedPos().pos; + //Sys.println("i="+i+" matchPos="+matchPos); + //TODO required?? + //if( i >= 0 && i < matchPos ) + // return ""; + + var cleanedStr = str; + // Translator comment + var comment : String = null; + if( cleanedStr.indexOf("||") >= 0 ) { + var parts = cleanedStr.split("||"); + if( parts.length != 2 ) { + Sys.println("Malformed translator comment"); + throw "Malformed translator comment"; + return ""; + } + comment = StringTools.trim(parts[1]); + cleanedStr = cleanedStr.substr(0,cleanedStr.indexOf("||")); + cleanedStr = StringTools.rtrim(cleanedStr); + } + + var n = e.matchedPos().pos; + //Sys.println("match line : "+n); + // New entry found + if( !strMap.exists(cleanedStr) ) { + //Sys.println("register key : "+cleanedStr); + strMap.set(cleanedStr, true); + data.push({ + id : cleanedStr, + str : "", + cRef : folder+"/"+f+":"+n, + cExtracted : comment, + }); + } else { + var previous = Lambda.find(data, function(e) return e.id == cleanedStr); + if( previous != null ) + previous.cRef += " "+folder+"/"+f+":"+n; + } + //return Locale.texts.get(cleanedStr); + //return fullStr; + Sys.println("out = "+cleanedStr); + return cleanedStr; + }); + } + } + #end +} + +/** + * GNU GetText MO file reader + * @doc https://www.gnu.org/software/gettext/manual/html_node/MO-Files.html + */ +class MoReader +{ + private var original_table_offset:UInt; + private var translated_table_offset:UInt; + private var hash_num_entries:UInt; + private var hash_offset:UInt; + private var data:haxe.io.BytesInput; + + static var MAGIC:UInt = 0x950412DE; + static var MAGIC2:UInt = 0xDE120495; + + public function new(data:haxe.io.Bytes):Void + { + this.data = new haxe.io.BytesInput(data); + } + + public function parse():Map + { + var d = data; + var header : UInt = d.readInt32(); + + if(header != MAGIC && header != MAGIC2) { + throw "Bad MO file header : " + header; + } + + var revision:UInt = d.readInt32(); + if (revision > 1){ + throw "Bad MO file format revision : "+revision; + } + + var num_strings:UInt = d.readInt32(); + original_table_offset= d.readInt32(); + translated_table_offset = d.readInt32(); + hash_num_entries= d.readInt32(); + hash_offset= d.readInt32(); + + var texts : Map = new Map(); + for (i in 1...num_strings) + texts.set( getOriginalString(i), getTranslatedString(i) ); + + return texts; + + } + + function getTranslatedString(index:Int):LocaleString { + return getString(translated_table_offset + 8 * index ); + } + + function getOriginalString(index:Int):String { + return getString(original_table_offset + 8 * index ); + } + + function getString(offset:UInt):LocaleString { + data.position = offset; + var length :UInt = data.readInt32(); + var pos :UInt = data.readInt32(); + data.position = pos; + return new LocaleString( data.readString(length) ); + } +} + +#if (potools || macro) + +class POTools { + + public static function parseFile( path : String ) : POData { + return parse( sys.io.File.getContent(path) ); + } + + public static function parse( data : String ) : POData { + var arr : POData = []; + var e : POEntry = cast { }; + var lnum = -1; + for ( line in data.split("\n") ) { + lnum++; + // Remove CR before LF + if ( line.length > 0 && line.substr( -1, 1) == "\r" ) + line = line.substr(0, line.length - 1); + + if( line.length == 0 ){ + arr.push(e); + e = cast {}; + continue; + } + + var f = line.charCodeAt(0); + if( f == '"'.code ){ + if( e.msgstr != null ){ + e.msgstr += "\n"+line; + e.str += getString(line,lnum); + }else if( e.msgid != null ){ + e.msgid += "\n"+line; + e.id += getString(line,lnum); + }else{ + throw "Parse error line "+lnum; + } + }else if( f == '#'.code ){ + var p = line.charCodeAt(1); + switch( p ){ + case ':'.code: + var s = line.substr(3); + if( e.cRef == null ) + e.cRef = s; + else + e.cRef += "\n"+s; + case '.'.code: + var s = line.substr(3); + if( e.cExtracted == null ) + e.cExtracted = s; + else + e.cExtracted += "\n"+s; + case ','.code: + var s = line.substr(3); + if( e.cFlags == null ) + e.cFlags = s; + else + e.cFlags += "\n"+s; + case '|'.code: + var s = line.substr(3); + if( e.cPrevious == null ) + e.cPrevious = s; + else + e.cPrevious += "\n"+s; + case '~'.code: + var s = line.substr(3); + if( e.cComment == null ) + e.cComment = s; + else + e.cComment += "\n"+s; + case ' '.code: + var s = line.substr(2); + if( e.cTranslator == null ) + e.cTranslator = s; + else + e.cTranslator += "\n"+s; + default: + throw "Parse error line "+lnum; + } + }else if( StringTools.startsWith(line, "msgid ") ){ + e.msgid = line; + e.id = getString(line,lnum); + }else if( StringTools.startsWith(line, "msgstr ") ){ + e.msgstr = line; + e.str = getString(line,lnum); + }else{ + throw "Parse error line "+lnum; + } + } + return arr; + } + + public static function mkHeaders( headers : Map ) : POEntry { + var str = 'msgstr ""'; + for( k in headers.keys() ) + str += '\n"'+k+': '+headers.get(k)+'\\n"'; + + return { + id: "", + str: null, + msgstr: str, + }; + } + + public static function exportFile( filePath : String, data : POData ){ + var fp = sys.io.File.write(filePath, true); + export(fp,data); + fp.close(); + } + + public static function exportTranslatedFile( potFilePath:String, refPoFilePath:String, data:POData ){ + var refData = parseFile( refPoFilePath ); + var expData : POData = []; + var mref = new Map}>(); + for( entry in data ){ + if( entry.id == "" ){ + expData.push( entry ); + continue; + } + + var refEntry = Lambda.find(refData,function(e) return e.id == entry.id); + var change = false; + var refid = null; + if( refEntry != null && refEntry.str != "" ){ + change = refEntry.str != refEntry.id; + if( entry.cExtracted != null ) + refid = entry.cExtracted+"\n\n--------\n"+refEntry.msgid; + else + refid = "--------\n"+refEntry.msgid; + entry.msgid = null; + entry.id = refEntry.str; + } + + if( mref.exists(entry.id) ){ + var mEntry = mref.get( entry.id ); + if( refid != null ){ + mEntry.refid.push(refid); + if( change ) + mEntry.change = true; + } + }else{ + var r = refid==null ? [] : [refid]; + mref.set( entry.id, {change: change, ref: entry, refid: r} ); + expData.push( entry ); + } + } + for( o in mref ){ + if( !o.change ) + continue; + o.ref.cExtracted = o.refid.join("\n"); + } + exportFile(potFilePath.split(".pot").join("-translated.pot"), expData); + } + + public static function export( out: haxe.io.Output, data : POData ){ + var ids = new Map(); + + for( e in data ){ + if( e.cTranslator != null ) + out.writeString("# "+e.cTranslator.split("\n").join("\n# ")+"\n"); + if( e.cExtracted != null ) + out.writeString("#. "+e.cExtracted.split("\n").join("\n#. ")+"\n"); + if( e.cRef != null ) + out.writeString("#: "+e.cRef.split("\n").join("\n#: ")+"\n"); + if( e.cFlags != null ) + out.writeString("#, "+e.cFlags.split("\n").join("\n#, ")+"\n"); + if( e.cPrevious != null ) + out.writeString("#| "+e.cPrevious.split("\n").join("\n#| ")+"\n"); + if( e.cComment != null ) + out.writeString("#~ "+e.cComment.split("\n").join("\n#~ ")+"\n"); + + var id = null; + if( e.msgid != null ){ + out.writeString(e.msgid+"\n"); + id = getMultiString(e.msgid,0); + }else if( e.id != null ){ + e.msgid = "msgid "+wrapQuote(e.id); + out.writeString(e.msgid+"\n"); + id = e.id; + } + + if( id != null ){ + #if gettext_warning + if( ids.exists(id) ) + Sys.println("Warning: duplicate id in pot: "+id); + #end + ids.set(id,true); + } + + if( e.msgstr != null ){ + out.writeString(e.msgstr+"\n"); + }else if( e.str != null ){ + e.msgstr = "msgstr "+wrapQuote(e.str); + out.writeString(e.msgstr+"\n"); + } + + out.writeString("\n"); + } + out.writeString("\n"); + } + + static var REG_STRING = ~/"((\\"|[^"]+)*)"$/; + static function getString( line : String, lnum:Int ){ + if( !REG_STRING.match(line) ) + throw "Parse error line "+lnum+ "("+line+")"; + return REG_STRING.matched(1); + } + + public static function getMultiString( lines : String, lnum ) { + return lines.split("\n").map(function(s) return getString(s,lnum++)).join(""); + } + + public static function wrapQuote( str : String ){ + var arr = [str]; + var i = 0; + while( arr[i].length > 72 ){ + var s = arr[i]; + var cut = s.indexOf(" ",72); + if( cut < 0 || cut >= s.length -1 ) + break; + arr[i] = s.substr(0,cut+1); + arr[i+1] = s.substr(cut+1); + i++; + } + if( arr.length > 1 ) + arr.unshift(""); + + return arr.map(quote).join("\n"); + } + + public static function quote( str : String ){ + return '"'+str+'"'; + } + + public static function unescape( str : String ){ + return str.split('\\"').join('"').split("\\n").join("\n").split("\\\\").join("\\"); + } + + public static function escape( str : String ){ + return str.split("\\").join("\\\\").split("\n").join("\\n").split('"').join('\\"'); + } + + public static function cloneEntry( e : POEntry ) : POEntry { + var ne : POEntry = { + id: e.id, + str: e.str, + }; + + if( e.msgid != null ) ne.msgid = e.msgid; + if( e.msgstr != null ) ne.msgstr = e.msgstr; + if( e.cTranslator != null ) ne.cTranslator = e.cTranslator; + if( e.cExtracted != null ) ne.cExtracted = e.cExtracted; + if( e.cRef != null ) ne.cRef = e.cRef; + if( e.cFlags != null ) ne.cFlags = e.cFlags; + if( e.cPrevious != null ) ne.cPrevious = e.cPrevious; + if( e.cComment != null ) ne.cComment = e.cComment; + + return ne; + } + +} + +#end diff --git a/src/sugoi/i18n/Locale.hx b/src/sugoi/i18n/Locale.hx new file mode 100644 index 0000000..4638623 --- /dev/null +++ b/src/sugoi/i18n/Locale.hx @@ -0,0 +1,75 @@ +package sugoi.i18n; + + +/** + * @author tpfeiffer + */ +class Locale +{ + static public var texts : GetText; + + /** + * Cache gettext objects in case of many language switches + * (like in a cron action which lead to send many emails in various languages) + */ + static public var cache = new Map(); + + public static function init(lang:String,?callback:GetText->Void):GetText + { + + //Load mo file in various runtimes : macro, js and serverside + #if macro + var filePath = sugoi.Web.getCwd() + "www/" + fileName(lang); + if( !sys.FileSystem.exists(filePath) ){ + //Create MO file from PO file. "Gettext" package should be installed. + var cmd = 'msgfmt -o $filePath '+filePath.substr(0,filePath.length-3)+'.po'; + Sys.println(cmd); + var p = new sys.io.Process(cmd); + p.exitCode(); + } + var file = sys.io.File.getBytes( filePath ); + texts = new GetText(); + texts.readMo(file); + return texts; + #elseif js + var file : haxe.io.Bytes = null; + var r = new js.html.XMLHttpRequest(); + r.responseType = js.html.XMLHttpRequestResponseType.ARRAYBUFFER; + r.onreadystatechange = function(e:js.html.Event){ + if (r.readyState == js.html.XMLHttpRequest.DONE){ + file = haxe.io.Bytes.ofData(r.response); + texts = new GetText(); + texts.readMo(file); + callback(texts); + } + } + r.open('GET', '/'+fileName(lang), true); + r.send(); + return null; + #else + /*var texts = cache.get(lang); + trace(texts); + if (texts == null){*/ + var file = null; + try{ + file = sys.io.File.getBytes(sugoi.Web.getCwd() + "/" + fileName(lang)); + }catch(e:Dynamic){ + //fail safely + App.current.session.addMessage("Cannot read translation file : "+Std.string(e)); + } + texts = new GetText(); + if(file!=null) texts.readMo(file); + /*cache.set(lang,texts); + }*/ + return texts; + #end + + + } + + + inline static function fileName(lang:String) + { + return "lang/texts_" +lang+ ".mo"; + } +} diff --git a/src/sugoi/i18n/TemplateTranslator.hx b/src/sugoi/i18n/TemplateTranslator.hx new file mode 100644 index 0000000..ca4c15e --- /dev/null +++ b/src/sugoi/i18n/TemplateTranslator.hx @@ -0,0 +1,133 @@ +package sugoi.i18n; + +import haxe.macro.Expr; +import haxe.macro.Context; + + /** + * Computes translated templates from master templates + * + * @author tpfeiffer + */ +class TemplateTranslator +{ + macro public static function parse(path:String) + { + var langs = new sugoi.Config(neko.Web.getCwd()).LANGS; + + for( lang in langs ) { + Sys.println(lang + " : Generating template files"); + Locale.init(lang); + translateTemplates(lang, path); + //translationForJs(lang); + } + + return macro {} + } + + #if macro + /*static public function translationForJs(lang:String){ + + var out = new StringBuf(); + var v = ""; + out.add("var texts = [];\n"); + for ( k in Locale.texts.texts.keys()){ + v = Locale.texts.get(k); + k = StringTools.replace(k, '\"', '\\"'); + v = StringTools.replace(v, '\"', '\\"'); + + out.add('texts["$k"] = "$v";\n'); + } + var path = sugoi.Web.getCwd() + "www/js/texts_" + lang + ".js"; + sys.io.File.saveContent(path, out.toString()); + Sys.println(lang +" : Save js translation file (" + path + ")"); + }*/ + + static public function translateTemplates(lang:String, folder:String) + { + //Sys.println('$lang : $folder'); + + //var strReg = ~/(::_\("([^"]*)"\)::)+/ig; + //var strReg = ~/(::_\([ ]*"([^"]+)+"[ ]*\)::)+/ig; + var strReg = ~/_\([ ]*"((?:[^"\\]+|\\.)*)"[ ]*(?:,[ ]*{[.,:\w\s\(\)]*})?\)/igm; + + for ( f in sys.FileSystem.readDirectory(folder) ) { + + // Parse sub folders + if(sys.FileSystem.isDirectory(folder+"/"+f) ) { + //create target directory + var langPath = StringTools.replace(folder+"/"+f, "master", lang); + sys.FileSystem.createDirectory(langPath); + translateTemplates(lang, folder+"/"+f); + continue; + } + + var isTemplateFile = f.substr(f.length - 4) == ".mtt"; + if( !isTemplateFile ) + continue; + + var filePath = StringTools.replace(folder+"/"+f, "master", lang); + Sys.println(lang + " : " + filePath); + + var c = sys.io.File.getContent(folder + "/" + f); + var out = ""; + try{ + out = strReg.map(c, function(e) { + var str = e.matched(1); + //Sys.println("str matched:"+str); + // Ignore commented strings + //var i = str.indexOf("//"); + //if( i >= 0 && i < strReg.matchedPos().pos ) + // return ""; + + var cleanedStr = str; + // Translator comment + var comment : String = null; + if( cleanedStr.indexOf("||") >= 0 ) { + var parts = cleanedStr.split("||"); + if( parts.length!=2 ) { + throw "Malformed translator comment"; + return ""; + } + comment = StringTools.trim(parts[1]); + cleanedStr = cleanedStr.substr(0,cleanedStr.indexOf("||")); + cleanedStr = StringTools.rtrim(cleanedStr); + } + + //Sys.println(e.matched(0)+" replace "+str+" by "+Locale.texts.get(cleanedStr)); + var output = StringTools.replace( e.matched(0), str, Locale.texts.get(cleanedStr) ); + //Sys.println("output:"+output); + return output; + //return Locale.texts.get(cleanedStr); + + /* + function getVars(ereg:EReg, input:String, index:Int = 0):Array { + var matches = []; + while (ereg.match(input)) { + matches.push(ereg.matched(index)); + input = ereg.matchedRight(); + } + return matches; + } + var eregVars = ~/(?:::([^:]+)::)/i; + var aVars = getVars(eregVars, strTmp,1); + var sVars = aVars.map(function(v) { return v+":"+v; }); + var variables = '{'+sVars.join(",")+'}'; + + var contentWithVars = StringTools.replace(e.matched(1), str, strTmp+","+variables); + return StringTools.replace(contentWithVars, "::_", "::__"); + */ + }); + }catch (e:Dynamic){ + throw "Error in " + f + " : " + e; + } + + //copy the file to the correct new folder + var langFile = sys.io.File.write(filePath, false); + out = StringTools.replace(out, "\r", "");//for an unknown reason, there was double newlines + langFile.writeString(out); + langFile.flush(); + langFile.close(); + } + } + #end +} diff --git a/src/sugoi/i18n/translator/GetText.hx b/src/sugoi/i18n/translator/GetText.hx new file mode 100644 index 0000000..33067ae --- /dev/null +++ b/src/sugoi/i18n/translator/GetText.hx @@ -0,0 +1,178 @@ +package sugoi.i18n.translator; + +/** + * GetText (*.po/*.mo) translator + * @doc https://www.gnu.org/software/gettext/manual/html_node/MO-Files.html + * @author fbarbut + */ +class GetText implements sugoi.i18n.translator.ITranslator{ + + public var texts : Map; + + + public function toString() { + return "#GetText"; + } + + public function _(str:String, ?params : Dynamic):String { + if(texts == null) throw "no data in dictionnary"; + if(texts.exists(str)) str = texts.get(str); + + var list = str.split("::"); + var n = 0; + if(params!=null){ + for (k in Reflect.fields(params)){ + str = StringTools.replace(str, "::" + k.substr(1) + "::", Reflect.field(params, k)); + } + } + + return str; + } + + + public function loadMoFile(d:haxe.io.Bytes) { + var r = new MoReader(d); + texts = r.parse(); + r = null; + } + + + public function new() + { + + + } + + /** + * parse all the project source code and generate the *.pot file + */ + macro public static function parse(codePath:String, potFilePath:String) { + Sys.println(codePath); + + var path = codePath; + var data = new Map(); + explore(path, data); + writePotFile(data,potFilePath); + return macro { }; //returns an empty expression + } + + + + #if macro + + /** + * Generates the POT (PO template) file which contains all the keys to translate + */ + static function writePotFile(data:Map, potFilePath:String ) { + var pot = new StringBuf(); + var d = null; + for(k in data.keys()) { + d = data.get(k); + pot.add( "#"+d.path+"\n" ); + pot.add( "msgid \""+k+"\"\n" ); + pot.add( "msgstr \"\"\n\n" ); + } + sys.io.File.saveContent(potFilePath, pot.toString()); + } + + /** + * function recursive qui analyse les dossiers/fichiers + * @param folder + * @param pot + */ + static function explore(folder:String, data) { + Sys.println("parsing "+folder); + for(f in sys.FileSystem.readDirectory(folder) ) { + + if(f.substr(f.length - 3) == ".hx") { + var c = sys.io.File.getContent(folder+"/"+f); + var reg = ~/t\._\("([^"]+)"\)/i; // match t._("truc"). Le point et la parenthese sont escapés. + for( line in c.split("\n")) { + + if(line == "") continue; + var r = reg.match(line); + if(r) { + data.set(reg.matched(1),{path:folder+"/"+f}); + } + } + + }else if( sys.FileSystem.isDirectory(folder+"/"+f) ) { + explore(folder+"/"+f,data); + + } + + } + + } + #end + +} + +/** + * GNU GetText MO file reader + */ +class MoReader +{ + private var original_table_offset:UInt; + private var translated_table_offset:UInt; + private var hash_num_entries:UInt; + private var hash_offset:UInt; + private var data:haxe.io.BytesInput; + + static var MAGIC:UInt = 0x950412DE; + static var MAGIC2:UInt = 0xDE120495; + + public function new(data:haxe.io.Bytes):Void + { + this.data = new haxe.io.BytesInput(data); + } + + public function parse():Map + { + var d = data; + var header : UInt = d.readInt32(); + + if(header != MAGIC && header != MAGIC2) { + throw "Bad MO file header : " + header; + } + + var revision:UInt = d.readInt32(); + if (revision > 1){ + throw "Bad MO file format revision : "+revision; + } + + var num_strings:UInt = d.readInt32(); + original_table_offset= d.readInt32(); + translated_table_offset = d.readInt32(); + hash_num_entries= d.readInt32(); + hash_offset= d.readInt32(); + + var texts = new Map(); + for (i in 1...num_strings) + { + texts.set(getOriginalString(i), getTranslatedString(i)); + } + + return texts; + + } + + function getTranslatedString(index:Int):String + { + return getString(translated_table_offset + 8 * index ); + } + + function getOriginalString(index:Int):String + { + return getString(original_table_offset + 8 * index ); + } + + function getString(offset:UInt):String + { + data.position = offset; + var length :UInt = data.readInt32(); + var pos :UInt = data.readInt32(); + data.position = pos; + return data.readString(length); + } +} \ No newline at end of file diff --git a/src/sugoi/i18n/translator/ITranslator.hx b/src/sugoi/i18n/translator/ITranslator.hx new file mode 100644 index 0000000..3c80b23 --- /dev/null +++ b/src/sugoi/i18n/translator/ITranslator.hx @@ -0,0 +1,21 @@ +package sugoi.i18n.translator; + +/** + * Text translation interface + * + * @author fbarbut + */ + +interface ITranslator{ + + /** + * Get translated text + * + * @param t Key to translate + * @param ?params Params to inject in string + */ + public function _( t : String, ?params : Dynamic ):String; + + public function getStrings():Map; + +} \ No newline at end of file diff --git a/src/sugoi/i18n/translator/TMap.hx b/src/sugoi/i18n/translator/TMap.hx new file mode 100644 index 0000000..7ab8142 --- /dev/null +++ b/src/sugoi/i18n/translator/TMap.hx @@ -0,0 +1,45 @@ +package sugoi.i18n.translator; + +/** + * Simple Map based Translator + * + * @author fbarbut + */ +class TMap implements ITranslator{ + + var texts : Map = null; + var lang : String; + + + public function new(arr: Map, lang:String) { + this.lang = lang; + texts = arr; + } + + public function _(key:String, ?data:Dynamic):String { + if (key == null) throw "key is null"; + + var str = texts.get(key); + + if(str == null) { + //App.log("key \""+key+"\" not found in "+texts); + str = key; + } + + + if(data!=null){ + //var list = str.split("::"); + for (k in Reflect.fields(data)){ + str = StringTools.replace(str, "::" + k.substr(1) + "::", Reflect.field(data, k)); + } + } + return str; + } + + public function getStrings(){ + return texts; + } + + + +} diff --git a/src/sugoi/i18n/translator/TSimple.hx b/src/sugoi/i18n/translator/TSimple.hx new file mode 100644 index 0000000..24ba1ea --- /dev/null +++ b/src/sugoi/i18n/translator/TSimple.hx @@ -0,0 +1,178 @@ +package mt.text.translate; + +/** + * Translate from "simple" text format. + * + * If many values are found for a key, + * a random one is chosen. + * + * e.g : + * + * key1 + * value1 + * value2 + * key2 + * value2 + * value2 + * + * + */ +class TSimple implements ITranslate{ + var texts : Hash>; + //var texts : Hash>; + var rseed : mt.Rand; + var used : Hash>; //stock already used sentences + + public var throwExceptionOnUnfoundKey:Bool; + + public function new() { + used = new Hash>(); + throwExceptionOnUnfoundKey = true; + + } + + static function fatal(err:String) { + throw "text.translate.TSimple : "+err; + } + + public function init(raw:String,?seed:Int):Void { + /* + #if neko + var raw = neko.io.File.getContent(App.config.TPL+"../../xml/"+App.config.LANG+"/texts.xml"); + #end + #if flash + var raw = haxe.Resource.getString(Game.LANG+".texts.xml"); + #end + */ + + if ( raw==null || raw=="" ) + fatal("no data"); + + if(seed == null) { + seed = 777; + } + initSeed(seed); + + /*if (TEXTS==null) {*/ + // parsing + texts = new Hash(); + + var lines = raw.split("\n"); + var key : String = null; + for (line in lines) { + var trimmed = StringTools.trim(line); + if ( trimmed.length==0 ) continue; + if ( line.charAt(0)==" " ) + fatal("unexpected leading space around key "+key); + if ( line.charAt(0)!="\t" ) + key = trimmed.toLowerCase(); + else { + if ( key==null ) fatal("unexpected : "+line); + if ( texts.get(key)==null ) texts.set(key,new Array()); + texts.get(key).push( trimmed ); + } + } + /*}*/ + } + + public inline function initSeed(s) { + rseed = new mt.Rand(0); + rseed.initSeed(s); + } + + function isAlreadyUsed(key:String,sentence:String):Bool { + if(used.get(key) != null) { + return Lambda.has( used.get(key) , sentence ); + }else { + return false; + } + + } + + /** + * get the random value from a key + * @param key + * @param ?rfunc Random function + * @param ?fl_firstRecurs=true + */ + public function get(key:String, ?rfunc:Int->Int, ?fl_firstRecurs=true):String { + if (rfunc==null){ + rfunc = rseed.random; + } + key = key.toLowerCase(); + + var list = texts.get(key); + if ( key == null || list == null || list.length == 0 ) { + if(throwExceptionOnUnfoundKey){ + fatal("Unknown key \"" + key + "\""); + }else { + return key; + } + } + + var str = ""; + + /** + * avoid selecting an already selected random sentence + */ + if(used.get(key) != null && used.get(key).length >= list.length) { //if all have been already selected, reset + //trace('reset stack'); + used.set(key,[]); + } + var i = 0; + do { + str = list[rfunc(list.length)]; + i++; + //trace("str chosen : "+str+", already chosen :"+isAlreadyUsed(key, str)); + }while(isAlreadyUsed(key, str) && i<100 ); + + //store the selected value + var x = used.get(key); + if(x == null) { + x = new Array(); + } + x.push(str); + used.set(key, x ); + + /** + * can use keys in a string like "Hi %buddies%" + */ + var list = str.split("%"); + if (list.length>1) { + str = ""; + var i = 1; + for (v in list) { + if (i%2==0) + str += get(v,false); + else + str+=v; + i++; + } + } + + return str; + } + + /** + * Return a translated text with params like ::name:: in the text + * @param key + * @param ?data + * @return + */ + public function format(key:String, ?data:Dynamic):String { + var str = get(key); + var list = str.split("::"); + var n = 0; + if(data!=null){ + for (k in Reflect.fields(data)){ + str = StringTools.replace(str, "::" + k.substr(1) + "::", Reflect.field(data, k)); + } + } + return str; + } + + public function _(key:String, ?data:Dynamic):String { + return format(key, data); + } + +} diff --git a/src/sugoi/i18n/translator/TXml.hx b/src/sugoi/i18n/translator/TXml.hx new file mode 100644 index 0000000..417fd7b --- /dev/null +++ b/src/sugoi/i18n/translator/TXml.hx @@ -0,0 +1,63 @@ +package mt.text.translate; + +/** + * Translation class fed by XML like + * + Atteindre un score de ::score:: + Atteindre le level ::level:: + Faire un temps de moins de ::time:: + Faire un truc spécial dans ce jeu + + */ + +class TXml implements ITranslate{ + + static var TEXTS = new Hash(); + + public function new() { + + } + + public function init (raw:String, ?seed:Int) { + + var xml = Xml.parse( raw ).firstChild(); + + //trace ( xml.elements() ); + + var h = new Hash(); + for( x in xml.elements() ) { + var id = x.get("id"); + if( id == null ) + throw "Missing 'id' in data.xml"; + if( h.exists(id) ) + throw "Duplicate id '"+id+"' in data.xml"; + var buf = new StringBuf(); + for( c in x ) + buf.add(c.toString()); + var s = mt.deepnight.Lib.replaceTag(buf.toString(), "*", "", ""); + s = mt.deepnight.Lib.replaceTag(s, "||", "", ""); + //trace("text : "+id+", "+s); + h.set(id,s); + } + + TEXTS = h; + } + + public function format(key:String, ?data:Dynamic):String { + var str = TEXTS.get(key); + var list = str.split("::"); + var n = 0; + if(data!=null){ + for (k in Reflect.fields(data)){ + str = StringTools.replace(str, "::" + k.substr(1) + "::", Reflect.field(data, k)); + } + } + return str; + } + + + public function _(key:String, ?data:Dynamic):String { + return format(key, data); + } + +} diff --git a/src/sugoi/mail/BufferedMailer.hx b/src/sugoi/mail/BufferedMailer.hx new file mode 100644 index 0000000..d722181 --- /dev/null +++ b/src/sugoi/mail/BufferedMailer.hx @@ -0,0 +1,61 @@ +package sugoi.mail; +import sugoi.mail.IMail; +import sugoi.mail.IMailer; + +/** + * Manage an email buffer in a table before sending them + * @author fbarbut + */ +class BufferedMailer implements IMailer +{ + var conf : Dynamic; + var type : String; + + public function new() {} + + public function init(?c:Dynamic):IMailer{ + return this; + } + + public function defineFinalMailer(type:String){ + this.type = type; + } + + public function send(m:sugoi.mail.IMail,?params:Dynamic,?callback:MailerResult->Void):Void{ + + var bm = new sugoi.db.BufferedMail(); + bm.headers = m.getHeaders(); + bm.title = m.getTitle(); + bm.htmlBody = m.getHtmlBody(); + bm.textBody = m.getTextBody(); + bm.recipients = m.getRecipients(); + bm.sender = m.getSender(); + + bm.mailerType = this.type; + + //custom params + if(params!=null){ + bm.data = params; + if(Reflect.hasField(params,"remoteId")){ + bm.remoteId = Reflect.getProperty(params,"remoteId"); + } + } + + //set sending status as "queued" + var map = new MailerResult(); + for( r in m.getRecipients() ){ + map.set( r.email , Success(Queued) ); + } + + bm.status = map; + bm.insert(); + + if(callback!=null) callback(map); + + } + + + +} + + \ No newline at end of file diff --git a/src/sugoi/mail/DebugMailer.hx b/src/sugoi/mail/DebugMailer.hx new file mode 100644 index 0000000..a976b75 --- /dev/null +++ b/src/sugoi/mail/DebugMailer.hx @@ -0,0 +1,45 @@ +package sugoi.mail; +import sugoi.mail.IMail; +import sugoi.mail.IMailer; + +/** + * A Debug Mailer to use in dev environment : + * logs the emails in the Error table + write html files in tmp folder + * + * @author fbarbut + */ +class DebugMailer implements IMailer +{ + public function new() {} + + public function init(?c:Dynamic):IMailer{ + return this; + } + + public function send(m:sugoi.mail.IMail,?params:Dynamic,?callback:MailerResult->Void):Void{ + + //log in the Error table + var t = new StringBuf(); + t.add("to:" + m.getRecipients()+"\n"); + t.add("subject:" + m.getSubject()+"\n"); + t.add("body:" + m.getHtmlBody()+"\n"); + App.current.logError( "[DEBUG] Email sent to " + m.getRecipients(), t.toString() ); + + //log in an html file + var tmpDir = sugoi.Web.getCwd() + "../tmp/"; + if ( !sys.FileSystem.exists(tmpDir) ) sys.FileSystem.createDirectory(tmpDir); + var dest = m.getRecipients()[0].email; + sys.io.File.saveContent( tmpDir + dest+"-"+Date.now().toString().substr(0,10)+ "-"+ m.getSubject() + ".html" , m.getHtmlBody() ); + + //callback + if (callback != null){ + + var map = new MailerResult(); + for ( u in m.getRecipients() ){ + map.set( u.email , Success(Sent) ); + } + callback(map); + } + } + +} diff --git a/src/sugoi/mail/IMail.hx b/src/sugoi/mail/IMail.hx new file mode 100644 index 0000000..ecac258 --- /dev/null +++ b/src/sugoi/mail/IMail.hx @@ -0,0 +1,23 @@ +package sugoi.mail; + +/** + * Interface that represents an email message. + */ +interface IMail +{ + public function setSender(email:String, ?name:String, ?userId:Int):IMail; + public function setRecipient(email:String, ?name:String, ?userId:Int):IMail; + public function addRecipient(email:String, ?name:String, ?userId:Int):IMail; + public function setSubject(subject:String):IMail; + public function setHeader(key:String, value:String):IMail; + public function setHtmlBody(body:String):IMail; + public function setTextBody(body:String):IMail; + + public function getSender(): {?userId:Int,email:String,name:String}; + public function getRecipients():Array<{?userId:Int,email:String,name:String}>; + public function getSubject():String; + public function getTitle():String; + public function getHtmlBody():String; + public function getTextBody():String; + public function getHeaders():Map; +} \ No newline at end of file diff --git a/src/sugoi/mail/IMailer.hx b/src/sugoi/mail/IMailer.hx new file mode 100644 index 0000000..1ad3518 --- /dev/null +++ b/src/sugoi/mail/IMailer.hx @@ -0,0 +1,41 @@ +package sugoi.mail; +import tink.core.Future; +import tink.core.Outcome; + +/** + * Errors that occurs after sending an email thru a mailer + */ +enum MailerError{ + GenericError(e:tink.core.Error); + HardBounce; //bad mailbox + SoftBounce; //mailbox exists but is full or not reachable + Spam; //email is considered spam + Unsub; //this user unsubscribed from this service/list + Unsigned; //the sender is invalid ( i.e does not match the SPF records ) +} + +enum MailerSuccess{ + Sent; + Queued; +} + +typedef MailerResult = Map> + +/** + * Interface for "Mailers" + * + * @author fbarbut + */ +interface IMailer +{ + /** + * init with a configuration object + */ + public function init(?conf:{smtp_host:String,smtp_port:Int,smtp_user:String,smtp_pass:String}):IMailer; + + /** + * Sends an email. A callback can be defined to handle the result + */ + public function send(email:IMail,?params:Dynamic,?callback:MailerResult->Void):Void; + +} \ No newline at end of file diff --git a/src/sugoi/mail/Mail.hx b/src/sugoi/mail/Mail.hx new file mode 100644 index 0000000..aa90aa9 --- /dev/null +++ b/src/sugoi/mail/Mail.hx @@ -0,0 +1,182 @@ +package sugoi.mail; +using Lambda; + +class Mail implements IMail +{ + + public var title : String; + public var htmlBody : String; + public var textBody : String; + var headers : Map; + var sender : {name:String,email:String,?userId:Int}; + var recipients : Array<{name:String,email:String,?userId:Int}>; + + + + public function new() { + recipients = []; + headers = new Map(); + } + + public function getRecipients(){ + return recipients; + } + + public function setSender(email, ?name,?userId) { + if(!isValid(email)) throw "invalid sender email : \""+email+"\""; + + sender = {name:name,email:email,userId:userId}; + return this; + } + + public function setReplyTo(email, ?name) { + if(!isValid(email)) throw "invalid reply-to email : \""+email+"\""; + + setHeader("Reply-To","<"+email+">"+(name==null?"":name)); + } + + public function setSubject(s:String) { + title = s; + return this; + } + + /** + * can add one or more recipient + * @param email + * @param ?name + * @param ?userId + */ + public function addRecipient(email:String, ?name:String, ?userId:Int) { + if(!isValid(email)) throw "invalid recipient \""+email+"\""; + recipients.push( {email:email, name:name, userId:userId } ); + return this; + } + + /** + * alias to addRecipient() + * @param email + * @param ?name + * @param ?userId + */ + public function setRecipient(email:String, ?name:String, ?userId:Int) { + addRecipient(email, name, userId); + return this; + } + + public static function isValid( addr : String ){ + var reg = ~/^[^()<>@,;:\\"\[\]\s[:cntrl:]]+@[A-Z0-9][A-Z0-9-]*(\.[A-Z0-9][A-Z0-9-]*)*\.(xn--[A-Z0-9]+|[A-Z]{2,8})$/i; + return addr != null && reg.match(addr); + } + + public function setHeader(k:String, v:String) { + headers.set(k, v); + return this; + } + + /** + * generate a custom key for transactionnal emails, valid during the current day + */ + public function getKey() { + return haxe.crypto.Md5.encode(App.config.get("key")+recipients[0].email+(Date.now().getDate())).substr(0,12); + } + + /** + * render html from a template + vars + * @param tpl A Template path + * @param ctx Vars to send to template + */ + public function setHtmlBodyWithTemplate(tpl, ctx:Dynamic) { + var app = App.current; + var tpl = app.loadTemplate(tpl); + if( ctx == null ) ctx = { }; + ctx.HOST = App.config.HOST; + ctx.key = getKey(); + ctx.senderName = sender.name; + ctx.senderEmail = sender.email; + ctx.recipientName = recipients[0].name; + ctx.recipientEmail = recipients[0].email; + ctx.recipients = recipients; + CSSInlining(ctx); + htmlBody = tpl.execute(ctx); + + } + + public function setHtmlBody(s) { + htmlBody = s; + return this; + } + + public function setTextBodyWithTemplate(tpl, ctx:Dynamic) { + var app = App.current; + var tpl = app.loadTemplate(tpl); + if( ctx == null ) ctx = { }; + ctx.HOST = App.config.HOST; + ctx.key = getKey(); + textBody = tpl.execute(ctx); + return this; + } + + + function CSSInlining(ctx) { + // CSS inlining + var css : Map> = new Map(); + ctx.addStyle = function(sel:String, style:String) { + sel = sel.toLowerCase(); + if (css.exists(sel)) + css.set(sel, css.get(sel).concat(style.split(";"))); + else + css.set(sel, style.split(";")); + return ""; + } + var applyStyleRec = null; + applyStyleRec = function(x:Xml) { + if (x.nodeType==Xml.Element) { + var name = x.nodeName.toLowerCase(); + if( css.exists(name) ) + if (x.get("style")!=null) + x.set("style", x.get("style")+";"+css.get(name).join(";")); + else + x.set("style", css.get(name).join(";")); + for (n in x) + applyStyleRec(n); + } + } + ctx.applyStyle = function(raw:String) { + var x = Xml.parse(raw); + for(n in x) + applyStyleRec(n); + return x.toString(); + } + } + + + public function getSubject(){ + return title; + } + + public function getTitle(){ + return getSubject(); + } + + public function getHtmlBody(){ + return htmlBody; + } + + public function getTextBody(){ + return textBody; + } + + public function setTextBody(t){ + textBody = t; + return this; + } + + public function getHeaders(){ + return headers; + } + + public function getSender(){ + return sender; + } + +} \ No newline at end of file diff --git a/src/sugoi/mail/MandrillMailer.hx b/src/sugoi/mail/MandrillMailer.hx new file mode 100644 index 0000000..846f597 --- /dev/null +++ b/src/sugoi/mail/MandrillMailer.hx @@ -0,0 +1,132 @@ +package sugoi.mail; +import sugoi.mail.IMail; +import sugoi.mail.IMailer; + + +/** + * Send an email via Mandrill.com API + * @author fbarbut + * @doc https://mandrillapp.com/api/docs/messages.JSON.html + */ +class MandrillMailer implements IMailer +{ + + var conf : Dynamic; + + public function new() {} + + public function init(?c:Dynamic):IMailer{ + conf = c; + return this; + } + + public function send(m:sugoi.mail.IMail,?params:Dynamic,?callback:MailerResult->Void):Void{ + + //build an object from headers map + var headersObj = { }; + var headers = m.getHeaders(); + for(k in headers.keys()) { + Reflect.setField(headersObj, k, headers.get(k)); + } + + var data = { + key: conf.smtp_pass, + message: { + html : m.getHtmlBody(), + text : m.getTextBody(), + subject : m.getSubject(), + from_email : m.getSender().email, + from_name : m.getSender().name, + to : [], + headers : headersObj, + //images : images, + } + }; + for (r in m.getRecipients()) { + data.message.to.push( { email:r.email, name:r.name, type:"to" } ); + } + + var raw = curlRequest("POST", "https://mandrillapp.com/api/1.0/messages/send.json", {}, haxe.Json.stringify(data)); + + if (callback != null){ + + if (raw == null) throw "CURL response is null"; + if (raw == "") throw "CURL response is empty"; + var apiResult : MandrillApiSendResult = null; + try{ + apiResult = haxe.Json.parse(raw); + + }catch (e:Dynamic){ + throw "unable to decode : " + raw + ", error is "+Std.string(e); + } + + var map = new MailerResult(); + for ( r in apiResult){ + var v : tink.core.Outcome = null; + + switch(r.status){ + case "sent" : + v = Success(Sent); + case "queued": + v = Success(Queued); + default: + //"hard-bounce", "soft-bounce", "spam", "unsub", "custom", "invalid-sender", "invalid", "test-mode-limit", "unsigned", or "rule" + switch(r.reject_reason){ + case "hard-bounce" : + v = Failure(HardBounce); + case "soft-bounce": + v = Failure(SoftBounce); + case "spam": + v = Failure(Spam); + case "unsub": + v = Failure(Unsub); + default : + v = Failure(GenericError(new tink.core.Error(raw))); + } + } + + map.set( r.email , v ); + } + callback(map); + } + } + + + public function curlRequest( method: String, url : String, ?headers : Dynamic, postData : String ) : Dynamic { + var cParams = ["-X"+method,"--max-time","15"]; + for( k in Reflect.fields(headers) ){ + cParams.push("-H"); + cParams.push(k+": "+Reflect.field(headers,k)); + } + cParams.push(url); + if( postData != null ){ + cParams.push("-d"); + cParams.push(postData); + } + + var p = new sys.io.Process("curl", cParams); + //var curlRq = "curl " + cParams.join(" "); + + #if neko + var str = neko.Lib.stringReference(p.stdout.readAll()); + #else + var str = p.stdout.readAll().toString(); + #end + + if (str == null || str == "") { + str = neko.Lib.stringReference(p.stderr.readAll()); + } + + p.exitCode(); + + return str; + } + +} + +typedef MandrillApiSendResult = Array <{ + email:String, + status:String, + _id:String, + reject_reason:String, +}>; \ No newline at end of file diff --git a/src/sugoi/mail/SmtpMailer.hx b/src/sugoi/mail/SmtpMailer.hx new file mode 100644 index 0000000..aa52f5d --- /dev/null +++ b/src/sugoi/mail/SmtpMailer.hx @@ -0,0 +1,68 @@ +package sugoi.mail; +import tink.core.Future; +import tink.core.Noise; +import sugoi.mail.IMailer; +import smtpmailer.Address; + +/** + * Send emails thru SMTP by using ben merckx's library + * @ref https://github.com/benmerckx/smtpmailer + */ +class SmtpMailer implements IMailer +{ + var m : smtpmailer.SmtpMailer; + + public function new(){} + + public function init(?conf:{smtp_host:String,smtp_port:Int,smtp_user:String,smtp_pass:String}) :IMailer + { + m = new smtpmailer.SmtpMailer({ + host: conf.smtp_host, + port: conf.smtp_port, + auth: { + username: conf.smtp_user, + password: conf.smtp_pass + } + }); + + return this; + } + + public function send(e:sugoi.mail.IMail,?params:Dynamic,?callback:MailerResult->Void) + { + var surprise = m.send({ + subject: e.getSubject(), + /*from: e.getSender().email, + to: Lambda.array(Lambda.map(e.getRecipients(), function(x) return smtpmailer.Address.ofString(x.email) )), + //headers : e.getHeaders(),*/ + from: new Address({address:e.getSender().email}), + to: Lambda.array(Lambda.map(e.getRecipients(), function(x) return new Address({address:x.email}) )), + headers : e.getHeaders(), + content: { + text: e.getTextBody(), + html: e.getHtmlBody() + }/*, + attachments: []*/ + }); + + + if (callback != null){ + + surprise.handle(function(s){ + + var map = new MailerResult(); + + switch(s){ + case Success(_): + map.set("*",Success(Sent)); + + case Failure(e): + map.set("*",Failure(GenericError(e))); + } + + callback(map); + }); + } + } + +} \ No newline at end of file diff --git a/src/sugoi/plugin/IPlugIn.hx b/src/sugoi/plugin/IPlugIn.hx new file mode 100644 index 0000000..1754566 --- /dev/null +++ b/src/sugoi/plugin/IPlugIn.hx @@ -0,0 +1,13 @@ +package sugoi.plugin; + +/** + * @author fbarbut + */ + +interface IPlugIn +{ + public function getName():String; + /*public function getController():sugoi.BaseController; + public function isInstalled():Bool; + public function install():Void;*/ +} \ No newline at end of file diff --git a/src/sugoi/plugin/PlugIn.hx b/src/sugoi/plugin/PlugIn.hx new file mode 100644 index 0000000..46800c1 --- /dev/null +++ b/src/sugoi/plugin/PlugIn.hx @@ -0,0 +1,135 @@ +package sugoi.plugin; +import haxe.macro.Context; +import haxe.macro.Expr; +import sys.FileSystem; +import sys.io.File; +/** + * Base plugin class + * @author fbarbut + */ +class PlugIn +{ + public var name :String; + public var file :String; + + public function new() + { + name = "Base Plugin"; + } + + /** + * create a simlink on windows/linux + * @param link + * @param target + */ + public function createSimLink(link:String, target:String) { + + if (Sys.systemName() == "Windows") { + + link = StringTools.replace(link,"/","\\"); + target = StringTools.replace(target,"/","\\"); + + trace("mklink /D " +link+" "+target+"
"); + //var p = new sys.io.Process("mklink /D", [link, target]); + //var p = new sys.io.Process("dir",[]); + }else { + //linux + new sys.io.Process("ln -s", [target, link]); + } + } + + /** + * copy plugins templates in lang/master/tpl/plugin at compilation time + */ + macro public static function copyTpl(){ + + for (p in Context.getClassPath()){ + + if (FileSystem.exists(p + "../haxelib.json")){ + + // load sugoi.tpl param in haxelib json + var hl = File.getContent(p + "../haxelib.json"); + var hl = haxe.Json.parse(hl); + if (hl.sugoi != null){ + //var from = FileSystem.fullPath(p + "../" + hl.sugoi.tpl + "/"); + var from = p + "../" + hl.sugoi.tpl + "/"; + //var to = FileSystem.fullPath(Sys.getCwd() + "lang/master/tpl/plugin/" + hl.sugoi.plugin + "/"); + var to = Sys.getCwd() + "lang/master/tpl/plugin/" + hl.sugoi.plugin + "/"; + copyDir( from , to ); + } + } + } + return macro {}; + } + + /** + * recursive file copy + * @param src Path to source directory + * @param dest Path to destination directory + */ + static function copyDir(src:String, dest:String){ + + if (!FileSystem.exists(dest)) FileSystem.createDirectory(dest); + + for ( r in FileSystem.readDirectory(src)){ + + if (FileSystem.isDirectory(src + r)) { + + //copyDir(FileSystem.fullPath(src + r +"/"), FileSystem.fullPath(dest + r + "/")); + copyDir( src + r + "/", dest + r + "/" ); + + }else if ( !FileSystem.exists(dest + r) ){ + + File.copy(src + r, dest + r); + Sys.println("Bundle plugin tpl :" + r); + + }else{ + + var srcStat = FileSystem.stat(src + r); + var destStat = FileSystem.stat(dest + r); + //Context.warning(destStat.mtime.toString()+" == "+srcStat.mtime.toString(), Context.currentPos()); + if (srcStat.mtime.getTime() > destStat.mtime.getTime()){ + File.copy(src + r, dest + r); + Sys.println("Bundle plugin tpl :" + r); + } + + } + } + + } + + public function getName() { + return name; + } + + /*public function getController():sugoi.BaseController { + return new pro.controller.Main(); + }*/ + + /*public function isInstalled():Bool { + var a = sys.FileSystem.exists(App.config.PATH + "/www/plugin/" + name); + var b = sys.FileSystem.exists(App.config.PATH + "/lang/fr/tpl/plugin/" + name); + return a && b; + } + + public function install() { + + //simlink de hosted/www dans www/plugin/hosted/ + //simlink de tpl/hosted dans fr/tpl/plugin/hosted ( pour que templo puisse compiler ) + + var pluginDir = file.split("/"); + pluginDir.pop(); + var pluginDir = pluginDir.join("/"); + + + //trace("de "+pluginDir+"/www/"); + //trace("vers "+App.config.PATH + "/www/plugin/" + name); + + //web root for the plugin + createSimLink(App.config.PATH + "/www/plugin/" + name, pluginDir + "/www/"); + + //templates + createSimLink(App.config.PATH + "/lang/fr/tpl/plugin/" + name, pluginDir + "/lang/fr/tpl/" + name); + }*/ + +} \ No newline at end of file diff --git a/src/sugoi/tools/Csv.hx b/src/sugoi/tools/Csv.hx new file mode 100644 index 0000000..98e06c0 --- /dev/null +++ b/src/sugoi/tools/Csv.hx @@ -0,0 +1,272 @@ +package sugoi.tools; + +/** + * CSV Import-Export Tool + * Based on thx.csv + * + * @author fbarbut + */ +class Csv +{ + public var separator :String; + var headers : Array; //datas accessible in both forms + public var datas : Array>; + public var datasAsMap : Array>; + public var step:Int;//@deprecated + + public function new() + { + separator = ","; + datas = []; + headers = []; + } + + public function setHeaders(_headers){ + headers = _headers; + } + + public function addDatas(d:Array) { + datas.push( Lambda.array(Lambda.map(d, function(x) return StringTools.trim(Std.string(x)) )) ); + } + + public function getHeaders(){ + return headers; + } + + public function getDatas():Array>{ + return datas; + } + + public function isEmpty(){ + return datasAsMap == null || datasAsMap.length == 0; + } + + + /** + * Import CSV / DCSV datas as Maps + */ + public function importDatasAsMap(d:String):Array>{ + + if (headers.length == 0) throw "CSV headers should be defined"; + + //separator detection + try{ + var x = d.split("\n"); + if (x[0].split(";").length > x[0].split(",").length) separator = ";"; + + }catch (e:Dynamic){} + + var _datas = []; + if (separator == ","){ + _datas = thx.csv.Csv.decode(d); + }else{ + _datas = thx.csv.DCsv.decode(d); + } + + //removes headers + _datas.shift(); + + //cleaning + for ( o in _datas.copy() ) { + //remove empty lines + if (o == null || o.length <= 1) { + _datas.remove(o); + continue; + } + + //nullify empty fields + for (i in 0...o.length) { + + if (o[i] == "" || o[i] == "null" || o[i]=="NULL") { + o[i] = null; + continue; + } + + //clean spaces and useless chars + o[i] = StringTools.trim(o[i]); + o[i] = StringTools.replace(o[i], "\n", ""); + o[i] = StringTools.replace(o[i], "\t", ""); + o[i] = StringTools.replace(o[i], "\r", ""); + } + + //remove empty lines + if (isNullRow(o)) _datas.remove(o); + + } + + //cut columns which are out of headers + for ( d in _datas){ + datas.push( d.copy().splice(0, headers.length) ); + } + + //maps + datasAsMap = new Array>(); + + for ( d in datas){ + + var m = new Map(); + + for ( h in 0...headers.length){ + + //nullify + var v = d[h]; + if ( v == "" || v == "null" || v=="NULL" ) v = null; + m[headers[h]] = v; + } + + datasAsMap.push(m); + } + + return datasAsMap; + + } + + /** + * Import CSV Datas + * + * @deprecated Use importDatasAsMap instead + */ + public function importDatas(d:String):Array> { + + d = StringTools.replace(d, "\r", ""); //vire les \r + + var data = d.split("\n"); + + //separator detection + if (data[0].split(";").length > data[0].split(",").length) separator = ";"; + + var out = new Array>(); + + var rowLen = null; + if (headers != null && headers.length > 0) rowLen = headers.length; + + //fix quoted fields with comas inside : "23 allée des Taupes, 46100 Camboulis" + for (d in data) { + var x = d.split('"'); + for (i in 0...x.length) { + if (i % 2 == 1) x[i] = StringTools.replace(x[i], separator, "|"); + } + d = x.join(""); + var row = d.split(separator); + if (rowLen != null) row = row.splice(0, rowLen); //no extra columns + + out.push(row); + } + + for (u in out) { + for (i in 0...u.length) { + u[i] = StringTools.replace(u[i], "|", separator); + } + } + + //cleaning + for ( o in out.copy() ) { + //remove empty lines + if (o == null || o.length <= 1) { + out.remove(o); + continue; + } + + //nullify empty fields + for (i in 0...o.length) { + + if (o[i] == "" || o[i] == "null" || o[i]=="NULL") { + o[i] = null; + continue; + } + + //clean spaces and useless chars + o[i] = StringTools.trim(o[i]); + o[i] = StringTools.replace(o[i], "\n", ""); + o[i] = StringTools.replace(o[i], "\t", ""); + o[i] = StringTools.replace(o[i], "\r", ""); + } + + //remove empty lines + if (isNullRow(o)) out.remove(o); + + } + + //remove headers + out.shift(); + + //utf-8 check + for ( row in out.copy()) { + for ( i in 0...row.length) { + var t = row[i]; + if (t != "" && t != null) { + try{ + if (!haxe.Utf8.validate(t)) { + t = haxe.Utf8.encode(t); + } + }catch (e:Dynamic) {} + row[i] = t; + } + } + } + + return out; + } + + + private function isNullRow(row:Array):Bool{ + for (c in row) if (c != null) return false; + return true; + } + + /** + * Print datas as a CSV file + * + * @param data + * @param headers + * @param fileName + */ + public static function printCsvDataFromObjects(data:Iterable,headers:Array,fileName:String) { + + App.current.setTemplate('empty.mtt'); + Web.setHeader("Content-type", "text/csv"); + Web.setHeader('Content-disposition', 'attachment;filename="$fileName.csv"'); + Sys.println(Lambda.map(headers,function(t) return App.t._(t)).join(";")); + + for (d in data) { + var row = []; + for ( f in headers){ + var v = Reflect.getProperty(d, f); + row.push( "\""+(v==null?"":v)+"\""); + } + Sys.println(row.join(";")); + } + return true; + } + + /** + * Separator is ";" for better compat with french excel users + */ + public static function printCsvDataFromStringArray(data:Array>,headers:Array,fileName:String) { + + App.current.setTemplate('empty.mtt'); + Web.setHeader("Content-type", "text/csv"); + Web.setHeader('Content-disposition', 'attachment;filename="$fileName.csv"'); + Sys.println(Lambda.map(headers,function(t) return App.t._(t)).join(";")); + + for (r in data) { + var row = []; + for ( v in r ){ + row.push( "\""+(v==null?"":v)+"\""); + } + Sys.println(row.join(";")); + } + return true; + } + + + /** + * do a "array.shift()" on datas + */ + public function shift(){ + if (datasAsMap != null) datasAsMap.shift(); + datas.shift(); + } + + +} \ No newline at end of file diff --git a/src/sugoi/tools/DebugConnection.hx b/src/sugoi/tools/DebugConnection.hx new file mode 100644 index 0000000..e1d8152 --- /dev/null +++ b/src/sugoi/tools/DebugConnection.hx @@ -0,0 +1,120 @@ +package sugoi.tools; +import haxe.CallStack; + +class DebugConnection implements sys.db.Connection { + + var cnx : sys.db.Connection; + public var log : List<{ t : Int, sql : String, length : Int, bad : Bool, explain : String, stack : String }>; + + public function new(cnx) { + this.cnx = cnx; + log = new List(); + } + + static function isBadSql( explain : { + select_type : String, + rows : Int, + type : String, + id : Int, + key : String, + ref : String, + Extra : String, + table : String, + possible_keys : String, + key_len : Int, + error : String, + } ) { + if( explain.error != null ) { + if( StringTools.startsWith(explain.error,"EXPLAIN INSERT INTO") || + StringTools.startsWith(explain.error,"EXPLAIN UPDATE") || + StringTools.startsWith(explain.error,"EXPLAIN COMMIT") + ) + return false; + return true; + } + var t = if( explain.table != null ) Type.resolveClass("db."+explain.table) else null; + if( t != null && (cast t).IGNORE_PERF_WARNING ) + return false; + if( explain.Extra != null ) { + if( ~/Using filesort/.match(explain.Extra) ) + return true; + // SELECT LAST_INSERT_ID() and other constants + if( ~/No tables used/.match(explain.Extra) ) + return false; + // WHERE on not existing primary key + if( ~/Impossible WHERE/.match(explain.Extra) ) + return false; + } + if( explain.type == "ALL" ) + return false; + if( explain.key == null ) + return true; + return false; + } + + public function request( rq ) { + var t = Sys.time(); + var r = cnx.request(rq); + var explain = try cnx.request("EXPLAIN "+rq).next() catch( e : Dynamic ) { error : Std.string(e) }; + var buf = new StringBuf(); + for( f in Reflect.fields(explain) ) { + buf.add(f); + buf.add(" : "); + buf.add(Reflect.field(explain,f)); + buf.add("\\n"); + } + var s = CallStack.callStack(); + s.pop(); + if( rq.length > 100 ) { + var rbig = ~/^(UPDATE Session SET.* data = )'(.*?)'([ ,])/; + if( rbig.match(rq) ) + rq = rbig.matched(1)+"["+rbig.matched(2).split("\\0").join("\x00").length +" bytes]"+rbig.matched(3)+rbig.matchedRight(); + } + log.add({ + t : Std.int((Sys.time() - t)*1000), + sql : rq, + length : r.length, + bad : isBadSql(explain), + explain : buf.toString().split("\\").join("\\\\").split("'").join("\\'").split("\r").join("\\r").split("\n").join("\\n"), + stack : CallStack.toString(s).split("\\").join("\\\\").split("'").join("\\'").split("\r").join("\\r").split("\n").join("\\n") + }); + return r; + } + + public function close() { + cnx.close(); + } + + public function startTransaction() { + cnx.startTransaction(); + } + + public function commit() { + cnx.commit(); + } + + public function rollback() { + cnx.rollback(); + } + + public function dbName() { + return cnx.dbName(); + } + + public function escape(s) { + return cnx.escape(s); + } + + public function quote(s) { + return cnx.quote(s); + } + + public function addValue(s:StringBuf,v:Dynamic) { + cnx.addValue(s,v); + } + + public function lastInsertId() { + return cnx.lastInsertId(); + } + +} diff --git a/src/sugoi/tools/Macros.hx b/src/sugoi/tools/Macros.hx new file mode 100644 index 0000000..34c5837 --- /dev/null +++ b/src/sugoi/tools/Macros.hx @@ -0,0 +1,76 @@ +package sugoi.tools; +import haxe.macro.Context; +import haxe.macro.Expr; + +class Macros { + + /** + * handles @tpl metas + */ + public static function buildController() { + var fields = Context.getBuildFields(); + var changed = false; + for( f in fields ) + for( m in f.meta ) + switch( m.name ) { + case "tpl": + if( m.params.length == 1 ){ + switch( m.params[0].expr ) { + case EConst(c): + switch(c) { + case CString(s): + // look for the template in the filesystem in all the paths + var found = false; + var cp = Context.getClassPath(); + cp.reverse(); + for ( path in cp) { + //Context.warning(path + s+" "+sys.FileSystem.exists(path + s),m.pos); + if ( sys.FileSystem.exists(path + s) ) { + found = true; + break; + } + } + + if( !found ) Context.error("File not found '"+s+"'", m.params[0].pos); + + //if( !sys.FileSystem.exists("lang/fr/tpl/"+s) ) + // Context.error("File not found '"+s+"'", m.params[0].pos); + default: + Context.error("Invalid @tpl", m.pos); + } + default: + Context.error("Invalid @tpl", m.pos); + } + }else{ + Context.error("Invalid @tpl", m.pos); + } + case "admin", "logged": + + default: + if( m.name.charCodeAt(0) != "_".code ) + Context.error("Unknown metadata", m.pos); + } + return changed ? fields : null; + } + + /** + * get compile date + */ + macro public static function getCompileDate() { + return haxe.macro.Context.makeExpr(Date.now().toString(), haxe.macro.Context.currentPos()); + } + + macro public static function getFilePath(){ + var p = Context.getPosInfos(Context.currentPos()); + //voir Context.resolvePath() + return haxe.macro.Context.makeExpr(p.file, Context.currentPos()); + } + + /** + * store classpathes at compilation time + */ + /*macro public static function getClassPathes() { + return haxe.macro.Context.makeExpr(Context.getClassPath(), haxe.macro.Context.currentPos()); + }*/ + +} diff --git a/src/sugoi/tools/ResultsBrowser.hx b/src/sugoi/tools/ResultsBrowser.hx new file mode 100644 index 0000000..1650c67 --- /dev/null +++ b/src/sugoi/tools/ResultsBrowser.hx @@ -0,0 +1,54 @@ +package sugoi.tools; + +class ResultsBrowser { + + public var page : Int; + public var pages : Int; + public var next : Int; + public var prev : Int; + public var size : Int; + public var paginationVisiblePages : Int; + + var index : Int; + var browse : Int -> Int -> List; + var paginationStartPage : Int; + var paginationEndPage : Int; + + + public function new( count : Int, size : Int, browse : Int -> Int -> List, ?defpos, ?paginationVisiblePages = 10 ) { + this.size = size; + this.browse = browse; + page = Std.parseInt(App.current.params.get("page")); + if( page == null ) { + if( defpos == null ) + page = 1; + else + page = Std.int(defpos()/size) + 1; + } + if( page < 1 ) + page = 1; + prev = if( page > 1 ) page - 1 else null; + if( count != null ) { + pages = Math.ceil(count/size); + if( pages == 0 ) + pages = 1; + } + next = if( pages == null || page < pages ) page + 1 else null; + index = (page - 1) * size; + + //Pagination Logic + this.paginationVisiblePages = paginationVisiblePages; + paginationStartPage = (Math.ceil(page/paginationVisiblePages) - 1) * paginationVisiblePages + 1; + paginationEndPage = paginationStartPage + paginationVisiblePages; + if( paginationEndPage > pages + 1 ) { + paginationEndPage = pages + 1; + } + + } + + public function current() { + return browse((page-1)*size,size); + } + + +} \ No newline at end of file diff --git a/src/sugoi/tools/UploadedImage.hx b/src/sugoi/tools/UploadedImage.hx new file mode 100644 index 0000000..88f5af7 --- /dev/null +++ b/src/sugoi/tools/UploadedImage.hx @@ -0,0 +1,38 @@ +package sugoi.tools; +import sugoi.Web; + +/** + * Manage images uploaded by users + * @author fbarbut + */ +class UploadedImage +{ + /** + * Resize an uploaded image with imagemagick then store it in db.File + */ + public static function resizeAndStore(imgData:String,fileName:String,maxWidth:Int,maxHeight:Int):sugoi.db.File { + + var name = haxe.crypto.Md5.encode(Std.string(Std.random(100000))); + var path = Web.getCwd() + "../tmp/" + name; + var path2 = Web.getCwd() + "../tmp/" + name + "_r"; + + //store file in a tmp folder + var ch = sys.io.File.write(path,true); + ch.write( new haxe.io.StringInput(imgData).readAll()); + ch.close(); + + //resize via imagemagick + var p = new sys.io.Process("convert" , [path, "-resize", maxWidth+"x"+maxHeight, path2 ]); + + //if we dont wait it seems the image is not ready (file open error)... + Sys.sleep(5); + + var data = sys.io.File.read(path2).readAll(); + + try{ sys.FileSystem.deleteFile(path2); }catch(e:Dynamic){} + + //record in a db.File + return sugoi.db.File.createFromBytes(data,fileName); + } + +} \ No newline at end of file diff --git a/src/sugoi/tools/Utils.hx b/src/sugoi/tools/Utils.hx new file mode 100644 index 0000000..e9f49bc --- /dev/null +++ b/src/sugoi/tools/Utils.hx @@ -0,0 +1,51 @@ +package sugoi.tools; +import sugoi.Web; + +class Utils { + + public static function getMultipart(maxSize) : Map { + var h = new Map(); + var buf : StringBuf = null; + var curname = null; //form field name + var curfname = null; //file name + Web.parseMultipart( + function(p, n) { + if( curname != null ){ + h.set(curname,buf.toString()); + if( curfname != null ) + h.set(curname+"_filename",curfname); + curfname = null; + } + //trace("onPart"); + curname = p; + curfname = n; + + buf = new StringBuf(); + maxSize -= p.length; + if( maxSize < 0 ) + throw "multipart_maximum_size_reached"; + }, + function(str, pos, len) { + //trace("onData "+str); + maxSize -= len; + if( maxSize < 0 ) + throw "multipart_maximum_size_reached"; + #if neko + buf.addSub(neko.Lib.stringReference(str), pos, len); + #else + buf.addSub(str.toString(), pos, len); + #end + } + ); + + if ( curname != null ) { + h.set(curname,buf.toString()); + } + if ( curfname != null ) { + h.set(curname+"_filename",curfname); + } + return h; + } + + +} diff --git a/src/thx/csv/DCsv.hx b/src/thx/csv/DCsv.hx new file mode 100644 index 0000000..786db43 --- /dev/null +++ b/src/thx/csv/DCsv.hx @@ -0,0 +1,31 @@ +package thx.csv; + +/** + * "dot comma separated" CSV files (widely used by French Excel) + */ +class DCsv { + static var encodeOptions = { + delimiter : ';', + quote : '"', + escapedQuote : '""', + newline : "\n" + }; + static var decodeOptions = { + delimiter : ';', + quote : '"', + escapedQuote : '""', + trimValues : false, + trimEmptyLines : true + }; + public inline static function decode(csv : String) : Array> + return Dsv.decode(csv, decodeOptions); + + public static function decodeObjects(csv : String) : Array<{}> + return Dsv.arrayToObjects(decode(csv)); + + public inline static function encode(data : Array>) : String + return Dsv.encode(data, encodeOptions); + + public static function encodeObjects(data : Array<{}>) : String + return Dsv.encodeObjects(data, encodeOptions); +}