From 24e69a5c08b293a12d25fdd5051deced60e4d9ae Mon Sep 17 00:00:00 2001 From: JM Marcastel <6306262+marcastel@users.noreply.github.com> Date: Fri, 21 Aug 2020 12:21:35 +0200 Subject: [PATCH] First commit --- .gitattributes | 7 + .gitignore | 1 + .htaccess | 8 + .travis.yml | 13 + LICENSE.md | 296 ++ README.md | 41 +- content/1-home/page.md | 7 + content/shared/page-error-404.md | 5 + content/shared/page-new-default.md | 4 + media/downloads/yellow.pdf | Bin 0 -> 13729 bytes media/images/photo.jpg | Bin 0 -> 62939 bytes media/thumbnails/photo-100x40.jpg | Bin 0 -> 2153 bytes robots.txt | 5 + system/extensions/bundle-7f745edc8a.min.js | 55 + system/extensions/bundle-bebbf8109f.min.css | 5 + system/extensions/bundle-f6126cc0e2.min.css | 2 + system/extensions/bundle.php | 1964 ++++++++ system/extensions/command.php | 617 +++ system/extensions/core.php | 3584 +++++++++++++++ system/extensions/edit.css | 579 +++ system/extensions/edit.js | 1500 ++++++ system/extensions/edit.php | 1863 ++++++++ system/extensions/edit.woff | Bin 0 -> 5688 bytes system/extensions/english.php | 23 + system/extensions/english.txt | 253 ++ system/extensions/french.php | 23 + system/extensions/french.txt | 253 ++ system/extensions/german.php | 23 + system/extensions/german.txt | 253 ++ system/extensions/image.php | 206 + system/extensions/markdown.php | 4033 +++++++++++++++++ system/extensions/meta.php | 65 + system/extensions/stockholm.php | 23 + system/extensions/update-current.ini | 133 + system/extensions/update-latest.ini | 787 ++++ system/extensions/update.php | 760 ++++ system/extensions/wiki/extension.ini | 16 + system/extensions/wiki/page-new-wiki.md | 6 + system/extensions/wiki/page.md | 9 + system/extensions/wiki/wiki-example.md | 8 + system/extensions/wiki/wiki.html | 20 + system/extensions/wiki/wiki.php | 275 ++ system/extensions/wiki/wikipages.html | 20 + system/extensions/yellow.log | 5 + system/layouts/default.html | 8 + system/layouts/error.html | 8 + system/layouts/footer.html | 10 + system/layouts/header.html | 21 + system/layouts/navigation.html | 10 + system/layouts/pagination.html | 11 + system/settings/language.ini | 9 + system/settings/system.ini | 70 + system/settings/user.ini | 13 + system/themes/stockholm-opensans-bold.woff | Bin 0 -> 14192 bytes system/themes/stockholm-opensans-license.txt | 201 + system/themes/stockholm-opensans-light.woff | Bin 0 -> 14140 bytes system/themes/stockholm-opensans-regular.woff | Bin 0 -> 14260 bytes system/themes/stockholm.css | 362 ++ system/themes/stockholm.png | Bin 0 -> 2033 bytes yellow.php | 14 + 60 files changed, 18486 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .htaccess create mode 100644 .travis.yml create mode 100644 LICENSE.md create mode 100644 content/1-home/page.md create mode 100644 content/shared/page-error-404.md create mode 100644 content/shared/page-new-default.md create mode 100644 media/downloads/yellow.pdf create mode 100644 media/images/photo.jpg create mode 100644 media/thumbnails/photo-100x40.jpg create mode 100644 robots.txt create mode 100644 system/extensions/bundle-7f745edc8a.min.js create mode 100644 system/extensions/bundle-bebbf8109f.min.css create mode 100644 system/extensions/bundle-f6126cc0e2.min.css create mode 100644 system/extensions/bundle.php create mode 100644 system/extensions/command.php create mode 100644 system/extensions/core.php create mode 100644 system/extensions/edit.css create mode 100644 system/extensions/edit.js create mode 100644 system/extensions/edit.php create mode 100644 system/extensions/edit.woff create mode 100644 system/extensions/english.php create mode 100644 system/extensions/english.txt create mode 100644 system/extensions/french.php create mode 100644 system/extensions/french.txt create mode 100644 system/extensions/german.php create mode 100644 system/extensions/german.txt create mode 100644 system/extensions/image.php create mode 100644 system/extensions/markdown.php create mode 100644 system/extensions/meta.php create mode 100644 system/extensions/stockholm.php create mode 100644 system/extensions/update-current.ini create mode 100644 system/extensions/update-latest.ini create mode 100644 system/extensions/update.php create mode 100644 system/extensions/wiki/extension.ini create mode 100644 system/extensions/wiki/page-new-wiki.md create mode 100644 system/extensions/wiki/page.md create mode 100644 system/extensions/wiki/wiki-example.md create mode 100644 system/extensions/wiki/wiki.html create mode 100644 system/extensions/wiki/wiki.php create mode 100644 system/extensions/wiki/wikipages.html create mode 100644 system/extensions/yellow.log create mode 100644 system/layouts/default.html create mode 100644 system/layouts/error.html create mode 100644 system/layouts/footer.html create mode 100644 system/layouts/header.html create mode 100644 system/layouts/navigation.html create mode 100644 system/layouts/pagination.html create mode 100644 system/settings/language.ini create mode 100644 system/settings/system.ini create mode 100644 system/settings/user.ini create mode 100644 system/themes/stockholm-opensans-bold.woff create mode 100644 system/themes/stockholm-opensans-license.txt create mode 100644 system/themes/stockholm-opensans-light.woff create mode 100644 system/themes/stockholm-opensans-regular.woff create mode 100644 system/themes/stockholm.css create mode 100644 system/themes/stockholm.png create mode 100644 yellow.php diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..90e8f40 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +.gitignore export-ignore +.gitattributes export-ignore +.travis.yml export-ignore +LICENSE.md export-ignore +README-de.md export-ignore +README-sv.md export-ignore +README.md export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..60d737f --- /dev/null +++ b/.htaccess @@ -0,0 +1,8 @@ + +RewriteEngine on +DirectoryIndex index.html yellow.php +RewriteRule ^(cache|content|system)/ error [L] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^ yellow.php [L] + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1a1b66c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: php +php: + - 7.4 + - 7.3 + - 7.2 + - 7.1 + - 7.0 + - 5.6 +install: + - echo "CoreStaticUrl:http://website/" >> system/settings/system.ini + - php yellow.php about +script: + - php yellow.php build test diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..a037211 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,296 @@ +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. + +## Terms and conditions + + **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. + + **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 software + +At the start of each source file add a link to a website, that contains this +license and information on how to contact you. For example: + + Datenstrom Yellow, https://github.com/datenstrom/yellow + +If your software is based on modified source code, you also have to keep the +original attribution in each source file. For example: + + Coffee extension, https://website/coffee + Based on Fika extension, https://website/fika + +## How to apply these terms to this software + +All files are licensed under GPLv2 and copyrighted by the respective author, +unless stated otherwise. diff --git a/README.md b/README.md index fea3a4b..a229597 100644 --- a/README.md +++ b/README.md @@ -1 +1,40 @@ -# ait-hawky \ No newline at end of file +--- +revision : 2020-08-21 (Fri) 12:18:29 +title : Hawky README +--- + +We recently came accross [Datenstrom Yellow][yellow]. And I liked it. We have been stumbling on the _static site_ generation +topic for some time now. We have tried many, and none are satisfying... Jekyll, Hugo and their likes have satisfied many, not us! + +Our (not so special) requirements are: + + - use (fully) Pandoc and CommonMark as the Markdown conversion engines + - enable online editing by end users of possibly complex HTML5 pages + - support GitHub pages for the versioning and backend storage of editorial content + - provide wiki-like page handling for easy update and management by end users + +One could debate on how this could be done in Jekyll and Hugo — and indeed we have working POCs for both tools. Jekyll is written +in Ruby, which is no longer on our technology roadmap; further such customisations would not be supported by GitHub's automated +Jekyll conversions. While the Go language is definitively on our technology roadmap, Hugo has grown into a complex beast and the +maintenance of our customisations for Hugo would incur a lot of overhead costs. + +This is where Datenstrom's [Yellow][] comes in. They say it is for small web sites. We wouldn't say that. From our first peeks +this looks like an interesting Open Source project which provides a ready-made and tested framework which could be carved to our +needs. Further the design is modular with many [extensions][] of interest. + +Why not simply fork the project? Hopefully our developments will be contributed back to Yellow. Old programmers have bad habits! +For now and for our programming convenience, we prefer pulling Yellow into our worflow rather than the over way round. There are +also some possibly diverging thoughts we want to investigate: + + - the generated static site should be mobile-first and PWA-ready + - i18n is not a _server side thing_ and should be handled on the client side + - client side editing should allow editing of _content portions_ + - a more _sophisticated_ administration panel à la Grav CMS + - use our NodeJS-based toolchain for the build process + - integrate CommonMark with custom extensions + +Why Hawky? Historically ISLE's knowledge vault was an Apple HyperCard database named Hawky (circa 1991). The database lived +several lives before being converted to a Dokuwiki site (circa 2007). This could be its next housing :smile: + + [yellow]: https://github.com/datenstrom/yellow + [extenssions]: https://github.com/datenstrom/yellow-extensions diff --git a/content/1-home/page.md b/content/1-home/page.md new file mode 100644 index 0000000..6f29e50 --- /dev/null +++ b/content/1-home/page.md @@ -0,0 +1,7 @@ +--- +Title: Home +TitleContent: Your website works! +--- +[image photo.jpg Example rounded] + +[edit - You can edit this page]. The help gives you more information about how to create small web pages, blogs and wikis. [Learn more](https://datenstrom.se/yellow/help/). diff --git a/content/shared/page-error-404.md b/content/shared/page-error-404.md new file mode 100644 index 0000000..62f98cb --- /dev/null +++ b/content/shared/page-error-404.md @@ -0,0 +1,5 @@ +--- +Title: File not found +Layout: error +--- +The requested file was not found. Oh no... diff --git a/content/shared/page-new-default.md b/content/shared/page-new-default.md new file mode 100644 index 0000000..02950a8 --- /dev/null +++ b/content/shared/page-new-default.md @@ -0,0 +1,4 @@ +--- +Title: Page +--- +This is a new page. \ No newline at end of file diff --git a/media/downloads/yellow.pdf b/media/downloads/yellow.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1cb98ad35513a4e3ba34ac8414dc3f9585b3ebea GIT binary patch literal 13729 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 literal 0 HcmV?d00001 diff --git a/media/images/photo.jpg b/media/images/photo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3d11eacf990cd9022507150c775b70a9b00858c0 GIT binary patch literal 62939 zcmcG#bzD^6_cwY70Rce-q!|>H?#=;`k``%@lL>2|x`V0Dzf|`R^L6^=zzv(-^Tg7q52!5e*w>4`+89 zXBP&3E*?PSiIOVz4F|CNr62tz?~c(${)z#&>pY6Z{z~9&MrP~ zw$?Tt47{SEq6{*Y&gQQew0zt>UOBomC^=a;ySX^KnR&dj1l#_j-%T%oG4APY~o*Z(@hz~l#j|EKf+^2neB0QGzTz()PI zP9O>ZvN-^NIO)H2k9h&$E;j(QW?Z)eBzPFHSnF6A4*^UP3@j3i>n;ESy80Hz-|}y@ zFfg&OZ{gtL-Nq*X8`RzfFfp*OFtM?2-MSg582;dO0Gs62y$8H9IHa0pxDQ>)_=4g- z;W0j``bPfz=ROm^xm)mU{QDG?RMd|iGqbR=2?z=ai-?NJK9!SKP*i%R^+H=m7gW*0 z@|Bgfjjf%#hv#cAZy(>c??OVu!rw>4CnP2%r=+H(=j7()7Zes1mwc(NsjaJTXl&}} z?CS18^nULf866v+n4J1Gy|B2nyt2BszOi|5cyxSniaa~NxUmZZ!1~85@cSRj{x5cs zfOcVGV`E|C-q?kK=>;BGB-pne@Z#K)(Zn@#C4I;jgh%!y?o-vb+l>6r_sPxOe&XL} z5?FY2aAVqE%l^*{3;zFP*?$cCuU#{MEO6^@!Mk+}j{px3kARo}EX1V5H-(gf^lzc~ zSE2n|7;XyFf66t;1Oo>L=l1Q}cfr5M)a2BU|6iBu-{8asy`BaLu`s~Fghc{Cfwc)V z{m#XBNLw8<24c~kVdE#;N>$vPpk#hhDXrLQtKIfVlyj|t#iKE_0Ns-bLGDHuRtlBR zn~ZdPtXT$LFUL=!FiN4`eURn!?LF&1t|N6M= z+`|N_Fvw|Q=@Fd6m&mTe)=l&`)3*C`rQ}J2_={lmLTF#EaKh&4Mii4G7aJiBUxKTU#|9XvlpXq*ndrBcVYxxZASAK_PD*{rAzcEl8aCNEq>qG zx2B58{xC40218^_UrH~R6McX}@$0WN7=c!-gg^j%?zt z6z|GCD1_OmgyB3mgP?0)P)x*h^o2>Xy-Ft}Y)UvOe0>YPJCsWF>88b|aUonFC zkyJ7=J0o|LZ1R%Toi{y9A(;~FrKamGJtM^t#1Jnk@8zc3{3 zx(1@;IShU7%__E^4zV;0(oJkiSh`A!`3sr2Q`j80tVs}mLU;OMeZP8)qRtS6P6=6U$5Bqq^Q zf#DtIKU5a5Tvy%DG&|8d%vZ>C@A_I=M_l^y!TY9~m;RFv%r4vq1#1aFm68el-3?&t zKciNpAun0C9#t2;(hTM!w(|{2x+?Xoc8+_ym!KJ?v&?f(X7UGozkDSQ80v;bJwa;T z79ukVaC4sl@HrU8=yWt)X*f6nJok3le@ftm(mmXq%1Etc|9MO-u86xlj(DG>_L^l) zw4(Z(JHOG^=^7wg{HEbJ{tuNXN^i0_xrM0(s`Y`g&f4{*&%sOLnm6JTqf|4_ZRj)8 zxsev4MsE6agZKm`i?q8FAh@pD12ix^WFO+EAb`Vy}S^f7+9bdd6Zb z{Dm4hou=`%C1m{$+&_ZiLf7rrpwDoaG1HgE2cn@{>Xh8zJ<6~B&jr^uv{QEabFHMDOL!+x(kk- zd~66JQGFzBO+I%m0EAmWb1RO&<`PzA@-pv zVS8?~%I@m9ZX0 zydi~=+`$Hy2CXvUiKyff)M=lP;vG%wx7272g;#oj)z%D#=G4?o3jC#wv`xW zmk6j#d$E^z7LHWSV#!+$_mgZn?P%T(d8LzU;AHd^zWajTh_|n}$8X$t1U9U7u7!2| zqdn0~6g&M@Y0yl%!`_T2`8_AZ?$)aMiy5DI!ADaV`d>CA7p@7+UwtuU-rMzX+gV$F#!Cj`O8 zwMrNGswXjPfwRJ^8#LM z_qdz)He0wN3fMZ*&!3mC9^%agd_W5-8p`w51S1*; zA4KM4WS|9%2%J4#QGi9=Y7dOM1Xyn)?xa8k&RDt2 z)hoc4rTZy^Y(M>1%Zzcs#uO?RD4Tav?3+C96=~8BF0_$YiqtG$5R8AaDZb!9+0fby z&0d=kQo(@&=8g4?$XDvlT8hRBgR#6X0)7MrYrD@xyxENN4#F?w~{DBbSs*)3v( zV-X^w{*P%}u+ig|Ou{X~%E zB_IEpRs%J+$CWUk;Bay=Zr$QJZy}WnUr}Vly_E)>%E zt_XU3JduzperW#fBqxa)Y%k(yiRt}@amw4HYv49r*UrR}Qq48c%Qrxbe#bu24{3X} za*^TPy_-8)3e|aMWUCuX68Gq9X-oT!PUlxNoDcqtQ!6eV&ac@k3#jvS?bG_;>d+^O`l1C8@Lm!LNEuyQtgQ zgd3=Hx40^G7q;3i;ZZ|>xHt3V{5+bGyuh@6cNa>kX6Uh54f#ZdHxy*4*X20wo1xu4 zYhg2};;5j-i?WS3>`}j1MX?6bQ-r9oSi8WvzQOI@8T168zVH-H_e{BlGF&7`BdEeR z6*g4)yE9S@SI5)E zQaagaTEa{7qNn1!O=SuPkR{Ek=FPjr{V1E{1H{3Di*flD(ap9H?1aVRXiVasRI^5j zns7FZH`(qH@?W8yXXa7ZqT_)Ky|8%+ZG{KE_awi6|CHB~hx(7AceE5WSgA zJP}0bL#~d?c~Wc^{diU7{&C5n@kZU%L#F7Ea3*b^TE&+f(SP0wIAEttV$fJhTQ`=q z)%0I2=<8B!&#`3{CTL3_PjXXrpw~aY#q)Or+Ie$MMn>W%(dETgVD_=E#^^u%SdtL) zY&d=JkwlQe)Su|}0o1L(QG7>ctqH+ybG48$Hc|kv|T4+zC zkZ=0Zr*1EqVQou#u=zv2dLq4KY23JTfzQ{^Q!3U$6P0qVOsUhlS&hZa)xql+57_)j zly%;t9V@{)3>LMgD z_2XWIyn<+@_b2a;HSj5A=)${;WL=r#zuft?PJ9i#tP3g2=c91BNPjax)JL@x=T6V| z{2PK%usge{ZjBI%WiXN6$(Fi|So(Hl^L!U4q|26BlJ|#}sm&zP`7mKL!q-kHY>!06 zGk6I{sPlC<)?;4M4s92u)s zy2RR~LYug&&NDs@KD~fZS$@y&!?BA)%D$BqTg!$%`$26N)ZE|W@BW;Cp^)J2a0SZ> zRfOWu>yx~`3!S}RQ?w3OY?_V;UgADDOSJzJVJ7V&v(T5kqti7^!GgwvB#f;;>~2LC z&rw&|@eO>pCCtmoHhFtf3OtHmf!F z9Z_H2?x|T25WS;6-e%+}rcJjCy)zmy;u#)^m0s32|0W{uGM?o#S-6`^XXJsgbx6Ul zMOCqfc>K2YAFhFbg79l#`(&U-i~f`Ux!_OlH$DbKQS9@)_PscK0Vw;&qF~_E{N;)b z744wg?t6k&bn#O`&3;y5^vI-JGyJPU@e}Au=1w1Mjvsn92{oSUDt%RZFjRXllDMvK z4*5d|UE85ejj$xsl)H6{tR@lAlq35eI&kv=f5ClC-cKgqw>;h+hQNAS{0DtZzV%8} z5*R7+r#V|PL$^zc(At|7_K&S19i2>=6h{0z%#UbBuy!>I#;T8iW z(odB_Yi8WLdCUydHg)pQ2kgfM33u0Ve4pcBeL@u*Sr{2Hp!ewz26n%>l|+iil8ihr z#`vb0O6US~5O!SH?=Yn_^0alHw7Ddm9G46n&Mfm~iTPTU$$G4iDK?6%Ww9FJa*S{j zVG-wxT7O^R5kDzzkCyA_f>K*NB*H*6uErAU@0=bgLJds)Y?U;8hJ6b;sFp<_`_yl@ zM=kHpgriuE?%>qEZ+>C*opMEuU!z1VH%{%I`YgQTLl8@K0+b^XO6^+RB;gabmHuHU zl@Jrgs3y#BQ7lsEfz9pgMFnlOfFEsWe`~{9Nf153EU^C=EFuX_VxG39eJl4PbhnLa z_63@3fpcoJ1a~CE`4Q7=`ow~TeUV{bF3j5n>_5pP!hcC=ihjx(i;-jyXhZG@E z6qv7)X%kj6{B2cYV`1KN?@O{&5!zTsZ8Anijnd@0CV6V=Gkc~CSxAqerg24q!a z!-K;5a_P6OfyokevPWO~;BkX`Jb^;XXiptwPYGU!(Yu{=#mm{`#^@d#Sd!|gUg~`E zaPi|*j`HzC7gbC~xf-XtjnGVvWa5VjG$cFGFzT;l@?!Y+8Ro=k13Z3|xim#`J9H^G zR~or0nf5Nk%W;0|el46F>@k&xe*DFJl&RMLyj|+8fB@t0-s(y+kIbOudHhBC?07wA ze;}|{Yx(8`?p zs1ljdcG!obvdZm6+~3y#HMBbQ8X#gB>ZbDCSq12m9pGXu&g=tKJI01#AKaitfjD2hW0Mgnjqwtg+*iF#bQ!m%{!u;5LicLq%|MLb+&k%H(T7(4i(Y2b>-38+kftjL!MV-oLg?` zw?5o|wSt1%)qQgxb77kd`6^tz5fr)0IJ_V2ZmhleB<~@5}BAGCp96jBy`<(%O=Gl6OWHI1%AdOG)K@>eQuX*INdeFvC$ zjl$RDI@UC>^LFo?kDbL9&8IKrjK7*!idBFe+B?xHI{pq);KMQ%D3V$uJJjr9(uNcs zm#vJA9mHNgFP2uA2P_7QI-HDV0Mc*^_ibTwF}cM8TR7ue67MM-V6R(MoS z!o=PcE`ze2TaWf#Iw&-w;;`K0z0lqw=H))tuMVOw!n%4?pTv6&_><@m-hVgmizcV- z5vO$Ay&KFp<`5%+(P{=k4;p(*vQ@&D&B7->N=C$Ul_zB%;{(QK5_xcOY9uT}Rr&A~bX#gs$*(Lf@Vg9y1cqeVtsJbkUIqxR zN2k+N2Ru4rq=Xn)eA3#Oi^!+zii0K>KKd+`T8^6{vJ@)eG2}&B6owYankTQkAJu*+l*}4N)5JQBdJU zHc>COd*mS+q;gq2wPHq3ac)C0c^D1fEf|=VuTSBW_h4n@;z)IA>ty|reNP5mw&UaA zjr|0hogZzWjv~EUY=DHwE@vFJ_-b1=M|0`uv~MGzLn%9b0gk_R`Q-!kc}ub~e6>vM zqItEcuXvs%l~lft$_RAdko)n0nB}+#JGXM(ckB*}3mcT5Ht6D~>;0$MwCOqx^qKyu z*3~$a2&jP)Cwz~~2br_TsU!?rNzXyXV~vt^*C(~27tY4weqk=Tie6~Cu+#j)>!m`m z228h`MiLuL-tXg+C!UOOt;aocJsW2a3UzXEyN!y1*e4x@F!{4y6!nCVi>;Xhk(-DIm)YJTKH-;j;_-aJHsO6qvLDeC=q#7J?~ z9%4l^`P*@!EA>mKrNiT4gNW68TlKEUlJ+E;#J5>vJ&j2+vo*VqT`vlf8O`UH;VEV}U>gN|PiiPq-tPv|F ze7=){In|*!v+vd_nc!<$MOeGYvN%a303XUH{e#JefnuG#qt1~qlGsIj%UwTsbHO(R znu)8`px#2+o~QkN_d!~2*DcMK9y`jif*EwYjyz~HqTG;ohRM%x%Rt%HSy&!RvZz6? zyJ>X1b(8i2zdLzEZ+Le+@j}<(Vfb?K@REjP(>;0KlRc$uQ4uGr@I`~!>bAfNz0mGw z(+0{zw*-%WVY*=SKMc>Vvzf#Q2(-b+jX~ znDez$6dP=1{s>tlAq-mHk9p`^tmf+^MD?}>2^ZXY>zvF?n5pGO}5s-S?>?n|?CiV|^}_Q&vz(wIp$| zT5*-mKWQs9MuYc$h3uylwsO&vC3_+NjUHud#DHi49p63Cu;U^_)sO%)dbXsl?|m~F zQNM2~c+$!$H(QGG;#q&Y1`7BYV^*}=s&^3Dj8dWTsuFb2=TCoM_7j9qF<+)Z`kHtK zFZC``Crp~HTj-Spl5W)m8`|VU5Y)n}*<&ZixQTBZFJ%qS1GsA&IDn7pGQ(KBy}!{> zFLA4{@~(l_u(QccQUuM>Lw1r)474XzZLP#PxT4GvpvjE#*&!)wb|;=FB3RPnW%LEQ zZNa4I(nMjff&P!vS31713*@5M}_Sm(HrlSh@K*8m&wF7wqPv?JX-4DC~RLwyM2hw`+V z>{YJGQ(SJHPj37Q#kvMKM$zYx1=T~-Yaq)O6cV$avoI83u>fmr%cQ(%Y*K($fgBA& z_u|lGS^uFbXxCQNeqJ4;)~BJZaO985QCY(Odn+T+U*{idVE)j(Px5o83Fr&*3scj2 zP?FMLNd>8#*T7mlY>pAd(%=qH7l%!&U#@HZ6&i>{IKhgZh&7-&KQHL?;GZWC7f#JbS_zv^caodvo@M#MA z!sMWGM9bL@wh{?Lc7VGg^m6s6ga0-z54-FKmw)cFpDXYe;N6@AdA~0&$6KWwMq$7@ zvW0|j%WI&>9au+FXBs@?BXJ3A&K@n`AXN~seXE>pYMYTx3Pkw z_ssz68bCCGPF6+>I3i6hH())jD6#vYuvLgXm_lozSi@v~fY+@-+vgHN?L^U?Du~)EHZZ2C#oMPCXO8NmBfY4{~F)|9O;tBRbKL zjjNB~=)B<{wJ=9?4g3%qMWYJOGzVx0&eUVHQHZmjh!V5~%6>QIOdbX5F5L{hJcKW( zUIXu}JFP2HtpE0AStYW1Blv9>%KIEabN&HwK)hz+SRe9+XFhbqAsl-A4cbux$MJr8 z4GhpDZUj7EB{HuJ)IiL)Jn(l+YTL_E^^}jtW2gz@ERQpf5X29Ei(euJ;7YS97MVuN zc5`9b#zD?)eH*TUK*qM?3Zg#wrq}VmQ|~jyO&wiOZ=#&t>2f#fpIe4MN;V?4F@~Mm z9n`ip2oqL)?>Gn%VM3FyE&0-s`DT@rN<@7~6`?~~u1!Y#3Vk8-UMgVruB!KJQ|7_D z)Y(u8(gb~y&FQ3!kI7ahE`DgiPHMM!M>>h`PX4&zD?rz;VRIfk=Af`Y*cKySXPzGT z7)3oz!H~u#QaX`3`O{g(sA~D3{~UR@vOif|IWkM!B4M3fN@t}{`?zs%E9*f^O>=_E zXBLS_jxTWydp*x8We%4yebyMOD&E*nPl-}ELFZYofv$X`f-){)X;tT>6T%GQrFXj zAiKV=8P#07;-;1`vC%0lv^J7pTD@@u)*)9}^}Q_xeI9cSya#92cOSX{i_0g@eNgnn zMpwFPzz-Z^ZMfG!w06-zmcjWHceHCeG)x4wekyPcB>8uoChR)YwN*e`{8=yFT?31D zk0xuwXS}ABFU5)>*|;ZVJ(4B-N}lN%cc6lhdC6jRq3%Nh^!Y8%Rgo#jHQH)%W$PKUSO#P;ozv@h>b7!lfKD? z<_jszN*SRk3$yidI2|=V_h*TbXbzHx@>o19b9&UX9W1hu9<330KVCZ9_oo^8y@~Gg z=UZo^=wT}yVd06aDtRpz5$3Tm1wv`M<^#ER+(`hGF(W+@(IX3~B!BiKJitj+A*yTN z=XhB|$=QBs58nG6*pxy-pXF@A-k}TP2rASTi5m-U?@4r3GdzHnn1!gY2=w}=OsWtoe9Bd2=f z(ix+vT`z13HK=5Y3Rb+b!Bm=Q6I2#&-9S_oW$EV&AY}yb^2y@zx{~L|=W05)^p6Y_`fpA9N9i0&#;iTtCcYk9 zTbktPi&{`REtS=&Vq~7*2dPwNcv=eYF-ah|0Po+_8C zC3L*dP>C1T&z{eDHlm8{_*IN$gr)QRsBMF%O`4{l1_NShqlddVu^W>F=j7O&_9wXd=tWKN@VOZlqM zGg6FRZ2u1TY5X$VcZT>F)YynSliM=-@{ zAutKeQ%c?>7pW*|3aKH(oH0&2{czUf#bkD>SB}Zs1D#*u)xpu@Cw@j_&)GzJ$4#DZMvuDqwcASDNkI?a=SRYV`hf3xcIyZC4L?>2M> zlz^C?s3)r>gILjYiURWw9AXkD7)HngwrH=wwc}T`=sjvdEQ{@*)(;fu6JwmhDinUC z_X&}5|4s`L(0^%hn-^*zC2dzGvBLD`l2lR0bwYQo@Vwi~KSfhBB;llJ*;vuT4Mxp{ z^Wk>;xn(aBvt0lEGW>g7kUq&0z8K6Uj^RMlIOxF7TiD+Z&P zd3n}?-Zg&0Vfja2p@k}HUIraX$SHn9^3^8h2nyv|bbnKILlWmy_~UxbhhJ5c85qUi zvWTlwE(CIs3zp@ zW+(d9pceW(44e{28=`YkRo&vx;4h-6<~q}hw5gcFy|LOZrjHr}MAmr}5Bkjl@Ofp! z#}0{z<)N8?pVQ_O`_5}%!OzQgSjYN4>`L+)m=^~n zGl)N}nV1L4nty5JtW=MM&bsj^ib9@cDb}VDI_Tj%_c9Aq%cT?T;m=r}#FOGb6GvyY z?SqR`N6eWKn&{wvXJC}GpcugCEq*!q&xtgBisNZu)M<3%x%9SFt>~bS%^7086Pedw zQvMbGLnDCaj34c%4u&3w*T8`Q#wJeu*ETc-ZgATg=!r98?rVTl0mVg(4Z7Q>ZJ*u* z{WwrkB+E8$G8GajbsLOX-~Utl_8{yVMR9ZceAYv2}ek~eZo*&5Nz1+KpS!q z?3sY3AP)93>IeP#2wYfgF*XX)?FV9dh_T`4J}GFFGPvfC!WNv*n9%{Rz`J(uuSy$v zqqi#spcE+5YXHgzn|lvCOLs0;p0>yRXcJ7W>c*e;Rba92)igZ;0_h3O6C71#$Va7n=Ulv~fK&PR8zN@614o z5PFD)Bl}??9tOf7Jn(1tw%;mw&l3ek3#kSzMJ|sl{hm8KJ+{P=Um0L4i|UsLpsz?2 z-&oAgyEuJ}xv;o#owSi{;SA&ak1VFs*2lTu+~AGVQT3V01L8e=z@ zkl7uC5@IhG)8Sdl_g<_C`bp?EQm)4csq46zScBnlmh(BaN%bsjSO^v6*NR5RYz4VO1=;S&e_ zqtVE~0xN`sOYhc|{n%2|JkE2G%UO$!iLZkzJoaLa?Mseb7SPk53MUlDPcl5jG>F3Tz#S`6f$U&NYFsNTy9Gh#MFo#KGHfHHUBP>{YO#@fg3Yq}W7PU+@tP$*fJ#so8H3!WE<==FhT>>qOdh(2lc4AxmN7`BkQ*BFa|pHSdgE zP$-q_aRD7ZFH(78?^i>wh5B8wt4RU9*I?SY9nIUgm9szjR;?0Nx3YZyCt%4K{3n9g zVf6X5^#ewxYFa=%VY;RH$tX@zVSeMP?2+Q1GGDYYdoDqkHgiR9Ppz8bMxzr#<@2cN z{Zk$$MRAtp%Y?CX!xo{wbBX4nc)4S<^k!MQ?E36s@}0v{0|a~kDY5kY%*U3?65Krd zdBxuXW$Z=WaiNY0LmB3+3}EgGqD2Cn2i2iNEj^OvSU}bSJ)j>-=hFdiIYyQX@(wh| zhSKLWmY8k~JKo8CY45&LRo+kaOR$kpEVu!hq}J3@9rLHdew}@XRAC&Mcc8#t*w^L1 z0@duTfM}d~XQzDr2-$wd2Vy)%f;@tZ`^}joU2?lVO?K2eE*R?Bg9b`9#ok0vwq~D& zkIfGQMsdc`hlg#cW%)mM=HkxgCuwsZ<;rgqR(Z%4KxtCon(D?<{Ux7KceIVEp$i`&O(o*!r7?Qa0ynewN{p! z!cp$>t-b2sL2<087u48Z7Nu{m0SxjC^9lZ-hZ$P!6AJQLXFpxIlbUvdp}&ikLl1hy zEJg*5q-xrJGj6F}%6}I91<%!VA!leXNwhvNw_UuQRGsX6MpD!|SyH^p2ksO%jzb$1 zmgIg=XjbR~ER3@wHPvt7tg6}+2C3_55KWoJaai+31Q(wy$F}RKCF&Zu7^|B3^Ltjo z+1V+7*)-eI6B?o`Y)~vh5K~++*WR(Yq-X0C*u_&#jC>CSq?NNOWZ6mf7IW#Cmt61{$3K-!BMuW}^Q97R3iPdSLvk9?b z*|e*z7|}K(OuEtYpS8d9D#g3btyKOT;r~|PpuQW^tNl0#lF?=t2qq@rz@?mpZP&nU z>%iBfPvpU9<%;@n^s@9EU!4uQ%e~CfM0MWh%%PygyhwcZ!dhb#yah1GHJ}|3XQ8u5s#qZm9m@1A_Vf#Q_V zWTV+O3KejBP?i=bs}?E&s}YDfKU2R!JJg=%gx$ z8s4CJ#u(>or#AvTz)kVf2&m-i(-K5}2~gV3AQ-Tky{AXP5r>P&sV z5bOkpI{$22YYMQJZv6=YzaU|f4c`wvdwTM-47S$+3lTVD|F%}~!3?yD6vcWoijH=g zZo~{|a0|fBx7+dq0wLJmJSRUnpf~NG{=wK6_unB!>*c*Pti6AHmRi0~~t%^LuA0Fu!YnE;|Z(i9nxM zeuXaRzY)J-Ifq<;cD&t3EX-XIGiVlAIGJJ?7^Y@Of2Me8Ta_tU052@9oD7#!6706vTId1 z(>xVdKVN{{>;;sWN5mzmMtvZp@s1g%K(TY}byDA1xWkzt^R3sCMt#HZ*gQ~5;H-Xr z(B$d3p3#!*l| z3o-W4_nESU3!|%AXCx*pzbGGR$4`~S=K?O$2o>t1Dt?drSJmRIe!YjUe`ioK^=?}@ z@0ZgUGwO~J%@|rs&mE4))DX8HUFa6Iw5-ZQq|V44Ii2W4Irof)$$!zXSvkGWmd%}6 zatpm2>068XErKiwc*sM?W$r5eh38h2=_!41liX)UyzXc*k8aJ$orN&f!~{xl;+H4i zSsSIS>~ddy>K=OG9lfY6N05_gPt0+rS~;7rmq>*HaEC3rHFavRkQ;Hd`f|Q^@TM60f_RH+WmU3TOQJy*foTKawQXo*)^Q?6RH<;{XE2x*{tZr43vKzn zgh&lJm`i@xMTfk601`R@CSZn!zMeORevy%n6W)pTdT{qXg}a6>lUbYE9A zZ45XEZxsb}J1EgRk0xl*2ug%fN+njB7YVZ>mZT#JIs_RlcVeE%e9Dd^y0;$TF0FWO z3C)x~JzCqtK_CMK@a$_BiIdIe_DMD0@3r?nXld&$1~TZ$NM~ZuE&4}~Wd`ijtIX6o z7>pG(UVeUijG4GuLcC{cHk_}z+9nzf&xLHV_IS*MMo3GB3t%X2>Nrfz#8{+P(dSz6 z)}CHkg7LC+otyALi2IN}v?H$E<4;H~>P3WaXlU@u86ky_85fH=@w*aPFP7%WWiN&L z)Ek!eMM|pIUxn%@&}Xbh$$iRw)aAzA2VBsOxu(AV{u<|tp=!%J;LB&M>5|W5j1CmP z&$OAB`SXu>C$!Fv)c?@AR1~iJR9FeOY4-4@XI60OURgc^Uz>`+wo@ikNE5jjqt`A( zhFJTTt8>zRI2I-I|3Z9Wd#>pMy`C735D^OeV(M2f$k*nW^!-a&GO6UU%>JF2(tRMLe0>k^O(PUwGN#%%!L5T`du4q2aNcC9g zc$1A|6VGjbm%b+`KDWoWl(-}B&fQ_A7nrNdZWBA|_BHKu&fN2j`7X?Ns1{!4-{P%z zfhN83FX*Z>zy;ru`<$?WNHT_N4pLBMzrUIwLvg>xIsnCu%bY$o^0|2nervq+4j0X3 zVr~6{EZOKrL@|nG?kRu6en32PgIHoa*@y6}nMM@*Xh|~s6;7r#U1n2=;uuEE+S&Lt z(&e}%gLCYNZ8%*Q-{6_SY_DaU9GAqHE<&}PHo!A$S`Pl0Zoi6YH>xiqEAME?UM&JB zv@oL!=93?~Os(Ql+H}_~W*0CXNGT`mL^@`sYPD;|D|?92{7{}hq^vp`GTZVBTSiGdJTRiLr@YS%vxv&hdf!AO z@Fn%k^S(^;Teu_T@RfbJzT{>NRVN=X_O{Td|Neor@^fc^d_&>8V)`%@`|gbCukEng zj|=WS3pnInQ1S4l>2JUhI(DrY9Zz(@t9YIMGkw;@hw`fX!YYI6!U3Z4qpan&$xuE~ z>g=;%8>;)X8Sm&(qk#^zMO4VbWXUvzr9qnZ-8;bJ9y$%4lR=&?*TcdeRs|Lf9+0d% z@+CNU*s%5}07nY_l6az79qX}ECDTkjUIdiiNM8e5Dfs8?4$R&Asy+uToxbs{bX=Y? z<+9HAmQZqhOJ_gj@)A~LhK^NkiLKaEd#+-yJ2{OVX&H~z>wG6Zo|u%s89rz@&te?S z-88RuldSKBW=-G2nhA?U(W|3E9oqIT!017l;=>zefe&N`Egzd>OL9EFGiTFIA||wK zcn#T1riKOP>(-Qniu+%=G}7VXPJHId(SMMHzhHR|KzA}rptM19p)oDoC!E~MatY6y zBgTyuR4PcBXC7B8M0{oQwVQw;uV6i2ahK#xOh(NZUBlG(hJ@QdJr4V23d&pBTSESr zw>7f=fcSE-!|ml3p+xEryNL?zcGvJr`QnwJ|tZK`EX@o=5?*8t!OGz~=(*;KQSx&D_sfA1Z#LBMvMGi#}g05k$T+3Jt`lZ{e zm2*%)X(i}ASsG^O>9@7XHUkd+-K~$!>(BwSx%!QDLcbo}`S4WSNScl*$7Mq1kC$WvsYnbD=1 zr24b82@z*AFQHuE#gc zHXT(s zOlGuRn*74RZL)-8V6wZ(A0BKlK~ikc;e>a!C7`0_<`)#56p`-0^U?7MgaJ|~O-_ob zJ+Mh$6&c8zu@=qpdHCzAWITZXS=HN7zgxp=8dr4)bU+Rmrh-3II#TEvo;xd`fcC`Ob=r8&zWN0i?FDsCT`FyfNXd+!E%=0_`)fg)-WP3r8 zd6J39-*zI|-LST<&}sb>*SvJ^%p>f95*nNHK3C?3dsEsU3lOu3H zJijtcN=TLctY8|42|*XnSIi7l*QtqF&g-UaY-Oh!ZAQJWb~?w#4-wjtbRY7vkowK? zIi?AVk(MJHF5N!zd8S_}rWmvJZcW7QyvG zP_211(;xj^Uzgd2V<#axO@#KR8C+%8NupE;_6qvGnppO zU%E>}+;=S6W6_BY36-g^XJ=OJn{hq{85H;AAEZk?9iY6s^~32q?b44JyJK*A(e--U zRrh*JqWIn~T)gPM=CmO-vY>>Z@1TFVtH8wK(-q6%GW75V`bjHsU6KJ!f~SusZ7GS+ zX1t>I@^+cmo7_JYhKLG%1WpzCKwsR~=QxI8v%(@XNx$bBB%46s7|ET&jXH+L=JtO? z`$Bqk2TiCeofX0bEjd=O6?F=n(*0|XWxe3 zWX@WQwgCRaE5l%(NW9E)xVG~E74n81`(metwM^F2E%+fVPh)JXX4rnpvZd)kuw>~N zTF6c;!u~=#Pgtv)U`mNqG2i1}jGGS7U#K|RFC zyp^C~(z6*Nwb-xK`k0t^5mxg|=~sLqVHfQ@?Olah|KHZ^>ekrXZkAD$aJTHyx)jVG z`MKpH_|gh|yCIX~dI6@Bh||-edG?;l`rsF_-ixi>jEv+5hsKm^>C8`_3_e7)Z8t&; zjt(~b+W1QekzDy%Jp*oisVB<;wWjhQe88=G++LhBW~NQ^p1up*P<-H|WP%`HX{IYG z`ix_4nJkZ(OfN6in~|(up;igzHMB4Kv_2m|PB%VHV=jBN3ug3H3Q>RLV`pum`3N?) z=!x_Az|EZP@k0Idj8^MYrr)lo>(%I$4vX`N0$vm4P_MkSrOB6Qa}9_kYA*90)`&NhQ(MGT8ER}qv6 z&cLsxnzSFM;@6Ac!LInbesit;(Xl$m_st;z(1VZAPG!Nd8r1loenhRMqHj!N^54Y2 z`J3w}RQW1*T25fvx^T~e=H)fLQvSu8^TGLtAEZ`;;6SWO#uDn+?cNu&{+*uxRc?W! zXVd_uqKI;b;D*1Hu>`%V-eDA~7jiO5@WUadV&;DxdlpJGJDl!qJ%M2N!7Bla=2Ds%{!fnAz{62&|=EqHO#)Jy}o|VQ8|J#`p ziu@_n`v$D9JlV}ul$j;rLEEicalc-)%Bch>pOZ^(IHBbqm;ZZsnVWW*+0W9V(`-{q ze)T{eSvM(s)wN$$3R!CIRI9fzIdM@-v$Fs_2)?l3z0LUg=3tO|k#C0YQeDh1!h-_e+x`t!@kST)s z;liY)2^<*+I}=VxSh%G!i*irh!KFW6aR!G4k7q3)LjN3-u0Q(Zqm3|JuE6DsTECa|I zG3CT$UzG%$?fQK~3(w<11ojW(l%@ZHlqU{VmKBCaH-7W=zjMq&u~D5tZv%HV^YO^K zx`&J;snxybmV({_5*w#fffSb#8@x_NCK`8nM~Q<|_)Vo<6fqxOQKy=_t04KQ64N8J`ao}+5j-4qI+2)1U zB+67^k*hGOIk`H@>+%3xwa?0+G>6ea4WVVHS<}+@#{GbX6~E#{=qu971U=QGAN36q zLp^bgl~-JR-l=Jad!dxb7i*3>*FOvahveLOq-!D>1N0dzc6t;R4yQDw>e|Km(>&NjYIR#}4ts_>l^p%yClA2%YA3#+MZ!n5W=ymF` z>+W9>vV0xZ#4w4?%RoBwWkJ8INRSjuO=P?U3y zUzl$5C%kM;?9VqI0VN#S6pqG|{Kdtyy+CV>lk6_Q^12Pwv$fCq5RxX(R2ZSWEIiCS z%Q)YQaD}qhwBb!cDZl=G4&m?9c&#p)TR@b}S)hZ5mw*z}15o&?$kdk#58%tZsI!pa z(MY)}jH|*F&R1(!h`OLCdBd*`4_2HQzz$4)a~^Wp-)u1&N+vxq)qIiVUMcWi1>HHI z&8$li_VG2-d%#&dBUG`9^>R2C?zc5Rtv;6~w_CD#t)!z(xF z!TiO`;1Ood03w%qqi-!l)-QaTY2$DkcwoC@9O1C%ad!O@$}w}L4v6W#y#h#cTKGpV zI4zU~0&iZs=r6xWed^UB7HG#;Vhh_tezh+2N9zk(GObSBy@$}|LNo!(%)lAPs2#1b z;J52Q+%$>?a~x<*qWzQ6glyHuKDjKT4n{f*mqo)Q9F$P1hD9@om!QbEu(p=+1 zgWG{+64e?7lD7$>AD1wkI0Y|+QQuqH4%bdprXRof7{I>AkNAqD2o1 zWw^{Xl};4+1MxzVq`k(3K_q(rE^Uf)Atl(%NPL@0TeszJRsHO#);d203?TNCet&N2 zS6M|Cf|a{NP(vkX=NJG?UC$A6Ka+`Iig>dxxeX=vVZS6gPbs$+B$?KC2x5NqH}Ul# zhI5rcWaNvCi?a8Hno~>9eQ8|WeXv~7%dRJ$2|FtwG1>a*uLVoRgYZ*%?m)6sMT?6FNGW3}nx@s{Q zm}(F;TL#;yt#WHqJl_iCa1;gU&??gMk<$i7zNHVlgmy@)uG7nk4B4X`Eus#_`tcow zm3^N^T(p3{%eWlr**o-*Qoc-7N!pvJFBFYjBAJ7OY;_msAqd=6>l%1>|FlJjyz@17 z3Nw@ghWz2v_fd)fGOxRHBZ_UMCa$_L``w24xwX%=>!LF~4Cy8eM+$dC3JWSaov| zcRb%Dh9IQEtk>>ISWj%>X?@UFt@n&zQN@7a8WpV6P!Up>oBaZSIvABwRchA@W z!%!OYWa|p(aM>nmpVA_f)hQyrSjH;Uvid983$gMZPbU~O>X)lqBgW^;dOhQ6>}dy# zEQS(sCu0fPwvp?FK&U|5R4l`rh)#^2KA-EONH-7wqfo~!jiiL z@seaT(L5814_CV6`bkM2h}MR_=7R7&J0BOyj!|O*+VN4Z&dDJ6XVNnME0=QN5^p`4 z_t?hlTnYYnYXPF6VFvrJYbgB1B5(GZp!A!PJN4CB&ML?4Xin{vO|~4W6H2z6XE7QZ^E=;L=+{-}zr;&eq@M zDR9VTifwCsk}v$q>_|Ex_C=Ktp0Ifz$!XF$Z!t^kkuahiAo^PYxPw}p&6^3uQFz?I zPciC_o6dirgE%9@ytP%GG2WGB#w8D3GDGZBjk(6daM1aTGMs?^r{H7l*I4YwM+;^JTs3kAkGnU-ScxvYzvzUz{}H9a?Z1 zn8Ie$96!Y%(+?b*yAi&R1unteoBm$fyqD?FJ{!n!H}1*!{&u;G?=_CO_FYfZO`b|1Oi3lt#Tvcr$sIZ%KUkFzYu;h!d8-r&MD zX7#88w_oJ8xT2UEnrHb_$_pB#$=lj*7+;OgqhSh(i_Q}#Y;pxP-yR(B99Ie3g;Z^=dF)it&*cN7iAT6YR~8XOj$y{6oV+`xR0)84A5n$-N&c+_2DSr0~dyv&}N-xg}IkX62loFoHh1 zktXu4O!X(HTuhm>SDy&N2KS~yqZiZk@=aM_qz#GObz;nI-aS})wbeqfyMvrUm{UyZ zdqK5qxG=}yP}E<=zj35^i5Nl?L_lfKO>t3n3B2RF0oV@ovgB%Q?8sWPp*Rcoi!s_W zd+dX{$GWB5gRattCX4_16j)pV%5J zfAQ<&8NanrJB$(qqG>{h_<$V_c+2X|4UM zv&bLC2-+Atg)*aJaw4ZvkA-{rY5@+(U|LNE^xmYI#P|z;(-wB)jI^d3{YZ112*LfF zal?F7Rjgc5={k12Q0>!f{D(CAxN2Fy8w=>29<0A~#!8mz>EKNLZU&W1TVe+Vut;t% zR=x?j0&1{427jD~sf>T1|DaJqN=TbZcmRF5-R@g6C)2}j7PKt4^@%gzsn)WTL`p=; zFA+P>ZX%fsWAa+2U*$5_B6NWI)_5e6ss{v{&-rHBMXT!vWVn^KHQ%gpRbuc}D9OYN zLa_v4%4`|P$lj6R;{plwP7IBwS14*6twZ;DrY^GspJWz(q9mYz4aiF_qg_z_ttP?8 z((J=hWy5PRBaxrAV0FVdcl1DA`5_dmSM{7A#uYrBF{vMiDD(S5v9em7urJqj4?Ibk zSzAQjR3;p6;JSd$nC>liB!TQI+-#OmR1RiTlFi*2Ba|zc{sD9ZSwk-Pp&?6n6%w3Dw$*!*kiAk=E=e zyrWr{nj0wJs}wFeeX>mxb72U4+4?|w#rNIM_LDDW5xJZZG3S0vlGMe06t6 z-$QDek5vpe;=gbq2Rx5L`APYPXnOH}+1-BR7?7FGHPr^*35!;JJC#%&76NvfMCq^zNHd-N4p&R(cj3V`ZBrSeu;kL6fG-$fD)J7uk}iSY=XZo$3&*ccTB1awWyw zG%Lxpi6LRW^}6#IeD~em&%h#FhF)MQe)`6IKR?Sj978Sw&ZqciR;A3Pz~jw!DX{Jp()#t{X&4j$`jYYB#5sTPh8G2|;bh=vS%;p~4CzZ@5F(Y7PM=WO^PVE{Vf zCWY+@4bfN?d_#&oyp$A5r5kR9&@fvwcWf5!{}$dXMg(SS&^NwfaJ74A4sad$IOeCWPL?K!#y~W&qEtVdqwo&lU?Uc z30=PqqbK|Ba)oX<_uyKz@NtaV!)NqzSv6Im5@XvqYN36pv~k|rSXIfH+3TCq8>FbB z13kB_kW+59hWn7=jo)3B)bro%YFD~2-S7rgkWA(28``}8jE!o!T^Sk(EN-cv_##K9 zK2X)Ql)LxKZvI`wZjxojqByJE6j>mC|8}=U?++g{+u}Kn<9mWugk@W1R~4p@==TzZ zwYF*%uc}JcaQ=7kRRp({<`>VV9DqCz<^JpDfBUHeC}7_X`cE47YE?3XiAR*ERxei1 z=ri+q}jpyUrjWkaYJ;C5h1|{Z=UH* zjmr4rQGYkT@ES)xu6PTC8s8bOFi|*N{2k|-V5@@wB_YR#=JDlE>0~PXSxi}xT(|UVwWXjJ<=|7}7SxG>>R?;9` zV$p0B5MnW}62+rev9HL2FrVcp{rHa%0+&EUV((&kMo=Jtdw)M+F zJG_T_!T&(DaUD@IzI>$tU|Ckp48meq-w8F2=#8hJ+!h zC-PO3`bbzXsugLz?+w|IQ9+eg(q(-N6n{l-w@68}jM`ZN4R=(P;r@KN&33ats#Myg z;)r)?ReyjZU28HeG!_2}h3#$0rU1xdpLDM|CM@%9-9ON4gF`J5o?zy-`uHR5II(Vl zU!P0fgLbboZLaR%J+M{Ssas~$UqHitfHJ-{eVZmvTi-atkHpSWw7E1cy9>gL{fQU1 zxcN9Ao%L9db;*0E9b3kOsir=@kE9td%%bWijzP4c<70X`)LyS2R?Zlt2gBdba_C?Q@Vw|XBS3>EF+dy0%_;)xqeuogB>ghKg5m6d$`~Wft>)n%f`b zMZcGm)Q7y4V1g&~AQ;nRr~0a;9VpFft5V2Px|rd=K>9L&AT!7=%LHnEGt zwCFm^vSw8VRieLe+ag_NDzl+9yp}T#rwDYZC|e7h9=6i%ATmAi!fZ=a$|tk+h@hDCGp?k)Bc+1iZ_hwng_37LWcudoKWs~4 z%4WV+7w+9oV-D}@YNV`;IU;1qNl?7}6|X+*IC{;8gi3IE1OrC<>c90hIvj-_8l$pq z61vL@w7;AsH{Y-^BS?%RM7vCVTO0m2OgmzAuDm(aMJei9KDv4I5U$39C<9P@*WLDo zZp*I4J`$lVg+e-a!vIBsvjQZgvbde9iU}(j5HyA z{i93b<>h#=R+VV+o6DGxdGdJ7NSA5jzC=jiMn`s~8ieBc6YRB_sa*Co?ro(M-2D<6 z)_1QLm631efUPS@)!j-Ttj(D(Z_-8KA$%@%Ej9bVc+PGWR9;@7YvIb1BbQ`p%8gt! z#E{{y-Qwf)5A<>N2D>zOSBjk}bnuxgrJcS(oY!APJW3EjV&6EnuYah88Fft~8i6MM z{sSefLUg*L{~S+VV5DGC=*El|TvYIa9>X~EtZh{&>559CwSW4qf&KU@9k0{?G> zmh}+LS+x-T!AIS^OqgY%v*y}2^8fPw+u*I2NC}-}qh@|7FT_lxb+rqot=1KEI#?Q{ zo|}1=XsXK`K!EkFa})ax8O^>9Ad@*eiefzy_w@B5$om~n=EXR6|$&@YlIgHbr{ zB4N4X#J?9m{t*J>=u2-oZ51}5%yj?HxjMt!Tqc#1Scoh1T7T*J!@~5LrvH$0JWX5x zu?DKN&iU|~F9b8~ZZKm6v2E0RZd75C&E zN4JOlC(XA#Ocj{_E4=@h(!4L-md|#_^SAWh6=?C)D6qHXSrao}*aeElmLPqK_W=2w{Z3BVTWD?ww))BVQ*C9i z{C;#;wkv(dUhFaTiJ%kTa~*AR_qe$mB@+&+L^lr2AjGX}r7*=565`o;>j_}Ft~Vhe zb>4>FlGXFybHACi1ewb(fSk674$)F#HDx=|Pdg5f`ziFg)|ORsLs`F4 zz+PF%Cuk0`2%2byJx>1}H9mO){$ex`fswhNTJdP!5|iM8SbJ@kW}ESbBMowrs$t>i zOwNKk87Jev*OyU_x7+d42TKrn_MA|POJ0F9YA~5%# z$6Uaz%ijoWS{yHOedQ62<=+P)zc*^!Ok5LD#(+JURLeZU!x5NS3k^jAZND*Qku;4p zEJ))g!JS$Yze#?!fMun4G?}dPQ9UOc3U+}cBGk`_sZifzI@}q4x^*cPzj3)u85Zm0 zT{f~?nwhz?=KiX`H*0~9GjEx)?K8c^oMsbJNl5{x-H;L+Sq+AH`Z>6|6UK5=BDJhRb>n|_unqn4y898lM zJ<3*PHh?>pt`nsmQw&JVgOb<5GBKkR383aYbdi5i5{xq52u8FIP}45U-@NAi9HLgI z^qa!D5fxAG%XMXdj{;|)?@K}v{@)f8B`CI+?`z-Jgb}P5(y_KxZkDfIKi0bEB&EF!9`A1uFWNV+ueeo9u21lWFqDuSCKAIh-vcl`U|C=CpX7zNyW;EueG5 zk>$G$?QZS}5iquzJhA^}QbB)B`eID~FgszI+jNshf{oT}pg<$PfK)BAUntQYP`8;u zw&25A4N{EHqr{taTGcX2E#=74C43(@XcDKL(Lb+T_*E+NzrtZa7aF7Y_WewbBJv;# zNOC_zPBCxZ9j`Fi0`jXZ@fWvR`sF0TGO9u8zc2zYgYU&4kG~MssTkq9iCo=T2FjWDO4uPX+qe-6Fm(R|4 zF;TZ%l4z)L?dA8WN=8YzSDE0yse1un+R^DyIedHFd0nG4k>TwdPmRC_@~|?Fh*x+% z4F+2Ykyahq1$4JGouo#f!!LFww%>Xgm%otp2#;KO5uB;R|sQEEZS(e5qCFzP&*p z-al{7m5#ebg$G63KeC;^p&+pRUNR`lHrS`R_a|xaESrM9K#q>9(aV*ttyI}C)wLQ% z8U{)97|rFe+s}Yt-5rpa<7FWT)gG`8I?HuQ+u}GF3AMAF@G2i?Ziz16WLtTbA3xSC z7R4o2itkfis9FDTd-8D#a4<4>LUIcGWgzbvvEz}NtcQudA5FahbfuQ-QK8uY1 zf^OnNFNZ;w$AQ($)*^OM<9^}F-tK+Qw0|$^LSYtxf5DRYfZ{z*0t24p)8$fjSc>DH zTTP#YwMv!M&KsSw-Nw6(+cWYHM50u%`fUN|h%+4+TNpO~S~&mYX7Ql6QY7A*d;k(} z=J=k_uFef%#*oop3{u7tTr62(VNY4YtG?bY2K8eHrilBIhfAZ*4bzs`;(b^?$DStZv%D+ zL+nNsJumCcPx`&_Xx`APZkc2L8@{~0Exg0sGPv`xtGkFbHy(rGf-MgoaWT`2puc20 zP9<1sb&)2tPsSJf9z}c>q}GGdvO+k2hkX$9HvDlKf~#7%Px9WtGxNWz-=EP>@VYPp zWRy4SHegglxR*u)kLV7EhPrUxOWdbaD+KD$KTyAI-UYNiA z%t+e}bC?i09Q07F;_W^C&5HI?<{o~o30dQ=+{Mp4Hh|CaO>G4hoc;rSdSA0sAd8bi z`rVw{*vE{%+-o8lKz}-PE@jTDa$mx2z55kFsoG(aC9Fg=^Z^nEfpH6@+kmOTWt;2y z31Hsfg#SR#mOtpZwsIUU_hJyE(}KI$fDrP_<$?s?0{Cg6{82U#u**C{;3nHI1bxKK z{q;$)D|%rRZ-=K_S~w@U)jm+hD_eMUY(b;WpOuP^$iHj3=o^k(M&se@kWR5Lhg4$OT!><4a&E*$PAZ$+iEyKi-uWc%)49t1Ia+6n4ej}q5BAf z?-MU=nFxNP9~TQ-9pTU7+fEh7Q~F(m#4{hm2n)2_eic4pfsi1SGuRpLwuv_?ZS0PH zU4V94M^msNR=IFV62~cT7<|fE8f+T!-QZ^Ty*`zGg5h1$MbCh0*?QZby7FFLgE2m5 zia-jY#1VhZ>@KnaW3O=_l|l8A=K3YY?jPuf8So}1+0r^t7Z9w6E7Sd|bJMLk2Y!gn zAMy6gkO!jUgUTR3luNk~e(!SCUgC=tdi@8yGUtb~zTZ5|SXoFh{jKXjqoCT$a?Xjr z+`YWbB_gYUHfcCKu0xgo{td1o2zL9Uz%KUioFK)1w1^&+bo1&L>h~Xk9n20AbqD`I zsL&mrN?<&HScKjPm;MI)T=cjdd2(=rY#??-#;c(_69t~|p?U6{f5p6(#BEByH%FBx zOoN_SR2KBmN%c70IFVZZP~-pD8v;}*M7c$2T1f=o2wCcXN=(J92T54^Ue7M*bzrsA z>3vMfTvFf;`siow{;sb60`qlXVC3}}-!m^fROBGiFZu01kovpugA)YN4jkGijrarT z20z5Nx{aKy+vOS|e|=d;P?{PqbM<`C!4Y~rS=p?I!O5<4cWZ(twvBe^B z3^32PIz9`*L$KJHC6lDdgE%NJh!|zxzu(-v8w4xj&vHg`7?w@M-8s~d$))Y3ebAz7 z`3G|C8W<%w_p3XQcniL?@$tZTzKR97h>`_PKk8@c^R45|rzgN2IbgdWMzIOSm)r#7 zTa!l>)wImxe+(cqQ~)>O1?&p4poN%v>@NqWN;hq)VfwA%E`zRG51vPFmKyCR>UmY> znP&Fe%nClx1Udf(v6z-^2z_Ay$?{AzbqQ4jJ)>QszMQ{V3p{0PdhuBfg<}k?J&OXp zjV~ZoxLlTWsh>=PptUL?fM8VlF;Bu2U3V@towvT2!r*dWv@8#{-i?b={!^@@nUjXc z2>H9aE0MD;P0sDSk|b8Aq0`hWYcX6{w*nAF;fLfI&{xfjeFKUs5@ua!gO0K z5yw-Ax!!h$beacLN*)y3^)yw++j=7*uXYumXpF_LXr zIK(@9#B}Nie~o`tEBdgtWV9M2&nV4p@FN4pl(1;CAcmwF@p}*yNB5N)Ni_JqFYkQB zwbM(svwhV*S?1^fy&EhYtg32rsNaPU)_x-x-KwA*sU~KF3 zLu-ZM-#DZsVIgV@Q6598jI2)`$loato<~OzwtBs53b2^5ewyYrlkS%|Nb9gW5`4RD z2JvoTkiJ*M_qX#t$p`Y_?jzz$flM)uSjI2R z;Pqi(Y=+l6eJlEQ=2a$`^69zhxJv{6rHZ>BeYvl`5eu2ED{f(jt~_OjIgl7i{!rkJ zIkWC`Z3dQ(qcv@HT4v?WY9L60Xkr;(`$7yo~MPRVtwzD$H(C;D16$v5y z>Xc4h@UHb^y`#F*8(z_{n)tYrk!E8y(o3nXlOA6?Lc4PlF*Rx%AIT$5eNG>`qgPxW zOyeqM-+eXk)k?#|Yh0E$-HZ+{t-T`N)~J1~aMDN{T%qYu>4QGb8LaEG(2ssl)tL5g zVon_uO1M@mrxku^&ApYDTghd{X63$RO0_3Sb7WC{n#Bwow%QA(heK~m%Hlfr(dQ@* zwaKlRgD?)}D0m41e!35K<imn+5EE&2Ws^Q*&N(u3gCyt^GL$4KRe{z{1XuF14V z--7}WTZE6AsJrAQ53_2Elv}-#Y}P3d|F}=rAmKHcI3{W95@F2!Oa52&I%%|wpYF-% znvwo%8)IpVY9@F4VDdWZs=4ZZFCF#Wz1|03UcK=Pe_0t#k5kf@E8O|t;r@Mt*OXu{ zL*T=Sr(G4d{=N)kk`WvVT(~VDf&80(NI>ANeT+PdxCV@Q>M?JQnBiN`13*fRQ7E0- zz1g_Wxy8&vB|`2xv@l+Rv~u!>#49n_6mCvi)GpQW}_%|_V0MuMFG?;75dtD)|0P4|Zo z$RFld5@ey8=tjDb$jsPTRbgN%T799k#Cb#qd>{vWuq|I5*DJrYWPoX}l!?mGVjYxz z(}GL?IG1MLU}ajxOE9hvnUC*R9dr}xgqvwoCWe5|x!@YfJ$e26=M3`KvNBM`!$+XU%n!7dZ0j=*q-y2=QTwU+t%FZZ~sc zq582!a$%tTcM~u03x}DGiF_=G5Cpm_{kl+zX6~rGwhAaENa56MH{Pe>Jj_~ZFKi^; z)I7-wiT^48m-yLjfX(gB!9m(+!2l92Mq*LkJ0q3NJg{lAo>yqr6#X$yI(twofK~C~ zN#T;XVY4*z!c(xfSV`H+z7Qh`@oO>$JMXKO_y2n$o2evd$@ zb++{dQdH6u%d;mUaTkn7=WYFU1~tPZ4vCrc>DlbI(rWtQiKCH{wQ>g?(w~$4S={{- zX@lCSUOnfJJawO&NxK6XK1$)U^1+>|C3d>pMJDX;idc~eZJGsQwp-o0k3I5JK z84~swf5!}{YmY#~+PIN zrW7Yf$cBY>a4r;#LSpnl@wOBSI|_MGK7)wkXi`7=hB_r2S>SlWrOR|)XGc|+^`gSN zyie|=yOAc^V#0^Xrb5qzS}9_aJhuG^E_jYOvpiy0_F-Rxz0>t78SsL2u7uyNM1+=q zjX&B77{E_<6Za?Mzo?9UmUP-ycGHcPPq{xu9C6`Q?hf6T*=nkV{RbPJ;Jm&)JTuxQ z-V}?}zqbhHUHlsThxJi)yS=12?vZTbivKW!y4C_&E)>CL6hf#@>C>oY1f8!e4O=4R z&!2cTt>5QP{wEJVN`dH%R^4Ef5QQ}`J>87T_U0E3j+c`$-(l>KK)HF43eM%Do5{KH zC+dP0?u0$%e6uwCKXbhWsjiH!>3zcHSLOT3PvNHzD8|SmjCHHn?DaM)&!2$`P( zcJTnpHnADR17QG**Ms!~)={Ry6n&G9N#?S8zX2p_nB}YbXZf3l& zjozi+f&TP*q=-WEFelBqE-6@Yu4cjnoaTAZni_e@J}4^hC#xcpf)tK z5u(%D&#HpI!<>WVb*g%bga-EutXxQcUqSnTnT3385sF%mHGC?RGTYu16 zxBSHobtV0Z8|g8;AhEa7Y(@gxz#x-kh$I!Ep)))l6sE;`dCEW#8JX^DJIXIEbp`v} z+T7TfxU3YmS|h(E*57&DiQ+GUWR!iO0Nu`;Wq@3bs7}1x0BLhko~R*Xs{n+^zHvXr z{5Y-TwA$oCTxj~)FeT?b=k9BID-4WS(zu1}u1aG#v#*=0uPIZiY#6&)vYF`n^L3ab zYC7ecHXsw_isD7#e7oUiFFo<_a9)f2SYzM_M2Pf3J2uzf$*Azc1joIdzDb-!eZJxwKUz^6T!ldgSFZ+RvO6YdXBmPLFc8D_Gc%3hvH3?#!#+a5c z6~d5uyAf14aD_QS?>S4Rjk(D;0F zn$Uf3a9i@$ctfe&rUQ{d-Rd-dc+E;}g*i`^KhLr;bEUXb>LuH$!mamEWqQ=Ku+V1^ zDE`AH4ouuD3(D z6&L#v0d%KHt8`a}R4uBdQzPPqx;(hXHXFt8BK;k$a~|u)0-pqB!P56UKa7+NVoupb5fq`8*qS(tpO5!U1dKNFl<> zb|j75LH@wZ)XMpIvO(egZJHOYR%_SnbJ$bcYDqL9<;|&4Wg&hCiYv;DNCfV4ikiwG z=Y${>A09~%rG5il>CJrp!se0OJ0+~ZFS=_X#5yyGDY2P4yoTyrmaN~J|JB$U1Y692 zi0B{p%~o1EjSoxGZ!n)=~Fh=rmhM zb_`$m1?|?~Uj36=ci73WttEs~{D^hBYv6Fw_v`D}`_ z_#;iLmH7zHZskSbabLwmn)$}t+z)p~2FC_YRFNGT^_6;*_Oe|hD9ypz;!{RGGvzfA#Dzj9IjYn5{8v$M)bW`1JsA1kDw=4HolY3cXA8I*<0T-ByC4dFX4* z*pKD+m)BL8hiN_qO#VB3%M>3mGRtt3rg?g1mo%Q0{rTQk1?#&9bfp-Ks# zxBWr25YEMs+r=amCxLk&5Lz=P4TMAz3{>%$FP!aQDi|KVA$^l#%;M-BiQpgo7|VCm zpfc5~IzA}KO?o1lKBxYQiYM;F*+hOjJ$#~9#4m8`shsIxX{cD%;?MRMcKIm%Z|kH; z2^lGsU@)GThcIHT8KQ;@=q@XIwm+pkQ*5R{JCMK+An>7)@CIM%EPiMt z6LYlxKrmO!jcwH!%M?1qvd4Y8eHs{S{FHH9uQR}m`n2syIxxRYe?L}*n zSV0A2d|YujMBvfy1R{TZiLAOqvEo0^#Cet}^2qJ}eB_m_)kL6*QXQ4unu3K+pT+RT zso!>INldu3H-^67U#{`xGM=PZZPiPA8iL9#MmPuwe%t}pch#21DRZU?Bf`h8h3cer zR~Q3eB4ZDi0kr`KLT83%pZ6Bmv!ca^aP5V;YmEL4+{4t&u#*a|+{B5+?m&uPo%qza zm3jj})Sc&dTbVizTBpDqe+TPGMA(o;W*G1f)i>sd{E{=-gGA_00|12HfU|i=TCRVP&d{0C-xTLGc?Tj@I zN;+|Q*TBHngFkFT)dm#zPav@V4e<4l$Ujgsn~TcDOGt>AoJo-+!x*JIi@u{Umh&rN zlq#m0_0=U+@xVy_vT2!^J87zrODSBKAzWK!zQAdIuWg5s=^NkjAvdFvq163w4v^XemEyml zQ8bq?IWLQ6s&fPIIr>d(lgyhYNsLw{rT=Z~bnm$`5ihux`9mRR#$qkRX#;noF4dcU zG?N`~X(WS4P%exwOQ07ObAQpUfpIp2H?Wl&pc+z<>2~ji@q7t>HpC+JgY74q8A{NV z_-+VZsv-DAeUzvIGej#OaTR*1sHA|h6d0g^FXW|}m<4Ma-$}tYeI4);@eJ@o&;;jz zsRX0kRF0W4(Cb8f^Si-;Vr{2&7q!_BR6YVqr0o*ktx*59yV-#k7wBc~(iV`aWi)L( z?5=c4pn7oEyT6?1M{8{sQ$q=DF<^xoQh*Pu_9y8xcL3=4BJ2Wp#gq&T*4FfN3q{89f0(;ENMYAM1+*IK?vIA0vEM4j^xP9;U&V2H+MK0+dyBzP6j*VM+~eld>6@{J{l)za%?I6m3#aMNrMJK zETn4fL>g^(~`72x9QLMMhE7bty%{e)LD@27z87K6Zhk*?AJ=%oMk zz>jUX`?%JSUD5nYjwA?d6I_nbTkn7An3?FfaS>x9ga89>{BpTx`p1|j$o&E^iDF8p z5Ki6zmiKNI{B{|lJ2?YwC?xcQ)WQc7}>$Q|>hx)9q=o6!l;f^RK|!x)+wua`3IG z>}Y8VXfk{Wy)zKAvYe`kv;s+GXFiNu1jXA=8eh0ku8P1u|3D$vm3ka@-_+;O*CoEF z0cVDFmO2ekmC>@bwLdtM;vM#lG}qaAJiVB8EmmPG=>UXfL&V_((#?V|GXO2sor= z@6Qd>a)uIYU-#LN(zu7>V|x!O`Q0Bh>2*$F%q+`RGu;Lf1bgcDoxCGWx8a?b5lZbQ zxE69pTQWsjowD+9@IZ;4|68-^3d`aTernoid`x0$=`${i>idMfTpEhjB3Pd$k;8dq zXfXIdN2=w5<#J+|`cn4Vx-~muV+v(9as4*QFa5E!%)sa1k+x)F4rUSp5c5&fK zW)h85Ejc?7MLpCI^6FCWXk1kXM4eMT zpbs+wH)t)Kd`JZ22l_WMgHrEHE})nLPRwCtqyAg&RnGu-73JOMwTN~mxaYW?+4?gt zA23_pRLuMuo;9D@nzpx?OHm&)ad8}X%n_sN&HSW2TKEZ0>=H4iE0^Wnam2PN!2n6# zd~J+Daas@EFvmSMrWExVIQ~)|C_Sf=c)^Q+-kSUap#aegSZ|^(_<+ztB`xrUXeENF zHJQ);KKFCAxY-l-rsrX)O)dfhsOWkZOtz?$wlD(iE!w8*C{mbH#A+A^Go%QFSzD6* zRgLu?HhFHdcy9Sb|7wZF(~3K{}0rAPyNrDeGREqirV*MZ$4#Bkq~SZ znVVsNh{}EL|8GOVam%sI4RWG<7hlo5Y2cZh`_lEL<5(px8Di49g=%XJ!u?$FJAC%CrM*L@sr!idU#a;%Iqm53|gU5|+-9oAy!C!p zb652f=#w5E=$gT=`sGwNlt`Gn?D*9go!YC}#^EBuPwx}NFz&3_(c61?Rx9JL=Z5g8 ztmDKbi>*PwOfcfC-CczS*uFA4*}jcqY>mh}aE*G5LQ3^+cYo0&ce z++hqo!D%vQlFguN{tsjTp)?bOLy{LgOf`CMl-mP}zs+;lKycd)4zpzHXP7+DPg${V2L>d~KFTCqm`&GD~TFp%583k}yyy@|hWTF-V zDSrS+&s-q8ZPoIC$TutBQx=ZV=FwE;4qwQ|99^p+ngzAL z;zC2*mZdp<(Vd;^7=kn@-D0^jg%s;rvuS1tsG4(AFYU+PA)Oe<>| zMPiz`enDQuPy_V-Nk-1buYqq|g+1Ul7_pHaDNCq&%%BKSQzQ z&hX~sA1+NIu_4S9!G!LAY0L7zrDJ4p5h;P+ygFXK^;#x~x>vSOJXh;a?i>-W;?;1q zOd>_la0_je8=EuAWBnp!S@1!d!Jb}z`_UapRm7&==INNhDu1pVWmUpeS(6S$L)t>d zy(bUR*mEgB(jfG2?|o#xUD)Uau`d(^?1bm=i{c#++p@eii1cx@kpPMmuHRc+mIk|W%GJJ6|hyjCga;$SD zy^D4TZfn)DoM_6aH_gZ|9K^dCU;VX|?e5;#lg_cahquOF8-=pLsj5($lF z?G5m+))_%*{*}SGK!!r0^R$_)Eo-C61>Aif6ESZOR0upc{sS4J)Fc5ymVoF>>X32jIRMIl`GDk@MCM7=pVm(@jBYGT6Z z+apF@E;B+js3W!)h7X=>rjsD!WFbq?mm&U?it^BjW`pcTs0Zteltyovkc*_|`O z-`zsXPJX)Azbr-VJ!yVyTF-4Dx)!}`@9H)02PqrdyOpzcn&ezMV`@j zExJ@z3@!yHZUVu*#kq+3nI_QH%AfvdAscazNwEq?Cnj;eeytBle-D~6nn;(7(Pve? zLnJ(oj=2wNi1vTk`G4|ubMOARa$_T{QZ3qT27Z)M(r>{W;ph)&YgJ~vqiwuS;@{m+`jY?|8w#k22&boY3k ziMs_KM)q-94rHSPhrdv)h!Xw5D9-(0sjPM9h8!u+WQrxdjc-%ZUi%Rd9&&q0B8hQD zeDP#QqsP)3O=ZD?@ng+FvUu)>BkuVOclq%j*C;X`_p&itf~dKLbYm}P>PQw(De$;9 z!(*;SB~k?MKakTo@*n5(G;LQHb~SgOw}D9Ki`;;rwTp`MPkX#;9c%wqPiA%$z3_^+ zYcyaNHIy@Q^5tvFD%^CJL0203Y_6^ER@;2TjK4(U{ZQ(0F+nrCC>$dP>4xEMb-K*7 zOA^PpIP?5#7U1#wN*dnsJpo51#Jjq2xwlhln$G)+-|e@kH&$(lcgA@OKa4u?629mh z;@hKXP1teUbb8k#BDS2EqHj)^-xA>uB|nIQtGru%2FwXoLikt%Y))BU7eURwo;meY z9Q$_{l~=nD^)HF!qt&;?n*5J92Aymo+Y+VG$+9mrd2f-0B_zIY|M$m>d7JEoL$C?tPbZOOe+!pduRSjH@3NP_rNc&-`;9Mk0N1y9v(iM3O zz+{1=0ulzL%L-yT^?X4SzExK;Ha^87xXy*3NM|wjtaQoj(lMUT|9(94%c-=uo}?{? z+G(pq%CGM-=T9HDAe)$Rc3+8ccU6kg}2NJZL{`FTd#OOhA6mvpLOctS?unS ziVJQyX*k}?TDjdyB<@EhiE+F_7S6AQYl9CPn3 zOZ)UT-d+rNxX@o=v?{$U9hVd9=AFaAG=Q%OUr=U5zQXD1Y*xQ0!f5mO?~`tm0WT2LtAZ`)(-qg&aMT$Fj;xx+iINc); z)7ntrbA_7gDHhEXi9rUHyz<4t?!lAHZ`e&mLu)6(cC&`q z-b`!`De3ZEc5uOxnps7+_4$ZZNSiKw&J%a$LNJG~r1@n+*QQdZAi9;%J#jr_n-Al4 z2SESpW-N^}HeXwk=WS>UrRaaIUDlmOjq?vP&Nfb$Y9`gkE^ocHQRDH@8obc)#(QR6hTe|2WZ^tuB^Z^HaCY85o_JMHssa+A5)jBSHl)y<6?&wUt2TAbv9tdD-xZv+*grtZAGEUDX z0xvjq{RY}7amp||9SM9BH!+f+o7kW7-ZY_7v^-@Do`fb}E#e=xI*8JA)zJf8L;Zn( z{dyO*3=F&yI=RfPmGJaYcov~!h5*pmXN0i_Yg7_WD=C;^n?=zP-os$Pg8d*#tm-V< zf2z;$X7l4#5b>k_1Fnu|*sFWe-+DfEd;vKG+cMYH4V0Nz<32t~%hFj+b~)*N`A(IJ zv%1M?EUwBGYqX#9$PUmVp>qJESp^N1wg$6Dk~?>j0r)Nm07?_|)6=$GKGU*Z8|7wf zwm6-BU>RZ_qWQ>T1>tjbTZ{Y?%4QL0q>Ym9v=EBsgHvb`p`Fp@Xee?EUMwuhb9?(xM8jfIInH zcgi)*KB%)_DN7_fhoPg$;+IIO7t=>uo&(C<~3W$JN=Hj;4Nfuv(jqkC48dAbD z%Ov0pnak?s_r*YG&>poIL*2fkLH9mvQ{o(@gamx)PG}tKgpdRuCNs-wW=a8n@!Xgi zB=tF}h^pd;p*iok_~n{HmT5%fGtE<3F&1l&a1AwBH=#QJmWc6?(Hf50XYf6`#J}fy z^ZFClH?S2c=G9aeT>`^karQiQ?gg(C*hW{3FpaR2_LQcmHWrx_w;R<{5$IQZ2s^zk zO*iG2whd8@--p}#@I0icG_)8;`22l`3U(muM}CMjLYtOBM~U_lMh-POb)njxBeTH6G~ zj3HvV9|u)P_#ddSsM;;RA7A8#H8aBkDu9oCPw!edWgHgr7O7NBPpQ|BdKE8anIc@bO&L#_6=Ij|ie!%jgQ6xpgH z`09!Ox<=!Cq4eCU_aW=>!D+DY4mU0u{>+O*c7+Us09}cuY8Pk#0NND{o}*Z6TU+(l zJI@tfHEx>QNoEPfw`dZ%g)gN2>7DSDakd-!N-xG^81_vZ~*-8sz~{LvCy%WT<=tdL{Dz9RFx*Y1Xxt_uxAZ zy=V>%9U1Z;g`WP5Jp7;a zI<5xywq&;?$puMJ&t(fm*LhSR>&dop?TsA_F(Ju!bnzdE30}MVt(-aDI+Hp1v%@Jx z?S6g^S@3tq6;Ie_8Nv&!wpF`ej?VU2k;M~oTBM4e*NUqZ4t2b z1CKX&G#f~~5dg+CGBH{*nex8Vj?9nqGZntk;b*x^Ri(h?M3s zIAQrI>7@xP2~hC88>Ienuw=FU!Z`>0KT0xQA30LQUTsS$eu%l?-k16)SG>ZJ{V~Wb zp|FlQIt6WCL;YLD>*ascER4jpHB?IZF>$fa?&}i$^?bW`6E-!Ye*vzXB2Pt;d+ao~ zC@(B3)&70F#vqTZQr1?|K5N^iPyc~#4#!;HX!?P|&))J>wwOh+T+D{1Z8yXWjSu;_ zywz146dt@~BDl!)EC2i4WmU36$v@so8)4yl&GH2316dXt_iyLyC7~VNlntbO%{YA~MjWRN%bijQmkwRS|T8)LQA>u3D(Z*mo956dw;ad6DPET^Is z)f`*O<3=z6C5cR(A291`pR->#SJ=wTGaO$W2BMW4&=rUw>c7w;Db z&^~SFyyfm)Le*oV1op}S+Uwx~r@e%e7pwl;>sP*{ge#4)@SE1P% zBjv%CZjFOWo!>xOh2xbw68wE%Au9;B=NZ#TBc~*Pi&IZkNhp8NlXUIz?U{JpW&4RM zjN)1bl@?>g7C9`?$I0+hHvL^_OK^Q8rlj)0nP#@3tGa-{D+dX@w$}D4So=^g$v@Fw zW{Ma~7SrplH~`<^J_M9-KXjRI^owqcz&4C1_$#ho{PU!Ki;Hme%ciFx=-WT~aY?Gb z_8R1NzU~_D4q1A<#aCcAL7hK%#Kxp}3EMJKNL>GJt4IMfm5?RYiO|Gz*sLX^?Srq!*Pnmgdey z^%8B2Ou8i>M`F12AJfh}tbON$QNa~`j`fp$Ub|U>U2}|5mQV!7fy(3Yc5L^*G7x{T&UR-F$&};|;?2oeYL#%ys-Q?T%P}L+gJKseTf6Mf6rbmT& zZTeBSMCXQ8guHt51$GHN*0(-JMxB#`8{rXo%F5H)h&98Ie2ft(ya&ggPR1y?Im*{) zuezj)iBNe_s{4Ifx2c!R#rJxXjnpaB=`7TE0$fJU_8wVxWVPLkrEGD@3#rMo@oV+{ zQY7|xDT-p?YP*dv1q>$tERgT@kzVzCfd9?q6Ie{R@;JX7BB<3XB#$j9bvfq)Ft z369g$CA=}S5-lZNHj$!+1iH5oxh@d77D$Q?#zhUmeEoMlDF}B^)*H-7%RU!yy!LAw z^FB$Ay{xU>4Bj+N4QKHhpD2wbw720WD9T+Z!H4Bf{Rve$@Rm0vfM{F}NXF{sSo(;y zYQkFqQ&xuzjrsZp=UiADM(FFKIkohuz%t;Z=Sk4mXUTun*bkS|EzWJx8`E>b>TSk? z^Lx@&(55=E+%%aV%L5}Q-=I?&Wnn+-!F^4*8KMxI>Be{YnB(CB4PC%iC>BkPBC=heVShqWg*t zXzLSG=oza0zs7P|l1%j4b+0Uy-2R9jAOb{nrslE!fvHYg7Kc%kZ; zbx~WF>PZFAd9{h;Ffws(FgFbFiv}MfvE8Oj3^L-&gGk|uR0h)gDhbL(pu|?e1h;#i z>RuX_MUe=Z_GFWfIcK|NNkCb%RSuO_N4O#td@HvMq^L&*gp;2=MS0zojqzG|2DdDxD&|hRmzACek3c*etM7OGKzF*#c8fiB_9kH9c$`q1mc1zwW%j; z5#cM+nj#rV<@LW0Z`DSwb_0VTkOz2#L%P3HxqRtrP_fv3q_`Q5wwSnZ&}ZxNJ`4oB z5|NV)`)u~CiQYwdG4avI3OV6NUkQoXk)||6$-*0va!qf}o5HSJ3}0hHJix6XuYvtz zfUnEXl$i9@Pgl9?>H^Pqkjkuj2t*7&g}6)XKM-4OLxrTY#Q2DdcLtADa<(m+E}x>J z23yu^X|(~=$Be#2^!|1!!69}?>vC>U7h8Mve;}>4`5)wK0ybLp7FZx#F;I}>Um2F( zsv-pRZSn%hY0|Wn4@@?FJi7K_Rv4C)VgW>2l!3B!!X+kYD?L<>{z z@ortB4|aXId%TCj=_PBgoA93;hi|g&c&xq|t?de z{as%jU{wm6rV^51df#A{?|k?xQC46sffNgjYnn0{W|e=4D-)W@v9B7VxQRFUL|iGw z%PNf_5H{fVEmJJWi9R-X3HKEm1I1Il#WV&7#Yugeo9r{S1kS}0^bp8^PaZ+nPQWbe zs5tL|^(d0LttQe6#ofV=u%th!=GPhB!WQr{dKH zx~-9yDC2A|Zn`8rWl(6sH#4#7!~rUyb;MP_!1BpT)A*2E_fQ52%32SyDQzIe>n!cT zuTevRHW6+HKQG=>xX{kKzcPP9SG@||6M>PH& zW|P$Xobb8qYjAyTcOp)Mkpi^d?)|pE-gH#zK`5sW!h^#Ff(=B?xHm@>hoZ0@HI1>ReaG+mnkP@ zg;ArLvGXcu!mtxU*DhVoUsc@qABbM~l6UA!po=3tnMjBGmM34KxYlThThx89&avZs zFksVX0? z&bfDNhu7A3qAk4?Dm5>|8N3QA%(9+f+1=KxJ>Edk(p4ilH^?xpa*-w4HmibJUXOlb zFI%(FAbXq|4NKs#zrd}qWy^I_H^b|t`zcwVb8aM7QOEfJYYHm5XwaZ9P4{hi`U8=S?Q+OmMpdAr z*LGrbIw9?fqmGT?g2M8Sh+eB_V)(INjUGaN8%p9Er8R%u;l6ibxkPOntE6Xut?2cU z^>vtxrV7l>3P>76 zL1v=Q>PEWgMyU5?{}p(q5&1AmN2tGVt&fs^HXz6SR@W(LNG2_t2YuN1pWD3-4Rsv$ zJxtwRDeq~+V@Or^{g{O)i@!F_GZnclbuIb=@^DKbfxJ^u z3=c=!@$V}_ba^Je zf#giH50jP^dF%HVIN%`^Anuo^w~kFb{UHGc&npmPZ)TpwDhZ84!(@~na>F`eJ-6Q( zu@n5*xsjw{Z_fNZ?)Rhetp?B(<|4!HFxx^UpoHhWdIg?~9RrAm5nEyIDL*{sZ2tq9 z)wkeGZ(+o`cdw=O`?70ZUIE7_AvQa_?Mu9)y$67y_G;&+9rNVQCDx3%qo%M_yocNl(5=J(U0t zwig+lJE7wF?weGI+<(`sLe1rGYTl>NIPGvVr@6=*yy!HIT-$w<+=OLlZP{|TSs?9g zlk{`DNDAZFVN`vK2!N3UPgws(uY@NWU7J(-3O&&puTs}__wkcJUk21PHTdO_%VS#d z{K6A>DR(fm(BAqrhoRV8(Lr@T{)C@Wp4;mg||njeyb^4ueF!JfF~U(y1JWUtXg6i$xqfil^~9+T!rv(68%ro=lhTk_v1awoWB$wjh6c30#E_T^Z{C80eh&g+V`F_hp#4Ov7pJd3N(s@# za)38Q6_yVUQNF(tv==;+qbVNb&L1{ORv-QmuDla*@1KhWk`mb-a&WyZn+6Gp4M2W@ z*%Y^k(17Mh4)kA&Egk~ivP9$zxQsV&bi|x851}_3o@u*<9d!+43ksT8iBy~#8?AcaL!t<>37``;B^k05E80_EMKc8Km zfJL`s&w|BFvF4nrlrG4CuyRDA$Ti=)Liy`dO*>)vd$WTvSZO(Fvt}msh!}lhRRC{} zBgF?L=@Dra>34xw=Y`ix`;F=Da-4J^GE7!tb(KLJc8rD2@LySU(Qkg`lvi3fLX`Ca z%Hv4GYBS$QYZl!6@ey>DiFsnu$QywdB{aANZEvDONuJC|uWwI?X1HPkmq z+8BJ9PY!hT7{1!~^0XvPQ11SOY*RV*NC=8BesuGbswfkwe*W*Vk2el&sLRM}FpYll zcZGXM1OW!t6iZwJA-6Zz(_r2EwZWNA8g;#JBIyBN^Tc+@QyN89Ln1N&EtBFu5YVMy z)EPoDK=4`sy^y#Y&73vR`eLOu7S=0XEV69y)frNm`+hPgL&~ayex)reQDi92=)TD( z>NHwPZj%|BBlAI!%+H-edrtA*Nna;75k-E& z)|n>*i9xEl|9Z3M7maDiDG5}%nQ@-8#cU1pe(1FSAR=4Ii{bRb5Tv`ulz_8qB8Q;a ztaK|gPejndE`(2Vjr{SX>aQs{1!}jT=Y@d+|AAQU?k@XRq&`k#n;j}^x+>w|2rv0I zb<;w>Y}Xr?+cX5QTE}c~C&kB=6W~I28R`anWQ_c^c1ug>C-3kXNr4e~u5?o6067Q9 z65G0S=g(D6st&<-696PnSL1s*H=D>S&3V;zz*M`FOM{RvMz|O?Ep*j8pIJBX%qa1v zmoa>;-rg-tk!1(vc+8cE_IKf2caaTFhrIu zl0)%ii#u^}4SZfx(B5!|+`r6NzbeX%NX#Yfe&6OB>oT~+e-3aydLvs-wrGeKmY@gb zZ~WANyY2FK?I{UNeV^(M=md&-d!M?HdasbU^Ux@BjX^k)u`~AnZ#8DFb3a|o4HnA_ zmFPM@I!B2_0p%QB*mmx3F>Eyw@zy!Ei&gWu)A=ViItg|x0SJ_YDF;NJ6BRtiX!`)SW=#( zE8vabGun4ikAn#=FP6ve<$|#h3$wzFO)Yajly#XUUq!#ywIFLV#ta=EDTakZNsOKM z=CU0kOs3fJB+wo^aiqDA7~-F{6&Fw!rQ>bEuz%b#08{8C!mkRUZ>oJ{n!X-{BwhY# zTqlMXF=9FXJjI-P$hl#c`&?rSE9;Az_nd7k>F&^rtT%iWy(w)(&lnUngm2b>yA?UY z{1ZL~EP7wb$OBQO=mn*_K7&xMIv+j2ybcD84k>*x`en%HYi@W0mn%&A=$|pP)#pHj zsCe{X9dVW&6~OgzqE&n@OtnH@KW5$iXS4_&-852qUj&5E8N0H z=}*P)fBSC0qgecLY1vZo7Va_DDFvFwFm{k*(|c8T9XOMU?j8p^LLi|lf=B;_kA)2F zCw5@B?M7&xYVA&%Hr30HA({H|^lqLYf7-)#27m?q>2P~uq^aqCcS=6I8)@I0o%2;w zRh8@bRO&^6LeXEClQ|n7tI1#~LJ5&o=i$LRKd^G{sHER^_3zSZ^LgwzDOKEtca8sd zVd~4G$((E-+t@(mi$}ZDR{qYiq z>;1o1Mu|qcmxAC@!AipY3eGek>{aSbyb6Y=Ri_qtXw)8N%NzV4zMp_qihn+*{c{%Z z_JHqML?FLcr32MME$1>f7W-`H_lt#EoNy^2qL|X})gpf2+kT6$(+A3b z(a<D_-yKsvLc>!>Edn(7cM_y7N_}^$rkK}5+0g*b@$e~C1`CmYFeLu z_5B+OBQOMRFbH~+0+6GZOZ zfh#;`-dHd^E4ep z0LEf42O_{AYw}sTo!t0K^q=pg2iJ-UY9wJXd6asS_k3L&*e{{iv@t#J-WLp9cF-JY zAq6kQ=xhWyq}8gBp$hus@C=e>FoRv|QrMSO^=9 zkCLP(*AEPz^zl8-8-k_^EfMnmL!aPfLi?W=Vgs4w@??-H0hl#e+lZcO@O{h*(Yk-* ziHv|Fp6-`_fnF;fSv3_sLCGFtEvKo0Y;}T3O>uN^yP;jPcU$}fhh}uGRCk}^=`#hD zi*{Y1$&6_gju(;lA@+E~8Xa<5PzKTH>-`fO1uSc#IEm{XaGI6bXA0-EFP~R$Y>7@d z#HjOM?MYR0u_YVcQiLLQu1u&znm4SO5|bVPZ=4>jw|qh6*=??zpjnd?Tpi7%vEB&k z>(W}BQt>k_p%7R9S&-X{yzh8>DazlAGa^CO3o}TS0yQG$rxwG|>042jM4hCcil% zvHgyyZ8P(Y4r)hZ~EWUk~y~0leEk-M|Gd<$;*7LPR{#cyU+5$mBIl*it!e+*w zLuub9%;V7XKu3!STjb18^&>L&h)s`H)Y?i;gx1iJx!~dFjP7gbXvftyB|L9Sp3&o% z)g|Rd9cQ*?td#pKGwPF&ku*s`%WYI&n|HWUW8$nZO7F;(t8f#9G3{m+u(2=oU4|08 zO1Yn$HAqLRT$hBU-LK&}AwUl*qxL@3koi&4oSeeH3b?U(G{}^8O%vJI>=phLR1r;T z9uzwC&kfHiREWS?${rI_9-Ph3p^`#{Rr(c4zBjZ@P{Mi8R-kt!j22AMFe{#GFS~QW zz*$U|CGDsrlH(2&$MhCvejyh&aDTXn`g)~%gTMc3nX~3p9TI^L?rYkXWpuj`>fd|& z3pHH#$;6b;8uD{!4y-R_F!Y?yi$PyyKCCRT{<`-D8Z+Tf3`mH2z@!%nAeMl|%A+I^ z=r-Q(0nSil#QrV1qI&@0EO16lPk-!P>1+1noLhrS>WS+ewk`~lA9%gYi@mSGey#zgmB${aA)FWPq z_&Us|ueJr)9uBs)FAzsUNPU9`bS?atwvQ&9;nOqvrIb5OP)FRXfSi^hpwz znP5CjqZ0x>OYdQ4Kol3jzr1msfqLpf=w+#bYMu(54%nX_VdurDnMT0AIqwe0O?s=W zXHsP;Vf9Wy=KniW>#4~8X%DdCc0o}(vc=^YfhwL#Y@hxC-9KO?2Ws{=>K-_xLvk%> zRME)Pqv<@VkD>fw+f^uvyd`hw6&#SXb2+cbl&QCVn8t#hND>2<(`-QK2CYAa?)?VH zGhF{Ps@UEMrorm3{{!8h=G6fISy~F4>J9dC%;F$5c+^n`M1iCK14(F=AM|gS4EzTQ z!mEoFAjZT4r832ZFQotME}$%ui!fj!mV={LYNOdvWCH55g|QrmKaZ!+*$+;OxzJ`cPvPi6<^DBth9}VtRo( z%tvtJntD&Tu=Y7M8M3cs&>_CvO>jlTRl*{67N3|d%vB=x{GY!d`SInp!=%SS=z!Fq z$jQ}ZaCj^zq-nf}0Xo&eT~#gmGf|tH&4}AnJeK(_Vq3hs#_rrJc?QXqL6&0M&Zf^| zv2hI!sloeZyl%d1Zx{P+yzCp}=c@;US&7a?=EzCGi1^oup9lt$rc)&^qM$b9iYG> z4C0YC7Z?-OBX~LYCTOSr2JXc21O>uruf!h-YIAj zJ-epp1na0>oz?crZ5rRREFMz$h);4&OnpAJe0?D?tn`V*arz`_fLc+UDR5}vR|A8d zsGo>!K<%KC!^j_>SDH!?Co9+Kl+gD6$C)m-!_o(x&9xMk3#wRMD0g`ucMxpSvE6Vg zJwL%MHEW`^PKELt0XZD$z*=45K6h4oDD&(>@Xz$311A2WfjIp$O&G}?xSxBWl3^p+ zTD`FI^{>{pxf;v12C>|~cO24m^I%J{n#y|PW&lZZDR}%{_Q{`d`&_t+wtdWWocr}M z11#pcS-68uaw5N-UHtaUMBzvHr_X}2Fudz0$P@FZvCY2jf^b#%XdcsJaX5nZP>Ea7 zGm9@9NEYmyv^WW5^oO`sqlGVF(Y)35@3gm7J7g#DEy#-g=}e=bQ^L0&=1wgbZEz?z zyQu5!PV=Ko8mY6uDIZTY?~sHZmgQGUna++1M>f869hF_dxXXuyKf>T@Nhblyfa!GU zZ>_}idY+OEL}a z*#W=FD1Hs%=?KJ6u5m8gIvv0nspE3z6IIM7I^#9ALFqYt4J zuPz=pV(%##VX(ta2xt2iS-bK&m@RMk2h-tD`M-1}!}&IPG+D|lxxH6YcUE*tEz+>w z9|_9U{*jMc(iH3V*puTaEqx@TruJ<+(`g4Puw$u!}hp*(Ivd?PXAmaG|TcTA-dgKK1DE^#7CGZ{n z-ao@jMFIchxmLem%NCCr7rquh&Web=4eecL()`HhJWKnn{7nnX+FJDlizkH3(bAr`{=H#Q^+) z4*e(gc5Z38^wp{3pX*TOLgz2iq1f#Xb$i*i4*L2M+$!qRn6?MT&-gKRgjH$2yNZye z8lmrAg@i^1IS`V*RP9TT-o7e*s(!h+q$n}Z>$%(#7fVnuDb+r|QmEe?^UXzPNS#ak zhi37K3lPmFVZ53fvUVC+|04{lF zuEap56wh8km7Wq?fUiKIiLRvv6r(8cfDI?s2&Bqs|Gxtc&`zUY1m1h!(FN-&Ir6YdBqXa2A zeVN!`fc~r*VWy$A*h%cUwZ9e%PqCH#&}>0kn(i<86YZy8zb^=i4ja_}U1PGm-lN}$ zoyejLWZzx)JssrjI{aGuEgAk*T4=PN6wU$*?|`N?QCt6Rw`V*aev+jf27Nj)r|-{JR?PkA!;k*0Y1>u0xS+?7qKptu{#5nn}{RQ0Ssk+kfq2vPHeR^CN+ zdYiiPHj&cv!r2!=Gvz%NvcjrUijM>e#dwz~%8)WZzJoOfu$)@-VM|$DJ*4`1$7gl_ zTjbo$q^d1@`So-fO`Po+QDOdfx8XB=2Bz1lNrprkf%*bnFs+-K@6JP`IFEYQO1)cCg)-Mm`tMq9w#5s-JRQB! zMG#w0KX(+ntu!{?P1J5_;t!$EZJ0e!W{%jEvY@^9={I9;{bJT0VDI`vKJ>cg^&`FM z)DJ!v%S&%(8sXhe0Xt`&kI_l)xR4{Wx4ArU>lYCB^$16!zd(K2g5g&c90IN)dso1g z{IAMpb9g&E!Qt5ZWayLrS@f%h*&FtHOBImke$WwbVC{T^-^=)HC2gyIXYtjnq`|#^ zgt0=;FwSi99xFG>2jt{&rSUC`@tZd~#46tp>mX)+lMn02lDur*G^=RWCT~u9XY8pV zueBL~q1vr)ok6{-Op+SuUsq3h_fB9C+Lon{jzQ-xuls>6-=V2iB2Dba%bJg`3>8H5 zr;ME8H^TNIhg6wEcU+$UG0d5(l2B-eehSv09Lrqi5D3M;@Kp}{ZY9WZPg`2@yH8Z0 zn$_m6pt;1)(G}~v8nfC3o*HH{c_t8G*L$yH(iBcI!a2%)WwhiHJ*S~(F8r0~+fK0F z`s*65+RuNcDnxM((HIM}AU&((7LT+S9grcqko&$wJ?Lx)dS8FKPl$CKO0kV1Rj77B zDZuSnf4FNEhf6_5+{OcKU&5g{&TvmuK{<_;OS?!`+bwHRv4Q@+gZPGVe8C7_sTdB8 zbj35HE%1#~vp;PM)eoF80WO%T&nchTz~-l3E51_+v?*23U8eX`T+G50&Y3CtA}+b| z7!xvoB8Oz20xQ40-W0XVIuNjC+tfIXolb2Ub~)C08E1e0Wu27SZK;8AQK2R8n)q$q z(-wp+jl~^b^fs^vq`kM0L>f1HL;0F23*dUqhAIy+CH96gYNBw@3x&_3y`pJv4gW4% ziH<7oi#*s?`T0a5`JH^K)v8)?mmbNYi|F)UOmi?015WQ#bfznYcA_HsliF%+c`Qzc z@*s_l4+tyu_J*_?*00CC#jYnK-P@&@uF?nZ+6>)P_eu{Moybdmrd<}%En;D?T;nu# zc&QChh8C#veXw7q+mVcoJD1=GQv2Fh5}!X^$~EzA^I-p5&Xb=;Qz6XG(f&id`<+Uu zMYAj_tc4|Xu_3ra;?*{a;I89TnC4 zeN7J^LPQx!X{3~H7>1DU?ru=J8xe#dh87S3=^Bt036bs&r38lV&S8A7-{1QOtb1p1 zX6|L(dG>SmIcIOmHY1ic_5wp8`{Z|Q&o^ANuC!8IwuTS>v!4F)rq{NWf8P7UaV=#0 z&4Y`d{+!*M;HTwKd+z679N(T#ZrPj*Z}(3aKpgNqGFTxRozL^+5^Xm zd=f2>XpKzmIg$edlo;KDcrogmFs3jKo-^*OjM)B)av3q9&ORCEb;ZZ=IpWu!tyZzc zMeQ|cRlqMxOA+FZ%yhgTkkXD#&Ey1sc<2%YkC?|)ZT5&|F#GS;rLRlqI7_Z5j))mj zUyY6(CzW3iD>JudY*8%GFK)^))9HgjsW_#Q9@~Gw#l^-<&T|~q{lcWDIe#%83#?Zk z5buPr;o(8p9Sb_rPJ4?>rY5i^fo!ND?f)asE3&2OGK}J)f^Y)1qI_&Yd5v3q`im&| zR#%N4-d~KQ4^t#SruX2CQNIFXE5iIgKH^0l?32vwA_*)Sc83Izak`(Jb##-8&32Qv=#z8z)@U8_5)jHLK? z5JxIWxq7Y&@-d5(-nXwuFf9Z%X!9NdH@|2-pZQr}t*=DhBw}NHIuIcZ_{&B4^F!G9 zjm&*-^(MBfUBvFk9zP$a3w`&wMZ`8LQEX1VklFf)>Klj)-|vJq!mL*)O-dTP6v`R! z>b9%GS-AxD>9f0TJv06u0}@Evrc{P(Mna}X>cf=3=BH?1NrN+-8V zwG=&S`Xd%$-fxim44a*)bd>@LB)5#;zfL)`*YH;o%Y$u1lzb(3n%z4e`wLbQ&W}S0 zBz&7sSU>oPk}OzTEQ!-y#z5ca9{ecVo@+m1=bVdAOP@sQ7jo#hC)XwDR9;uj1ZFD1 z>}7p2Gu8(pXTprTCLHSL8!F+1F7%SZve9Uk!#n-iNxp?P%T*-oJSdh{JT=Tw)vE+M zVo!`JmPyf9(qj%qUVek!_v1G?k@nPKOtd{6Q=hsN>uez1cuUduW|x3$Aqx8WIc*@- zhnzxRza=-u693k*pGYeTlGj0dibxoYb#R=Nw5~3~>s^v>n_7B=9Z52fO&a3VGqeyA zt8sBe;B%hzi@kuTKVnt#R%i5kfnNGPYPv>UPb5_y@tW|lAiCD2vkSdSjm+4L3@BDf z7wSAzm-!PL+S^uEv|ie-ZfEaSy&0{NR|=bh8aETSf6lfhqyEtrIHNu0)fnI$tIRta z>1xbKNk00SiMDNiwgY#{ykD*{9G)xP9&uoj#MzA^UURxMljbUsv5w!^ZUOpcI-2Sl zBb6KP^?nn8y)FquBTtpDzXX=W79U>Y=S4Zo$RsCnI%JuS&d_z^xuX>(|&?#aKeCafO#bt`}5jpPvr|5x8) z_m`Bh8(&KTw(q<0Z#@<9(qXhSrvD{$_6ZZj4Cf|j(Q)55yzk#tK9D(jzL4q(aHoZ@ zXxUp+Q82D&fJQVDt8cj+v6<*~r)zHnB9P-0UnT!plM7)Ix?7+wi&xixh#Z>-czzV~ zE@su%k)XQR^0i9u-!!`Rjq{;Y3b(r0y+GKo)IT*#S7W$L&Bnd-?!}9UW$pZ!bS04wO$o_6K9Lu}_feqz@2ldk}mw%%vg;$cP_M(*;}r*{vWQlrh5=o41SaKEa7<28E= zh;Q8mYq1kvbnw$im{ZEc&XHA`t2~a4AmhVI(E05QBb#1lf}Xo*=V5MMY$c@Py~n~_ zeDk%|AY*wVqpgIeArVPeGJT_e&l6y7Z$CahIUZ=2HP@t_rKu$7GnON&5VL_M6da$iivY$SW?X)sP(X^4!L*Bz*dgb1;8FFd->^K?JaORvT zIsY>eB?gWW1z=&@!+z1oFNFcA1OOl*l-ni9s@Sl})@!o_+R_#;@JX~0>7L#`NaQOZOro(c#o_DD8y>wW>S5S& zFg6986d_e-C{;s7N@S;TTlr&&r_(L@jDe{)lU7N9c27>w3Os{o@IQJ!{BoK zhoHvF0LTG&2oR1e7~t1`F$Sc^i#VV{&h5IGqo|Ha1=K?L%c&_XMckr)P*?&}=9eN= z`Uj0OSw#S?3qWWVfT)E6X&5cQFHKC45%dqhr|UV24Ws{JpocgD@!0%M>g|g}c>mzf?Dvir>CIW*8hc$Ds8s$!BGfVG0&f6-4%(~}$if)equs4a3811Dp zlKQ`MknzA|u1OiC-Wc9=PWi7XCvB*zxe8C zrJv*98O%c;p~671hoSv|*_sUQ?RIMw8w1bA&Yic z{pa91Ik*mUNcdiH`6sH^Thh)oKwZd}D+{DLPgpI@()PzGGJH8i^24ur{c)p@pX_Ad z0|NMGK~1@YwdGFn*552HlO3qTk4O-Kw8^K8Q+w0*(4MbVCnCQ#XGkwB0}U7|or-Mx zglnvpLOA2Hj6ux7EBy$E!C%vPg%DY-8;#YP$L#GV#n)Fs!jgvKYbf#xxTQ;U`wb`3 z#9FT2WI2Cl;0Bru6t6cse!nVuXTo086k6(pJVWej(A^YF;Hu2JL)#69U?Ifm21{i> zKqxQH!yvPc`)KHA-v#oYl&e19=3gXWY*c$00z=?!TJMNQd^FHc!btbE-=rK4KxBwQ zOVZpY+Yg@9aBY7V1jYc{D4lAbQ%<;()8*H&oPy$yQ3|}VO40KY+`hDzxE&uUZ;vq3 z;o94R#{0^O)T8gMlr{nlWZ+&PuHH{Vofz)8u02Zxy-jcm3Pfg~NF_g4`FhpsfQm>7 zy|TWg+CwR(VTrdQA%^tZ)|jTSj=)(ssYTppXM6f2Y7DsZ2+5(jnvuEceqwLeXe$Lp zehUc4x)M*cPi%S^8zmTL2RrIchFS0=Vw9axc$chVc9wq|Y^L!h>vMu6$jb-mcuMWc zbtbj!YEe=PES*keHB5brZR<{Sir;?D9ueXsbS{ONl&p46%$r2zmH{0s)cQ>aXgQ`o zrDV)`ZFn0|KErLJepblJ`l2KF0@iBH#8G-$!Ot@W*4Q*rh2YBU$os-Dw(}%5l~5d2 za}#6VQl9@ll5-oRsR}sO0+>`TkqT9P5yC&ji)tgvnrNT#;kDMCNBh3eh#C|Bq-tF+ zUE79i*+zK~;w{y=nWBe6+U@to6bdn*z4F#->PSMzxP33LCGU}$|{?YEYA_0w_u2dBBT&)TTWz{h&);oZDl z-Hf}vUk9x*@5CbvXzfl~VS{;QyXFEgP5dODD^c*Gv5UT+2y^@$>?UJaryJCiR2Ww0 zVTSk+U-OoSODb8x-Q5{!yufFwQT;PfZ|HINWznzGoSt5iVt=9JQ`(aO zXKgIz-gzO=INI;!MMAj(HJ<#S9XmEZlqak4>06FUok=(Q?F_9n>c@i*PbUQpfu)aY zbe^ta(9{!*K|q?ixZ0pstisSRQ`V|T+vItY59&_ab!atxobYB42}+zt(4|>oea=o5AXrYUYLk^(ASpM=nlRtugclex)YgBf{~ot03GO_L{$X{d_av zR9I5b^nhCV&0+75`~!=?$*4(@v(w!5tjCt&U=Wx!!_b#(Snk)6Ft=)u?&M&Sd*!G@ z0j_D2NV(pCjGo7l6PfurKAJRJnmH5dt?#e^sC*D(?WsCA-c7m-yhn^Peh203K>qZ40|W&O*x zwC&>o4M}5H$A4g@@{e&;#e%W3VLHD%4}#Y9L1hi)yNJeFy(|`6pxBV$)^1CNz&U?(oXFw zDq7(SCe3A_yBS^{dN+9KyxRePx&L^%b1BD z@XAf!{~FPC*fpSC*NG?9D|X-rwRFLjeK(gcPdyt3Sn;&$hhQ5t&myG%xZZL=V>MPR zdc3cD{Q^_!E7|*%&+m7PXf|}>b8L%Pe&;!LhdI?aJ$AlQe{P0t>xbj+BI;(TzqrK} zTF_y8k{46+r6CWP19x_@{l2vz<^D{)gfEB_{Xw$e+h?^#an1}SyHC+fTYgLUdn6Er zJ<%F7gGqrX1?)p^Cpx#{kvDwtBoj0HY)pt?CcO5_MVjsaF`FrQbPHlpBZiiv!P4eO za`?e24?1m*={uK^nR@)abkRTsKTQqzJR|iK4$z^_Us<~R9MSqHIOx2_YBJcY&>!|{ z?kN`XSbj<5F49=OAbHS$SUayLfiQhc&GWixU?U}e-iT*Oq{<07@Uv2M?8#Oi^k`0? ziK%YAwLE`Z@Doe55?POzkg-LcXENz^BIneBcML+8gQUFLm(Yd=wm+*b=QCW!E~T7< z%%*r6BXg>IEJSt|F(`1=lA?7+i6gh! z|GV!;ff0A#Bu*3bv^ct$<5EBCIQ=a(RZK2wb#P~ma@Oo16K03&x|LQsp2IeFr*mA?-ZqD7DYRqtvqFhOl@Tla`pHQC;+jib$Dq;Qj22R7!vlR)1X=ljQGD8$1K4@+(%ca6`9G&2=9C=N*MTa!AYBcv$*w)J)6*~eg zAm&r|xL$h)I>g&GEi@a+F4b_}8Wiq^^8AAf5$G~>s_lh}*5Lk)o?HSljE_;P-5}C{ z>hQ^Wh?7gz!ZOBxIOxXWCiZBph%3Rf_Kz`_oG)ly0y+av{(SZmbAZ#cXe+QS3k^OH zd-r`0N?HwaKR>*Ujk$twEE#{|ep(Ib^|+#CZO)SR?F$K@`|M3>X1v5D-4~RYUY8Q| zc-~7t#C`~8$*}gIf=|t#q?e9V&B2C$u;(oPfUZl75ilZ(K0Q3NcG}1gNhA6QaoPGq z#l6_bGQ+y`=FrOvXfkL!5Q+6To~Ki0sa|X9Pi?O^DH8LrYKMl1US>$=hAq>LH66Td zek^QVznZX>T>WB8$*`5qGptnkuwmFR)yZS<&LG*mv>`8f9z0a#_Nz;u{>xY1x$i;l z#dzDDy>{K391%^8;8X`IZS9wbsy3s7^tw#3YEnjIK46|KUCdr38II1jfcMSYI#xoK zyrNfSYF*wK4s$s##^)lEBow`d181#^9wkKVIUA%Y^l1nZ4t(-dDz{XuyLAcA(^d-< z?wWTxYGj9192%y=D`{PZ;<%P_UFJHTpsuoWe)YM6q0FGWd3lnXTJ(2{`B1=X1Ey?z zO(Mcp0K@qAqs%r6kbT3Nj*E46BcOrUqXS)LZ*o+5A>X1!Bi*IFz`|Z?1A(nB~<`OREkH1BBTia zt^I2N)d$Pb^Y6IGJ0^L+(Y+ut=9dR*V8tcd*~S>Y|B|EU*~WHUj+kC*0`^J3WdN(f z)E4j}zgp2K22>!cWGj9GH1g*$p#oBTR_k-ui`_jy9=uoHSGrSYx7R!Di6)#}b;#rU z3efWYgd9~Kzc+UCFVBl4NN&N$>Djq@A4ymx&(oT%CF!tXIsU%L&$|m135?@$p-67Sgmf5{8idiU$8+1FBzUB5HCx zt#I1FM;{2$dDl3$`?1-fBh7<6*qGUztIsZ zcwIN_LqWW(o42p{1HWX9>QVuP*BnfEs`@w^8Utq~sLM@26QlNj+cv@c(bM+pisKT# zHQEGEjF~8rE~S+c;7}40WC!#e%<4v`{I_zmn0aB{>rKOIe3UfZN9C)zr4Jy4z*u;2 z4++A8If(RfVcgd%@afGvwvNFX4)N0EZ2oSNtB4&JbG>~P$YqgK7K;`<@Mr6(`iHkY z&1_Io^Hh zo%6~|lJQh9Bg%Q4iTEKS)rqsV*s~dilEsU&UVUt56$L(@Np0Ua_TYIr=A|~<*!tP^ zwA#U@8Om{|Edw|R)d;n_+I!1Cle^Gec(jo7wjW#2%wDswU3}a;-?}9yxi}8n&8HBq zoG98ST5^k=DsJF2>z+y&3bM{Wk0{Cd;B0{9IV7&5BCny0*d{ucPFE@(dG_MgOL3s3 zjObk?gk0KQaiINN>v3c;Z|Nv+5zeUsA4kvoS1-ut@*TE5b$sM81f&YWrygB0);}sG zD`47JzS@lDH^d;-dhOSsw zy1{#}oG&O+dDU1r@+hio#n-A6jJebwKNC}>Az+FZJ|ar?p0)rHb0SLC9aVzs9^ak) zej$FVDgZH^=%hlrq@$|OmpzbTXge!ZPv;F=wr76A(a{Ix*L%T9*)&gn7i2$YFa0p( z31u>K{fvx@(|&deY&DxH2T*dJDN=r6Vjm8we-Gd_B*-_h^jr?0q)YZJRbGv}i5pz$ zwAtevLfIh}_O3atN|d%b;ah4v_jMxo6&)2F9X}E@#lo_#C+HALw5jK|r+N~qId78W z39k985W*sq$aWw1m&zEdB1H{f2^SjkYc+2n`{|rirc9Z1X|}V`EottB0tGY7SzsS) zJtpjnl0F#J@f&M8Q+Cd$k3WCICa`#Z|12lO`gN#1DfjrJA&#YPge%7FC-57xLlx$M z$RJBhx$wwAk=xkfvyU|KFfDf)SM11r&0)_N(XFJR>BT<4i)G&F`L`PT8}-_f1pnG5 zPxo9G4)@X0Z>Gx7u&u z|9k10#8{ZbICS7I#y2R3+ef-<^0R!rtyF=(7+l;(@S)VYC@3=Ud6M2bVoSb(A(0cEb3ja3Hpl2vm6a zZa6LlHrD5C3}beFMKvrk?SBA4ftt*AbsWVI7s&eG{f)i=ZaEvn^dE|-PGFzQI{^gM zzZ+ioFP|g_&2g*&W-7m*OWr=QuNGr3`rYieZDR4%$FN$$=B3u>^#zJ`k%OgLx@7R6 zLI@?O8N9wu*Db<4UsDSh1(?oFt(Q5sOziD6rh+X9@G16m0ZKj z9x5ZxJ`nQxthZ_q2FuL_3+xkB%|GkDG29HlVy!_M&g;hx>KJ;-CgN9r7lb(}8Rd>6 z{Ok2k@!TXJJ``|_%$eZ5pm)RppRZchpgDE2 zO?9Uk`3+m}m{+5O^ggSwzZPt(;VgQta}KV1uQezUqEJ`c=@drXz)2$zyTxL23bM8;vPWBO zE`~SlC5zFCy{d>`^C>Qp67!9H9Mv}QSH`irW>^BBAU}GcPu_UXx&%FYGCZ9)yLx^1 z8Qtja%1x1zi{}qcz-^0Pu`Q~n-D+ZtpC@*npV)~fQ(}mVFxD!=%8$3y3)j#vM!vD# z>*V59Y4MoK$C*%K5-s~Sth^g%zbs{ zKs~*2FKsM-^Z#}-A_jJ~qmY7zJpBk|X6(}1NHVaD)eBc!?O%lG8n)gWctYF2fp^-!*r z9mTE%bQ21yy17RUvMI)8VR}t5+^o!+&lrq^g_S>sJ__M@9U)DDReCS>tk^1T!6Fmh zmnIZIqxYj;HaBPDZ6Sy-`z2qJy-t#sqS>&ecV)q>f~V84GdZS{A<-NkO*lu++;j`* zRB(cVT7p!oLHkiB>?KHIP%Pe)6Qq;4ahSm<%f3XZj?V;agtl5sABnJMfAM&e%DTLG zr(^tIK?n$G5Od)_H*L~LmFCo$u9)~HDDtfKdf?6gm@Fs8E-2V~`{E%q7idrVy+4?z zY{8R~4@`B*HvOahC8MblLljk(e#^F9dN9QqvfQf$E@><^$_8P;`slC=L8 z2uKV|u3@GU&^Rj!2`O_UiJ~bR)$UdK)}9tb`%O~f2Kwb4K+K~AMSxFm^LP93(AwyK z=jdFT*}thdSkPxetB-jM3o}j%V!MLm2m75^Tyy_80W@5?J_8nBi$5WA_rT2o9bN>d zEE7N91X$`uN)3C4+Z00)wL#!tSGA|}o!jUzg@^CMXkb~nG0(+CUml~efO-9%`44cC zK0|vRIZ*d$WREN!90+;3q|gt=qC>XPUF~xNZm=43G{HE5qu$WNIXA!#-f(}D+()YbA2OhvFZ;>jT^;vjN$kF8!*@B^y4P`gziQ`lkOWa zsJj;V9{Z$l|4rx+1Re(Tk}sazw{LX+Pi}ghL;AFI?R5L_Nb;-7ze^h@Ufa33RoGN> zBY7bbuYI=^`CFm(!tiGN=%pU&QOh2r!TmJ;s7kSs3SOUTGb?4eLVQB!#+h^^j;V0RNCpiC~+Xcu`)% zq_EvM_(QqwM^!iVj+wb>(?{u#T8}KaKdw53b#33g6I(O{CMtm$(RCopiJC>CuY95Z zQ%1M@%sWmG6vOR>HQ9q4mPCUSv`(A*Ep2zNSkY(#|C=>r=-u0o50|Vr`s~vbxoN%A z)VPK^SI6I4$Zfn)o~QZy#lOS;cmmU{rNyX#8DxvxDPBt48$}Y9%6c)lS$v_$1t6q5 zPb07eqW01&nfm=xJAA9$nq|p+)K@acA~qD;he)uMAvxDhGXUj;To;ZoJgxqBt-z!r z)$KD2k{2rAitX;&?JaXo3j|+Cn#6c*e-8<6vzk4yHCVI*{wmWS+4XALpdNZP8`jss z?e<76GV=x^QU>U_k~ae8(6Z=Tx;2%3J`y`R{=;M>H$k(S{sa)K2gIfa8?9yRmILFV ze`bNxn}D`gZ*v(f?l;1s7)}r!qETVT$!M^Jk+pY0$cTi-+T3p0BHg|74kvKJfJL2ONreS zA>8!9NI@K0@I>hZ0X%CNPCy*s`l}IzXn9!pUtEY?n8nHZmBOD$gJ}|#G7S`QSYM8y zn#e^<4Df8uqg>8`Zyu??I0qUZ+Lh*S{xkOMpQ+udHK)A$h{5*4 zj!w;Tw%wf~AemXNp6pwSwY3_yXav2fc*gquY0-E73z9>gu#hk@i5s{A-RICy;3;xe zCl0 z43)|zJ`MgXX0O?-7FNIz>z7_sGgFQY#CgW!TRFbFD8ywfwPo^2To?Wku@oaysqr*# zt?5=}W+u^AL9gjJ?|@gN7MU&Lc_hU7nat8<@enzmsl!Pb+MVRNi_-ewdj45nOZg^Y zmgg6{U859}qdm?qq|K+gPvj2O%y7q)O=NR9#Z>H{SJtO2*@#qVo80T4R8N}r)0~Lx z_P&ENGZA$A`q1XfDw{)Xv=7to z#C&Z+>BRJ!rkEB+y;S{KyLFXVYBY{>?tbj}*vKDkjdO&JD@a;7Ibs3+<~zNn8Nq~+ z>*g&&(I!EptS17Gqtcws#!xZbr3(^m=;;#$HUrLSXog1Pc>1k^75HL)SH7t`7keKEB$ zjj5xOqlvXG&<@Zk6*CwMB+6Ys6*J%wwx1MmMyl*IUXA4WI4%t5uf5AQcuA+h4DKY$g(Gy!`F67PP0r8Yq8uK%B@Nvd)Ew43l4r1&c)a2m z`?)i%i!>WlbBs@C<>Yst`D@9Aj}t*-vOm*uMeTW!8SStm$jrivu&_SMo~!HR7U=}X zd_!L5cUDN6!uujJ3La@_8DNBI8P(>GDG55#(?7rY!t-+NgM>~~N1!g-`}hy*Y0NpO z+Y+4cZxHs4lpt6&Q!2zuhY`vQp9_ELD~7G=gwZEm-kIcGx+VS7bedAfUqql^&*L4( zaj4Eki4~t`LltA4asRtO`%hP9ZRIp~bBVKr(=Y@(_H(CSc=(Bc;{Im}np0i77J|}k z>|d~fR3lR15$QV`Eq5_1XG+h0tS_X}l*sZ5U$Gux4w*Bleq-3y`fCZW9AvOC^HH=g z{HVtKl6$}e2iDn?!jv#|eCLhOg4E@(QI=#rBcq4N@l@HNos(S|CEh=~mu|-vuzEe?`b7F085W3*h#4dhtYcx(5#U%j3TE>LvNugB zY~yxT)boCWXg!>R_(Q3ivpj)+rL8hF+&byDomZ}_7y8OeSzGe*O%_NFL^i$kbmcURG83tB}+YC1BrqJ<_s6Vp(=h7|CSSe zl|!}_&VEaJH0BhIvpl08vlQ5`w0oulj=y@x7o;8XIVjnflIcdY7=+H6bg#{vtBl{! zbs@0C3cN-LG1Fa!6)OOas^q~J1#31U3#DcGi?20s^s^lAV(x-;yeQ!T@yK1dUK^f; zFWP(@bagMp53?!kYGo#BjIHI+>zWH$5x zv=Eor=hOhN1=IaX{>}rg(uW>R3-$D*(*1i=)&l}Ww|$Zk)>8HygLD@Is)C1%SNu`X zs&*?OrJ6PWJeaZ$zkFCw`_SZWvd1x!1@D>c-M=Z9Bj#KTuomHwU<&ndCiA-Pu^Q!QdtMyq9vW0%yM zW`?Uaw}s4*AcYB2FI%YcpfZtIMw4_d{klGQ>IGRO6{3CCdG?Sh<2iWThI+G`K?&@z% zwaBYp0d*2DlTqr&2C$>H2Ag5-|sjdW!EzQ`;x>{I%XAU%Lp z)%Qt*ta5(a826%qW^C*#V|J+e`MA Ks}j)P-~SJqGRd$2 literal 0 HcmV?d00001 diff --git a/media/thumbnails/photo-100x40.jpg b/media/thumbnails/photo-100x40.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cdcdfc99d9efc89d3a8a23805c211d54e46f739a GIT binary patch literal 2153 zcmbW!c{J3E7Xa|@n2o{9Sf(tQZ`P625K+{GW-OH$gDh=~>_wIuAw|8+o0>+3$m7`w zDN&ZPmrybGCEEy*!Kjd>dHjCA^M2?2{`$Rl@40{7d+xcPa|OME5kSI{WI+NT5D0K~ zBLTrrz|fRTG^U&&9{@e5THgNd9$<*}Zajz!4)nU<0a{p^ftJ=pGcd#eudxdZ2tMcQ z6+{CMf(Ce$U;r=$5HL6b4nrW|2qY3AjFLd1L_|>1zibhc*ebnk+g51|MsB+*R!&}7 z0fPZ|fy!#?8ap*)v08X799|Wtf!kaHK_ZbTVU!dKC54m2$l?BW2x2#B zgoZ%T5J3Zg1po+qgSx5xPe6pA8}}lGMNpy}1Emsx5CjSpf)~~l?=&|?X*zpMu5|h$1GPAOC za`PS)KPxGH{-UhBqOQK-^_#}GP0j5con75My?y4E^zf3yCQ{f`U1;Sz$upfKd73nFxRqo8OQe1|qd(%1%hEZRH0Wm0K<9SdtKmeL!_L4o>(Y!{snRRMc6|yw=t8Qhn zUAZU-!)8F%cODcy9NW>*jUUd4JX$fj?3drm42uXZ!BKL^T90!RDU6Ys*1Nlnc5rby zEJRd3-d>aCp>9C1WGve@C_j$Mzc!+fI(o+b3bVnw>9IFj>v3kvk+};-g(S~=GiNKG zD1#pNhtmp%c`cU@bH#lw`UgAKLe*cedw_sh&HnQG4`FY@w2SUmC_WZ%eCyh^V-GgDC`Zwv``EVe>or^BrX&?!2#f_-mf;LDNt{<1Fj+DTdU zrm-|aLg7~I{>fos-UR36!Q7U-dacyN5W3Mf?N6{n)lRdjJ-#4|J>Du|&(GPzwln03|cue*9uuiMN4i z;$=gx2NrTo3v6ZZdJn(gOWxg%Qp(k2J5M)u$v}w3!)RGR(V$=;#@(c*bzr%A<@-c+ za7ob6^tcwu`DXcg!;=|h0)Bsbp5gmS=C0K!<~Y$XHsmy%^Bs$xz9G|D)IFqKMT33J zok5t9uN7WpOsU<^`WVh~cx#@>AU=I|<5JG|#%aSJ!*z4gS1up#NH~@>p?T_fZ^;eY z7w^8+8lf3Mb4k3lQ?Qky=wKf|9pWlwu~mU8)pbip`-8s^r{0*PUsW)XuQnJHCLeIo zZ`5y}=$&~Zf29`>vJR_+rVyO!=2LRRjQL6vKQ)s3oL{S!7Hf(Z(EZQwU9G}}Iu5D& zZ?Zx?OD##WMNkv%V`G2TufNg=*(MvTMKiWEm!o& zvnuwfD4;93>D8O(Fdr^49_(kXXZ(oJv-hbqe|NY^GXM7mw#_}O!3J759nttI+}xLE z(exrZbqQ{)_B+)X(LMmZzM3TfP+!Wge}y@87hAzJMS@?M z+Z%n`oEh%3pb4oB0_3WLx5f+xTAfyGSNGK@tl#bFSho}aHwmkIPk`s&wDARikwb@K zN}S7NrX0Q|LBfyT5k=9(7=sPq#?ZTsohZc?9<@ulI=v+Ui zac0hatN@U5s@_tvZf3@pZV6Ov?Q>FhPL%t^W8XIDubB++4VqfE!%8Ne>nhU9sw|6@ zNeHtsdXKw}M|x_t$~KO=ba@R7k($p?Csmy9HEcHt6ac;*0w5+mo?;>Z!Z zviA>(M!8r!n##Q`fl1SA@?2`$p55-9TC?ZrC+qT}HTsR`;uUO&dSpwNb+hJVniVHP z*~?ewd1#E{+tjsFX8H|&+NSv8&IbY@h?Gcp$fav$xn{f1xH_DPadMVaY_w$#W|*!e zcc#ui3nH#^yrDmPuYLcVp7i>d72_CFCXB?HTO)*f`6k>6&~>-gRV9Tt6-iWd%>}I+ zKQ8D#Z`)$Bn;LWUtoX39SPBve(s2de_Hmh6y=6qAr(+cNK!3gFq4@S`Nml&AT3P4f yhwZ;bq}~kFmn-R!I;vMYsLOEd<;QxaXBT=M*NpokJ~w@-u##azl)2q1==&3QOx7L% literal 0 HcmV?d00001 diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..11bb756 --- /dev/null +++ b/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Disallow: /harming/humans +Disallow: /harming/machines +Disallow: /risking/own/existence +Disallow: /edit/ diff --git a/system/extensions/bundle-7f745edc8a.min.js b/system/extensions/bundle-7f745edc8a.min.js new file mode 100644 index 0000000..624d887 --- /dev/null +++ b/system/extensions/bundle-7f745edc8a.min.js @@ -0,0 +1,55 @@ +/* edit.js */ +var yellow={onLoad:function(e){yellow.edit.load(e)},onKeydown:function(e){yellow.edit.keydown(e)},onDrag:function(e){yellow.edit.drag(e)},onDrop:function(e){yellow.edit.drop(e)},onClick:function(e){yellow.edit.click(e)},onClickAction:function(e){yellow.edit.clickAction(e)},onPageShow:function(e){yellow.edit.pageShow(e)},onUpdatePane:function(){yellow.edit.updatePane(yellow.edit.paneId,yellow.edit.paneAction,yellow.edit.paneStatus)},onResizePane:function(){yellow.edit.resizePane(yellow.edit.paneId,yellow.edit.paneAction,yellow.edit.paneStatus)},action:function(action,status,arguments){yellow.edit.processAction(action,status,arguments)}};yellow.edit={paneId:0,paneAction:0,paneStatus:0,popupId:0,intervalId:0,load:function(e){var body=document.getElementsByTagName("body")[0];if(body&&body.firstChild&&!document.getElementById("yellow-bar")){this.createBar("yellow-bar");this.processAction(yellow.page.action,yellow.page.status);clearInterval(this.intervalId)} +if(e.type=="DOMContentLoaded"){var page=document.getElementsByClassName("page")[0];if(page)this.bindActions(page)}},keydown:function(e){if(this.paneId=="yellow-pane-create"||this.paneId=="yellow-pane-edit"||this.paneId=="yellow-pane-delete")this.processShortcut(e);if(this.paneId&&e.keyCode==27)this.hidePane(this.paneId)},drag:function(e){e.stopPropagation();e.preventDefault()},drop:function(e){e.stopPropagation();e.preventDefault();var elementText=document.getElementById(this.paneId+"-text");var files=e.dataTransfer?e.dataTransfer.files:e.target.files;for(var i=0;i"+this.getRawDataPaneAction("edit")+""+"
"+this.getRawDataPaneAction("create")+this.getRawDataPaneAction("delete")+this.getRawDataPaneAction("menu",yellow.user.name,!0)+"
"+"
"}else{elementDiv.innerHTML=" "} +elementBar.appendChild(elementDiv);yellow.toolbox.insertBefore(elementBar,document.getElementsByTagName("body")[0].firstChild);this.bindActions(elementBar)},updateBar:function(paneId,name){if(paneId){var element=document.getElementById(paneId+"-bar");if(element){if(name.indexOf("selected")!=-1)element.setAttribute("aria-expanded","true");yellow.toolbox.addClass(element,name)}}else{var elements=document.getElementsByClassName(name);for(var i=0,l=elements.length;i"+""+"

"+this.getText("LoginTitle")+"

"+"
"+""+"


"+"


"+"

"+"

"+this.getText("LoginForgot")+"
"+this.getText("LoginSignup")+"

"+"
"+"";break;case "yellow-pane-signup":elementDiv.innerHTML="
"+""+"

"+this.getText("SignupTitle")+"

"+"

"+this.getText("SignupStatus","",paneStatus)+"

"+"
"+""+"


"+"


"+"


"+"

"+"

"+"
"+"
";break;case "yellow-pane-forgot":elementDiv.innerHTML="
"+""+"

"+this.getText("ForgotTitle")+"

"+"

"+this.getText("ForgotStatus","",paneStatus)+"

"+"
"+""+"


"+"

"+"
"+"
";break;case "yellow-pane-recover":elementDiv.innerHTML="
"+""+"

"+this.getText("RecoverTitle")+"

"+"

"+this.getText("RecoverStatus","",paneStatus)+"

"+"
"+"


"+"

"+"
"+"
";break;case "yellow-pane-quit":elementDiv.innerHTML="
"+""+"

"+this.getText("QuitTitle")+"

"+"

"+this.getText("QuitStatus","",paneStatus)+"

"+"
"+""+""+"


"+"

"+"
"+"
";break;case "yellow-pane-account":elementDiv.innerHTML="
"+""+"

"+this.getText("AccountTitle")+"

"+"

"+this.getText("AccountStatus","",paneStatus)+"

"+"
"+"

"+this.getRawDataSettingsActions(paneAction)+"

"+"
 
"+"
"+""+""+"


"+"


"+"


"+"

"+this.getRawDataLanguages(paneId)+"

"+"

"+this.getText("AccountInformation")+" "+this.getText("AccountMore")+"

"+"

"+"
"+"
"+"
"+"
";break;case "yellow-pane-system":elementDiv.innerHTML="
"+""+"

"+this.getText("SystemTitle")+"

"+"

"+this.getText("SystemStatus","",paneStatus)+"

"+"
"+"

"+this.getRawDataSettingsActions(paneAction)+"

"+"
 
"+"
"+""+""+"


"+"


"+"


"+"

"+this.getText("SystemInformation")+"

"+"

"+"
"+"
"+"
"+"
";break;case "yellow-pane-update":elementDiv.innerHTML="
"+""+"

"+yellow.toolbox.encodeHtml(yellow.system.coreProductRelease)+"

"+"

"+this.getText("UpdateStatus","",paneStatus)+"

"+"
"+yellow.page.rawDataOutput+"
"+""+"
";break;case "yellow-pane-create":elementDiv.innerHTML="
"+"
"+"

"+this.getText("Create")+"

"+"
    "+this.getRawDataButtons(paneId)+"
"+""+"
    "+"
    "+""+"
    "+"
    ";break;case "yellow-pane-edit":elementDiv.innerHTML="
    "+"
    "+"

    "+this.getText("Edit")+"

    "+"
      "+this.getRawDataButtons(paneId)+"
    "+""+"
      "+"
      "+""+"
      "+"
      ";break;case "yellow-pane-delete":elementDiv.innerHTML="
      "+"
      "+"

      "+this.getText("Delete")+"

      "+"
        "+this.getRawDataButtons(paneId)+"
      "+""+"
        "+"
        "+""+"
        "+"
        ";break;case "yellow-pane-menu":elementDiv.innerHTML="";break;case "yellow-pane-information":elementDiv.innerHTML="
        "+""+"

        "+this.getText(paneAction+"Title")+"

        "+"

        "+this.getText(paneAction+"Status","",paneStatus)+"

        "+""+"
        ";break;default:elementDiv.innerHTML=""+"
        Pane '"+paneId+"' was not found. Oh no...
        "} +elementPane.appendChild(elementDiv);yellow.toolbox.insertAfter(elementPane,document.getElementsByTagName("body")[0].firstChild);this.bindActions(elementPane)},updatePane:function(paneId,paneAction,paneStatus,paneInit){switch(paneId){case "yellow-pane-login":if(paneInit&&yellow.system.editLoginRestriction){yellow.toolbox.setVisible(document.getElementById("yellow-pane-login-signup"),!1)} +break;case "yellow-pane-quit":if(paneStatus=="none"){document.getElementById("yellow-pane-quit-status").innerHTML=this.getText("QuitStatusNone");document.getElementById("yellow-pane-quit-name").value=""} +break;case "yellow-pane-account":if(paneInit&&yellow.system.editSettingsActions=="none"){document.getElementById("yellow-pane-account-title").innerHTML=this.getText("MenuSettings")} +if(paneStatus=="none"){document.getElementById("yellow-pane-account-status").innerHTML=this.getText("AccountStatusNone");document.getElementById("yellow-pane-account-name").value=yellow.user.name;document.getElementById("yellow-pane-account-email").value=yellow.user.email;document.getElementById("yellow-pane-account-password").value="";document.getElementById("yellow-pane-account-"+yellow.user.language).checked=!0} +break;case "yellow-pane-system":if(paneStatus=="none"){document.getElementById("yellow-pane-system-status").innerHTML=this.getText("SystemStatusNone");document.getElementById("yellow-pane-system-sitename").value=yellow.system.sitename;document.getElementById("yellow-pane-system-author").value=yellow.system.author;document.getElementById("yellow-pane-system-email").value=yellow.system.email} +break;case "yellow-pane-update":if(paneStatus=="none"){document.getElementById("yellow-pane-update-status").innerHTML=this.getText("UpdateStatusCheck");document.getElementById("yellow-pane-update-output").innerHTML="";setTimeout("yellow.action('submit', '', 'action:update/option:check/');",500)} +if(paneStatus=="updates"){document.getElementById(paneId+"-submit").innerHTML=this.getText("UpdateButton");document.getElementById(paneId+"-submit").setAttribute("data-action","submit");document.getElementById(paneId+"-submit").setAttribute("data-arguments","action:update")} +break;case "yellow-pane-create":case "yellow-pane-edit":case "yellow-pane-delete":document.getElementById(paneId+"-text").focus();if(paneInit){yellow.toolbox.setVisible(document.getElementById(paneId+"-text"),!0);yellow.toolbox.setVisible(document.getElementById(paneId+"-preview"),!1);document.getElementById(paneId+"-toolbar-title").innerHTML=yellow.toolbox.encodeHtml(yellow.page.title);document.getElementById(paneId+"-text").value=paneId=="yellow-pane-create"?yellow.page.rawDataNew:yellow.page.rawDataEdit;var matches=document.getElementById(paneId+"-text").value.match(/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+/);var position=document.getElementById(paneId+"-text").value.indexOf("\n",matches?matches[0].length:0);document.getElementById(paneId+"-text").setSelectionRange(position,position);if(yellow.system.editToolbarButtons!="none"){yellow.toolbox.setVisible(document.getElementById(paneId+"-toolbar-title"),!1);this.updateToolbar(0,"yellow-toolbar-checked")} +if(!this.isUserAccess(paneAction,yellow.page.location)||(yellow.page.rawDataReadonly&&paneId!="yellow-pane-create")){yellow.toolbox.setVisible(document.getElementById(paneId+"-submit"),!1);document.getElementById(paneId+"-text").readOnly=!0}} +if(!document.getElementById(paneId+"-text").readOnly){paneAction=this.paneAction=this.getPaneAction(paneId);var className="yellow-toolbar-btn yellow-toolbar-btn-"+paneAction;if(document.getElementById(paneId+"-submit").className!=className){document.getElementById(paneId+"-submit").className=className;document.getElementById(paneId+"-submit").innerHTML=this.getText(paneAction+"Button");document.getElementById(paneId+"-submit").setAttribute("data-arguments","action:"+paneAction);this.resizePane(paneId,paneAction,paneStatus)}} +break} +this.bindActions(document.getElementById(paneId))},resizePane:function(paneId,paneAction,paneStatus){var elementBar=document.getElementById("yellow-bar-content");var paneLeft=yellow.toolbox.getOuterLeft(elementBar);var paneTop=yellow.toolbox.getOuterTop(elementBar)+yellow.toolbox.getOuterHeight(elementBar)+10;var paneWidth=yellow.toolbox.getOuterWidth(elementBar);var paneHeight=yellow.toolbox.getWindowHeight()-paneTop-Math.min(yellow.toolbox.getOuterHeight(elementBar)+10,(yellow.toolbox.getWindowWidth()-yellow.toolbox.getOuterWidth(elementBar))/2);switch(paneId){case "yellow-pane-account":case "yellow-pane-system":yellow.toolbox.setOuterLeft(document.getElementById(paneId),paneLeft);yellow.toolbox.setOuterTop(document.getElementById(paneId),paneTop);yellow.toolbox.setOuterWidth(document.getElementById(paneId),paneWidth);var elementWidth=yellow.toolbox.getWidth(document.getElementById(paneId));var actionsWidth=yellow.toolbox.getOuterWidth(document.getElementById(paneId+"-settings-actions"));var fieldsWidth=yellow.toolbox.getOuterWidth(document.getElementById(paneId+"-settings-fields"));var separatorWidth=Math.max(10,((elementWidth-fieldsWidth)/2)-actionsWidth);yellow.toolbox.setOuterWidth(document.getElementById(paneId+"-settings-separator"),separatorWidth);break;case "yellow-pane-create":case "yellow-pane-edit":case "yellow-pane-delete":yellow.toolbox.setOuterLeft(document.getElementById(paneId),paneLeft);yellow.toolbox.setOuterTop(document.getElementById(paneId),paneTop);yellow.toolbox.setOuterHeight(document.getElementById(paneId),paneHeight);yellow.toolbox.setOuterWidth(document.getElementById(paneId),paneWidth);var elementWidth=yellow.toolbox.getWidth(document.getElementById(paneId));yellow.toolbox.setOuterWidth(document.getElementById(paneId+"-text"),elementWidth);yellow.toolbox.setOuterWidth(document.getElementById(paneId+"-preview"),elementWidth);var buttonsWidth=0;var buttonsWidthMax=yellow.toolbox.getOuterWidth(document.getElementById(paneId+"-toolbar"))-yellow.toolbox.getOuterWidth(document.getElementById(paneId+"-toolbar-main"))-1;var element=document.getElementById(paneId+"-toolbar-buttons").firstChild;for(;element;element=element.nextSibling){element.removeAttribute("style");buttonsWidth+=yellow.toolbox.getOuterWidth(element);if(buttonsWidth>buttonsWidthMax)yellow.toolbox.setVisible(element,!1)} +yellow.toolbox.setOuterWidth(document.getElementById(paneId+"-toolbar-title"),buttonsWidthMax);var height1=yellow.toolbox.getHeight(document.getElementById(paneId));var height2=yellow.toolbox.getOuterHeight(document.getElementById(paneId+"-toolbar"));yellow.toolbox.setOuterHeight(document.getElementById(paneId+"-text"),height1-height2);yellow.toolbox.setOuterHeight(document.getElementById(paneId+"-preview"),height1-height2);var elementLink=document.getElementById(paneId+"-bar");var position=yellow.toolbox.getOuterLeft(elementLink)+yellow.toolbox.getOuterWidth(elementLink)/2;position-=yellow.toolbox.getOuterLeft(document.getElementById(paneId))+1;yellow.toolbox.setOuterLeft(document.getElementById(paneId+"-arrow"),position);break;case "yellow-pane-menu":yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-menu"),paneLeft+paneWidth-yellow.toolbox.getOuterWidth(document.getElementById("yellow-pane-menu")));yellow.toolbox.setOuterTop(document.getElementById("yellow-pane-menu"),paneTop);var elementLink=document.getElementById("yellow-pane-menu-bar");var position=yellow.toolbox.getOuterLeft(elementLink)+yellow.toolbox.getOuterWidth(elementLink)/2;position-=yellow.toolbox.getOuterLeft(document.getElementById("yellow-pane-menu"));yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-menu-arrow"),position);break;default:yellow.toolbox.setOuterLeft(document.getElementById(paneId),paneLeft);yellow.toolbox.setOuterTop(document.getElementById(paneId),paneTop);yellow.toolbox.setOuterWidth(document.getElementById(paneId),paneWidth);break}},showPane:function(paneId,paneAction,paneStatus,paneModal){if(this.paneId!=paneId||this.paneAction!=paneAction){this.hidePane(this.paneId);var paneInit=!document.getElementById(paneId);if(!document.getElementById(paneId))this.createPane(paneId,paneAction,paneStatus);var element=document.getElementById(paneId);if(!yellow.toolbox.isVisible(element)){if(yellow.system.debug)console.log("yellow.edit.showPane id:"+paneId);yellow.toolbox.setVisible(element,!0);if(paneModal){yellow.toolbox.addClass(document.body,"yellow-body-modal-open");yellow.toolbox.addValue("meta[name=viewport]","content",", maximum-scale=1, user-scalable=0")} +this.paneId=paneId;this.paneAction=paneAction;this.paneStatus=paneStatus;this.updatePane(paneId,paneAction,paneStatus,paneInit);this.resizePane(paneId,paneAction,paneStatus);this.updateBar(paneId,"yellow-bar-selected")}}else{this.hidePane(this.paneId,!0)}},hidePane:function(paneId,fadeout){var element=document.getElementById(paneId);if(yellow.toolbox.isVisible(element)){if(yellow.system.debug)console.log("yellow.edit.hidePane id:"+paneId);yellow.toolbox.removeClass(document.body,"yellow-body-modal-open");yellow.toolbox.removeValue("meta[name=viewport]","content",", maximum-scale=1, user-scalable=0");yellow.toolbox.setVisible(element,!1,fadeout);this.paneId=0;this.paneAction=0;this.paneStatus=0;this.updateBar(0,"yellow-bar-selected")} +this.hidePopup(this.popupId)},processAction:function(action,status,arguments){action=action?action:"none";status=status?status:"none";arguments=arguments?arguments:"none";if(action!="none"){if(yellow.system.debug)console.log("yellow.edit.processAction action:"+action+" status:"+status);var paneId=(status!="next"&&status!="done")?"yellow-pane-"+action:"yellow-pane-information";switch(action){case "login":this.showPane(paneId,action,status);break;case "signup":this.showPane(paneId,action,status);break;case "confirm":this.showPane(paneId,action,status);break;case "approve":this.showPane(paneId,action,status);break;case "forgot":this.showPane(paneId,action,status);break;case "recover":this.showPane(paneId,action,status);break;case "reactivate":this.showPane(paneId,action,status);break;case "verify":this.showPane(paneId,action,status);break;case "change":this.showPane(paneId,action,status);break;case "quit":this.showPane(paneId,action,status);break;case "remove":this.showPane(paneId,action,status);break;case "account":this.showPane(paneId,action,status);break;case "system":this.showPane(paneId,action,status);break;case "update":this.showPane(paneId,action,status);break;case "create":this.showPane(paneId,action,status,!0);break;case "edit":this.showPane(paneId,action,status,!0);break;case "delete":this.showPane(paneId,action,status,!0);break;case "menu":this.showPane(paneId,action,status);break;case "close":this.hidePane(this.paneId);break;case "toolbar":this.processToolbar(status,arguments);break;case "settings":this.processSettings(arguments);break;case "submit":this.processSubmit(arguments);break;case "help":this.processHelp();break}}},processToolbar:function(status,arguments){if(yellow.system.debug)console.log("yellow.edit.processToolbar status:"+status);var elementText=document.getElementById(this.paneId+"-text");var elementPreview=document.getElementById(this.paneId+"-preview");if(!yellow.toolbox.isVisible(elementPreview)&&!elementText.readOnly){switch(status){case "h1":yellow.editor.setMarkdown(elementText,"# ","insert-multiline-block",!0);break;case "h2":yellow.editor.setMarkdown(elementText,"## ","insert-multiline-block",!0);break;case "h3":yellow.editor.setMarkdown(elementText,"### ","insert-multiline-block",!0);break;case "paragraph":yellow.editor.setMarkdown(elementText,"","remove-multiline-block");yellow.editor.setMarkdown(elementText,"","remove-fenced-block");break;case "notice":yellow.editor.setMarkdown(elementText,"! ","insert-multiline-block",!0);break;case "quote":yellow.editor.setMarkdown(elementText,"> ","insert-multiline-block",!0);break;case "pre":yellow.editor.setMarkdown(elementText,"```\n","insert-fenced-block",!0);break;case "bold":yellow.editor.setMarkdown(elementText,"**","insert-inline",!0);break;case "italic":yellow.editor.setMarkdown(elementText,"*","insert-inline",!0);break;case "strikethrough":yellow.editor.setMarkdown(elementText,"~~","insert-inline",!0);break;case "code":yellow.editor.setMarkdown(elementText,"`","insert-autodetect",!0);break;case "ul":yellow.editor.setMarkdown(elementText,"* ","insert-multiline-block",!0);break;case "ol":yellow.editor.setMarkdown(elementText,"1. ","insert-multiline-block",!0);break;case "tl":yellow.editor.setMarkdown(elementText,"- [ ] ","insert-multiline-block",!0);break;case "link":yellow.editor.setMarkdown(elementText,"[link](url)","insert",!1,yellow.editor.getMarkdownLink);break;case "text":yellow.editor.setMarkdown(elementText,arguments,"insert");break;case "status":yellow.editor.setMetaData(elementText,"status",!0);break;case "file":this.showFileDialog();break;case "undo":yellow.editor.undo();break;case "redo":yellow.editor.redo();break}} +if(status=="preview"&&!elementText.readOnly)this.showPreview(elementText,elementPreview);if(status=="save"&&!elementText.readOnly&&this.paneAction!="delete")this.processSubmit("action:"+this.paneAction);if(status=="help")window.open(this.getText("YellowHelpUrl"),"_blank");if(this.isExpandable(status)){this.showPopup("yellow-popup-"+status,status)}else{this.hidePopup(this.popupId)}},updateToolbar:function(status,name){if(status){var element=document.getElementById(this.paneId+"-toolbar-"+status);if(element){if(name.indexOf("selected")!=-1)element.setAttribute("aria-expanded","true");yellow.toolbox.addClass(element,name)}}else{var elements=document.getElementsByClassName(name);for(var i=0,l=elements.length;i"+"
      • "+this.getText("ToolbarH1")+"
      • "+"
      • "+this.getText("ToolbarH2")+"
      • "+"
      • "+this.getText("ToolbarH3")+"
      • "+"
      • "+this.getText("ToolbarParagraph")+"
      • "+"
      • "+this.getText("ToolbarPre")+"
      • "+"
      • "+this.getText("ToolbarNotice")+"
      • "+"
      • "+this.getText("ToolbarQuote")+"
      • "+"";break;case "yellow-popup-heading":elementDiv.innerHTML="";break;case "yellow-popup-list":elementDiv.innerHTML="";break;case "yellow-popup-emojiawesome":var rawDataEmojis="";if(yellow.system.emojiawesomeToolbarButtons&&yellow.system.emojiawesomeToolbarButtons!="none"){var tokens=yellow.system.emojiawesomeToolbarButtons.split(" ");for(var i=0;i"}} +elementDiv.innerHTML="
          "+rawDataEmojis+"
        ";break;case "yellow-popup-fontawesome":var rawDataIcons="";if(yellow.system.fontawesomeToolbarButtons&&yellow.system.fontawesomeToolbarButtons!="none"){var tokens=yellow.system.fontawesomeToolbarButtons.split(" ");for(var i=0;i"}} +elementDiv.innerHTML="
          "+rawDataIcons+"
        ";break} +elementPopup.appendChild(elementDiv);yellow.toolbox.insertAfter(elementPopup,document.getElementsByTagName("body")[0].firstChild);this.bindActions(elementPopup)},showPopup:function(popupId,status){if(this.popupId!=popupId){this.hidePopup(this.popupId);if(!document.getElementById(popupId))this.createPopup(popupId);var element=document.getElementById(popupId);if(yellow.system.debug)console.log("yellow.edit.showPopup id:"+popupId);yellow.toolbox.setVisible(element,!0);this.popupId=popupId;this.updateToolbar(status,"yellow-toolbar-selected");var elementParent=document.getElementById(this.paneId+"-toolbar-"+status);var popupLeft=yellow.toolbox.getOuterLeft(elementParent);var popupTop=yellow.toolbox.getOuterTop(elementParent)+yellow.toolbox.getOuterHeight(elementParent)-1;yellow.toolbox.setOuterLeft(document.getElementById(popupId),popupLeft);yellow.toolbox.setOuterTop(document.getElementById(popupId),popupTop)}else{this.hidePopup(this.popupId,!0)}},hidePopup:function(popupId,fadeout){var element=document.getElementById(popupId);if(yellow.toolbox.isVisible(element)){if(yellow.system.debug)console.log("yellow.edit.hidePopup id:"+popupId);yellow.toolbox.setVisible(element,!1,fadeout);this.popupId=0;this.updateToolbar(0,"yellow-toolbar-selected")}},showPreview:function(elementText,elementPreview){if(!yellow.toolbox.isVisible(elementPreview)){var thisObject=this;var formData=new FormData();formData.append("action","preview");formData.append("csrftoken",this.getCookie("csrftoken"));formData.append("rawdataedit",elementText.value);formData.append("rawdataendofline",yellow.page.rawDataEndOfLine);var request=new XMLHttpRequest();request.open("POST",window.location.pathname,!0);request.onload=function(){if(this.status==200)thisObject.showPreviewDone.call(thisObject,elementText,elementPreview,this.responseText)};request.send(formData)}else{this.showPreviewDone(elementText,elementPreview,"")}},showPreviewDone:function(elementText,elementPreview,responseText){var showPreview=responseText.length!=0;yellow.toolbox.setVisible(elementText,!showPreview);yellow.toolbox.setVisible(elementPreview,showPreview);if(showPreview){this.updateToolbar("preview","yellow-toolbar-checked");elementPreview.innerHTML=responseText;dispatchEvent(new Event("load"))}else{this.updateToolbar(0,"yellow-toolbar-checked");elementText.focus()}},showFileDialog:function(){var element=document.createElement("input");element.setAttribute("id","yellow-file-dialog");element.setAttribute("type","file");element.setAttribute("accept",yellow.system.editUploadExtensions);element.setAttribute("multiple","multiple");yellow.toolbox.addEvent(element,"change",yellow.onDrop);element.click()},uploadFile:function(elementText,file){if(this.isUserAccess("upload",yellow.page.location)){var extension=(file.name.lastIndexOf(".")!=-1?file.name.substring(file.name.lastIndexOf("."),file.name.length):"").toLowerCase();var extensions=yellow.system.editUploadExtensions.split(/\s*,\s*/);if(file.size<=yellow.system.coreFileSizeMax&&extensions.indexOf(extension)!=-1){var text=this.getText("UploadProgress")+"\u200b";yellow.editor.setMarkdown(elementText,text,"insert");var thisObject=this;var formData=new FormData();formData.append("action","upload");formData.append("csrftoken",this.getCookie("csrftoken"));formData.append("file",file);var request=new XMLHttpRequest();request.open("POST",window.location.pathname,!0);request.onload=function(){if(this.status==200){thisObject.uploadFileDone.call(thisObject,elementText,this.responseText)}else{thisObject.uploadFileError.call(thisObject,elementText,this.responseText)}};request.send(formData)}else{var textError=extensions.indexOf(extension)!=-1?"file too big!":"file format not supported!";var textNew="[Can't upload file '"+file.name+"', "+textError+"]";yellow.editor.setMarkdown(elementText,textNew,"insert")}}else{var textNew="[Can't upload file '"+file.name+"', access is restricted!]";yellow.editor.setMarkdown(elementText,textNew,"insert")}},uploadFileDone:function(elementText,responseText){var result=JSON.parse(responseText);if(result){var textOld=this.getText("UploadProgress")+"\u200b";var textNew;if(result.location.substring(0,yellow.system.coreImageLocation.length)==yellow.system.coreImageLocation){textNew="[image "+result.location.substring(yellow.system.coreImageLocation.length)+"]"}else{textNew="[link]("+result.location+")"} +yellow.editor.replace(elementText,textOld,textNew)}},uploadFileError:function(elementText,responseText){var result=JSON.parse(responseText);if(result){var textOld=this.getText("UploadProgress")+"\u200b";var textNew="["+result.error+"]";yellow.editor.replace(elementText,textOld,textNew)}},bindActions:function(element){var elements=element.getElementsByTagName("a");for(var i=0,l=elements.length;i"+yellow.toolbox.encodeHtml(text)+""} +return rawDataAction},getRawDataSettingsActions:function(paneAction){var rawDataActions="";if(yellow.system.editSettingsActions&&yellow.system.editSettingsActions!="none"){var tokens=yellow.system.editSettingsActions.split(/\s*,\s*/);for(var i=0;i"+this.getText(token+"Title")+"
        "}} +return rawDataActions},getRawDataLanguages:function(paneId){var rawDataLanguages="";if(yellow.system.coreLanguages&&Object.keys(yellow.system.coreLanguages).length>1){for(var language in yellow.system.coreLanguages){var checked=language==this.getRequest("language")?" checked=\"checked\"":"";rawDataLanguages+="
        "}} +return rawDataLanguages},getRawDataButtons:function(paneId){var rawDataButtons="";if(yellow.system.editToolbarButtons&&yellow.system.editToolbarButtons!="none"){var tokens=yellow.system.editToolbarButtons.split(/\s*,\s*/);for(var i=0;i"}else{rawDataButtons+="
      • "}}} +return rawDataButtons},getRequest:function(key,prefix){if(!prefix)prefix="request";key=prefix+yellow.toolbox.toUpperFirst(key);return(key in yellow.page)?yellow.page[key]:""},getShortcut:function(key){var shortcut="";var tokens=yellow.system.editKeyboardShortcuts.split(/\s*,\s*/);for(var i=0;i0)top--;if(bottom==top&&bottom\s]+)?(\s+\[.\]|\s*\d+\.)?[ \t]+/);if(matches){textSelectionNew+=lines[i].substring(matches[0].length)}else{textSelectionNew+=lines[i]}} +textSelection=textSelectionNew;if(information.type.indexOf("remove")==-1){textSelectionNew="";var linePrefix=information.prefix;lines=yellow.toolbox.getTextLines(textSelection.length!=0?textSelection:"\n");for(var i=0;i=48&&e.keyCode<=90){shortcut+=(e.ctrlKey?"ctrl+":"")+(e.metaKey?"meta+":"")+(e.altKey?"alt+":"")+(e.shiftKey?"shift+":"");shortcut+=String.fromCharCode(e.keyCode).toLowerCase()} +return shortcut},getWidth:function(element){return element.offsetWidth-this.getBoxSize(element).width},getHeight:function(element){return element.offsetHeight-this.getBoxSize(element).height},setOuterWidth:function(element,width){element.style.width=Math.max(0,width-this.getBoxSize(element).width)+"px"},setOuterHeight:function(element,height){element.style.height=Math.max(0,height-this.getBoxSize(element).height)+"px"},getOuterWidth:function(element,includeMargin){var width=element.offsetWidth;if(includeMargin)width+=this.getMarginSize(element).width;return width},getOuterHeight:function(element,includeMargin){var height=element.offsetHeight;if(includeMargin)height+=this.getMarginSize(element).height;return height},setOuterLeft:function(element,left){element.style.left=Math.max(0,left)+"px"},setOuterTop:function(element,top){element.style.top=Math.max(0,top)+"px"},getOuterLeft:function(element){return element.getBoundingClientRect().left+window.pageXOffset},getOuterTop:function(element){return element.getBoundingClientRect().top+window.pageYOffset},getWindowWidth:function(){return window.innerWidth},getWindowHeight:function(){return window.innerHeight},getStyle:function(element,property){return window.getComputedStyle(element).getPropertyValue(property)},getBoxSize:function(element){var paddingLeft=parseFloat(this.getStyle(element,"padding-left"))||0;var paddingRight=parseFloat(this.getStyle(element,"padding-right"))||0;var borderLeft=parseFloat(this.getStyle(element,"border-left-width"))||0;var borderRight=parseFloat(this.getStyle(element,"border-right-width"))||0;var width=paddingLeft+paddingRight+borderLeft+borderRight;var paddingTop=parseFloat(this.getStyle(element,"padding-top"))||0;var paddingBottom=parseFloat(this.getStyle(element,"padding-bottom"))||0;var borderTop=parseFloat(this.getStyle(element,"border-top-width"))||0;var borderBottom=parseFloat(this.getStyle(element,"border-bottom-width"))||0;var height=paddingTop+paddingBottom+borderTop+borderBottom;return{"width":width,"height":height}},getMarginSize:function(element){var marginLeft=parseFloat(this.getStyle(element,"margin-left"))||0;var marginRight=parseFloat(this.getStyle(element,"margin-right"))||0;var width=marginLeft+marginRight;var marginTop=parseFloat(this.getStyle(element,"margin-top"))||0;var marginBottom=parseFloat(this.getStyle(element,"margin-bottom"))||0;var height=marginTop+marginBottom;return{"width":width,"height":height}},setVisible:function(element,show,fadeout){if(fadeout&&!show){var opacity=1;function renderFrame(){opacity-=.1;if(opacity<=0){element.style.opacity="initial";element.style.display="none"}else{element.style.opacity=opacity;requestAnimationFrame(renderFrame)}} +renderFrame()}else{element.style.display=show?"block":"none"}},isVisible:function(element){return element&&element.style.display!="none"},toLowerFirst:function(string){return string.charAt(0).toLowerCase()+string.slice(1)},toUpperFirst:function(string){return string.charAt(0).toUpperCase()+string.slice(1)},getTextLines:function(string){var lines=string.split("\n");for(var i=0;i/g,">").replace(/"/g,""")},submitForm:function(arguments){var elementForm=document.createElement("form");elementForm.setAttribute("method","post");for(var key in arguments){if(!arguments.hasOwnProperty(key))continue;var elementInput=document.createElement("input");elementInput.setAttribute("type","hidden");elementInput.setAttribute("name",key);elementInput.setAttribute("value",arguments[key]);elementForm.appendChild(elementInput)} +document.body.appendChild(elementForm);elementForm.submit()}};yellow.edit.intervalId=setInterval("yellow.onLoad(new Event('DOMContentLoading'))",1);window.addEventListener("DOMContentLoaded",yellow.onLoad,!1); \ No newline at end of file diff --git a/system/extensions/bundle-bebbf8109f.min.css b/system/extensions/bundle-bebbf8109f.min.css new file mode 100644 index 0000000..fb8d123 --- /dev/null +++ b/system/extensions/bundle-bebbf8109f.min.css @@ -0,0 +1,5 @@ +/* edit.css */ +.yellow-bar{position:relative}.yellow-bar-left{display:block;float:left}.yellow-bar-right{display:block;float:right}.yellow-bar-right a{margin-left:1em}.yellow-bar-banner{clear:both}.yellow-body-modal-open{overflow:hidden}.yellow-pane{position:absolute;display:none;z-index:100;padding:10px;background-color:#fff;color:#000;border:1px solid #bbb;border-radius:4px;box-shadow:2px 4px 10px rgba(0,0,0,.2);text-align:center}.yellow-pane h1{color:#000;font-size:2em;margin:0 1em;overflow:hidden;text-overflow:ellipsis}.yellow-pane p{margin:.5em 0}.yellow-pane .yellow-status{margin-bottom:1em}.yellow-pane .yellow-fields{width:14em;margin:0 auto;text-align:left}.yellow-pane .yellow-fields .yellow-center{width:14em;display:inline-block;text-align:center}.yellow-pane .yellow-fields .yellow-form-control{width:15em;box-sizing:border-box}.yellow-pane .yellow-fields .yellow-btn{width:15em;margin:1em 0 .5em 0}.yellow-pane .yellow-buttons .yellow-btn{width:15em;margin:.5em 0}.yellow-close{position:absolute;top:.8em;right:1em;cursor:pointer;font-size:.9em;color:#bbb;text-decoration:none}.yellow-close:hover{color:#000;text-decoration:none}.yellow-arrow{position:absolute;top:0;left:0}.yellow-arrow:after,.yellow-arrow:before{position:absolute;pointer-events:none;bottom:100%;height:0;width:0;border:solid transparent;content:""}.yellow-arrow:after{border-color:rgba(255,255,255,0);border-bottom-color:#fff;border-width:10px;margin-left:-10px}.yellow-arrow:before{border-color:rgba(187,187,187,0);border-bottom-color:#bbb;border-width:11px;margin-left:-11px}.yellow-settings{text-align:left}.yellow-settings-left{float:left;padding:0 .5em}.yellow-settings-right{float:left}.yellow-settings-separator{visibility:hidden;padding:20px}.yellow-settings-banner{clear:both}.yellow-popup{position:absolute;display:none;z-index:200;padding:10px 0;background-color:#fff;color:#000;border:1px solid #bbb;border-radius:4px;box-shadow:2px 4px 10px rgba(0,0,0,.2)}.yellow-dropdown{list-style:none;margin:0;padding:0}.yellow-dropdown span{display:block;margin:0;padding:.25em 1em}.yellow-dropdown a{display:block;padding:.2em 1em;text-decoration:none}.yellow-dropdown a:hover{color:#fff;background-color:#18e;text-decoration:none}.yellow-dropdown-menu a{color:#000}.yellow-toolbar{list-style:none;margin:0;padding:0}.yellow-toolbar-left{display:inline-block;float:left}.yellow-toolbar-right{display:inline-block;float:right}.yellow-toolbar-banner{clear:both}.yellow-toolbar h1{margin:-5px 0 0 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.yellow-toolbar li{display:inline-block;vertical-align:top}.yellow-toolbar a{display:inline-block;padding:6px 16px;text-decoration:none;background-color:#fff;color:#000;font-size:.9em;font-weight:400;border:1px solid #bbb;border-radius:4px}.yellow-toolbar a:hover{background-color:#18e;background-image:none;border-color:#18e;color:#fff;text-decoration:none}.yellow-toolbar-left a{margin-right:4px;margin-bottom:10px}.yellow-toolbar-right a{margin-left:4px;margin-bottom:10px}.yellow-toolbar .yellow-icon{font-size:.9em;min-width:1em;text-align:center}.yellow-toolbar .yellow-toolbar-btn{padding:6px 10px;min-width:4em;text-align:center}.yellow-toolbar .yellow-toolbar-btn-edit{background-color:#29f;border-color:#29f;color:#fff}.yellow-toolbar .yellow-toolbar-btn-create{background-color:#29f;border-color:#29f;color:#fff}.yellow-toolbar .yellow-toolbar-btn-delete{background-color:#e55;border-color:#e55;color:#fff}.yellow-toolbar .yellow-toolbar-btn-delete:hover{background-color:#d44;border-color:#d44}.yellow-toolbar .yellow-toolbar-btn-separator{visibility:hidden;padding:6px}.yellow-toolbar .yellow-toolbar-checked{background-color:#666;border-color:#666;color:#fff}.yellow-toolbar-tooltip{position:relative}.yellow-toolbar-tooltip::after,.yellow-toolbar-tooltip::before{position:absolute;z-index:300;display:none;pointer-events:none}.yellow-toolbar-tooltip::after{padding:2px 9px;font-weight:400;font-size:.9em;text-align:center;white-space:nowrap;content:attr(aria-label);background-color:#111;color:#ddd;border-radius:3px;top:100%;right:50%;margin-top:6px;transform:translateX(50%)}.yellow-toolbar-tooltip::before{width:0;height:0;content:"";border:4px solid transparent;top:auto;right:50%;bottom:-6px;margin-right:-4px;border-bottom-color:#111}.yellow-toolbar-tooltip:hover::before,.yellow-toolbar-tooltip:hover::after{display:inline-block}.yellow-toolbar-selected.yellow-toolbar-tooltip::before,.yellow-toolbar-selected.yellow-toolbar-tooltip::after{display:none}.yellow-edit-text{margin:0;padding:0 2px;outline:none;resize:none;border:none;font-size:.9em;font-family:inherit;font-weight:400;line-height:normal}.yellow-edit-preview{padding:0;overflow:auto}.yellow-edit-preview h1{margin:.67em 0}.yellow-edit-preview p{margin:1em 0}.yellow-edit-preview .content{margin:0;padding:0}.yellow-form-control{margin:0;padding:2px 4px;display:inline-block;background-color:#fff;color:#000;background-image:linear-gradient(to bottom,#fff,#fff);border:1px solid #bbb;border-radius:4px;font-size:.9em;font-family:inherit;font-weight:400;line-height:normal}.yellow-btn{margin:0;padding:4px 22px;display:inline-block;min-width:8em;background-color:#eaeaea;color:#333;background-image:linear-gradient(to bottom,#f8f8f8,#e1e1e1);border:1px solid #bbb;border-color:#c1c1c1 #c1c1c1 #aaa;border-radius:4px;outline-offset:-2px;font-size:.9em;font-family:inherit;font-weight:400;line-height:1;text-align:center;text-decoration:none;box-sizing:border-box}.yellow-btn:hover,.yellow-btn:focus,.yellow-btn:active{color:#333;background-image:none;text-decoration:none}.yellow-btn:active{box-shadow:inset 0 2px 4px rgba(0,0,0,.1)}#yellow-pane-create-bar{padding:0 .5em}#yellow-pane-delete-bar{padding:0 .5em}#yellow-pane-create,#yellow-pane-edit,#yellow-pane-delete{text-align:left}#yellow-pane-menu{padding:10px 0;text-align:left}#yellow-popup-format,#yellow-popup-heading,#yellow-popup-list{width:16em}#yellow-popup-format a,#yellow-popup-heading a{padding:.25em 16px}#yellow-popup-format #yellow-popup-format-h1,#yellow-popup-heading #yellow-popup-heading-h1{font-size:2em;font-weight:700}#yellow-popup-format #yellow-popup-format-h2,#yellow-popup-heading #yellow-popup-heading-h2{font-size:1.6em;font-weight:700}#yellow-popup-format #yellow-popup-format-h3,#yellow-popup-heading #yellow-popup-heading-h3{font-size:1.3em;font-weight:700}#yellow-popup-format #yellow-popup-format-notice{font-weight:700}#yellow-popup-format #yellow-popup-format-quote{font-style:italic}#yellow-popup-format #yellow-popup-format-pre{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace;font-size:.9em;line-height:1.8}#yellow-popup-emojiawesome{padding:10px;width:14em}#yellow-popup-emojiawesome a{padding:.2em}#yellow-popup-emojiawesome .yellow-dropdown li{display:inline-block}#yellow-popup-fontawesome{padding:10px;width:13em}#yellow-popup-fontawesome a{padding:.18em .3em;min-width:1em;text-align:center}#yellow-popup-fontawesome .yellow-dropdown li{display:inline-block}@font-face{font-family:"Edit";font-weight:400;font-style:normal;src:url(/sandbox/www/yellow/media/extensions/edit.woff) format("woff")}.yellow-icon{display:inline-block;font-family:Edit;font-style:normal;font-weight:400;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.yellow-spin{-webkit-animation:yellow-spin 1s infinite steps(16);animation:yellow-spin 1s infinite steps(16)}@-webkit-keyframes yellow-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes yellow-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.yellow-icon-preview:before{content:"\f100"}.yellow-icon-format:before{content:"\f101"}.yellow-icon-paragraph:before{content:"\f101"}.yellow-icon-heading:before{content:"\f102"}.yellow-icon-h1:before{content:"\f103"}.yellow-icon-h2:before{content:"\f104"}.yellow-icon-h3:before{content:"\f105"}.yellow-icon-bold:before{content:"\f106"}.yellow-icon-italic:before{content:"\f0f7"}.yellow-icon-strikethrough:before{content:"\f108"}.yellow-icon-quote:before{content:"\f109"}.yellow-icon-code:before{content:"\f10a"}.yellow-icon-pre:before{content:"\f10a"}.yellow-icon-link:before{content:"\f10b"}.yellow-icon-file:before{content:"\f10c"}.yellow-icon-list:before{content:"\f10d"}.yellow-icon-ul:before{content:"\f10d"}.yellow-icon-ol:before{content:"\f10e"}.yellow-icon-tl:before{content:"\f10f"}.yellow-icon-hr:before{content:"\f110"}.yellow-icon-table:before{content:"\f111"}.yellow-icon-emojiawesome:before{content:"\f112"}.yellow-icon-fontawesome:before{content:"\f113"}.yellow-icon-status:before{content:"\f114"}.yellow-icon-undo:before{content:"\f115"}.yellow-icon-redo:before{content:"\f116"}.yellow-icon-spinner:before{content:"\f200"}.yellow-icon-search:before{content:"\f201"}.yellow-icon-close:before{content:"\f202"}.yellow-icon-help:before{content:"\f203"}.yellow-icon-markdown:before{content:"\f203"}.yellow-icon-logo:before{content:"\f8ff"} + +/* stockholm.css */ +html,body,div,form,pre,span,tr,th,td,img{margin:0;padding:0;border:0;vertical-align:baseline}@font-face{font-family:"Open Sans";font-style:normal;font-weight:300;src:url(/sandbox/www/yellow/media/themes/stockholm-opensans-light.woff) format("woff")}@font-face{font-family:"Open Sans";font-style:normal;font-weight:400;src:url(/sandbox/www/yellow/media/themes/stockholm-opensans-regular.woff) format("woff")}@font-face{font-family:"Open Sans";font-style:normal;font-weight:700;src:url(/sandbox/www/yellow/media/themes/stockholm-opensans-bold.woff) format("woff")}body{margin:1em;background-color:#fff;color:#666;font-family:"Open Sans",Helvetica,sans-serif;font-size:1em;font-weight:300;line-height:1.5}h1,h2,h3,h4,h5,h6{color:#111;font-weight:400}h1{font-size:2em}hr{height:1px;background:#ddd;border:0}strong{font-weight:700}code{font-size:1.1em}a{color:#07d;text-decoration:none}a:hover{color:#07d;text-decoration:underline}.content h1{margin:1em 0}.content h1 a{color:#111}.content h1 a:hover{color:#111;text-decoration:none}.content img{max-width:100%;height:auto}.content form{margin:1em 0}.content table{border-spacing:0;border-collapse:collapse}.content th{text-align:left;padding:.3em}.content td{text-align:left;padding:.3em;border-top:1px solid #ddd;border-bottom:1px solid #ddd}.content code,.content pre{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace;font-size:90%}.content code{padding:.15em .4em;margin:0;background-color:#f7f7f7;border-radius:3px}.content pre>code{padding:0;margin:0;white-space:pre;background:transparent;border:0;font-size:inherit}.content pre{padding:1em;overflow:auto;line-height:1.45;background-color:#f7f7f7;border-radius:3px}.content blockquote{margin-left:0;padding-left:1em;border-left:1px solid #ddd}.content .notice1{margin:1em 0;padding:10px 1em;background-color:#fffbf0;border-left:10px solid #fb0}.content .notice2{margin:1em 0;padding:10px 1em;background-color:#fdf0f0;border-left:10px solid #d00}.content .notice3,.content .notice4,.content .notice5,.content .notice6{margin:1em 0;padding:10px 1em;background-color:#f0f8fe;border-left:10px solid #08e}.content .flexible{position:relative;padding-top:0;padding-bottom:56.25%}.content .flexible iframe{position:absolute;top:0;left:0;width:100%;height:100%}.content .task-list-item{list-style-type:none}.content .task-list-item input{margin:0 .2em .25em -1.75em;vertical-align:middle}.content .toc{margin:0;padding:0;list-style:none}.content .wikipages ul,.content .wikitags ul,.content .wikilinks ul{padding:0;list-style:none;column-width:19em}.content .entry-links .previous{margin-right:1em}.content .pagination .previous{margin-right:1em}.content .pagination{margin:1em 0}.content .left{float:left;margin:0 1em 0 0}.content .center{display:block;margin:0 auto}.content .right{float:right;margin:0 0 0 1em}.content .rounded{border-radius:4px}.header{margin:2em 0}.header .sitename{display:block;float:left}.header .sitename h1{margin:0;font-size:1em;font-weight:300}.header .sitename h1 a{color:#666;border-bottom:solid 3px #fff;text-decoration:none;padding:.5em 0}.header .sitename h1 a:hover{color:#07d;border-bottom:solid 3px #29f}.header .sitename p{margin-top:0;color:#666}.navigation{display:block;float:right}.navigation a{color:#666;border-bottom:solid 3px #fff;text-decoration:none;padding:.5em 0;margin:0 .5em}.navigation a:hover{color:#07d;border-bottom:solid 3px #29f}.navigation ul{margin:0 -.5em;padding:0;list-style:none}.navigation li{display:inline}.navigation li a.active{border-bottom:solid 3px #29f}.navigation-banner{clear:both}.footer{margin:2em 0}.footer .siteinfo a{color:#07d}.footer .siteinfo a:hover{color:#07d;text-decoration:underline}.form-control{margin:0;padding:2px 4px;display:inline-block;min-width:7em;background-color:#fff;color:#666;background-image:linear-gradient(to bottom,#fff,#fff);border:1px solid #bbb;border-radius:4px;font-size:.9em;font-family:inherit;font-weight:400;line-height:normal}.btn{margin:0;padding:4px 22px;display:inline-block;min-width:7em;background-color:#eaeaea;color:#333;background-image:linear-gradient(to bottom,#f8f8f8,#e1e1e1);border:1px solid #bbb;border-color:#c1c1c1 #c1c1c1 #aaa;border-radius:4px;outline-offset:-2px;font-size:.9em;font-family:inherit;font-weight:400;line-height:1;text-align:center;text-decoration:none;box-sizing:border-box}.btn:hover,.btn:focus,.btn:active{color:#333;background-image:none;text-decoration:none}.btn:active{box-shadow:inset 0 2px 4px rgba(0,0,0,.1)}.page{margin:0 auto;max-width:1000px}@media screen and (min-width:62em){body{width:60em;margin:1em auto}.page{margin:0;max-width:none}}@media screen and (max-width:32em){body{margin:.5em;font-size:.9em}.content h1,.content h2{font-size:1.5em}}@media print{.page{border:none!important}} \ No newline at end of file diff --git a/system/extensions/bundle-f6126cc0e2.min.css b/system/extensions/bundle-f6126cc0e2.min.css new file mode 100644 index 0000000..c6b07e0 --- /dev/null +++ b/system/extensions/bundle-f6126cc0e2.min.css @@ -0,0 +1,2 @@ +/* stockholm.css */ +html,body,div,form,pre,span,tr,th,td,img{margin:0;padding:0;border:0;vertical-align:baseline}@font-face{font-family:"Open Sans";font-style:normal;font-weight:300;src:url(/sandbox/www/yellow/media/themes/stockholm-opensans-light.woff) format("woff")}@font-face{font-family:"Open Sans";font-style:normal;font-weight:400;src:url(/sandbox/www/yellow/media/themes/stockholm-opensans-regular.woff) format("woff")}@font-face{font-family:"Open Sans";font-style:normal;font-weight:700;src:url(/sandbox/www/yellow/media/themes/stockholm-opensans-bold.woff) format("woff")}body{margin:1em;background-color:#fff;color:#666;font-family:"Open Sans",Helvetica,sans-serif;font-size:1em;font-weight:300;line-height:1.5}h1,h2,h3,h4,h5,h6{color:#111;font-weight:400}h1{font-size:2em}hr{height:1px;background:#ddd;border:0}strong{font-weight:700}code{font-size:1.1em}a{color:#07d;text-decoration:none}a:hover{color:#07d;text-decoration:underline}.content h1{margin:1em 0}.content h1 a{color:#111}.content h1 a:hover{color:#111;text-decoration:none}.content img{max-width:100%;height:auto}.content form{margin:1em 0}.content table{border-spacing:0;border-collapse:collapse}.content th{text-align:left;padding:.3em}.content td{text-align:left;padding:.3em;border-top:1px solid #ddd;border-bottom:1px solid #ddd}.content code,.content pre{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace;font-size:90%}.content code{padding:.15em .4em;margin:0;background-color:#f7f7f7;border-radius:3px}.content pre>code{padding:0;margin:0;white-space:pre;background:transparent;border:0;font-size:inherit}.content pre{padding:1em;overflow:auto;line-height:1.45;background-color:#f7f7f7;border-radius:3px}.content blockquote{margin-left:0;padding-left:1em;border-left:1px solid #ddd}.content .notice1{margin:1em 0;padding:10px 1em;background-color:#fffbf0;border-left:10px solid #fb0}.content .notice2{margin:1em 0;padding:10px 1em;background-color:#fdf0f0;border-left:10px solid #d00}.content .notice3,.content .notice4,.content .notice5,.content .notice6{margin:1em 0;padding:10px 1em;background-color:#f0f8fe;border-left:10px solid #08e}.content .flexible{position:relative;padding-top:0;padding-bottom:56.25%}.content .flexible iframe{position:absolute;top:0;left:0;width:100%;height:100%}.content .task-list-item{list-style-type:none}.content .task-list-item input{margin:0 .2em .25em -1.75em;vertical-align:middle}.content .toc{margin:0;padding:0;list-style:none}.content .wikipages ul,.content .wikitags ul,.content .wikilinks ul{padding:0;list-style:none;column-width:19em}.content .entry-links .previous{margin-right:1em}.content .pagination .previous{margin-right:1em}.content .pagination{margin:1em 0}.content .left{float:left;margin:0 1em 0 0}.content .center{display:block;margin:0 auto}.content .right{float:right;margin:0 0 0 1em}.content .rounded{border-radius:4px}.header{margin:2em 0}.header .sitename{display:block;float:left}.header .sitename h1{margin:0;font-size:1em;font-weight:300}.header .sitename h1 a{color:#666;border-bottom:solid 3px #fff;text-decoration:none;padding:.5em 0}.header .sitename h1 a:hover{color:#07d;border-bottom:solid 3px #29f}.header .sitename p{margin-top:0;color:#666}.navigation{display:block;float:right}.navigation a{color:#666;border-bottom:solid 3px #fff;text-decoration:none;padding:.5em 0;margin:0 .5em}.navigation a:hover{color:#07d;border-bottom:solid 3px #29f}.navigation ul{margin:0 -.5em;padding:0;list-style:none}.navigation li{display:inline}.navigation li a.active{border-bottom:solid 3px #29f}.navigation-banner{clear:both}.footer{margin:2em 0}.footer .siteinfo a{color:#07d}.footer .siteinfo a:hover{color:#07d;text-decoration:underline}.form-control{margin:0;padding:2px 4px;display:inline-block;min-width:7em;background-color:#fff;color:#666;background-image:linear-gradient(to bottom,#fff,#fff);border:1px solid #bbb;border-radius:4px;font-size:.9em;font-family:inherit;font-weight:400;line-height:normal}.btn{margin:0;padding:4px 22px;display:inline-block;min-width:7em;background-color:#eaeaea;color:#333;background-image:linear-gradient(to bottom,#f8f8f8,#e1e1e1);border:1px solid #bbb;border-color:#c1c1c1 #c1c1c1 #aaa;border-radius:4px;outline-offset:-2px;font-size:.9em;font-family:inherit;font-weight:400;line-height:1;text-align:center;text-decoration:none;box-sizing:border-box}.btn:hover,.btn:focus,.btn:active{color:#333;background-image:none;text-decoration:none}.btn:active{box-shadow:inset 0 2px 4px rgba(0,0,0,.1)}.page{margin:0 auto;max-width:1000px}@media screen and (min-width:62em){body{width:60em;margin:1em auto}.page{margin:0;max-width:none}}@media screen and (max-width:32em){body{margin:.5em;font-size:.9em}.content h1,.content h2{font-size:1.5em}}@media print{.page{border:none!important}} \ No newline at end of file diff --git a/system/extensions/bundle.php b/system/extensions/bundle.php new file mode 100644 index 0000000..523bd67 --- /dev/null +++ b/system/extensions/bundle.php @@ -0,0 +1,1964 @@ +yellow = $yellow; + } + + // Handle page output data + public function onParsePageOutput($page, $text) { + $output = null; + if ($text && preg_match("/^(.*[\r\n]+)(.*)(<\/head>.*)$/s", $text, $matches)) { + $output = $matches[1].$this->normaliseHead($matches[2]).$matches[3]; + } + return $output; + } + + // Handle command + public function onCommand($command, $text) { + switch ($command) { + case "clean": $statusCode = $this->processCommandClean($command, $text); break; + default: $statusCode = 0; + } + return $statusCode; + } + + // Process command to clean bundles + public function processCommandClean($command, $text) { + $statusCode = 0; + if ($command=="clean" && $text=="all") { + $path = $this->yellow->system->get("coreExtensionDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/bundle-.*/", false, false) as $entry) { + if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; + } + if ($statusCode==500) echo "ERROR cleaning bundles: Can't delete files in directory '$path'!\n"; + } + return $statusCode; + } + + // Normalise page head + public function normaliseHead($text) { + $dataMeta = $dataLink = $dataCss = $dataScriptDefer = $dataScriptNow = $dataOther = array(); + foreach ($this->yellow->toolbox->getTextLines($text) as $line) { + if (preg_match("/^$/i", $line) || preg_match("/^(.*?)<\/title>$/i", $line)) { + array_push($dataMeta, $line); + } elseif (preg_match("/^<link (.*?)href=\"([^\"]+)\"(.*?)>$/i", $line, $matches)) { + if (preg_match("/\"stylesheet\"/i", $line)) { + if (!isset($dataCss[$matches[2]])) $dataCss[$matches[2]] = $line; + } else { + array_push($dataLink, $line); + } + } elseif (preg_match("/^<script (.*?)src=\"([^\"]+)\"(.*?)><\/script>$/i", $line, $matches)) { + if (preg_match("/\"defer\"/i", $line)) { + if (!isset($dataScriptDefer[$matches[2]])) $dataScriptDefer[$matches[2]] = $line; + } else { + if (!isset($dataScriptNow[$matches[2]])) $dataScriptNow[$matches[2]] = $line; + } + } else { + array_push($dataOther, $line); + } + } + if (!defined("DEBUG") || DEBUG==0) { + $dataCss = $this->processBundle($dataCss, "css"); + $dataScriptDefer = $this->processBundle($dataScriptDefer, "js", "defer"); + $dataScriptNow = $this->processBundle($dataScriptNow, "js"); + } + $output = implode($dataMeta).implode($dataLink).implode($dataCss). + implode($dataScriptDefer).implode($dataScriptNow).implode($dataOther); + return $output; + } + + // Process bundle, create file on demand + public function processBundle($data, $type, $attribute = "") { + $fileNames = array(); + $modified = 0; + $scheme = $this->yellow->system->get("coreServerScheme"); + $address = $this->yellow->system->get("coreServerAddress"); + $base = $this->yellow->system->get("coreServerBase"); + foreach ($data as $key=>$value) { + if (preg_match("/^\w+:/", $key)) continue; + if (preg_match("/data-bundle=\"exclude\"/i", $value)) continue; + if (substru($key, 0, strlenu($base))!=$base) continue; + $location = substru($key, strlenu($base)); + $fileName = $this->yellow->lookup->findFileFromSystem($location); + $modified = max($modified, $this->yellow->toolbox->getFileModified($fileName)); + if (is_readable($fileName)) { + array_push($fileNames, $fileName); + unset($data[$key]); + } + } + if (!empty($fileNames)) { + $autoVersioning = intval($modified/(60*60*24)); + $id = substru(md5($autoVersioning.$base.implode($fileNames)), 0, 10); + $fileNameBundle = $this->yellow->system->get("coreExtensionDirectory")."bundle-$id.min.$type"; + $locationBundle = $base.$this->yellow->system->get("coreExtensionLocation")."bundle-$id.min.$type"; + $rawDataAttribute = $attribute=="defer" ? "defer=\"defer\" " : ""; + if ($type=="css") { + $data[$locationBundle] = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"".htmlspecialchars($locationBundle)."\" />\n"; + } else { + $data[$locationBundle] = "<script type=\"text/javascript\" ${rawDataAttribute}src=\"".htmlspecialchars($locationBundle)."\"></script>\n"; + } + if ($this->yellow->toolbox->getFileModified($fileNameBundle)!=$modified) { + $fileDataBundle = ""; + foreach ($fileNames as $fileName) { + $fileData = $this->yellow->toolbox->readFile($fileName); + $fileData = $this->processBundleConvert($scheme, $address, $base, $fileData, $fileName, $type); + $fileData = $this->processBundleMinify($scheme, $address, $base, $fileData, $fileName, $type); + if (substrb($fileData, 0, 3)=="\xEF\xBB\xBF") $fileData = substrb($fileData, 3); + if (substrb($fileData, 0, 13)=="\"use strict\";" || substrb($fileData, 0, 13)=="'use strict';") $fileData = substrb($fileData, 13); + if (!empty($fileDataBundle)) $fileDataBundle .= "\n\n"; + $fileDataBundle .= "/* ".basename($fileName)." */\n"; + $fileDataBundle .= $fileData; + } + if (is_file($fileNameBundle)) $this->yellow->toolbox->deleteFile($fileNameBundle); + if (!$this->yellow->toolbox->createFile($fileNameBundle, $fileDataBundle) || + !$this->yellow->toolbox->modifyFile($fileNameBundle, $modified)) { + $this->yellow->page->error(500, "Can't write file '$fileNameBundle'!"); + } + } + } + return $data; + } + + // Process bundle, convert URLs + public function processBundleConvert($scheme, $address, $base, $fileData, $fileName, $type) { + if ($type=="css") { + $themeDirectoryLength = strlenu($this->yellow->system->get("coreThemeDirectory")); + if (substru($fileName, 0, $themeDirectoryLength) == $this->yellow->system->get("coreThemeDirectory")) { + $base .= $this->yellow->system->get("coreThemeLocation"); + } else { + $base .= $this->yellow->system->get("coreExtensionLocation"); + } + $thisCompatible = $this; + $callback = function ($matches) use ($thisCompatible, $scheme, $address, $base) { + $url = $thisCompatible->yellow->lookup->normaliseUrl($scheme, $address, $base, $matches[1], false); + $url = str_replace("$scheme://$address", "", $url); + return "url(\"$url\")"; + }; + $fileData = preg_replace_callback("/url\([\'\"]?(.*?)[\'\"]?\)/", $callback, $fileData); + } + return $fileData; + } + + // Process bundle, minify data + public function processBundleMinify($scheme, $address, $base, $fileData, $fileName, $type) { + $minifier = $type=="css" ? new MinifyCss() : new MinifyJavaScript(); + if (preg_match("/\.min/", $fileName)) $minifier = new MinifyBasic(); + $minifier->add($fileData); + return $minifier->minify(); + } +} + +/** + * Abstract minifier class. + * + * Please report bugs on https://github.com/matthiasmullie/minify/issues + * + * @package Minify + * @author Matthias Mullie <minify@mullie.eu> + * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved + * @license MIT License + */ +abstract class Minify +{ + /** + * The data to be minified. + * + * @var string[] + */ + protected $data = array(); + + /** + * Array of patterns to match. + * + * @var string[] + */ + protected $patterns = array(); + + /** + * This array will hold content of strings and regular expressions that have + * been extracted from the JS source code, so we can reliably match "code", + * without having to worry about potential "code-like" characters inside. + * + * @var string[] + */ + public $extracted = array(); + + /** + * Init the minify class - optionally, code may be passed along already. + */ + public function __construct(/* $data = null, ... */) + { + // it's possible to add the source through the constructor as well ;) + if (func_num_args()) { + call_user_func_array(array($this, 'add'), func_get_args()); + } + } + + /** + * Add a file or straight-up code to be minified. + * + * @param string|string[] $data + * + * @return static + */ + public function add($data /* $data = null, ... */) + { + // bogus "usage" of parameter $data: scrutinizer warns this variable is + // not used (we're using func_get_args instead to support overloading), + // but it still needs to be defined because it makes no sense to have + // this function without argument :) + $args = array($data) + func_get_args(); + + // this method can be overloaded + foreach ($args as $data) { + if (is_array($data)) { + call_user_func_array(array($this, 'add'), $data); + continue; + } + + // redefine var + $data = (string) $data; + + // load data + $value = $this->load($data); + $key = ($data != $value) ? $data : count($this->data); + + // replace CR linefeeds etc. + // @see https://github.com/matthiasmullie/minify/pull/139 + $value = str_replace(array("\r\n", "\r"), "\n", $value); + + // store data + $this->data[$key] = $value; + } + + return $this; + } + + /** + * Minify the data & (optionally) saves it to a file. + * + * @param string[optional] $path Path to write the data to + * + * @return string The minified data + */ + public function minify($path = null) + { + $content = $this->execute($path); + + // save to path + if ($path !== null) { + $this->save($content, $path); + } + + return $content; + } + + /** + * Minify & gzip the data & (optionally) saves it to a file. + * + * @param string[optional] $path Path to write the data to + * @param int[optional] $level Compression level, from 0 to 9 + * + * @return string The minified & gzipped data + */ + public function gzip($path = null, $level = 9) + { + $content = $this->execute($path); + $content = gzencode($content, $level, FORCE_GZIP); + + // save to path + if ($path !== null) { + $this->save($content, $path); + } + + return $content; + } + + /** + * Minify the data & write it to a CacheItemInterface object. + * + * @param CacheItemInterface $item Cache item to write the data to + * + * @return CacheItemInterface Cache item with the minifier data + */ + public function cache(CacheItemInterface $item) + { + $content = $this->execute(); + $item->set($content); + + return $item; + } + + /** + * Minify the data. + * + * @param string[optional] $path Path to write the data to + * + * @return string The minified data + */ + abstract public function execute($path = null); + + /** + * Load data. + * + * @param string $data Either a path to a file or the content itself + * + * @return string + */ + protected function load($data) + { + // check if the data is a file + if ($this->canImportFile($data)) { + $data = file_get_contents($data); + + // strip BOM, if any + if (substr($data, 0, 3) == "\xef\xbb\xbf") { + $data = substr($data, 3); + } + } + + return $data; + } + + /** + * Save to file. + * + * @param string $content The minified data + * @param string $path The path to save the minified data to + * + * @throws IOException + */ + protected function save($content, $path) + { + $handler = $this->openFileForWriting($path); + + $this->writeToFile($handler, $content); + + @fclose($handler); + } + + /** + * Register a pattern to execute against the source content. + * + * @param string $pattern PCRE pattern + * @param string|callable $replacement Replacement value for matched pattern + */ + protected function registerPattern($pattern, $replacement = '') + { + // study the pattern, we'll execute it more than once + $pattern .= 'S'; + + $this->patterns[] = array($pattern, $replacement); + } + + /** + * We can't "just" run some regular expressions against JavaScript: it's a + * complex language. E.g. having an occurrence of // xyz would be a comment, + * unless it's used within a string. Of you could have something that looks + * like a 'string', but inside a comment. + * The only way to accurately replace these pieces is to traverse the JS one + * character at a time and try to find whatever starts first. + * + * @param string $content The content to replace patterns in + * + * @return string The (manipulated) content + */ + protected function replace($content) + { + $processed = ''; + $positions = array_fill(0, count($this->patterns), -1); + $matches = array(); + + while ($content) { + // find first match for all patterns + foreach ($this->patterns as $i => $pattern) { + list($pattern, $replacement) = $pattern; + + // we can safely ignore patterns for positions we've unset earlier, + // because we know these won't show up anymore + if (array_key_exists($i, $positions) == false) { + continue; + } + + // no need to re-run matches that are still in the part of the + // content that hasn't been processed + if ($positions[$i] >= 0) { + continue; + } + + $match = null; + if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE)) { + $matches[$i] = $match; + + // we'll store the match position as well; that way, we + // don't have to redo all preg_matches after changing only + // the first (we'll still know where those others are) + $positions[$i] = $match[0][1]; + } else { + // if the pattern couldn't be matched, there's no point in + // executing it again in later runs on this same content; + // ignore this one until we reach end of content + unset($matches[$i], $positions[$i]); + } + } + + // no more matches to find: everything's been processed, break out + if (!$matches) { + $processed .= $content; + break; + } + + // see which of the patterns actually found the first thing (we'll + // only want to execute that one, since we're unsure if what the + // other found was not inside what the first found) + $discardLength = min($positions); + $firstPattern = array_search($discardLength, $positions); + $match = $matches[$firstPattern][0][0]; + + // execute the pattern that matches earliest in the content string + list($pattern, $replacement) = $this->patterns[$firstPattern]; + $replacement = $this->replacePattern($pattern, $replacement, $content); + + // figure out which part of the string was unmatched; that's the + // part we'll execute the patterns on again next + $content = (string) substr($content, $discardLength); + $unmatched = (string) substr($content, strpos($content, $match) + strlen($match)); + + // move the replaced part to $processed and prepare $content to + // again match batch of patterns against + $processed .= substr($replacement, 0, strlen($replacement) - strlen($unmatched)); + $content = $unmatched; + + // first match has been replaced & that content is to be left alone, + // the next matches will start after this replacement, so we should + // fix their offsets + foreach ($positions as $i => $position) { + $positions[$i] -= $discardLength + strlen($match); + } + } + + return $processed; + } + + /** + * This is where a pattern is matched against $content and the matches + * are replaced by their respective value. + * This function will be called plenty of times, where $content will always + * move up 1 character. + * + * @param string $pattern Pattern to match + * @param string|callable $replacement Replacement value + * @param string $content Content to match pattern against + * + * @return string + */ + protected function replacePattern($pattern, $replacement, $content) + { + if (is_callable($replacement)) { + return preg_replace_callback($pattern, $replacement, $content, 1, $count); + } else { + return preg_replace($pattern, $replacement, $content, 1, $count); + } + } + + /** + * Strings are a pattern we need to match, in order to ignore potential + * code-like content inside them, but we just want all of the string + * content to remain untouched. + * + * This method will replace all string content with simple STRING# + * placeholder text, so we've rid all strings from characters that may be + * misinterpreted. Original string content will be saved in $this->extracted + * and after doing all other minifying, we can restore the original content + * via restoreStrings(). + * + * @param string[optional] $chars + * @param string[optional] $placeholderPrefix + */ + protected function extractStrings($chars = '\'"', $placeholderPrefix = '') + { + // PHP only supports $this inside anonymous functions since 5.4 + $minifier = $this; + $callback = function ($match) use ($minifier, $placeholderPrefix) { + // check the second index here, because the first always contains a quote + if ($match[2] === '') { + /* + * Empty strings need no placeholder; they can't be confused for + * anything else anyway. + * But we still needed to match them, for the extraction routine + * to skip over this particular string. + */ + return $match[0]; + } + + $count = count($minifier->extracted); + $placeholder = $match[1].$placeholderPrefix.$count.$match[1]; + $minifier->extracted[$placeholder] = $match[1].$match[2].$match[1]; + + return $placeholder; + }; + + /* + * The \\ messiness explained: + * * Don't count ' or " as end-of-string if it's escaped (has backslash + * in front of it) + * * Unless... that backslash itself is escaped (another leading slash), + * in which case it's no longer escaping the ' or " + * * So there can be either no backslash, or an even number + * * multiply all of that times 4, to account for the escaping that has + * to be done to pass the backslash into the PHP string without it being + * considered as escape-char (times 2) and to get it in the regex, + * escaped (times 2) + */ + $this->registerPattern('/(['.$chars.'])(.*?(?<!\\\\)(\\\\\\\\)*+)\\1/s', $callback); + } + + /** + * This method will restore all extracted data (strings, regexes) that were + * replaced with placeholder text in extract*(). The original content was + * saved in $this->extracted. + * + * @param string $content + * + * @return string + */ + protected function restoreExtractedData($content) + { + if (!$this->extracted) { + // nothing was extracted, nothing to restore + return $content; + } + + $content = strtr($content, $this->extracted); + + $this->extracted = array(); + + return $content; + } + + /** + * Check if the path is a regular file and can be read. + * + * @param string $path + * + * @return bool + */ + protected function canImportFile($path) + { + $parsed = parse_url($path); + if ( + // file is elsewhere + isset($parsed['host']) || + // file responds to queries (may change, or need to bypass cache) + isset($parsed['query']) + ) { + return false; + } + + return strlen($path) < PHP_MAXPATHLEN && @is_file($path) && is_readable($path); + } + + /** + * Attempts to open file specified by $path for writing. + * + * @param string $path The path to the file + * + * @return resource Specifier for the target file + * + * @throws IOException + */ + protected function openFileForWriting($path) + { + if (($handler = @fopen($path, 'w')) === false) { + throw new IOException('The file "'.$path.'" could not be opened for writing. Check if PHP has enough permissions.'); + } + + return $handler; + } + + /** + * Attempts to write $content to the file specified by $handler. $path is used for printing exceptions. + * + * @param resource $handler The resource to write to + * @param string $content The content to write + * @param string $path The path to the file (for exception printing only) + * + * @throws IOException + */ + protected function writeToFile($handler, $content, $path = '') + { + if (($result = @fwrite($handler, $content)) === false || ($result < strlen($content))) { + throw new IOException('The file "'.$path.'" could not be written to. Check your disk space and file permissions.'); + } + } +} + +class CSS extends Minify +{ + /** + * @var int maximum inport size in kB + */ + protected $maxImportSize = 5; + + /** + * @var string[] valid import extensions + */ + protected $importExtensions = array( + 'gif' => 'data:image/gif', + 'png' => 'data:image/png', + 'jpe' => 'data:image/jpeg', + 'jpg' => 'data:image/jpeg', + 'jpeg' => 'data:image/jpeg', + 'svg' => 'data:image/svg+xml', + 'woff' => 'data:application/x-font-woff', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'xbm' => 'image/x-xbitmap', + ); + + /** + * Set the maximum size if files to be imported. + * + * Files larger than this size (in kB) will not be imported into the CSS. + * Importing files into the CSS as data-uri will save you some connections, + * but we should only import relatively small decorative images so that our + * CSS file doesn't get too bulky. + * + * @param int $size Size in kB + */ + public function setMaxImportSize($size) + { + $this->maxImportSize = $size; + } + + /** + * Set the type of extensions to be imported into the CSS (to save network + * connections). + * Keys of the array should be the file extensions & respective values + * should be the data type. + * + * @param string[] $extensions Array of file extensions + */ + public function setImportExtensions(array $extensions) + { + $this->importExtensions = $extensions; + } + + /** + * Move any import statements to the top. + * + * @param string $content Nearly finished CSS content + * + * @return string + */ + protected function moveImportsToTop($content) + { + if (preg_match_all('/(;?)(@import (?<url>url\()?(?P<quotes>["\']?).+?(?P=quotes)(?(url)\)));?/', $content, $matches)) { + // remove from content + foreach ($matches[0] as $import) { + $content = str_replace($import, '', $content); + } + + // add to top + $content = implode(';', $matches[2]).';'.trim($content, ';'); + } + + return $content; + } + + /** + * Combine CSS from import statements. + * + * @import's will be loaded and their content merged into the original file, + * to save HTTP requests. + * + * @param string $source The file to combine imports for + * @param string $content The CSS content to combine imports for + * @param string[] $parents Parent paths, for circular reference checks + * + * @return string + * + * @throws FileImportException + */ + protected function combineImports($source, $content, $parents) + { + $importRegexes = array( + // @import url(xxx) + '/ + # import statement + @import + + # whitespace + \s+ + + # open url() + url\( + + # (optional) open path enclosure + (?P<quotes>["\']?) + + # fetch path + (?P<path>.+?) + + # (optional) close path enclosure + (?P=quotes) + + # close url() + \) + + # (optional) trailing whitespace + \s* + + # (optional) media statement(s) + (?P<media>[^;]*) + + # (optional) trailing whitespace + \s* + + # (optional) closing semi-colon + ;? + + /ix', + + // @import 'xxx' + '/ + + # import statement + @import + + # whitespace + \s+ + + # open path enclosure + (?P<quotes>["\']) + + # fetch path + (?P<path>.+?) + + # close path enclosure + (?P=quotes) + + # (optional) trailing whitespace + \s* + + # (optional) media statement(s) + (?P<media>[^;]*) + + # (optional) trailing whitespace + \s* + + # (optional) closing semi-colon + ;? + + /ix', + ); + + // find all relative imports in css + $matches = array(); + foreach ($importRegexes as $importRegex) { + if (preg_match_all($importRegex, $content, $regexMatches, PREG_SET_ORDER)) { + $matches = array_merge($matches, $regexMatches); + } + } + + $search = array(); + $replace = array(); + + // loop the matches + foreach ($matches as $match) { + // get the path for the file that will be imported + $importPath = dirname($source).'/'.$match['path']; + + // only replace the import with the content if we can grab the + // content of the file + if (!$this->canImportByPath($match['path']) || !$this->canImportFile($importPath)) { + continue; + } + + // check if current file was not imported previously in the same + // import chain. + if (in_array($importPath, $parents)) { + throw new FileImportException('Failed to import file "'.$importPath.'": circular reference detected.'); + } + + // grab referenced file & minify it (which may include importing + // yet other @import statements recursively) + $minifier = new static($importPath); + $minifier->setMaxImportSize($this->maxImportSize); + $minifier->setImportExtensions($this->importExtensions); + $importContent = $minifier->execute($source, $parents); + + // check if this is only valid for certain media + if (!empty($match['media'])) { + $importContent = '@media '.$match['media'].'{'.$importContent.'}'; + } + + // add to replacement array + $search[] = $match[0]; + $replace[] = $importContent; + } + + // replace the import statements + return str_replace($search, $replace, $content); + } + + /** + * Import files into the CSS, base64-ized. + * + * @url(image.jpg) images will be loaded and their content merged into the + * original file, to save HTTP requests. + * + * @param string $source The file to import files for + * @param string $content The CSS content to import files for + * + * @return string + */ + protected function importFiles($source, $content) + { + $regex = '/url\((["\']?)(.+?)\\1\)/i'; + if ($this->importExtensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { + $search = array(); + $replace = array(); + + // loop the matches + foreach ($matches as $match) { + $extension = substr(strrchr($match[2], '.'), 1); + if ($extension && !array_key_exists($extension, $this->importExtensions)) { + continue; + } + + // get the path for the file that will be imported + $path = $match[2]; + $path = dirname($source).'/'.$path; + + // only replace the import with the content if we're able to get + // the content of the file, and it's relatively small + if ($this->canImportFile($path) && $this->canImportBySize($path)) { + // grab content && base64-ize + $importContent = $this->load($path); + $importContent = base64_encode($importContent); + + // build replacement + $search[] = $match[0]; + $replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')'; + } + } + + // replace the import statements + $content = str_replace($search, $replace, $content); + } + + return $content; + } + + /** + * Minify the data. + * Perform CSS optimizations. + * + * @param string[optional] $path Path to write the data to + * @param string[] $parents Parent paths, for circular reference checks + * + * @return string The minified data + */ + public function execute($path = null, $parents = array()) + { + $content = ''; + + // loop CSS data (raw data and files) + foreach ($this->data as $source => $css) { + /* + * Let's first take out strings & comments, since we can't just + * remove whitespace anywhere. If whitespace occurs inside a string, + * we should leave it alone. E.g.: + * p { content: "a test" } + */ + $this->extractStrings(); + $this->stripComments(); + $this->extractCalcs(); + $css = $this->replace($css); + + $css = $this->stripWhitespace($css); + $css = $this->shortenColors($css); + $css = $this->shortenZeroes($css); + $css = $this->shortenFontWeights($css); + $css = $this->stripEmptyTags($css); + + // restore the string we've extracted earlier + $css = $this->restoreExtractedData($css); + + $source = is_int($source) ? '' : $source; + $parents = $source ? array_merge($parents, array($source)) : $parents; + $css = $this->combineImports($source, $css, $parents); + $css = $this->importFiles($source, $css); + + /* + * If we'll save to a new path, we'll have to fix the relative paths + * to be relative no longer to the source file, but to the new path. + * If we don't write to a file, fall back to same path so no + * conversion happens (because we still want it to go through most + * of the move code, which also addresses url() & @import syntax...) + */ + $converter = $this->getPathConverter($source, $path ?: $source); + $css = $this->move($converter, $css); + + // combine css + $content .= $css; + } + + $content = $this->moveImportsToTop($content); + + return $content; + } + + /** + * Moving a css file should update all relative urls. + * Relative references (e.g. ../images/image.gif) in a certain css file, + * will have to be updated when a file is being saved at another location + * (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper). + * + * @param ConverterInterface $converter Relative path converter + * @param string $content The CSS content to update relative urls for + * + * @return string + */ + protected function move(ConverterInterface $converter, $content) + { + /* + * Relative path references will usually be enclosed by url(). @import + * is an exception, where url() is not necessary around the path (but is + * allowed). + * This *could* be 1 regular expression, where both regular expressions + * in this array are on different sides of a |. But we're using named + * patterns in both regexes, the same name on both regexes. This is only + * possible with a (?J) modifier, but that only works after a fairly + * recent PCRE version. That's why I'm doing 2 separate regular + * expressions & combining the matches after executing of both. + */ + $relativeRegexes = array( + // url(xxx) + '/ + # open url() + url\( + + \s* + + # open path enclosure + (?P<quotes>["\'])? + + # fetch path + (?P<path>.+?) + + # close path enclosure + (?(quotes)(?P=quotes)) + + \s* + + # close url() + \) + + /ix', + + // @import "xxx" + '/ + # import statement + @import + + # whitespace + \s+ + + # we don\'t have to check for @import url(), because the + # condition above will already catch these + + # open path enclosure + (?P<quotes>["\']) + + # fetch path + (?P<path>.+?) + + # close path enclosure + (?P=quotes) + + /ix', + ); + + // find all relative urls in css + $matches = array(); + foreach ($relativeRegexes as $relativeRegex) { + if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) { + $matches = array_merge($matches, $regexMatches); + } + } + + $search = array(); + $replace = array(); + + // loop all urls + foreach ($matches as $match) { + // determine if it's a url() or an @import match + $type = (strpos($match[0], '@import') === 0 ? 'import' : 'url'); + + $url = $match['path']; + if ($this->canImportByPath($url)) { + // attempting to interpret GET-params makes no sense, so let's discard them for awhile + $params = strrchr($url, '?'); + $url = $params ? substr($url, 0, -strlen($params)) : $url; + + // fix relative url + $url = $converter->convert($url); + + // now that the path has been converted, re-apply GET-params + $url .= $params; + } + + /* + * Urls with control characters above 0x7e should be quoted. + * According to Mozilla's parser, whitespace is only allowed at the + * end of unquoted urls. + * Urls with `)` (as could happen with data: uris) should also be + * quoted to avoid being confused for the url() closing parentheses. + * And urls with a # have also been reported to cause issues. + * Urls with quotes inside should also remain escaped. + * + * @see https://developer.mozilla.org/nl/docs/Web/CSS/url#The_url()_functional_notation + * @see https://hg.mozilla.org/mozilla-central/rev/14abca4e7378 + * @see https://github.com/matthiasmullie/minify/issues/193 + */ + $url = trim($url); + if (preg_match('/[\s\)\'"#\x{7f}-\x{9f}]/u', $url)) { + $url = $match['quotes'] . $url . $match['quotes']; + } + + // build replacement + $search[] = $match[0]; + if ($type === 'url') { + $replace[] = 'url('.$url.')'; + } elseif ($type === 'import') { + $replace[] = '@import "'.$url.'"'; + } + } + + // replace urls + return str_replace($search, $replace, $content); + } + + /** + * Shorthand hex color codes. + * #FF0000 -> #F00. + * + * @param string $content The CSS content to shorten the hex color codes for + * + * @return string + */ + protected function shortenColors($content) + { + $content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?:([0-9a-z])\\4)?(?=[; }])/i', '#$1$2$3$4', $content); + + // remove alpha channel if it's pointless... + $content = preg_replace('/(?<=[: ])#([0-9a-z]{6})ff?(?=[; }])/i', '#$1', $content); + $content = preg_replace('/(?<=[: ])#([0-9a-z]{3})f?(?=[; }])/i', '#$1', $content); + + $colors = array( + // we can shorten some even more by replacing them with their color name + '#F0FFFF' => 'azure', + '#F5F5DC' => 'beige', + '#A52A2A' => 'brown', + '#FF7F50' => 'coral', + '#FFD700' => 'gold', + '#808080' => 'gray', + '#008000' => 'green', + '#4B0082' => 'indigo', + '#FFFFF0' => 'ivory', + '#F0E68C' => 'khaki', + '#FAF0E6' => 'linen', + '#800000' => 'maroon', + '#000080' => 'navy', + '#808000' => 'olive', + '#CD853F' => 'peru', + '#FFC0CB' => 'pink', + '#DDA0DD' => 'plum', + '#800080' => 'purple', + '#F00' => 'red', + '#FA8072' => 'salmon', + '#A0522D' => 'sienna', + '#C0C0C0' => 'silver', + '#FFFAFA' => 'snow', + '#D2B48C' => 'tan', + '#FF6347' => 'tomato', + '#EE82EE' => 'violet', + '#F5DEB3' => 'wheat', + // or the other way around + 'WHITE' => '#fff', + 'BLACK' => '#000', + ); + + return preg_replace_callback( + '/(?<=[: ])('.implode('|', array_keys($colors)).')(?=[; }])/i', + function ($match) use ($colors) { + return $colors[strtoupper($match[0])]; + }, + $content + ); + } + + /** + * Shorten CSS font weights. + * + * @param string $content The CSS content to shorten the font weights for + * + * @return string + */ + protected function shortenFontWeights($content) + { + $weights = array( + 'normal' => 400, + 'bold' => 700, + ); + + $callback = function ($match) use ($weights) { + return $match[1].$weights[$match[2]]; + }; + + return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content); + } + + /** + * Shorthand 0 values to plain 0, instead of e.g. -0em. + * + * @param string $content The CSS content to shorten the zero values for + * + * @return string + */ + protected function shortenZeroes($content) + { + // we don't want to strip units in `calc()` expressions: + // `5px - 0px` is valid, but `5px - 0` is not + // `10px * 0` is valid (equates to 0), and so is `10 * 0px`, but + // `10 * 0` is invalid + // we've extracted calcs earlier, so we don't need to worry about this + + // reusable bits of code throughout these regexes: + // before & after are used to make sure we don't match lose unintended + // 0-like values (e.g. in #000, or in http://url/1.0) + // units can be stripped from 0 values, or used to recognize non 0 + // values (where wa may be able to strip a .0 suffix) + $before = '(?<=[:(, ])'; + $after = '(?=[ ,);}])'; + $units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)'; + + // strip units after zeroes (0px -> 0) + // NOTE: it should be safe to remove all units for a 0 value, but in + // practice, Webkit (especially Safari) seems to stumble over at least + // 0%, potentially other units as well. Only stripping 'px' for now. + // @see https://github.com/matthiasmullie/minify/issues/60 + $content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content); + + // strip 0-digits (.0 -> 0) + $content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content); + // strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px + $content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content); + // strip trailing 0: 50.00 -> 50, 50.00px -> 50px + $content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content); + // strip leading 0: 0.1 -> .1, 01.1 -> 1.1 + $content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content); + + // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0) + $content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content); + + // IE doesn't seem to understand a unitless flex-basis value (correct - + // it goes against the spec), so let's add it in again (make it `%`, + // which is only 1 char: 0%, 0px, 0 anything, it's all just the same) + // @see https://developer.mozilla.org/nl/docs/Web/CSS/flex + $content = preg_replace('/flex:([0-9]+\s[0-9]+\s)0([;\}])/', 'flex:${1}0%${2}', $content); + $content = preg_replace('/flex-basis:0([;\}])/', 'flex-basis:0%${1}', $content); + + return $content; + } + + /** + * Strip empty tags from source code. + * + * @param string $content + * + * @return string + */ + protected function stripEmptyTags($content) + { + $content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content); + $content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content); + + return $content; + } + + /** + * Strip comments from source code. + */ + protected function stripComments() + { + // PHP only supports $this inside anonymous functions since 5.4 + $minifier = $this; + $callback = function ($match) use ($minifier) { + $count = count($minifier->extracted); + $placeholder = '/*'.$count.'*/'; + $minifier->extracted[$placeholder] = $match[0]; + + return $placeholder; + }; + $this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback); + + $this->registerPattern('/\/\*.*?\*\//s', ''); + } + + /** + * Strip whitespace. + * + * @param string $content The CSS content to strip the whitespace for + * + * @return string + */ + protected function stripWhitespace($content) + { + // remove leading & trailing whitespace + $content = preg_replace('/^\s*/m', '', $content); + $content = preg_replace('/\s*$/m', '', $content); + + // replace newlines with a single space + $content = preg_replace('/\s+/', ' ', $content); + + // remove whitespace around meta characters + // inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex + $content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content); + $content = preg_replace('/([\[(:>\+])\s+/', '$1', $content); + $content = preg_replace('/\s+([\]\)>\+])/', '$1', $content); + $content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content); + + // whitespace around + and - can only be stripped inside some pseudo- + // classes, like `:nth-child(3+2n)` + // not in things like `calc(3px + 2px)`, shorthands like `3px -2px`, or + // selectors like `div.weird- p` + $pseudos = array('nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type'); + $content = preg_replace('/:('.implode('|', $pseudos).')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content); + + // remove semicolon/whitespace followed by closing bracket + $content = str_replace(';}', '}', $content); + + return trim($content); + } + + /** + * Replace all `calc()` occurrences. + */ + protected function extractCalcs() + { + // PHP only supports $this inside anonymous functions since 5.4 + $minifier = $this; + $callback = function ($match) use ($minifier) { + $length = strlen($match[1]); + $expr = ''; + $opened = 0; + + for ($i = 0; $i < $length; $i++) { + $char = $match[1][$i]; + $expr .= $char; + if ($char === '(') { + $opened++; + } elseif ($char === ')' && --$opened === 0) { + break; + } + } + $rest = str_replace($expr, '', $match[1]); + $expr = trim(substr($expr, 1, -1)); + + $count = count($minifier->extracted); + $placeholder = 'calc('.$count.')'; + $minifier->extracted[$placeholder] = 'calc('.$expr.')'; + + return $placeholder.$rest; + }; + + $this->registerPattern('/calc(\(.+?)(?=$|;|}|calc\()/', $callback); + $this->registerPattern('/calc(\(.+?)(?=$|;|}|calc\()/m', $callback); + } + + /** + * Check if file is small enough to be imported. + * + * @param string $path The path to the file + * + * @return bool + */ + protected function canImportBySize($path) + { + return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024; + } + + /** + * Check if file a file can be imported, going by the path. + * + * @param string $path + * + * @return bool + */ + protected function canImportByPath($path) + { + return preg_match('/^(data:|https?:|\\/)/', $path) === 0; + } + + /** + * Return a converter to update relative paths to be relative to the new + * destination. + * + * @param string $source + * @param string $target + * + * @return ConverterInterface + */ + protected function getPathConverter($source, $target) + { + return new Converter($source, $target); + } +} + +class JS extends Minify +{ + /** + * Var-matching regex based on http://stackoverflow.com/a/9337047/802993. + * + * Note that regular expressions using that bit must have the PCRE_UTF8 + * pattern modifier (/u) set. + * + * @var string + */ + const REGEX_VARIABLE = '\b[$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\x{02c1}\x{02c6}-\x{02d1}\x{02e0}-\x{02e4}\x{02ec}\x{02ee}\x{0370}-\x{0374}\x{0376}\x{0377}\x{037a}-\x{037d}\x{0386}\x{0388}-\x{038a}\x{038c}\x{038e}-\x{03a1}\x{03a3}-\x{03f5}\x{03f7}-\x{0481}\x{048a}-\x{0527}\x{0531}-\x{0556}\x{0559}\x{0561}-\x{0587}\x{05d0}-\x{05ea}\x{05f0}-\x{05f2}\x{0620}-\x{064a}\x{066e}\x{066f}\x{0671}-\x{06d3}\x{06d5}\x{06e5}\x{06e6}\x{06ee}\x{06ef}\x{06fa}-\x{06fc}\x{06ff}\x{0710}\x{0712}-\x{072f}\x{074d}-\x{07a5}\x{07b1}\x{07ca}-\x{07ea}\x{07f4}\x{07f5}\x{07fa}\x{0800}-\x{0815}\x{081a}\x{0824}\x{0828}\x{0840}-\x{0858}\x{08a0}\x{08a2}-\x{08ac}\x{0904}-\x{0939}\x{093d}\x{0950}\x{0958}-\x{0961}\x{0971}-\x{0977}\x{0979}-\x{097f}\x{0985}-\x{098c}\x{098f}\x{0990}\x{0993}-\x{09a8}\x{09aa}-\x{09b0}\x{09b2}\x{09b6}-\x{09b9}\x{09bd}\x{09ce}\x{09dc}\x{09dd}\x{09df}-\x{09e1}\x{09f0}\x{09f1}\x{0a05}-\x{0a0a}\x{0a0f}\x{0a10}\x{0a13}-\x{0a28}\x{0a2a}-\x{0a30}\x{0a32}\x{0a33}\x{0a35}\x{0a36}\x{0a38}\x{0a39}\x{0a59}-\x{0a5c}\x{0a5e}\x{0a72}-\x{0a74}\x{0a85}-\x{0a8d}\x{0a8f}-\x{0a91}\x{0a93}-\x{0aa8}\x{0aaa}-\x{0ab0}\x{0ab2}\x{0ab3}\x{0ab5}-\x{0ab9}\x{0abd}\x{0ad0}\x{0ae0}\x{0ae1}\x{0b05}-\x{0b0c}\x{0b0f}\x{0b10}\x{0b13}-\x{0b28}\x{0b2a}-\x{0b30}\x{0b32}\x{0b33}\x{0b35}-\x{0b39}\x{0b3d}\x{0b5c}\x{0b5d}\x{0b5f}-\x{0b61}\x{0b71}\x{0b83}\x{0b85}-\x{0b8a}\x{0b8e}-\x{0b90}\x{0b92}-\x{0b95}\x{0b99}\x{0b9a}\x{0b9c}\x{0b9e}\x{0b9f}\x{0ba3}\x{0ba4}\x{0ba8}-\x{0baa}\x{0bae}-\x{0bb9}\x{0bd0}\x{0c05}-\x{0c0c}\x{0c0e}-\x{0c10}\x{0c12}-\x{0c28}\x{0c2a}-\x{0c33}\x{0c35}-\x{0c39}\x{0c3d}\x{0c58}\x{0c59}\x{0c60}\x{0c61}\x{0c85}-\x{0c8c}\x{0c8e}-\x{0c90}\x{0c92}-\x{0ca8}\x{0caa}-\x{0cb3}\x{0cb5}-\x{0cb9}\x{0cbd}\x{0cde}\x{0ce0}\x{0ce1}\x{0cf1}\x{0cf2}\x{0d05}-\x{0d0c}\x{0d0e}-\x{0d10}\x{0d12}-\x{0d3a}\x{0d3d}\x{0d4e}\x{0d60}\x{0d61}\x{0d7a}-\x{0d7f}\x{0d85}-\x{0d96}\x{0d9a}-\x{0db1}\x{0db3}-\x{0dbb}\x{0dbd}\x{0dc0}-\x{0dc6}\x{0e01}-\x{0e30}\x{0e32}\x{0e33}\x{0e40}-\x{0e46}\x{0e81}\x{0e82}\x{0e84}\x{0e87}\x{0e88}\x{0e8a}\x{0e8d}\x{0e94}-\x{0e97}\x{0e99}-\x{0e9f}\x{0ea1}-\x{0ea3}\x{0ea5}\x{0ea7}\x{0eaa}\x{0eab}\x{0ead}-\x{0eb0}\x{0eb2}\x{0eb3}\x{0ebd}\x{0ec0}-\x{0ec4}\x{0ec6}\x{0edc}-\x{0edf}\x{0f00}\x{0f40}-\x{0f47}\x{0f49}-\x{0f6c}\x{0f88}-\x{0f8c}\x{1000}-\x{102a}\x{103f}\x{1050}-\x{1055}\x{105a}-\x{105d}\x{1061}\x{1065}\x{1066}\x{106e}-\x{1070}\x{1075}-\x{1081}\x{108e}\x{10a0}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{10fa}\x{10fc}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1380}-\x{138f}\x{13a0}-\x{13f4}\x{1401}-\x{166c}\x{166f}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16ea}\x{16ee}-\x{16f0}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17d7}\x{17dc}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191c}\x{1950}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19c1}-\x{19c7}\x{1a00}-\x{1a16}\x{1a20}-\x{1a54}\x{1aa7}\x{1b05}-\x{1b33}\x{1b45}-\x{1b4b}\x{1b83}-\x{1ba0}\x{1bae}\x{1baf}\x{1bba}-\x{1be5}\x{1c00}-\x{1c23}\x{1c4d}-\x{1c4f}\x{1c5a}-\x{1c7d}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf1}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{2160}-\x{2188}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{2e2f}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{31a0}-\x{31ba}\x{31f0}-\x{31ff}\x{3400}-\x{4db5}\x{4e00}-\x{9fcc}\x{a000}-\x{a48c}\x{a4d0}-\x{a4fd}\x{a500}-\x{a60c}\x{a610}-\x{a61f}\x{a62a}\x{a62b}\x{a640}-\x{a66e}\x{a67f}-\x{a697}\x{a6a0}-\x{a6ef}\x{a717}-\x{a71f}\x{a722}-\x{a788}\x{a78b}-\x{a78e}\x{a790}-\x{a793}\x{a7a0}-\x{a7aa}\x{a7f8}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a822}\x{a840}-\x{a873}\x{a882}-\x{a8b3}\x{a8f2}-\x{a8f7}\x{a8fb}\x{a90a}-\x{a925}\x{a930}-\x{a946}\x{a960}-\x{a97c}\x{a984}-\x{a9b2}\x{a9cf}\x{aa00}-\x{aa28}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa60}-\x{aa76}\x{aa7a}\x{aa80}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aadd}\x{aae0}-\x{aaea}\x{aaf2}-\x{aaf4}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{abc0}-\x{abe2}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{f900}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb36}\x{fb38}-\x{fb3c}\x{fb3e}\x{fb40}\x{fb41}\x{fb43}\x{fb44}\x{fb46}-\x{fbb1}\x{fbd3}-\x{fd3d}\x{fd50}-\x{fd8f}\x{fd92}-\x{fdc7}\x{fdf0}-\x{fdfb}\x{fe70}-\x{fe74}\x{fe76}-\x{fefc}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}][$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\x{02c1}\x{02c6}-\x{02d1}\x{02e0}-\x{02e4}\x{02ec}\x{02ee}\x{0370}-\x{0374}\x{0376}\x{0377}\x{037a}-\x{037d}\x{0386}\x{0388}-\x{038a}\x{038c}\x{038e}-\x{03a1}\x{03a3}-\x{03f5}\x{03f7}-\x{0481}\x{048a}-\x{0527}\x{0531}-\x{0556}\x{0559}\x{0561}-\x{0587}\x{05d0}-\x{05ea}\x{05f0}-\x{05f2}\x{0620}-\x{064a}\x{066e}\x{066f}\x{0671}-\x{06d3}\x{06d5}\x{06e5}\x{06e6}\x{06ee}\x{06ef}\x{06fa}-\x{06fc}\x{06ff}\x{0710}\x{0712}-\x{072f}\x{074d}-\x{07a5}\x{07b1}\x{07ca}-\x{07ea}\x{07f4}\x{07f5}\x{07fa}\x{0800}-\x{0815}\x{081a}\x{0824}\x{0828}\x{0840}-\x{0858}\x{08a0}\x{08a2}-\x{08ac}\x{0904}-\x{0939}\x{093d}\x{0950}\x{0958}-\x{0961}\x{0971}-\x{0977}\x{0979}-\x{097f}\x{0985}-\x{098c}\x{098f}\x{0990}\x{0993}-\x{09a8}\x{09aa}-\x{09b0}\x{09b2}\x{09b6}-\x{09b9}\x{09bd}\x{09ce}\x{09dc}\x{09dd}\x{09df}-\x{09e1}\x{09f0}\x{09f1}\x{0a05}-\x{0a0a}\x{0a0f}\x{0a10}\x{0a13}-\x{0a28}\x{0a2a}-\x{0a30}\x{0a32}\x{0a33}\x{0a35}\x{0a36}\x{0a38}\x{0a39}\x{0a59}-\x{0a5c}\x{0a5e}\x{0a72}-\x{0a74}\x{0a85}-\x{0a8d}\x{0a8f}-\x{0a91}\x{0a93}-\x{0aa8}\x{0aaa}-\x{0ab0}\x{0ab2}\x{0ab3}\x{0ab5}-\x{0ab9}\x{0abd}\x{0ad0}\x{0ae0}\x{0ae1}\x{0b05}-\x{0b0c}\x{0b0f}\x{0b10}\x{0b13}-\x{0b28}\x{0b2a}-\x{0b30}\x{0b32}\x{0b33}\x{0b35}-\x{0b39}\x{0b3d}\x{0b5c}\x{0b5d}\x{0b5f}-\x{0b61}\x{0b71}\x{0b83}\x{0b85}-\x{0b8a}\x{0b8e}-\x{0b90}\x{0b92}-\x{0b95}\x{0b99}\x{0b9a}\x{0b9c}\x{0b9e}\x{0b9f}\x{0ba3}\x{0ba4}\x{0ba8}-\x{0baa}\x{0bae}-\x{0bb9}\x{0bd0}\x{0c05}-\x{0c0c}\x{0c0e}-\x{0c10}\x{0c12}-\x{0c28}\x{0c2a}-\x{0c33}\x{0c35}-\x{0c39}\x{0c3d}\x{0c58}\x{0c59}\x{0c60}\x{0c61}\x{0c85}-\x{0c8c}\x{0c8e}-\x{0c90}\x{0c92}-\x{0ca8}\x{0caa}-\x{0cb3}\x{0cb5}-\x{0cb9}\x{0cbd}\x{0cde}\x{0ce0}\x{0ce1}\x{0cf1}\x{0cf2}\x{0d05}-\x{0d0c}\x{0d0e}-\x{0d10}\x{0d12}-\x{0d3a}\x{0d3d}\x{0d4e}\x{0d60}\x{0d61}\x{0d7a}-\x{0d7f}\x{0d85}-\x{0d96}\x{0d9a}-\x{0db1}\x{0db3}-\x{0dbb}\x{0dbd}\x{0dc0}-\x{0dc6}\x{0e01}-\x{0e30}\x{0e32}\x{0e33}\x{0e40}-\x{0e46}\x{0e81}\x{0e82}\x{0e84}\x{0e87}\x{0e88}\x{0e8a}\x{0e8d}\x{0e94}-\x{0e97}\x{0e99}-\x{0e9f}\x{0ea1}-\x{0ea3}\x{0ea5}\x{0ea7}\x{0eaa}\x{0eab}\x{0ead}-\x{0eb0}\x{0eb2}\x{0eb3}\x{0ebd}\x{0ec0}-\x{0ec4}\x{0ec6}\x{0edc}-\x{0edf}\x{0f00}\x{0f40}-\x{0f47}\x{0f49}-\x{0f6c}\x{0f88}-\x{0f8c}\x{1000}-\x{102a}\x{103f}\x{1050}-\x{1055}\x{105a}-\x{105d}\x{1061}\x{1065}\x{1066}\x{106e}-\x{1070}\x{1075}-\x{1081}\x{108e}\x{10a0}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{10fa}\x{10fc}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1380}-\x{138f}\x{13a0}-\x{13f4}\x{1401}-\x{166c}\x{166f}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16ea}\x{16ee}-\x{16f0}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17d7}\x{17dc}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191c}\x{1950}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19c1}-\x{19c7}\x{1a00}-\x{1a16}\x{1a20}-\x{1a54}\x{1aa7}\x{1b05}-\x{1b33}\x{1b45}-\x{1b4b}\x{1b83}-\x{1ba0}\x{1bae}\x{1baf}\x{1bba}-\x{1be5}\x{1c00}-\x{1c23}\x{1c4d}-\x{1c4f}\x{1c5a}-\x{1c7d}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf1}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{2160}-\x{2188}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{2e2f}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{31a0}-\x{31ba}\x{31f0}-\x{31ff}\x{3400}-\x{4db5}\x{4e00}-\x{9fcc}\x{a000}-\x{a48c}\x{a4d0}-\x{a4fd}\x{a500}-\x{a60c}\x{a610}-\x{a61f}\x{a62a}\x{a62b}\x{a640}-\x{a66e}\x{a67f}-\x{a697}\x{a6a0}-\x{a6ef}\x{a717}-\x{a71f}\x{a722}-\x{a788}\x{a78b}-\x{a78e}\x{a790}-\x{a793}\x{a7a0}-\x{a7aa}\x{a7f8}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a822}\x{a840}-\x{a873}\x{a882}-\x{a8b3}\x{a8f2}-\x{a8f7}\x{a8fb}\x{a90a}-\x{a925}\x{a930}-\x{a946}\x{a960}-\x{a97c}\x{a984}-\x{a9b2}\x{a9cf}\x{aa00}-\x{aa28}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa60}-\x{aa76}\x{aa7a}\x{aa80}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aadd}\x{aae0}-\x{aaea}\x{aaf2}-\x{aaf4}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{abc0}-\x{abe2}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{f900}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb36}\x{fb38}-\x{fb3c}\x{fb3e}\x{fb40}\x{fb41}\x{fb43}\x{fb44}\x{fb46}-\x{fbb1}\x{fbd3}-\x{fd3d}\x{fd50}-\x{fd8f}\x{fd92}-\x{fdc7}\x{fdf0}-\x{fdfb}\x{fe70}-\x{fe74}\x{fe76}-\x{fefc}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}0-9\x{0300}-\x{036f}\x{0483}-\x{0487}\x{0591}-\x{05bd}\x{05bf}\x{05c1}\x{05c2}\x{05c4}\x{05c5}\x{05c7}\x{0610}-\x{061a}\x{064b}-\x{0669}\x{0670}\x{06d6}-\x{06dc}\x{06df}-\x{06e4}\x{06e7}\x{06e8}\x{06ea}-\x{06ed}\x{06f0}-\x{06f9}\x{0711}\x{0730}-\x{074a}\x{07a6}-\x{07b0}\x{07c0}-\x{07c9}\x{07eb}-\x{07f3}\x{0816}-\x{0819}\x{081b}-\x{0823}\x{0825}-\x{0827}\x{0829}-\x{082d}\x{0859}-\x{085b}\x{08e4}-\x{08fe}\x{0900}-\x{0903}\x{093a}-\x{093c}\x{093e}-\x{094f}\x{0951}-\x{0957}\x{0962}\x{0963}\x{0966}-\x{096f}\x{0981}-\x{0983}\x{09bc}\x{09be}-\x{09c4}\x{09c7}\x{09c8}\x{09cb}-\x{09cd}\x{09d7}\x{09e2}\x{09e3}\x{09e6}-\x{09ef}\x{0a01}-\x{0a03}\x{0a3c}\x{0a3e}-\x{0a42}\x{0a47}\x{0a48}\x{0a4b}-\x{0a4d}\x{0a51}\x{0a66}-\x{0a71}\x{0a75}\x{0a81}-\x{0a83}\x{0abc}\x{0abe}-\x{0ac5}\x{0ac7}-\x{0ac9}\x{0acb}-\x{0acd}\x{0ae2}\x{0ae3}\x{0ae6}-\x{0aef}\x{0b01}-\x{0b03}\x{0b3c}\x{0b3e}-\x{0b44}\x{0b47}\x{0b48}\x{0b4b}-\x{0b4d}\x{0b56}\x{0b57}\x{0b62}\x{0b63}\x{0b66}-\x{0b6f}\x{0b82}\x{0bbe}-\x{0bc2}\x{0bc6}-\x{0bc8}\x{0bca}-\x{0bcd}\x{0bd7}\x{0be6}-\x{0bef}\x{0c01}-\x{0c03}\x{0c3e}-\x{0c44}\x{0c46}-\x{0c48}\x{0c4a}-\x{0c4d}\x{0c55}\x{0c56}\x{0c62}\x{0c63}\x{0c66}-\x{0c6f}\x{0c82}\x{0c83}\x{0cbc}\x{0cbe}-\x{0cc4}\x{0cc6}-\x{0cc8}\x{0cca}-\x{0ccd}\x{0cd5}\x{0cd6}\x{0ce2}\x{0ce3}\x{0ce6}-\x{0cef}\x{0d02}\x{0d03}\x{0d3e}-\x{0d44}\x{0d46}-\x{0d48}\x{0d4a}-\x{0d4d}\x{0d57}\x{0d62}\x{0d63}\x{0d66}-\x{0d6f}\x{0d82}\x{0d83}\x{0dca}\x{0dcf}-\x{0dd4}\x{0dd6}\x{0dd8}-\x{0ddf}\x{0df2}\x{0df3}\x{0e31}\x{0e34}-\x{0e3a}\x{0e47}-\x{0e4e}\x{0e50}-\x{0e59}\x{0eb1}\x{0eb4}-\x{0eb9}\x{0ebb}\x{0ebc}\x{0ec8}-\x{0ecd}\x{0ed0}-\x{0ed9}\x{0f18}\x{0f19}\x{0f20}-\x{0f29}\x{0f35}\x{0f37}\x{0f39}\x{0f3e}\x{0f3f}\x{0f71}-\x{0f84}\x{0f86}\x{0f87}\x{0f8d}-\x{0f97}\x{0f99}-\x{0fbc}\x{0fc6}\x{102b}-\x{103e}\x{1040}-\x{1049}\x{1056}-\x{1059}\x{105e}-\x{1060}\x{1062}-\x{1064}\x{1067}-\x{106d}\x{1071}-\x{1074}\x{1082}-\x{108d}\x{108f}-\x{109d}\x{135d}-\x{135f}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}\x{1753}\x{1772}\x{1773}\x{17b4}-\x{17d3}\x{17dd}\x{17e0}-\x{17e9}\x{180b}-\x{180d}\x{1810}-\x{1819}\x{18a9}\x{1920}-\x{192b}\x{1930}-\x{193b}\x{1946}-\x{194f}\x{19b0}-\x{19c0}\x{19c8}\x{19c9}\x{19d0}-\x{19d9}\x{1a17}-\x{1a1b}\x{1a55}-\x{1a5e}\x{1a60}-\x{1a7c}\x{1a7f}-\x{1a89}\x{1a90}-\x{1a99}\x{1b00}-\x{1b04}\x{1b34}-\x{1b44}\x{1b50}-\x{1b59}\x{1b6b}-\x{1b73}\x{1b80}-\x{1b82}\x{1ba1}-\x{1bad}\x{1bb0}-\x{1bb9}\x{1be6}-\x{1bf3}\x{1c24}-\x{1c37}\x{1c40}-\x{1c49}\x{1c50}-\x{1c59}\x{1cd0}-\x{1cd2}\x{1cd4}-\x{1ce8}\x{1ced}\x{1cf2}-\x{1cf4}\x{1dc0}-\x{1de6}\x{1dfc}-\x{1dff}\x{200c}\x{200d}\x{203f}\x{2040}\x{2054}\x{20d0}-\x{20dc}\x{20e1}\x{20e5}-\x{20f0}\x{2cef}-\x{2cf1}\x{2d7f}\x{2de0}-\x{2dff}\x{302a}-\x{302f}\x{3099}\x{309a}\x{a620}-\x{a629}\x{a66f}\x{a674}-\x{a67d}\x{a69f}\x{a6f0}\x{a6f1}\x{a802}\x{a806}\x{a80b}\x{a823}-\x{a827}\x{a880}\x{a881}\x{a8b4}-\x{a8c4}\x{a8d0}-\x{a8d9}\x{a8e0}-\x{a8f1}\x{a900}-\x{a909}\x{a926}-\x{a92d}\x{a947}-\x{a953}\x{a980}-\x{a983}\x{a9b3}-\x{a9c0}\x{a9d0}-\x{a9d9}\x{aa29}-\x{aa36}\x{aa43}\x{aa4c}\x{aa4d}\x{aa50}-\x{aa59}\x{aa7b}\x{aab0}\x{aab2}-\x{aab4}\x{aab7}\x{aab8}\x{aabe}\x{aabf}\x{aac1}\x{aaeb}-\x{aaef}\x{aaf5}\x{aaf6}\x{abe3}-\x{abea}\x{abec}\x{abed}\x{abf0}-\x{abf9}\x{fb1e}\x{fe00}-\x{fe0f}\x{fe20}-\x{fe26}\x{fe33}\x{fe34}\x{fe4d}-\x{fe4f}\x{ff10}-\x{ff19}\x{ff3f}]*\b'; + + /** + * Full list of JavaScript reserved words. + * Will be loaded from /data/js/keywords_reserved.txt. + * + * @see https://mathiasbynens.be/notes/reserved-keywords + * + * @var string[] + */ + protected $keywordsReserved = array(); + + /** + * List of JavaScript reserved words that accept a <variable, value, ...> + * after them. Some end of lines are not the end of a statement, like with + * these keywords. + * + * E.g.: we shouldn't insert a ; after this else + * else + * console.log('this is quite fine') + * + * Will be loaded from /data/js/keywords_before.txt + * + * @var string[] + */ + protected $keywordsBefore = array(); + + /** + * List of JavaScript reserved words that accept a <variable, value, ...> + * before them. Some end of lines are not the end of a statement, like when + * continued by one of these keywords on the newline. + * + * E.g.: we shouldn't insert a ; before this instanceof + * variable + * instanceof String + * + * Will be loaded from /data/js/keywords_after.txt + * + * @var string[] + */ + protected $keywordsAfter = array(); + + /** + * List of all JavaScript operators. + * + * Will be loaded from /data/js/operators.txt + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators + * + * @var string[] + */ + protected $operators = array(); + + /** + * List of JavaScript operators that accept a <variable, value, ...> after + * them. Some end of lines are not the end of a statement, like with these + * operators. + * + * Note: Most operators are fine, we've only removed ++ and --. + * ++ & -- have to be joined with the value they're in-/decrementing. + * + * Will be loaded from /data/js/operators_before.txt + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators + * + * @var string[] + */ + protected $operatorsBefore = array(); + + /** + * List of JavaScript operators that accept a <variable, value, ...> before + * them. Some end of lines are not the end of a statement, like when + * continued by one of these operators on the newline. + * + * Note: Most operators are fine, we've only removed ), ], ++, --, ! and ~. + * There can't be a newline separating ! or ~ and whatever it is negating. + * ++ & -- have to be joined with the value they're in-/decrementing. + * ) & ] are "special" in that they have lots or usecases. () for example + * is used for function calls, for grouping, in if () and for (), ... + * + * Will be loaded from /data/js/operators_after.txt + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators + * + * @var string[] + */ + protected $operatorsAfter = array(); + + /** + * {@inheritdoc} + */ + public function __construct() + { + call_user_func_array(array('parent', '__construct'), func_get_args()); + + $dataDir = __DIR__.'/../data/js/'; + $options = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES; + $this->keywordsReserved = file($dataDir.'keywords_reserved.txt', $options); + $this->keywordsBefore = file($dataDir.'keywords_before.txt', $options); + $this->keywordsAfter = file($dataDir.'keywords_after.txt', $options); + $this->operators = file($dataDir.'operators.txt', $options); + $this->operatorsBefore = file($dataDir.'operators_before.txt', $options); + $this->operatorsAfter = file($dataDir.'operators_after.txt', $options); + } + + /** + * Minify the data. + * Perform JS optimizations. + * + * @param string[optional] $path Path to write the data to + * + * @return string The minified data + */ + public function execute($path = null) + { + $content = ''; + + /* + * Let's first take out strings, comments and regular expressions. + * All of these can contain JS code-like characters, and we should make + * sure any further magic ignores anything inside of these. + * + * Consider this example, where we should not strip any whitespace: + * var str = "a test"; + * + * Comments will be removed altogether, strings and regular expressions + * will be replaced by placeholder text, which we'll restore later. + */ + $this->extractStrings('\'"`'); + $this->stripComments(); + $this->extractRegex(); + + // loop files + foreach ($this->data as $source => $js) { + // take out strings, comments & regex (for which we've registered + // the regexes just a few lines earlier) + $js = $this->replace($js); + + $js = $this->propertyNotation($js); + $js = $this->shortenBools($js); + $js = $this->stripWhitespace($js); + + // combine js: separating the scripts by a ; + $content .= $js.";"; + } + + // clean up leftover `;`s from the combination of multiple scripts + $content = ltrim($content, ';'); + $content = (string) substr($content, 0, -1); + + /* + * Earlier, we extracted strings & regular expressions and replaced them + * with placeholder text. This will restore them. + */ + $content = $this->restoreExtractedData($content); + + return $content; + } + + /** + * Strip comments from source code. + */ + protected function stripComments() + { + // PHP only supports $this inside anonymous functions since 5.4 + $minifier = $this; + $callback = function ($match) use ($minifier) { + $count = count($minifier->extracted); + $placeholder = '/*'.$count.'*/'; + $minifier->extracted[$placeholder] = $match[0]; + + return $placeholder; + }; + // multi-line comments + $this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback); + $this->registerPattern('/\/\*.*?\*\//s', ''); + + // single-line comments + $this->registerPattern('/\/\/.*$/m', ''); + } + + /** + * JS can have /-delimited regular expressions, like: /ab+c/.match(string). + * + * The content inside the regex can contain characters that may be confused + * for JS code: e.g. it could contain whitespace it needs to match & we + * don't want to strip whitespace in there. + * + * The regex can be pretty simple: we don't have to care about comments, + * (which also use slashes) because stripComments() will have stripped those + * already. + * + * This method will replace all string content with simple REGEX# + * placeholder text, so we've rid all regular expressions from characters + * that may be misinterpreted. Original regex content will be saved in + * $this->extracted and after doing all other minifying, we can restore the + * original content via restoreRegex() + */ + protected function extractRegex() + { + // PHP only supports $this inside anonymous functions since 5.4 + $minifier = $this; + $callback = function ($match) use ($minifier) { + $count = count($minifier->extracted); + $placeholder = '"'.$count.'"'; + $minifier->extracted[$placeholder] = $match[0]; + + return $placeholder; + }; + + // match all chars except `/` and `\` + // `\` is allowed though, along with whatever char follows (which is the + // one being escaped) + // this should allow all chars, except for an unescaped `/` (= the one + // closing the regex) + // then also ignore bare `/` inside `[]`, where they don't need to be + // escaped: anything inside `[]` can be ignored safely + $pattern = '\\/(?!\*)(?:[^\\[\\/\\\\\n\r]++|(?:\\\\.)++|(?:\\[(?:[^\\]\\\\\n\r]++|(?:\\\\.)++)++\\])++)++\\/[gimuy]*'; + + // a regular expression can only be followed by a few operators or some + // of the RegExp methods (a `\` followed by a variable or value is + // likely part of a division, not a regex) + $keywords = array('do', 'in', 'new', 'else', 'throw', 'yield', 'delete', 'return', 'typeof'); + $before = '([=:,;\+\-\*\/\}\(\{\[&\|!]|^|'.implode('|', $keywords).')\s*'; + $propertiesAndMethods = array( + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Properties_2 + 'constructor', + 'flags', + 'global', + 'ignoreCase', + 'multiline', + 'source', + 'sticky', + 'unicode', + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Methods_2 + 'compile(', + 'exec(', + 'test(', + 'toSource(', + 'toString(', + ); + $delimiters = array_fill(0, count($propertiesAndMethods), '/'); + $propertiesAndMethods = array_map('preg_quote', $propertiesAndMethods, $delimiters); + $after = '(?=\s*([\.,;\)\}&\|+]|\/\/|$|\.('.implode('|', $propertiesAndMethods).')))'; + $this->registerPattern('/'.$before.'\K'.$pattern.$after.'/', $callback); + + // regular expressions following a `)` are rather annoying to detect... + // quite often, `/` after `)` is a division operator & if it happens to + // be followed by another one (or a comment), it is likely to be + // confused for a regular expression + // however, it's perfectly possible for a regex to follow a `)`: after + // a single-line `if()`, `while()`, ... statement, for example + // since, when they occur like that, they're always the start of a + // statement, there's only a limited amount of ways they can be useful: + // by calling the regex methods directly + // if a regex following `)` is not followed by `.<property or method>`, + // it's quite likely not a regex + $before = '\)\s*'; + $after = '(?=\s*\.('.implode('|', $propertiesAndMethods).'))'; + $this->registerPattern('/'.$before.'\K'.$pattern.$after.'/', $callback); + + // 1 more edge case: a regex can be followed by a lot more operators or + // keywords if there's a newline (ASI) in between, where the operator + // actually starts a new statement + // (https://github.com/matthiasmullie/minify/issues/56) + $operators = $this->getOperatorsForRegex($this->operatorsBefore, '/'); + $operators += $this->getOperatorsForRegex($this->keywordsReserved, '/'); + $after = '(?=\s*\n\s*('.implode('|', $operators).'))'; + $this->registerPattern('/'.$pattern.$after.'/', $callback); + } + + /** + * Strip whitespace. + * + * We won't strip *all* whitespace, but as much as possible. The thing that + * we'll preserve are newlines we're unsure about. + * JavaScript doesn't require statements to be terminated with a semicolon. + * It will automatically fix missing semicolons with ASI (automatic semi- + * colon insertion) at the end of line causing errors (without semicolon.) + * + * Because it's sometimes hard to tell if a newline is part of a statement + * that should be terminated or not, we'll just leave some of them alone. + * + * @param string $content The content to strip the whitespace for + * + * @return string + */ + protected function stripWhitespace($content) + { + // uniform line endings, make them all line feed + $content = str_replace(array("\r\n", "\r"), "\n", $content); + + // collapse all non-line feed whitespace into a single space + $content = preg_replace('/[^\S\n]+/', ' ', $content); + + // strip leading & trailing whitespace + $content = str_replace(array(" \n", "\n "), "\n", $content); + + // collapse consecutive line feeds into just 1 + $content = preg_replace('/\n+/', "\n", $content); + + $operatorsBefore = $this->getOperatorsForRegex($this->operatorsBefore, '/'); + $operatorsAfter = $this->getOperatorsForRegex($this->operatorsAfter, '/'); + $operators = $this->getOperatorsForRegex($this->operators, '/'); + $keywordsBefore = $this->getKeywordsForRegex($this->keywordsBefore, '/'); + $keywordsAfter = $this->getKeywordsForRegex($this->keywordsAfter, '/'); + + // strip whitespace that ends in (or next line begin with) an operator + // that allows statements to be broken up over multiple lines + unset($operatorsBefore['+'], $operatorsBefore['-'], $operatorsAfter['+'], $operatorsAfter['-']); + $content = preg_replace( + array( + '/('.implode('|', $operatorsBefore).')\s+/', + '/\s+('.implode('|', $operatorsAfter).')/', + ), + '\\1', + $content + ); + + // make sure + and - can't be mistaken for, or joined into ++ and -- + $content = preg_replace( + array( + '/(?<![\+\-])\s*([\+\-])(?![\+\-])/', + '/(?<![\+\-])([\+\-])\s*(?![\+\-])/', + ), + '\\1', + $content + ); + + // collapse whitespace around reserved words into single space + $content = preg_replace('/(^|[;\}\s])\K('.implode('|', $keywordsBefore).')\s+/', '\\2 ', $content); + $content = preg_replace('/\s+('.implode('|', $keywordsAfter).')(?=([;\{\s]|$))/', ' \\1', $content); + + /* + * We didn't strip whitespace after a couple of operators because they + * could be used in different contexts and we can't be sure it's ok to + * strip the newlines. However, we can safely strip any non-line feed + * whitespace that follows them. + */ + $operatorsDiffBefore = array_diff($operators, $operatorsBefore); + $operatorsDiffAfter = array_diff($operators, $operatorsAfter); + $content = preg_replace('/('.implode('|', $operatorsDiffBefore).')[^\S\n]+/', '\\1', $content); + $content = preg_replace('/[^\S\n]+('.implode('|', $operatorsDiffAfter).')/', '\\1', $content); + + /* + * Whitespace after `return` can be omitted in a few occasions + * (such as when followed by a string or regex) + * Same for whitespace in between `)` and `{`, or between `{` and some + * keywords. + */ + $content = preg_replace('/\breturn\s+(["\'\/\+\-])/', 'return$1', $content); + $content = preg_replace('/\)\s+\{/', '){', $content); + $content = preg_replace('/}\n(else|catch|finally)\b/', '}$1', $content); + + /* + * Get rid of double semicolons, except where they can be used like: + * "for(v=1,_=b;;)", "for(v=1;;v++)" or "for(;;ja||(ja=true))". + * I'll safeguard these double semicolons inside for-loops by + * temporarily replacing them with an invalid condition: they won't have + * a double semicolon and will be easy to spot to restore afterwards. + */ + $content = preg_replace('/\bfor\(([^;]*);;([^;]*)\)/', 'for(\\1;-;\\2)', $content); + $content = preg_replace('/;+/', ';', $content); + $content = preg_replace('/\bfor\(([^;]*);-;([^;]*)\)/', 'for(\\1;;\\2)', $content); + + /* + * Next, we'll be removing all semicolons where ASI kicks in. + * for-loops however, can have an empty body (ending in only a + * semicolon), like: `for(i=1;i<3;i++);`, of `for(i in list);` + * Here, nothing happens during the loop; it's just used to keep + * increasing `i`. With that ; omitted, the next line would be expected + * to be the for-loop's body... Same goes for while loops. + * I'm going to double that semicolon (if any) so after the next line, + * which strips semicolons here & there, we're still left with this one. + */ + $content = preg_replace('/(for\([^;\{]*;[^;\{]*;[^;\{]*\));(\}|$)/s', '\\1;;\\2', $content); + $content = preg_replace('/(for\([^;\{]+\s+in\s+[^;\{]+\));(\}|$)/s', '\\1;;\\2', $content); + /* + * Below will also keep `;` after a `do{}while();` along with `while();` + * While these could be stripped after do-while, detecting this + * distinction is cumbersome, so I'll play it safe and make sure `;` + * after any kind of `while` is kept. + */ + $content = preg_replace('/(while\([^;\{]+\));(\}|$)/s', '\\1;;\\2', $content); + + /* + * We also can't strip empty else-statements. Even though they're + * useless and probably shouldn't be in the code in the first place, we + * shouldn't be stripping the `;` that follows it as it breaks the code. + * We can just remove those useless else-statements completely. + * + * @see https://github.com/matthiasmullie/minify/issues/91 + */ + $content = preg_replace('/else;/s', '', $content); + + /* + * We also don't really want to terminate statements followed by closing + * curly braces (which we've ignored completely up until now) or end-of- + * script: ASI will kick in here & we're all about minifying. + * Semicolons at beginning of the file don't make any sense either. + */ + $content = preg_replace('/;(\}|$)/s', '\\1', $content); + $content = ltrim($content, ';'); + + // get rid of remaining whitespace af beginning/end + return trim($content); + } + + /** + * We'll strip whitespace around certain operators with regular expressions. + * This will prepare the given array by escaping all characters. + * + * @param string[] $operators + * @param string $delimiter + * + * @return string[] + */ + protected function getOperatorsForRegex(array $operators, $delimiter = '/') + { + // escape operators for use in regex + $delimiters = array_fill(0, count($operators), $delimiter); + $escaped = array_map('preg_quote', $operators, $delimiters); + + $operators = array_combine($operators, $escaped); + + // ignore + & - for now, they'll get special treatment + unset($operators['+'], $operators['-']); + + // dot can not just immediately follow a number; it can be confused for + // decimal point, or calling a method on it, e.g. 42 .toString() + $operators['.'] = '(?<![0-9]\s)\.'; + + // don't confuse = with other assignment shortcuts (e.g. +=) + $chars = preg_quote('+-*\=<>%&|', $delimiter); + $operators['='] = '(?<!['.$chars.'])\='; + + return $operators; + } + + /** + * We'll strip whitespace around certain keywords with regular expressions. + * This will prepare the given array by escaping all characters. + * + * @param string[] $keywords + * @param string $delimiter + * + * @return string[] + */ + protected function getKeywordsForRegex(array $keywords, $delimiter = '/') + { + // escape keywords for use in regex + $delimiter = array_fill(0, count($keywords), $delimiter); + $escaped = array_map('preg_quote', $keywords, $delimiter); + + // add word boundaries + array_walk($keywords, function ($value) { + return '\b'.$value.'\b'; + }); + + $keywords = array_combine($keywords, $escaped); + + return $keywords; + } + + /** + * Replaces all occurrences of array['key'] by array.key. + * + * @param string $content + * + * @return string + */ + protected function propertyNotation($content) + { + // PHP only supports $this inside anonymous functions since 5.4 + $minifier = $this; + $keywords = $this->keywordsReserved; + $callback = function ($match) use ($minifier, $keywords) { + $property = trim($minifier->extracted[$match[1]], '\'"'); + + /* + * Check if the property is a reserved keyword. In this context (as + * property of an object literal/array) it shouldn't matter, but IE8 + * freaks out with "Expected identifier". + */ + if (in_array($property, $keywords)) { + return $match[0]; + } + + /* + * See if the property is in a variable-like format (e.g. + * array['key-here'] can't be replaced by array.key-here since '-' + * is not a valid character there. + */ + if (!preg_match('/^'.$minifier::REGEX_VARIABLE.'$/u', $property)) { + return $match[0]; + } + + return '.'.$property; + }; + + /* + * Figure out if previous character is a variable name (of the array + * we want to use property notation on) - this is to make sure + * standalone ['value'] arrays aren't confused for keys-of-an-array. + * We can (and only have to) check the last character, because PHP's + * regex implementation doesn't allow unfixed-length look-behind + * assertions. + */ + preg_match('/(\[[^\]]+\])[^\]]*$/', static::REGEX_VARIABLE, $previousChar); + $previousChar = $previousChar[1]; + + /* + * Make sure word preceding the ['value'] is not a keyword, e.g. + * return['x']. Because -again- PHP's regex implementation doesn't allow + * unfixed-length look-behind assertions, I'm just going to do a lot of + * separate look-behind assertions, one for each keyword. + */ + $keywords = $this->getKeywordsForRegex($keywords); + $keywords = '(?<!'.implode(')(?<!', $keywords).')'; + + return preg_replace_callback('/(?<='.$previousChar.'|\])'.$keywords.'\[\s*(([\'"])[0-9]+\\2)\s*\]/u', $callback, $content); + } + + /** + * Replaces true & false by !0 and !1. + * + * @param string $content + * + * @return string + */ + protected function shortenBools($content) + { + /* + * 'true' or 'false' could be used as property names (which may be + * followed by whitespace) - we must not replace those! + * Since PHP doesn't allow variable-length (to account for the + * whitespace) lookbehind assertions, I need to capture the leading + * character and check if it's a `.` + */ + $callback = function ($match) { + if (trim($match[1]) === '.') { + return $match[0]; + } + + return $match[1].($match[2] === 'true' ? '!0' : '!1'); + }; + $content = preg_replace_callback('/(^|.\s*)\b(true|false)\b(?!:)/', $callback, $content); + + // for(;;) is exactly the same as while(true), but shorter :) + $content = preg_replace('/\bwhile\(!0\){/', 'for(;;){', $content); + + // now make sure we didn't turn any do ... while(true) into do ... for(;;) + preg_match_all('/\bdo\b/', $content, $dos, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); + + // go backward to make sure positional offsets aren't altered when $content changes + $dos = array_reverse($dos); + foreach ($dos as $do) { + $offsetDo = $do[0][1]; + + // find all `while` (now `for`) following `do`: one of those must be + // associated with the `do` and be turned back into `while` + preg_match_all('/\bfor\(;;\)/', $content, $whiles, PREG_OFFSET_CAPTURE | PREG_SET_ORDER, $offsetDo); + foreach ($whiles as $while) { + $offsetWhile = $while[0][1]; + + $open = substr_count($content, '{', $offsetDo, $offsetWhile - $offsetDo); + $close = substr_count($content, '}', $offsetDo, $offsetWhile - $offsetDo); + if ($open === $close) { + // only restore `while` if amount of `{` and `}` are the same; + // otherwise, that `for` isn't associated with this `do` + $content = substr_replace($content, 'while(!0)', $offsetWhile, strlen('for(;;)')); + break; + } + } + } + + return $content; + } +} + +interface ConverterInterface { + public function convert($path); +} + +class Converter implements ConverterInterface { + public function convert($path) { + return $path; + } +} + +// Bundle extension, Copyright Datenstrom, License GPLv2 + +class MinifyCss extends CSS { } + +class MinifyJavaScript extends JS { + + public function __construct() { + $this->keywordsReserved = array("do", "if", "in", "for", "let", "new", "try", "var", "case", "else", "enum", "eval", "null", "this", "true", "void", "with", "break", "catch", "class", "const", "false", "super", "throw", "while", "yield", "delete", "export", "import", "public", "return", "static", "switch", "typeof", "default", "extends", "finally", "package", "private", "continue", "debugger", "function", "arguments", "interface", "protected", "implements", "instanceof", "abstract", "boolean", "byte", "char", "double", "final", "float", "goto", "int", "long", "native", "short", "synchronized", "throws", "transient", "volatile"); + $this->keywordsBefore = array("do", "in", "let", "new", "var", "case", "else", "enum", "void", "with", "class", "const", "yield", "delete", "export", "import", "public", "static", "typeof", "extends", "package", "private", "function", "protected", "implements", "instanceof"); + $this->keywordsAfter = array("in", "public", "extends", "private", "protected", "implements", "instanceof"); + $this->operators = array("+", "-", "*", "/", "%", "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "&=", "^=", "|=", "&", "|", "^", "~", "<<", ">>", ">>>", "==", "===", "!=", "!==", ">", "<", ">=", "<=", "&&", "||", "!", ".", "[", "]", "?", ":", ",", ";", "(", ")", "{", "}"); + $this->operatorsBefore = array("+", "-", "*", "/", "%", "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "&=", "^=", "|=", "&", "|", "^", "~", "<<", ">>", ">>>", "==", "===", "!=", "!==", ">", "<", ">=", "<=", "&&", "||", "!", ".", "[", "?", ":", ",", ";", "(", "{"); + $this->operatorsAfter = array("+", "-", "*", "/", "%", "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "&=", "^=", "|=", "&", "|", "^", "<<", ">>", ">>>", "==", "===", "!=", "!==", ">", "<", ">=", "<=", "&&", "||", ".", "[", "]", "?", ":", ",", ";", "(", ")", "}"); + } + + // Minify data, add semicolon as separator between multiple files + public function execute($path = null) { + return parent::execute($path).";"; + } +} + +class MinifyBasic extends Minify { + + // Minify data, remove only comments and empty lines + public function execute($path = null) { + $content = ""; + $this->extractStrings(); + foreach ($this->data as $source => $data) { + $data = $this->replace($data); + $data = preg_replace("/\/\*.*?\*\//s", "", $data); + $data = preg_replace("/\/\/.*?[\r\n]+/", "", $data); + $data = preg_replace("/[\r\n]+/", "\n", $data); + $content .= trim($data); + } + return $this->restoreExtractedData($content); + } +} diff --git a/system/extensions/command.php b/system/extensions/command.php new file mode 100644 index 0000000..9e4b10d --- /dev/null +++ b/system/extensions/command.php @@ -0,0 +1,617 @@ +<?php +// Command extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/command + +class YellowCommand { + const VERSION = "0.8.22"; + public $yellow; // access to API + public $files; // number of files + public $links; // number of links + public $errors; // number of errors + public $locationsArguments; // locations with location arguments detected + public $locationsArgumentsPagination; // locations with pagination arguments detected + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + $this->yellow->system->setDefault("commandStaticBuildDirectory", "public/"); + $this->yellow->system->setDefault("commandStaticDefaultFile", "index.html"); + $this->yellow->system->setDefault("commandStaticErrorFile", "404.html"); + } + + // Handle request + public function onRequest($scheme, $address, $base, $location, $fileName) { + return $this->processRequestCache($scheme, $address, $base, $location, $fileName); + } + + // Handle command + public function onCommand($command, $text) { + switch ($command) { + case "build": $statusCode = $this->processCommandBuild($command, $text); break; + case "check": $statusCode = $this->processCommandCheck($command, $text); break; + case "clean": $statusCode = $this->processCommandClean($command, $text); break; + case "serve": $statusCode = $this->processCommandServe($command, $text); break; + default: $statusCode = 0; + } + return $statusCode; + } + + // Handle command help + public function onCommandHelp() { + $help .= "build [directory location]\n"; + $help .= "check [directory location]\n"; + $help .= "clean [directory location]\n"; + $help .= "serve [directory url]\n"; + return $help; + } + + // Process command to build static website + public function processCommandBuild($command, $text) { + $statusCode = 0; + list($path, $location) = $this->yellow->toolbox->getTextArguments($text); + if (empty($location) || substru($location, 0, 1)=="/") { + if ($this->checkStaticSettings()) { + $statusCode = $this->buildStaticFiles($path, $location); + } else { + $statusCode = 500; + $this->files = 0; + $this->errors = 1; + $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); + echo "ERROR building files: Please configure CoreStaticUrl in file '$fileName'!\n"; + } + echo "Yellow $command: $this->files file".($this->files!=1 ? "s" : ""); + echo ", $this->errors error".($this->errors!=1 ? "s" : "")."\n"; + } else { + $statusCode = 400; + echo "Yellow $command: Invalid arguments\n"; + } + return $statusCode; + } + + // Build static files + public function buildStaticFiles($path, $locationFilter) { + $path = rtrim(empty($path) ? $this->yellow->system->get("commandStaticBuildDirectory") : $path, "/"); + $this->files = $this->errors = 0; + $this->locationsArguments = $this->locationsArgumentsPagination = array(); + $statusCode = empty($locationFilter) ? $this->cleanStaticFiles($path, $locationFilter) : 200; + $staticUrl = $this->yellow->system->get("coreStaticUrl"); + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); + $locations = $this->getContentLocations(); + $filesEstimated = count($locations); + foreach ($locations as $location) { + echo "\rBuilding static website ".$this->getProgressPercent($this->files, $filesEstimated, 5, 60)."%... "; + if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; + $statusCode = max($statusCode, $this->buildStaticFile($path, $location, true)); + } + foreach ($this->locationsArguments as $location) { + echo "\rBuilding static website ".$this->getProgressPercent($this->files, $filesEstimated, 5, 60)."%... "; + if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; + $statusCode = max($statusCode, $this->buildStaticFile($path, $location, true)); + } + $filesEstimated = $this->files + count($this->locationsArguments) + count($this->locationsArgumentsPagination); + foreach ($this->locationsArgumentsPagination as $location) { + echo "\rBuilding static website ".$this->getProgressPercent($this->files, $filesEstimated, 5, 95)."%... "; + if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; + if (substru($location, -1)!=$this->yellow->toolbox->getLocationArgumentsSeparator()) { + $statusCode = max($statusCode, $this->buildStaticFile($path, $location, false, true)); + } + for ($pageNumber=2; $pageNumber<=999; ++$pageNumber) { + $statusCodeLocation = $this->buildStaticFile($path, $location.$pageNumber, false, true); + $statusCode = max($statusCode, $statusCodeLocation); + if ($statusCodeLocation==100) break; + } + } + if (empty($locationFilter)) { + foreach ($this->getMediaLocations() as $location) { + $statusCode = max($statusCode, $this->buildStaticFile($path, $location)); + } + foreach ($this->getSystemLocations() as $location) { + $statusCode = max($statusCode, $this->buildStaticFile($path, $location)); + } + foreach ($this->getExtraLocations($path) as $location) { + $statusCode = max($statusCode, $this->buildStaticFile($path, $location)); + } + $statusCode = max($statusCode, $this->buildStaticFile($path, "/error/", false, false, true)); + } + echo "\rBuilding static website 100%... done\n"; + return $statusCode; + } + + // Build static file + public function buildStaticFile($path, $location, $analyse = false, $probe = false, $error = false) { + $this->yellow->content = new YellowContent($this->yellow); + $this->yellow->page = new YellowPage($this->yellow); + $this->yellow->page->fileName = substru($location, 1); + if (!is_readable($this->yellow->page->fileName)) { + ob_start(); + $staticUrl = $this->yellow->system->get("coreStaticUrl"); + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); + $statusCode = $this->requestStaticFile($scheme, $address, $base, $location); + if ($statusCode<400 || $error) { + $fileData = ob_get_contents(); + $statusCode = $this->saveStaticFile($path, $location, $fileData, $statusCode); + } + ob_end_clean(); + } else { + $statusCode = $this->copyStaticFile($path, $location); + } + if ($statusCode==200 && $analyse) $this->analyseLocations($scheme, $address, $base, $fileData); + if ($statusCode==404 && $probe) $statusCode = 100; + if ($statusCode==404 && $error) $statusCode = 200; + if ($statusCode>=200) ++$this->files; + if ($statusCode>=400) { + ++$this->errors; + echo "\rERROR building location '$location', ".$this->yellow->page->getStatusCode(true)."\n"; + } + if (defined("DEBUG") && DEBUG>=1) echo "YellowCommand::buildStaticFile status:$statusCode location:$location<br/>\n"; + return $statusCode; + } + + // Request static file + public function requestStaticFile($scheme, $address, $base, $location) { + list($serverName, $serverPort) = $this->yellow->toolbox->getTextList($address, ":", 2); + if (empty($serverPort)) $serverPort = $scheme=="https" ? 443 : 80; + $_SERVER["SERVER_PROTOCOL"] = "HTTP/1.1"; + $_SERVER["SERVER_NAME"] = $serverName; + $_SERVER["SERVER_PORT"] = $serverPort; + $_SERVER["REQUEST_METHOD"] = "GET"; + $_SERVER["REQUEST_SCHEME"] = $scheme; + $_SERVER["REQUEST_URI"] = $base.$location; + $_SERVER["SCRIPT_NAME"] = $base."/yellow.php"; + $_SERVER["REMOTE_ADDR"] = "127.0.0.1"; + $_REQUEST = array(); + return $this->yellow->request(); + } + + // Save static file + public function saveStaticFile($path, $location, $fileData, $statusCode) { + $modified = strtotime($this->yellow->page->getHeader("Last-Modified")); + if ($modified==0) $modified = $this->yellow->toolbox->getFileModified($this->yellow->page->fileName); + if ($statusCode>=301 && $statusCode<=303) { + $fileData = $this->getStaticRedirect($this->yellow->page->getHeader("Location")); + $modified = time(); + } + $fileName = $this->getStaticFile($path, $location, $statusCode); + if (is_file($fileName)) $this->yellow->toolbox->deleteFile($fileName); + if (!$this->yellow->toolbox->createFile($fileName, $fileData, true) || + !$this->yellow->toolbox->modifyFile($fileName, $modified)) { + $statusCode = 500; + $this->yellow->page->statusCode = $statusCode; + $this->yellow->page->set("pageError", "Can't write file '$fileName'!"); + } + return $statusCode; + } + + // Copy static file + public function copyStaticFile($path, $location) { + $statusCode = 200; + $modified = $this->yellow->toolbox->getFileModified($this->yellow->page->fileName); + $fileName = $this->getStaticFile($path, $location, $statusCode); + if (is_file($fileName)) $this->yellow->toolbox->deleteFile($fileName); + if (!$this->yellow->toolbox->copyFile($this->yellow->page->fileName, $fileName, true) || + !$this->yellow->toolbox->modifyFile($fileName, $modified)) { + $statusCode = 500; + $this->yellow->page->statusCode = $statusCode; + $this->yellow->page->set("pageError", "Can't write file '$fileName'!"); + } + return $statusCode; + } + + // Analyse locations with arguments + public function analyseLocations($scheme, $address, $base, $rawData) { + preg_match_all("/<(.*?)href=\"([^\"]+)\"(.*?)>/i", $rawData, $matches); + foreach ($matches[2] as $match) { + $location = rawurldecode($match); + if (preg_match("/^(.*?)#(.*)$/", $location, $tokens)) $location = $tokens[1]; + if (preg_match("/^(\w+):\/\/([^\/]+)(.*)$/", $location, $tokens)) { + if ($tokens[1]!=$scheme) continue; + if ($tokens[2]!=$address) continue; + $location = $tokens[3]; + } + if (substru($location, 0, strlenu($base))!=$base) continue; + $location = substru($location, strlenu($base)); + if (!$this->yellow->toolbox->isLocationArguments($location)) continue; + if (!$this->yellow->toolbox->isLocationArgumentsPagination($location)) { + $location = rtrim($location, "/")."/"; + if (!isset($this->locationsArguments[$location])) { + $this->locationsArguments[$location] = $location; + if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseLocations detected location:$location<br/>\n"; + } + } else { + $location = rtrim($location, "0..9"); + if (!isset($this->locationsArgumentsPagination[$location])) { + $this->locationsArgumentsPagination[$location] = $location; + if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseLocations detected location:$location<br/>\n"; + } + } + } + } + + // Process command to check static files for broken links + public function processCommandCheck($command, $text) { + $statusCode = 0; + list($path, $location) = $this->yellow->toolbox->getTextArguments($text); + if (empty($location) || substru($location, 0, 1)=="/") { + if ($this->checkStaticSettings()) { + $statusCode = $this->checkStaticFiles($path, $location); + } else { + $statusCode = 500; + $this->links = 0; + $this->errors = 1; + $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); + echo "ERROR checking files: Please configure CoreStaticUrl in file '$fileName'!\n"; + } + echo "Yellow $command: $this->links link".($this->links!=1 ? "s" : ""); + echo ", $this->errors error".($this->errors!=1 ? "s" : "")."\n"; + } else { + $statusCode = 400; + echo "Yellow $command: Invalid arguments\n"; + } + return $statusCode; + } + + // Check static files for broken links + public function checkStaticFiles($path, $locationFilter) { + $path = rtrim(empty($path) ? $this->yellow->system->get("commandStaticBuildDirectory") : $path, "/"); + $this->links = $this->errors = 0; + $regex = "/^[^.]+$|".$this->yellow->system->get("commandStaticDefaultFile")."$/"; + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($path, $regex, false, false); + list($statusCodeFiles, $links) = $this->analyseLinks($path, $locationFilter, $fileNames); + list($statusCodeLinks, $broken, $redirect) = $this->analyseStatus($path, $links); + if ($statusCodeLinks!=200) { + $this->showLinks($broken, "Broken links"); + $this->showLinks($redirect, "Redirect links"); + } + return max($statusCodeFiles, $statusCodeLinks); + } + + // Analyse links in static files + public function analyseLinks($path, $locationFilter, $fileNames) { + $statusCode = 200; + $links = array(); + if (!empty($fileNames)) { + $staticUrl = $this->yellow->system->get("coreStaticUrl"); + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); + foreach ($fileNames as $fileName) { + if (is_readable($fileName)) { + $locationSource = $this->getStaticLocation($path, $fileName); + if (!preg_match("#^$base$locationFilter#", "$base$locationSource")) continue; + $fileData = $this->yellow->toolbox->readFile($fileName); + preg_match_all("/<(.*?)href=\"([^\"]+)\"(.*?)>/i", $fileData, $matches); + foreach ($matches[2] as $match) { + $location = rawurldecode($match); + if (preg_match("/^(.*?)#(.*)$/", $location, $tokens)) $location = $tokens[1]; + if (preg_match("/^(\w+):\/\/([^\/]+)(.*)$/", $location, $matches)) { + $url = $location.(empty($matches[3]) ? "/" : ""); + if (!isset($links[$url])) { + $links[$url] = $locationSource; + } else { + $links[$url] .= ",".$locationSource; + } + if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseLinks detected url:$url<br/>\n"; + } elseif (substru($location, 0, 1)=="/") { + $url = "$scheme://$address$location"; + if (!isset($links[$url])) { + $links[$url] = $locationSource; + } else { + $links[$url] .= ",".$locationSource; + } + if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseLinks detected url:$url<br/>\n"; + } + } + if (defined("DEBUG") && DEBUG>=1) echo "YellowCommand::analyseLinks location:$locationSource<br/>\n"; + } else { + $statusCode = 500; + ++$this->errors; + echo "ERROR reading files: Can't read file '$fileName'!\n"; + } + } + $this->links = count($links); + } else { + $statusCode = 500; + ++$this->errors; + echo "ERROR reading files: Can't find files in directory '$path'!\n"; + } + return array($statusCode, $links); + } + + // Analyse link status + public function analyseStatus($path, $links) { + $statusCode = 200; + $remote = $broken = $redirect = $data = array(); + $staticUrl = $this->yellow->system->get("coreStaticUrl"); + $staticUrlLength = strlenu(rtrim($staticUrl, "/")); + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); + $staticLocations = $this->getContentLocations(true); + foreach ($links as $url=>$value) { + if (preg_match("#^$staticUrl#", $url)) { + $location = substru($url, $staticUrlLength); + $fileName = $path.substru($url, $staticUrlLength); + if (is_readable($fileName)) continue; + if (in_array($location, $staticLocations)) continue; + } + if (preg_match("/^(http|https):/", $url)) $remote[$url] = $value; + } + $remoteNow = 0; + uksort($remote, "strnatcasecmp"); + foreach ($remote as $url=>$value) { + echo "\rChecking static website ".$this->getProgressPercent(++$remoteNow, count($remote), 5, 95)."%... "; + if (defined("DEBUG") && DEBUG>=1) echo "YellowCommand::analyseStatus url:$url\n"; + $referer = "$scheme://$address$base".(($pos = strposu($value, ",")) ? substru($value, 0, $pos) : $value); + $statusCodeUrl = $this->getLinkStatus($url, $referer); + if ($statusCodeUrl!=200) { + $statusCode = max($statusCode, $statusCodeUrl); + $data[$url] = "$statusCodeUrl,$value"; + } + } + foreach ($data as $url=>$value) { + $locations = preg_split("/\s*,\s*/", $value); + $statusCodeUrl = array_shift($locations); + foreach ($locations as $location) { + if ($statusCodeUrl==302) continue; + if ($statusCodeUrl>=300 && $statusCodeUrl<=399) { + $redirect["$scheme://$address$base$location -> $url - ".$this->getStatusFormatted($statusCodeUrl)] = $statusCodeUrl; + } else { + $broken["$scheme://$address$base$location -> $url - ".$this->getStatusFormatted($statusCodeUrl)] = $statusCodeUrl; + } + ++$this->errors; + } + } + echo "\rChecking static website 100%... done\n"; + return array($statusCode, $broken, $redirect); + } + + // Show links + public function showLinks($data, $text) { + if (!empty($data)) { + echo "$text\n\n"; + uksort($data, "strnatcasecmp"); + $data = array_slice($data, 0, 99); + foreach ($data as $key=>$value) { + echo "- $key\n"; + } + echo "\n"; + } + } + + // Process command to clean static files + public function processCommandClean($command, $text) { + $statusCode = 0; + list($path, $location) = $this->yellow->toolbox->getTextArguments($text); + if (empty($location) || substru($location, 0, 1)=="/") { + $statusCode = $this->cleanStaticFiles($path, $location); + echo "Yellow $command: Static file".(empty($location) ? "s" : "")." ".($statusCode!=200 ? "not " : "")."cleaned\n"; + } else { + $statusCode = 400; + echo "Yellow $command: Invalid arguments\n"; + } + return $statusCode; + } + + // Clean static files and directories + public function cleanStaticFiles($path, $location) { + $statusCode = 200; + $path = rtrim(empty($path) ? $this->yellow->system->get("commandStaticBuildDirectory") : $path, "/"); + if (empty($location)) { + $statusCode = max($statusCode, $this->broadcastCommand("clean", "all")); + $statusCode = max($statusCode, $this->cleanStaticDirectory($path)); + } else { + if ($this->yellow->lookup->isFileLocation($location)) { + $fileName = $this->getStaticFile($path, $location, $statusCode); + $statusCode = $this->cleanStaticFile($fileName); + } else { + $statusCode = $this->cleanStaticDirectory($path.$location); + } + } + return $statusCode; + } + + // Clean static directory + public function cleanStaticDirectory($path) { + $statusCode = 200; + if (is_dir($path) && $this->checkStaticDirectory($path)) { + if (!$this->yellow->toolbox->deleteDirectory($path)) { + $statusCode = 500; + echo "ERROR cleaning files: Can't delete directory '$path'!\n"; + } + } + return $statusCode; + } + + // Clean static file + public function cleanStaticFile($fileName) { + $statusCode = 200; + if (is_file($fileName)) { + if (!$this->yellow->toolbox->deleteFile($fileName)) { + $statusCode = 500; + echo "ERROR cleaning files: Can't delete file '$fileName'!\n"; + } + } + return $statusCode; + } + + // Broadcast command to other extensions + public function broadcastCommand($command, $text) { + $statusCode = 0; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onCommand") && $key!="command") { + $statusCode = max($statusCode, $value["object"]->onCommand($command, $text)); + } + } + return $statusCode; + } + + // Process command to start built-in web server + public function processCommandServe($command, $text) { + list($path, $url) = $this->yellow->toolbox->getTextArguments($text); + if (empty($path) && is_dir($this->yellow->system->get("commandStaticBuildDirectory"))) { + $path = $this->yellow->system->get("commandStaticBuildDirectory"); + } + if (empty($url)) $url = "http://localhost:8000"; + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($url); + if ($scheme=="http" && !empty($address)) { + if (!preg_match("/\:\d+$/", $address)) $address .= ":8000"; + echo "Starting built-in web server on $scheme://$address/\n"; + echo "Press Ctrl-C to quit...\n"; + if (empty($path) || $path=="dynamic") { + system("php -S $address yellow.php", $returnStatus); + } else { + system("php -S $address -t $path", $returnStatus); + } + $statusCode = $returnStatus!=0 ? 500 : 200; + if ($statusCode!=200) echo "ERROR starting web server: Please check your arguments!\n"; + } else { + $statusCode = 400; + echo "Yellow $command: Invalid arguments\n"; + } + return $statusCode; + } + + // Process request for cached files + public function processRequestCache($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if (is_dir($this->yellow->system->get("coreCacheDirectory"))) { + $location .= $this->yellow->toolbox->getLocationArguments(); + $fileName = rtrim($this->yellow->system->get("coreCacheDirectory"), "/").$location; + if (!$this->yellow->lookup->isFileLocation($location)) $fileName .= $this->yellow->system->get("commandStaticDefaultFile"); + if (is_file($fileName) && is_readable($fileName) && !$this->yellow->isCommandLine()) { + $statusCode = $this->yellow->sendFile(200, $fileName, true); + } + } + return $statusCode; + } + + // Check static settings + public function checkStaticSettings() { + return !empty($this->yellow->system->get("coreStaticUrl")); + } + + // Check static directory + public function checkStaticDirectory($path) { + $ok = false; + if (!empty($path)) { + if ($path==rtrim($this->yellow->system->get("commandStaticBuildDirectory"), "/")) $ok = true; + if ($path==rtrim($this->yellow->system->get("coreTrashDirectory"), "/")) $ok = true; + if (is_file("$path/".$this->yellow->system->get("commandStaticDefaultFile"))) $ok = true; + if (is_file("$path/yellow.php")) $ok = false; + } + return $ok; + } + + // Return human readable status + public function getStatusFormatted($statusCode) { + return $this->yellow->toolbox->getHttpStatusFormatted($statusCode, true); + } + + // Return progress in percent + public function getProgressPercent($now, $total, $increments, $max) + { + $percent = intval(($max / $total) * $now); + if ($increments>1) $percent = intval($percent / $increments) * $increments; + return min($max, $percent); + } + + // Return static file + public function getStaticFile($path, $location, $statusCode) { + if ($statusCode<400) { + $fileName = $path.$location; + if (!$this->yellow->lookup->isFileLocation($location)) $fileName .= $this->yellow->system->get("commandStaticDefaultFile"); + } elseif ($statusCode==404) { + $fileName = $path."/".$this->yellow->system->get("commandStaticErrorFile"); + } + return $fileName; + } + + // Return static location + public function getStaticLocation($path, $fileName) { + $location = substru($fileName, strlenu($path)); + if (basename($location)==$this->yellow->system->get("commandStaticDefaultFile")) { + $defaultFileLength = strlenu($this->yellow->system->get("commandStaticDefaultFile")); + $location = substru($location, 0, -$defaultFileLength); + } + return $location; + } + + // Return static redirect + public function getStaticRedirect($location) { + $output = "<!DOCTYPE html><html>\n<head>\n"; + $output .= "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n"; + $output .= "<meta http-equiv=\"refresh\" content=\"0;url=".htmlspecialchars($location)."\" />\n"; + $output .= "</head>\n</html>"; + return $output; + } + + // Return content locations + public function getContentLocations($includeAll = false) { + $locations = array(); + $staticUrl = $this->yellow->system->get("coreStaticUrl"); + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); + $this->yellow->page->setRequestInformation($scheme, $address, $base, "", ""); + foreach ($this->yellow->content->index(true, true) as $page) { + if (preg_match("/exclude/i", $page->get("build")) && !$includeAll) continue; + if ($page->get("status")=="private" || $page->get("status")=="draft") continue; + array_push($locations, $page->location); + } + if (!$this->yellow->content->find("/") && $this->yellow->system->get("coreMultiLanguageMode")) array_unshift($locations, "/"); + return $locations; + } + + // Return media locations + public function getMediaLocations() { + $locations = array(); + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->system->get("coreMediaDirectory"), "/.*/", false, false); + foreach ($fileNames as $fileName) { + array_push($locations, "/".$fileName); + } + return $locations; + } + + // Return system locations + public function getSystemLocations() { + $locations = array(); + $regex = "/\.(css|gif|ico|js|jpg|png|svg|woff|woff2)$/"; + $extensionDirectoryLength = strlenu($this->yellow->system->get("coreExtensionDirectory")); + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->system->get("coreExtensionDirectory"), $regex, false, false); + foreach ($fileNames as $fileName) { + array_push($locations, $this->yellow->system->get("coreExtensionLocation").substru($fileName, $extensionDirectoryLength)); + } + $themeDirectoryLength = strlenu($this->yellow->system->get("coreThemeDirectory")); + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->system->get("coreThemeDirectory"), $regex, false, false); + foreach ($fileNames as $fileName) { + array_push($locations, $this->yellow->system->get("coreThemeLocation").substru($fileName, $themeDirectoryLength)); + } + return $locations; + } + + // Return extra locations + public function getExtraLocations($path) { + $locations = array(); + $pathIgnore = "($path/|". + $this->yellow->system->get("commandStaticBuildDirectory")."|". + $this->yellow->system->get("coreCacheDirectory")."|". + $this->yellow->system->get("coreContentDirectory")."|". + $this->yellow->system->get("coreMediaDirectory")."|". + $this->yellow->system->get("coreSystemDirectory").")"; + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive(".", "/.*/", false, false); + foreach ($fileNames as $fileName) { + $fileName = substru($fileName, 2); + if (preg_match("#^$pathIgnore#", $fileName) || $fileName=="yellow.php") continue; + array_push($locations, "/".$fileName); + } + return $locations; + } + + // Return link status + public function getLinkStatus($url, $referer) { + $curlHandle = curl_init(); + curl_setopt($curlHandle, CURLOPT_URL, $url); + curl_setopt($curlHandle, CURLOPT_REFERER, $referer); + curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; YellowCommand/".YellowCommand::VERSION."; LinkChecker)"); + curl_setopt($curlHandle, CURLOPT_NOBODY, 1); + curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30); + curl_exec($curlHandle); + $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); + curl_close($curlHandle); + if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::getLinkStatus status:$statusCode url:$url<br/>\n"; + return $statusCode; + } +} diff --git a/system/extensions/core.php b/system/extensions/core.php new file mode 100644 index 0000000..d863d4f --- /dev/null +++ b/system/extensions/core.php @@ -0,0 +1,3584 @@ +<?php +// Core extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/core + +class YellowCore { + const VERSION = "0.8.20"; + const RELEASE = "0.8.15"; + public $page; // current page + public $content; // content files + public $media; // media files + public $system; // system settings + public $user; // user settings + public $language; // language settings + public $extension; // extensions + public $lookup; // location and file lookup + public $toolbox; // toolbox with helper functions + public $text; // TODO: remove later, for backwards compatibility + public $extensions; // TODO: remove later, for backwards compatibility + + public function __construct() { + $this->checkRequirements(); + $this->page = new YellowPage($this); + $this->content = new YellowContent($this); + $this->media = new YellowMedia($this); + $this->system = new YellowSystem($this); + $this->user = new YellowUser($this); + $this->language = new YellowLanguage($this); + $this->extension = new YellowExtension($this); + $this->lookup = new YellowLookup($this); + $this->toolbox = new YellowToolbox(); + $this->text = new YellowText($this); // TODO: remove later, for backwards compatibility + $this->extensions = new YellowExtensions($this); // TODO: remove later, for backwards compatibility + $this->system->setDefault("sitename", "Yellow"); + $this->system->setDefault("author", "Yellow"); + $this->system->setDefault("email", "webmaster"); + $this->system->setDefault("language", "en"); + $this->system->setDefault("layout", "default"); + $this->system->setDefault("theme", "default"); + $this->system->setDefault("parser", "markdown"); + $this->system->setDefault("status", "public"); + $this->system->setDefault("coreStaticUrl", ""); + $this->system->setDefault("coreServerUrl", "auto"); + $this->system->setDefault("coreServerTimezone", "UTC"); + $this->system->setDefault("coreMultiLanguageMode", "0"); + $this->system->setDefault("coreMediaLocation", "/media/"); + $this->system->setDefault("coreDownloadLocation", "/media/downloads/"); + $this->system->setDefault("coreImageLocation", "/media/images/"); + $this->system->setDefault("coreExtensionLocation", "/media/extensions/"); + $this->system->setDefault("coreThemeLocation", "/media/themes/"); + $this->system->setDefault("coreMediaDirectory", "media/"); + $this->system->setDefault("coreDownloadDirectory", "media/downloads/"); + $this->system->setDefault("coreImageDirectory", "media/images/"); + $this->system->setDefault("coreSystemDirectory", "system/"); + $this->system->setDefault("coreExtensionDirectory", "system/extensions/"); + $this->system->setDefault("coreSettingDirectory", "system/settings/"); + $this->system->setDefault("coreLayoutDirectory", "system/layouts/"); + $this->system->setDefault("coreThemeDirectory", "system/themes/"); + $this->system->setDefault("coreTrashDirectory", "system/trash/"); + $this->system->setDefault("coreCacheDirectory", "cache/"); + $this->system->setDefault("coreContentDirectory", "content/"); + $this->system->setDefault("coreContentRootDirectory", "default/"); + $this->system->setDefault("coreContentHomeDirectory", "home/"); + $this->system->setDefault("coreContentSharedDirectory", "shared/"); + $this->system->setDefault("coreContentDefaultFile", "page.md"); + $this->system->setDefault("coreContentErrorFile", "page-error-(.*).md"); + $this->system->setDefault("coreContentExtension", ".md"); + $this->system->setDefault("coreDownloadExtension", ".download"); + $this->system->setDefault("coreSystemFile", "system.ini"); + $this->system->setDefault("coreUserFile", "user.ini"); + $this->system->setDefault("coreLanguageFile", "language.ini"); + $this->system->setDefault("coreLogFile", "yellow.log"); + $this->language->setDefault("coreDateFormatShort"); + $this->language->setDefault("coreDateFormatMedium"); + $this->language->setDefault("coreDateFormatLong"); + } + + public function __destruct() { + $this->shutdown(); + } + + // Check requirements + public function checkRequirements() { + $troubleshooting = PHP_SAPI!="cli" ? "<a href=\"https://datenstrom.se/yellow/help/troubleshooting\">See troubleshooting</a>." : ""; + version_compare(PHP_VERSION, "5.6", ">=") || die("Datenstrom Yellow requires PHP 5.6 or higher! $troubleshooting\n"); + extension_loaded("curl") || die("Datenstrom Yellow requires PHP curl extension! $troubleshooting\n"); + extension_loaded("gd") || die("Datenstrom Yellow requires PHP gd extension! $troubleshooting\n"); + extension_loaded("exif") || die("Datenstrom Yellow requires PHP exif extension! $troubleshooting\n"); + extension_loaded("mbstring") || die("Datenstrom Yellow requires PHP mbstring extension! $troubleshooting\n"); + extension_loaded("zip") || die("Datenstrom Yellow requires PHP zip extension! $troubleshooting\n"); + mb_internal_encoding("UTF-8"); + if (defined("DEBUG") && DEBUG>=1) { + ini_set("display_errors", 1); + error_reporting(E_ALL); + } + error_reporting(E_ALL ^ E_NOTICE); // TODO: remove later, for backwards compatibility + } + + // Handle initialisation + public function load() { + $this->system->load($this->system->get("coreSettingDirectory").$this->system->get("coreSystemFile")); + $this->user->load($this->system->get("coreSettingDirectory").$this->system->get("coreUserFile")); + $this->language->load($this->system->get("coreExtensionDirectory").".*\.txt"); + $this->language->load($this->system->get("coreSettingDirectory").$this->system->get("coreLanguageFile")); + $this->extension->load($this->system->get("coreExtensionDirectory")); + $this->lookup->detectFileSystem(); + $this->startup(); + } + + // Handle request + public function request() { + $statusCode = 0; + $this->toolbox->timerStart($time); + ob_start(); + list($scheme, $address, $base, $location, $fileName) = $this->getRequestInformation(); + $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName); + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onRequest")) { + $this->lookup->requestHandler = $key; + $statusCode = $value["object"]->onRequest($scheme, $address, $base, $location, $fileName); + if ($statusCode!=0) break; + } + } + if ($statusCode==0) { + $this->lookup->requestHandler = "core"; + $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName, true); + } + if ($this->page->isExisting("pageError")) $statusCode = $this->processRequestError(); + ob_end_flush(); + $this->toolbox->timerStop($time); + if (defined("DEBUG") && DEBUG>=1 && $this->lookup->isContentFile($fileName)) { + echo "YellowCore::request status:$statusCode time:$time ms<br/>\n"; + } + return $statusCode; + } + + // Process request + public function processRequest($scheme, $address, $base, $location, $fileName, $cacheable) { + $statusCode = 0; + if (is_readable($fileName)) { + if ($this->lookup->isRequestCleanUrl($location)) { + $location = $location.$this->toolbox->getLocationArgumentsCleanUrl(); + $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->sendStatus(303, $location); + } + } else { + if ($this->lookup->isRedirectLocation($location)) { + $location = $this->lookup->getRedirectLocation($location); + $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->sendStatus(301, $location); + } + } + if ($statusCode==0) { + if ($this->lookup->isContentFile($fileName) || !is_readable($fileName)) { + $fileName = $this->readPage($scheme, $address, $base, $location, $fileName, $cacheable, + max(is_readable($fileName) ? 200 : 404, $this->page->statusCode), $this->page->get("pageError")); + $statusCode = $this->sendPage(); + } else { + $statusCode = $this->sendFile(200, $fileName, true); + } + } + if (defined("DEBUG") && DEBUG>=1 && $this->lookup->isContentFile($fileName)) { + echo "YellowCore::processRequest file:$fileName<br/>\n"; + } + return $statusCode; + } + + // Process request with error + public function processRequestError() { + ob_clean(); + $fileName = $this->readPage($this->page->scheme, $this->page->address, $this->page->base, + $this->page->location, $this->page->fileName, $this->page->cacheable, $this->page->statusCode, + $this->page->get("pageError")); + $statusCode = $this->sendPage(); + if (defined("DEBUG") && DEBUG>=1) echo "YellowCore::processRequestError file:$fileName<br/>\n"; + return $statusCode; + } + + // Read page + public function readPage($scheme, $address, $base, $location, $fileName, $cacheable, $statusCode, $pageError) { + if ($statusCode>=400) { + $locationError = $this->content->getHomeLocation($this->page->location).$this->system->get("coreContentSharedDirectory"); + $fileNameError = $this->lookup->findFileFromLocation($locationError, true).$this->system->get("coreContentErrorFile"); + $fileNameError = str_replace("(.*)", $statusCode, $fileNameError); + if (is_file($fileNameError)) { + $rawData = $this->toolbox->readFile($fileNameError); + } else { + $language = $this->lookup->findLanguageFromFile($fileName, $this->system->get("language")); + $rawData = "---\nTitle: ".$this->language->getText("coreError${statusCode}Title", $language)."\n"; + $rawData .= "Layout: error\n---\n".$this->language->getText("coreError${statusCode}Text", $language); + } + $cacheable = false; + } else { + $rawData = $this->toolbox->readFile($fileName); + } + $this->page = new YellowPage($this); + $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName); + $this->page->parseData($rawData, $cacheable, $statusCode, $pageError); + $this->language->set($this->page->get("language")); + $this->page->parseContent(); + return $fileName; + } + + // Send page response + public function sendPage() { + $this->page->parsePage(); + $statusCode = $this->page->statusCode; + $lastModifiedFormatted = $this->page->getHeader("Last-Modified"); + if ($statusCode==200 && $this->page->isCacheable() && $this->toolbox->isNotModified($lastModifiedFormatted)) { + $statusCode = 304; + @header($this->toolbox->getHttpStatusFormatted($statusCode)); + } else { + @header($this->toolbox->getHttpStatusFormatted($statusCode)); + foreach ($this->page->headerData as $key=>$value) { + @header("$key: $value"); + } + if (!is_null($this->page->outputData)) echo $this->page->outputData; + } + if (defined("DEBUG") && DEBUG>=1) { + foreach ($this->page->headerData as $key=>$value) { + echo "YellowCore::sendPage $key: $value<br/>\n"; + } + $language = $this->page->get("language"); + $layout = $this->page->get("layout"); + $theme = $this->page->get("theme"); + $parser = $this->page->get("parser"); + echo "YellowCore::sendPage language:$language layout:$layout theme:$theme parser:$parser<br/>\n"; + } + return $statusCode; + } + + // Send file response + public function sendFile($statusCode, $fileName, $cacheable) { + $lastModifiedFormatted = $this->toolbox->getHttpDateFormatted($this->toolbox->getFileModified($fileName)); + if ($statusCode==200 && $cacheable && $this->toolbox->isNotModified($lastModifiedFormatted)) { + $statusCode = 304; + @header($this->toolbox->getHttpStatusFormatted($statusCode)); + } else { + @header($this->toolbox->getHttpStatusFormatted($statusCode)); + if (!$cacheable) @header("Cache-Control: no-cache, no-store"); + @header("Content-Type: ".$this->toolbox->getMimeContentType($fileName)); + @header("Last-Modified: ".$lastModifiedFormatted); + echo $this->toolbox->readFile($fileName); + } + return $statusCode; + } + + // Send data response + public function sendData($statusCode, $rawData, $fileName, $cacheable) { + @header($this->toolbox->getHttpStatusFormatted($statusCode)); + if (!$cacheable) @header("Cache-Control: no-cache, no-store"); + @header("Content-Type: ".$this->toolbox->getMimeContentType($fileName)); + @header("Last-Modified: ".$this->toolbox->getHttpDateFormatted(time())); + echo $rawData; + return $statusCode; + } + + // Send status response + public function sendStatus($statusCode, $location = "") { + if (!empty($location)) $this->page->clean($statusCode, $location); + @header($this->toolbox->getHttpStatusFormatted($statusCode)); + foreach ($this->page->headerData as $key=>$value) { + @header("$key: $value"); + } + if (defined("DEBUG") && DEBUG>=1) { + foreach ($this->page->headerData as $key=>$value) { + echo "YellowCore::sendStatus $key: $value<br/>\n"; + } + } + return $statusCode; + } + + // Handle command + public function command($line = "") { + $statusCode = 0; + $this->toolbox->timerStart($time); + list($command, $text) = $this->getCommandInformation($line); + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onCommand")) { + $this->lookup->commandHandler = $key; + $statusCode = $value["object"]->onCommand($command, $text); + if ($statusCode!=0) break; + } + } + if ($statusCode==0 && empty($text)) { + $lineCounter = 0; + echo "Datenstrom Yellow is for people who make small websites.\n"; + foreach ($this->getCommandHelp() as $line) { + echo(++$lineCounter>1 ? " " : "Syntax: ")."php yellow.php $line\n"; + } + $statusCode = 200; + } + if ($statusCode==0) { + $this->lookup->commandHandler = "core"; + $statusCode = 400; + echo "Yellow $command: Command not found\n"; + } + $this->toolbox->timerStop($time); + if (defined("DEBUG") && DEBUG>=1) { + echo "YellowCore::command status:$statusCode time:$time ms<br/>\n"; + } + return $statusCode<400 ? 0 : 1; + } + + // Handle startup + public function startup() { + if ($this->isLoaded()) { + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onStartup")) $value["object"]->onStartup(); + } + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("startup"); + } + } + } + + // Handle shutdown + public function shutdown() { + if ($this->isLoaded()) { + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onShutdown")) $value["object"]->onShutdown(); + } + } + } + + // Handle logging + public function log($action, $message) { + $statusCode = 0; + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onLog")) { + $statusCode = $value["object"]->onLog($action, $message); + if ($statusCode!=0) break; + } + } + if ($statusCode==0) { + $line = date("Y-m-d H:i:s")." ".trim($action)." ".trim($message)."\n"; + $this->toolbox->appendFile($this->system->get("coreExtensionDirectory").$this->system->get("coreLogFile"), $line); + } + } + + // Include layout + public function layout($name, $arguments = null) { + $this->lookup->layoutArguments = func_get_args(); + $this->page->includeLayout($name); + } + + // Return layout arguments + public function getLayoutArguments($sizeMin = 9) { + return array_pad($this->lookup->layoutArguments, $sizeMin, null); + } + + public function getLayoutArgs($sizeMin = 9) { // TODO: remove later, for backwards compatibility + return $this->getLayoutArguments($sizeMin); + } + + // Return request information + public function getRequestInformation($scheme = "", $address = "", $base = "") { + if (empty($scheme) && empty($address) && empty($base)) { + $url = $this->system->get("coreServerUrl"); + if ($url=="auto" || $this->isCommandLine()) $url = $this->toolbox->detectServerUrl(); + list($scheme, $address, $base) = $this->lookup->getUrlInformation($url); + $this->system->set("coreServerScheme", $scheme); + $this->system->set("coreServerAddress", $address); + $this->system->set("coreServerBase", $base); + if (defined("DEBUG") && DEBUG>=3) echo "YellowCore::getRequestInformation $scheme://$address$base<br/>\n"; + } + $location = substru($this->toolbox->detectServerLocation(), strlenu($base)); + if (empty($fileName)) $fileName = $this->lookup->findFileFromSystem($location); + if (empty($fileName)) $fileName = $this->lookup->findFileFromMedia($location); + if (empty($fileName)) $fileName = $this->lookup->findFileFromLocation($location); + return array($scheme, $address, $base, $location, $fileName); + } + + // Return command information + public function getCommandInformation($line = "") { + if (empty($line)) { + $line = $this->toolbox->getTextString(array_slice($this->toolbox->getServer("argv"), 1)); + if (defined("DEBUG") && DEBUG>=3) echo "YellowCore::getCommandInformation $line<br/>\n"; + } + return $this->toolbox->getTextList($line, " ", 2); + } + + // Return command help + public function getCommandHelp() { + $data = array(); + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onCommandHelp")) { + foreach (preg_split("/[\r\n]+/", $value["object"]->onCommandHelp()) as $line) { + list($command, $dummy) = $this->toolbox->getTextList($line, " ", 2); + if (!empty($command) && !isset($data[$command])) $data[$command] = $line; + } + } + } + uksort($data, "strnatcasecmp"); + return $data; + } + + // Return request handler + public function getRequestHandler() { + return $this->lookup->requestHandler; + } + + // Return command handler + public function getCommandHandler() { + return $this->lookup->commandHandler; + } + + // Check if running at command line + public function isCommandLine() { + return isset($this->lookup->commandHandler); + } + + // Check if all extensions loaded + public function isLoaded() { + return isset($this->extension->data); + } +} + +class YellowPage { + public $yellow; // access to API + public $scheme; // server scheme + public $address; // server address + public $base; // base location + public $location; // page location + public $fileName; // content file name + public $rawData; // raw data of page + public $metaDataOffsetBytes; // meta data offset + public $metaData; // meta data + public $pageCollections; // additional pages + public $sharedPages; // shared pages + public $headerData; // response header + public $outputData; // response output + public $parser; // content parser + public $parserData; // content data of page + public $available; // page is available? (boolean) + public $visible; // page is visible location? (boolean) + public $active; // page is active location? (boolean) + public $cacheable; // page is cacheable? (boolean) + public $lastModified; // last modification date + public $statusCode; // status code + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->metaData = new YellowArray(); + $this->pageCollections = array(); + $this->sharedPages = array(); + $this->headerData = array(); + } + + // Set request information + public function setRequestInformation($scheme, $address, $base, $location, $fileName) { + $this->scheme = $scheme; + $this->address = $address; + $this->base = $base; + $this->location = $location; + $this->fileName = $fileName; + } + + // Parse page data + public function parseData($rawData, $cacheable, $statusCode, $pageError = "") { + $this->rawData = $rawData; + $this->parser = null; + $this->parserData = ""; + $this->available = $this->yellow->lookup->isAvailableLocation($this->location, $this->fileName); + $this->visible = true; + $this->active = $this->yellow->lookup->isActiveLocation($this->location, $this->yellow->page->location); + $this->cacheable = $cacheable; + $this->lastModified = 0; + $this->statusCode = $statusCode; + $this->parseMeta($pageError); + } + + // Parse page data update + public function parseDataUpdate() { + if ($this->statusCode==0) { + $this->rawData = $this->yellow->toolbox->readFile($this->fileName); + $this->statusCode = 200; + $this->parseMeta(); + } + } + + // Parse page meta data + public function parseMeta($pageError = "") { + $this->metaData = new YellowArray(); + if (!is_null($this->rawData)) { + $this->set("title", $this->yellow->toolbox->createTextTitle($this->location)); + $this->set("language", $this->yellow->lookup->findLanguageFromFile($this->fileName, $this->yellow->system->get("language"))); + $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName))); + $this->parseMetaRaw(array("sitename", "author", "layout", "theme", "parser", "status")); + $titleHeader = ($this->location==$this->yellow->content->getHomeLocation($this->location)) ? + $this->get("sitename") : $this->get("title")." - ".$this->get("sitename"); + if (!$this->isExisting("titleContent")) $this->set("titleContent", $this->get("title")); + if (!$this->isExisting("titleNavigation")) $this->set("titleNavigation", $this->get("title")); + if (!$this->isExisting("titleHeader")) $this->set("titleHeader", $titleHeader); + if ($this->get("status")=="unlisted") $this->visible = false; + if ($this->get("status")=="shared") $this->available = false; + $this->set("pageRead", $this->yellow->lookup->normaliseUrl( + $this->yellow->system->get("coreServerScheme"), + $this->yellow->system->get("coreServerAddress"), + $this->yellow->system->get("coreServerBase"), + $this->location)); + $this->set("pageEdit", $this->yellow->lookup->normaliseUrl( + $this->yellow->system->get("coreServerScheme"), + $this->yellow->system->get("coreServerAddress"), + $this->yellow->system->get("coreServerBase"), + rtrim($this->yellow->system->get("editLocation"), "/").$this->location)); + $this->setPage("main", $this); + } else { + $this->set("type", $this->yellow->toolbox->getFileType($this->fileName)); + $this->set("group", $this->yellow->toolbox->getFileGroup($this->fileName, $this->yellow->system->get("coreMediaDirectory"))); + $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName))); + } + if (!empty($pageError)) $this->set("pageError", $pageError); + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParseMeta")) $value["object"]->onParseMeta($this); + } + } + + // Parse page meta data from raw data + public function parseMetaRaw($defaultKeys) { + foreach ($defaultKeys as $key) { + $value = $this->yellow->system->get($key); + if (!empty($key) && !strempty($value)) $this->set($key, $value); + } + if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+/s", $this->rawData, $parts)) { + $this->metaDataOffsetBytes = strlenb($parts[0]); + foreach (preg_split("/[\r\n]+/", $parts[2]) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!empty($matches[1]) && !strempty($matches[2])) $this->set($matches[1], $matches[2]); + } + } + } elseif (preg_match("/^(\xEF\xBB\xBF)?([^\r\n]+)[\r\n]+=+[\r\n]+/", $this->rawData, $parts)) { + $this->metaDataOffsetBytes = strlenb($parts[0]); + $this->set("title", $parts[2]); + } + } + + // Parse page content on demand + public function parseContent($sizeMax = 0) { + if (!is_null($this->rawData) && !is_object($this->parser)) { + if ($this->yellow->extension->isExisting($this->get("parser"))) { + $value = $this->yellow->extension->data[$this->get("parser")]; + if (method_exists($value["object"], "onParseContentRaw")) { + $this->parser = $value["object"]; + $this->parserData = $this->getContent(true, $sizeMax); + $this->parserData = preg_replace("/@pageRead/i", $this->get("pageRead"), $this->parserData); + $this->parserData = preg_replace("/@pageEdit/i", $this->get("pageEdit"), $this->parserData); + $this->parserData = $this->parser->onParseContentRaw($this, $this->parserData); + $this->parserData = $this->yellow->toolbox->normaliseData($this->parserData, "html"); + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParseContentHtml")) { + $output = $value["object"]->onParseContentHtml($this, $this->parserData); + if (!is_null($output)) $this->parserData = $output; + } + } + } + } else { + $this->parserData = $this->getContent(true, $sizeMax); + $this->parserData = preg_replace("/\[yellow error\]/i", $this->get("pageError"), $this->parserData); + } + if (!$this->isExisting("description")) { + $description = $this->yellow->toolbox->createTextDescription($this->parserData, 150); + $this->set("description", !empty($description) ? $description : $this->get("title")); + } + if (defined("DEBUG") && DEBUG>=3) echo "YellowPage::parseContent location:".$this->location."<br/>\n"; + } + } + + // Parse page content shortcut + public function parseContentShortcut($name, $text, $type) { + $output = null; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParseContentShortcut")) { + $output = $value["object"]->onParseContentShortcut($this, $name, $text, $type); + if (!is_null($output)) break; + } + } + if (is_null($output)) { + if ($name=="yellow" && $type=="inline") { + if ($text=="about") { + $output = "Datenstrom Yellow ".YellowCore::RELEASE."<br />\n"; + $dataCurrent = $this->yellow->extension->data; + uksort($dataCurrent, "strnatcasecmp"); + foreach ($dataCurrent as $key=>$value) { + $output .= ucfirst($key)." ".$value["version"]."<br />\n"; + } + } + if ($text=="error") $output = $this->get("pageError"); + if ($text=="log") { + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreLogFile"); + $fileHandle = @fopen($fileName, "r"); + if ($fileHandle) { + $dataBufferSize = 512; + fseek($fileHandle, max(0, filesize($fileName) - $dataBufferSize)); + $dataBuffer = fread($fileHandle, $dataBufferSize); + if (strlenb($dataBuffer)==$dataBufferSize) { + $dataBuffer = ($pos = strposu($dataBuffer, "\n")) ? substru($dataBuffer, $pos+1) : $dataBuffer; + } + fclose($fileHandle); + } + $output = str_replace("\n", "<br />\n", htmlspecialchars($dataBuffer)); + } + } + } + if (defined("DEBUG") && DEBUG>=3 && !empty($name)) echo "YellowPage::parseContentShortcut name:$name type:$type<br/>\n"; + return $output; + } + + // Parse page + public function parsePage() { + $this->parsePageLayout($this->get("layout")); + if (!$this->isCacheable()) $this->setHeader("Cache-Control", "no-cache, no-store"); + if (!$this->isHeader("Content-Type")) $this->setHeader("Content-Type", "text/html; charset=utf-8"); + if (!$this->isHeader("Content-Modified")) $this->setHeader("Content-Modified", $this->getModified(true)); + if (!$this->isHeader("Last-Modified")) $this->setHeader("Last-Modified", $this->getLastModified(true)); + $fileNameTheme = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; + if (!is_file($fileNameTheme)) { + $this->error(500, "Theme '".$this->get("theme")."' does not exist!"); + } + if (!is_object($this->parser)) { + $this->error(500, "Parser '".$this->get("parser")."' does not exist!"); + } + if (!$this->yellow->language->isExisting($this->get("language"))) { + $this->error(500, "Language '".$this->get("language")."' does not exist!"); + } + if ($this->yellow->lookup->isNestedLocation($this->location, $this->fileName, true)) { + $this->error(500, "Folder '".dirname($this->fileName)."' may not contain subfolders!"); + } + if ($this->yellow->getRequestHandler()=="core" && $this->isExisting("redirect") && $this->statusCode==200) { + $location = $this->yellow->lookup->normaliseLocation($this->get("redirect"), $this->location); + $location = $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, "", $location); + $this->clean(301, $location); + } + if ($this->yellow->getRequestHandler()=="core" && !$this->isAvailable() && $this->statusCode==200) { + $this->error(404); + } + if ($this->isExisting("pageClean")) $this->outputData = null; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParsePageOutput")) { + $output = $value["object"]->onParsePageOutput($this, $this->outputData); + if (!is_null($output)) $this->outputData = $output; + } + } + } + + // Parse page layout + public function parsePageLayout($name) { + foreach ($this->yellow->content->getShared($this->location) as $page) { + $this->sharedPages[basename($page->location)] = $page; + $page->sharedPages["main"] = $this; + } + $this->outputData = null; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParsePageLayout")) { + $value["object"]->onParsePageLayout($this, $name); + } + } + if (is_null($this->outputData)) { + ob_start(); + $this->includeLayout($name); + $this->outputData = ob_get_contents(); + ob_end_clean(); + } + } + + // Include page layout + public function includeLayout($name) { + $fileNameLayoutNormal = $this->yellow->system->get("coreLayoutDirectory").$this->yellow->lookup->normaliseName($name).".html"; + $fileNameLayoutTheme = $this->yellow->system->get("coreLayoutDirectory"). + $this->yellow->lookup->normaliseName($this->get("theme"))."-".$this->yellow->lookup->normaliseName($name).".html"; + if (is_file($fileNameLayoutTheme)) { + if (defined("DEBUG") && DEBUG>=2) echo "YellowPage::includeLayout file:$fileNameLayoutTheme<br>\n"; + $this->setLastModified(filemtime($fileNameLayoutTheme)); + require($fileNameLayoutTheme); + } elseif (is_file($fileNameLayoutNormal)) { + if (defined("DEBUG") && DEBUG>=2) echo "YellowPage::includeLayout file:$fileNameLayoutNormal<br>\n"; + $this->setLastModified(filemtime($fileNameLayoutNormal)); + require($fileNameLayoutNormal); + } else { + $this->error(500, "Layout '$name' does not exist!"); + echo "Layout error<br/>\n"; + } + } + + // Set page setting + public function set($key, $value) { + $this->metaData[$key] = $value; + } + + // Return page setting + public function get($key) { + return $this->isExisting($key) ? $this->metaData[$key] : ""; + } + + // Return page setting, HTML encoded + public function getHtml($key) { + return htmlspecialchars($this->get($key)); + } + + // Return page setting as language specific date + public function getDate($key, $format = "") { + if (!empty($format)) { + $format = $this->yellow->language->getText($format); + } else { + $format = $this->yellow->language->getText("coreDateFormatMedium"); + } + return $this->yellow->language->getDateFormatted(strtotime($this->get($key)), $format); + } + + // Return page setting as language specific date, HTML encoded + public function getDateHtml($key, $format = "") { + return htmlspecialchars($this->getDate($key, $format)); + } + + // Return page setting as language specific date, relative to today + public function getDateRelative($key, $format = "", $daysLimit = 30) { + if (!empty($format)) { + $format = $this->yellow->language->getText($format); + } else { + $format = $this->yellow->language->getText("coreDateFormatMedium"); + } + return $this->yellow->language->getDateRelative(strtotime($this->get($key)), $format, $daysLimit); + } + + // Return page setting as language specific date, relative to today, HTML encoded + public function getDateRelativeHtml($key, $format = "", $daysLimit = 30) { + return htmlspecialchars($this->getDateRelative($key, $format, $daysLimit)); + } + + // Return page setting as date + public function getDateFormatted($key, $format) { + return $this->yellow->language->getDateFormatted(strtotime($this->get($key)), $format); + } + + // Return page setting as date, HTML encoded + public function getDateFormattedHtml($key, $format) { + return htmlspecialchars($this->getDateFormatted($key, $format)); + } + + // Return page content, HTML encoded or raw format + public function getContent($rawFormat = false, $sizeMax = 0) { + if ($rawFormat) { + $this->parseDataUpdate(); + $text = substrb($this->rawData, $this->metaDataOffsetBytes); + } else { + $this->parseContent($sizeMax); + $text = $this->parserData; + } + return $sizeMax ? substrb($text, 0, $sizeMax) : $text; + } + + // Return parent page, null if none + public function getParent() { + $parentLocation = $this->yellow->content->getParentLocation($this->location); + return $this->yellow->content->find($parentLocation); + } + + // Return top-level parent page, null if none + public function getParentTop($homeFallback = false) { + $parentTopLocation = $this->yellow->content->getParentTopLocation($this->location); + if (!$this->yellow->content->find($parentTopLocation) && $homeFallback) { + $parentTopLocation = $this->yellow->content->getHomeLocation($this->location); + } + return $this->yellow->content->find($parentTopLocation); + } + + // Return page collection with pages on the same level + public function getSiblings($showInvisible = false) { + $parentLocation = $this->yellow->content->getParentLocation($this->location); + return $this->yellow->content->getChildren($parentLocation, $showInvisible); + } + + // Return page collection with child pages + public function getChildren($showInvisible = false) { + return $this->yellow->content->getChildren($this->location, $showInvisible); + } + + // Return page collection with child pages recursively + public function getChildrenRecursive($showInvisible = false, $levelMax = 0) { + return $this->yellow->content->getChildrenRecursive($this->location, $showInvisible, $levelMax); + } + + // Set page collection with additional pages + public function setPages($key, $pages) { + $this->pageCollections[$key] = $pages; + } + + // Return page collection with additional pages + public function getPages($key) { + return isset($this->pageCollections[$key]) ? $this->pageCollections[$key] : new YellowPageCollection($this->yellow); + } + + // Set shared page + public function setPage($key, $page) { + $this->sharedPages[$key] = $page; + } + + // Return shared page + public function getPage($key) { + return isset($this->sharedPages[$key]) ? $this->sharedPages[$key] : new YellowPage($this->yellow); + } + + // Return page URL + public function getUrl() { + return $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, $this->base, $this->location); + } + + // Return page base + public function getBase($multiLanguage = false) { + return $multiLanguage ? rtrim($this->base.$this->yellow->content->getHomeLocation($this->location), "/") : $this->base; + } + + // Return page location + public function getLocation($absoluteLocation = false) { + return $absoluteLocation ? $this->base.$this->location : $this->location; + } + + // Set page request argument + public function setRequest($key, $value) { + $_REQUEST[$key] = $value; + } + + // Return page request argument + public function getRequest($key) { + return isset($_REQUEST[$key]) ? $_REQUEST[$key] : ""; + } + + // Return page request argument, HTML encoded + public function getRequestHtml($key) { + return htmlspecialchars($this->getRequest($key)); + } + + // Set page response header + public function setHeader($key, $value) { + $this->headerData[$key] = $value; + } + + // Return page response header + public function getHeader($key) { + return $this->isHeader($key) ? $this->headerData[$key] : ""; + } + + // Return page extra data + public function getExtra($name) { + $output = ""; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParsePageExtra")) { + $outputExtension = $value["object"]->onParsePageExtra($this, $name); + if (!is_null($outputExtension)) $output .= $outputExtension; + } + } + if ($name=="header") { + $fileNameTheme = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; + if (is_file($fileNameTheme)) { + $locationTheme = $this->yellow->system->get("coreServerBase"). + $this->yellow->system->get("coreThemeLocation").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; + $output .= "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"$locationTheme\" />\n"; + } + $fileNameScript = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".js"; + if (is_file($fileNameScript)) { + $locationScript = $this->yellow->system->get("coreServerBase"). + $this->yellow->system->get("coreThemeLocation").$this->yellow->lookup->normaliseName($this->get("theme")).".js"; + $output .= "<script type=\"text/javascript\" src=\"$locationScript\"></script>\n"; + } + $fileNameFavicon = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".png"; + if (is_file($fileNameFavicon)) { + $locationFavicon = $this->yellow->system->get("coreServerBase"). + $this->yellow->system->get("coreThemeLocation").$this->yellow->lookup->normaliseName($this->get("theme")).".png"; + $output .= "<link rel=\"icon\" type=\"image/png\" href=\"$locationFavicon\" />\n"; + } + } + return $output; + } + + // Set page response output + public function setOutput($output) { + $this->outputData = $output; + } + + // Return page modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + $modified = strtotime($this->get("modified")); + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified; + } + + // Set last modification date, Unix time + public function setLastModified($modified) { + $this->lastModified = max($this->lastModified, $modified); + } + + // Return last modification date, Unix time or HTTP format + public function getLastModified($httpFormat = false) { + $lastModified = max($this->lastModified, $this->getModified(), $this->yellow->system->getModified(), + $this->yellow->language->getModified(), $this->yellow->extension->getModified()); + foreach ($this->pageCollections as $pages) $lastModified = max($lastModified, $pages->getModified()); + foreach ($this->sharedPages as $page) $lastModified = max($lastModified, $page->getModified()); + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($lastModified) : $lastModified; + } + + // Return page status code, number or HTTP format + public function getStatusCode($httpFormat = false) { + $statusCode = $this->statusCode; + if ($httpFormat) { + $statusCode = $this->yellow->toolbox->getHttpStatusFormatted($statusCode); + if ($this->isExisting("pageError")) $statusCode .= ": ".$this->get("pageError"); + } + return $statusCode; + } + + // Respond with error page + public function error($statusCode, $pageError = "") { + if (!$this->isExisting("pageError") && $statusCode>0) { + $this->statusCode = $statusCode; + $this->set("pageError", empty($pageError) ? "Layout error!" : $pageError); + } + } + + // Respond with status code, no page content + public function clean($statusCode, $location = "") { + if (!$this->isExisting("pageClean") && $statusCode>0) { + $this->statusCode = $statusCode; + $this->lastModified = 0; + $this->headerData = array(); + if (!empty($location)) { + $this->setHeader("Location", $location); + $this->setHeader("Cache-Control", "no-cache, no-store"); + } + $this->set("pageClean", (string)$statusCode); + } + } + + // Check if page is available + public function isAvailable() { + return $this->available; + } + + // Check if page is visible + public function isVisible() { + return $this->visible; + } + + // Check if page is within current HTTP request + public function isActive() { + return $this->active; + } + + // Check if page is cacheable + public function isCacheable() { + return $this->cacheable; + } + + // Check if page with error + public function isError() { + return $this->statusCode>=400; + } + + // Check if page setting exists + public function isExisting($key) { + return isset($this->metaData[$key]); + } + + // Check if request argument exists + public function isRequest($key) { + return isset($_REQUEST[$key]); + } + + // Check if response header exists + public function isHeader($key) { + return isset($this->headerData[$key]); + } + + // Check if shared page exists + public function isPage($key) { + return isset($this->sharedPages[$key]); + } +} + +class YellowPageCollection extends ArrayObject { + public $yellow; // access to API + public $filterValue; // current page filter value + public $paginationNumber; // current page number in pagination + public $paginationCount; // highest page number in pagination + + public function __construct($yellow) { + parent::__construct(array()); + $this->yellow = $yellow; + } + + // Filter page collection by page setting + public function filter($key, $value, $exactMatch = true) { + $array = array(); + $value = str_replace(" ", "-", strtoloweru($value)); + $valueLength = strlenu($value); + $this->filterValue = ""; + foreach ($this->getArrayCopy() as $page) { + if ($page->isExisting($key)) { + foreach (preg_split("/\s*,\s*/", $page->get($key)) as $pageValue) { + $pageValueLength = $exactMatch ? strlenu($pageValue) : $valueLength; + if ($value==substru(str_replace(" ", "-", strtoloweru($pageValue)), 0, $pageValueLength)) { + if (empty($this->filterValue)) $this->filterValue = substru($pageValue, 0, $pageValueLength); + array_push($array, $page); + break; + } + } + } + } + $this->exchangeArray($array); + return $this; + } + + // Filter page collection by file name + public function match($regex = "/.*/") { + $array = array(); + foreach ($this->getArrayCopy() as $page) { + if (preg_match($regex, $page->fileName)) array_push($array, $page); + } + $this->exchangeArray($array); + return $this; + } + + // Sort page collection by page setting + public function sort($key, $ascendingOrder = true) { + $array = $this->getArrayCopy(); + $sortIndex = 0; + foreach ($array as $page) { + $page->set("sortindex", ++$sortIndex); + } + $callback = function ($a, $b) use ($key, $ascendingOrder) { + $result = $ascendingOrder ? + strnatcasecmp($a->get($key), $b->get($key)) : + strnatcasecmp($b->get($key), $a->get($key)); + return $result==0 ? $a->get("sortindex") - $b->get("sortindex") : $result; + }; + usort($array, $callback); + $this->exchangeArray($array); + return $this; + } + + // Sort page collection by settings similarity + public function similar($page, $ascendingOrder = false) { + $location = $page->location; + $keywords = strtoloweru($page->get("title").",".$page->get("tag").",".$page->get("author")); + $tokens = array_unique(array_filter(preg_split("/[,\s\(\)\+\-]/", $keywords), "strlen")); + if (!empty($tokens)) { + $array = array(); + foreach ($this->getArrayCopy() as $page) { + $searchScore = 0; + foreach ($tokens as $token) { + if (stristr($page->get("title"), $token)) $searchScore += 10; + if (stristr($page->get("tag"), $token)) $searchScore += 5; + if (stristr($page->get("author"), $token)) $searchScore += 2; + } + if ($page->location!=$location) { + $page->set("searchscore", $searchScore); + array_push($array, $page); + } + } + $this->exchangeArray($array); + $this->sort("modified", $ascendingOrder)->sort("searchscore", $ascendingOrder); + } + return $this; + } + + // Calculate union, merge page collection + public function merge($input) { + $this->exchangeArray(array_merge($this->getArrayCopy(), (array)$input)); + return $this; + } + + // Calculate intersection, remove pages that are not present in another page collection + public function intersect($input) { + $callback = function ($a, $b) { + return strcmp(spl_object_hash($a), spl_object_hash($b)); + }; + $this->exchangeArray(array_uintersect($this->getArrayCopy(), (array)$input, $callback)); + return $this; + } + + // Calculate difference, remove pages that are present in another page collection + public function diff($input) { + $callback = function ($a, $b) { + return strcmp(spl_object_hash($a), spl_object_hash($b)); + }; + $this->exchangeArray(array_udiff($this->getArrayCopy(), (array)$input, $callback)); + return $this; + } + + // Append to end of page collection + public function append($page) { + parent::append($page); + return $this; + } + + // Prepend to start of page collection + public function prepend($page) { + $array = $this->getArrayCopy(); + array_unshift($array, $page); + $this->exchangeArray($array); + return $this; + } + + // Limit the number of pages in page collection + public function limit($pagesMax) { + $this->exchangeArray(array_slice($this->getArrayCopy(), 0, $pagesMax)); + return $this; + } + + // Reverse page collection + public function reverse() { + $this->exchangeArray(array_reverse($this->getArrayCopy())); + return $this; + } + + // Randomize page collection + public function shuffle() { + $array = $this->getArrayCopy(); + shuffle($array); + $this->exchangeArray($array); + return $this; + } + + // Paginate page collection + public function pagination($limit, $reverse = true) { + $this->paginationNumber = 1; + $this->paginationCount = ceil($this->count() / $limit); + if ($this->yellow->page->isRequest("page")) $this->paginationNumber = intval($this->yellow->page->getRequest("page")); + if ($this->paginationNumber>$this->paginationCount) $this->paginationNumber = 0; + if ($this->paginationNumber>=1) { + $array = $this->getArrayCopy(); + if ($reverse) $array = array_reverse($array); + $this->exchangeArray(array_slice($array, ($this->paginationNumber - 1) * $limit, $limit)); + } + return $this; + } + + // Return current page number in pagination + public function getPaginationNumber() { + return $this->paginationNumber; + } + + // Return highest page number in pagination + public function getPaginationCount() { + return $this->paginationCount; + } + + // Return location for a page in pagination + public function getPaginationLocation($absoluteLocation = true, $pageNumber = 1) { + $location = $locationArguments = ""; + if ($pageNumber>=1 && $pageNumber<=$this->paginationCount) { + $location = $this->yellow->page->getLocation($absoluteLocation); + $locationArguments = $this->yellow->toolbox->getLocationArgumentsNew("page", $pageNumber>1 ? "$pageNumber" : ""); + } + return $location.$locationArguments; + } + + // Return location for previous page in pagination + public function getPaginationPrevious($absoluteLocation = true) { + $pageNumber = $this->paginationNumber-1; + return $this->getPaginationLocation($absoluteLocation, $pageNumber); + } + + // Return location for next page in pagination + public function getPaginationNext($absoluteLocation = true) { + $pageNumber = $this->paginationNumber+1; + return $this->getPaginationLocation($absoluteLocation, $pageNumber); + } + + // Return current page number in collection + public function getPageNumber($page) { + $pageNumber = 0; + foreach ($this->getIterator() as $key=>$value) { + if ($page->getLocation()==$value->getLocation()) { + $pageNumber = $key+1; + break; + } + } + return $pageNumber; + } + + // Return page in collection, null if none + public function getPage($pageNumber = 1) { + return ($pageNumber>=1 && $pageNumber<=$this->count()) ? $this->offsetGet($pageNumber-1) : null; + } + + // Return previous page in collection, null if none + public function getPagePrevious($page) { + $pageNumber = $this->getPageNumber($page)-1; + return $this->getPage($pageNumber); + } + + // Return next page in collection, null if none + public function getPageNext($page) { + $pageNumber = $this->getPageNumber($page)+1; + return $this->getPage($pageNumber); + } + + // Return current page filter + public function getFilter() { + return $this->filterValue; + } + + // Return page collection modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + $modified = 0; + foreach ($this->getIterator() as $page) { + $modified = max($modified, $page->getModified()); + } + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified; + } + + // Check if there is a pagination + public function isPagination() { + return $this->paginationCount>1; + } +} + +class YellowContent { + public $yellow; // access to API + public $pages; // scanned pages + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->pages = array(); + } + + // Scan file system on demand + public function scanLocation($location) { + if (!isset($this->pages[$location])) { + if (defined("DEBUG") && DEBUG>=2) echo "YellowContent::scanLocation location:$location<br/>\n"; + $this->pages[$location] = array(); + $scheme = $this->yellow->page->scheme; + $address = $this->yellow->page->address; + $base = $this->yellow->page->base; + if (empty($location)) { + $rootLocations = $this->yellow->lookup->findRootLocations(); + foreach ($rootLocations as $rootLocation) { + list($rootLocation, $fileName) = $this->yellow->toolbox->getTextList($rootLocation, " ", 2); + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $rootLocation, $fileName); + $page->parseData("", false, 0); + array_push($this->pages[$location], $page); + } + } else { + $fileNames = $this->yellow->lookup->findChildrenFromLocation($location); + foreach ($fileNames as $fileName) { + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, + $this->yellow->lookup->findLocationFromFile($fileName), $fileName); + $page->parseData($this->yellow->toolbox->readFile($fileName, 4096), false, 0); + if (strlenb($page->rawData)<4096) $page->statusCode = 200; + array_push($this->pages[$location], $page); + } + } + } + return $this->pages[$location]; + } + + // Return page from, null if not found + public function find($location, $absoluteLocation = false) { + $found = false; + if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); + foreach ($this->scanLocation($this->getParentLocation($location)) as $page) { + if ($page->location==$location) { + if (!$this->yellow->lookup->isRootLocation($page->location)) { + $found = true; + break; + } + } + } + return $found ? $page : null; + } + + // Return page collection with all pages + public function index($showInvisible = false, $multiLanguage = false, $levelMax = 0) { + $rootLocation = $multiLanguage ? "" : $this->getRootLocation($this->yellow->page->location); + return $this->getChildrenRecursive($rootLocation, $showInvisible, $levelMax); + } + + // Return page collection with top-level navigation + public function top($showInvisible = false, $showOnePager = true) { + $rootLocation = $this->getRootLocation($this->yellow->page->location); + $pages = $this->getChildren($rootLocation, $showInvisible); + if (count($pages)==1 && $showOnePager) { + $scheme = $this->yellow->page->scheme; + $address = $this->yellow->page->address; + $base = $this->yellow->page->base; + $one = ($pages->offsetGet(0)->location!=$this->yellow->page->location) ? $pages->offsetGet(0) : $this->yellow->page; + preg_match_all("/<h(\d) id=\"([^\"]+)\">(.*?)<\/h\d>/i", $one->getContent(), $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + if ($match[1]==2) { + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $one->location."#".$match[2], $one->fileName); + $page->parseData("---\nTitle: $match[3]\n---\n", false, 0); + $pages->append($page); + } + } + } + return $pages; + } + + // Return page collection with path ancestry + public function path($location, $absoluteLocation = false) { + $pages = new YellowPageCollection($this->yellow); + if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); + if ($page = $this->find($location)) { + $pages->prepend($page); + for (; $parent = $page->getParent(); $page=$parent) { + $pages->prepend($parent); + } + $home = $this->find($this->getHomeLocation($page->location)); + if ($home && $home->location!=$page->location) $pages->prepend($home); + } + return $pages; + } + + // Return page collection with multiple languages + public function multi($location, $absoluteLocation = false, $showInvisible = false) { + $pages = new YellowPageCollection($this->yellow); + if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); + $locationEnd = substru($location, strlenu($this->getRootLocation($location)) - 4); + foreach ($this->scanLocation("") as $page) { + if ($content = $this->find(substru($page->location, 4).$locationEnd)) { + if ($content->isAvailable() && ($content->isVisible() || $showInvisible)) { + if (!$this->yellow->lookup->isRootLocation($content->location)) $pages->append($content); + } + } + } + return $pages; + } + + // Return page collection that's empty + public function clean() { + return new YellowPageCollection($this->yellow); + } + + // Return languages in multi language mode + public function getLanguages($showInvisible = false) { + $languages = array(); + foreach ($this->scanLocation("") as $page) { + if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) array_push($languages, $page->get("language")); + } + return $languages; + } + + // Return child pages + public function getChildren($location, $showInvisible = false) { + $pages = new YellowPageCollection($this->yellow); + foreach ($this->scanLocation($location) as $page) { + if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) { + if (!$this->yellow->lookup->isRootLocation($page->location) && is_readable($page->fileName)) $pages->append($page); + } + } + return $pages; + } + + // Return child pages recursively + public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) { + --$levelMax; + $pages = new YellowPageCollection($this->yellow); + foreach ($this->scanLocation($location) as $page) { + if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) { + if (!$this->yellow->lookup->isRootLocation($page->location) && is_readable($page->fileName)) $pages->append($page); + } + if (!$this->yellow->lookup->isFileLocation($page->location) && $levelMax!=0) { + $pages->merge($this->getChildrenRecursive($page->location, $showInvisible, $levelMax)); + } + } + return $pages; + } + + // Return shared pages + public function getShared($location) { + $pages = new YellowPageCollection($this->yellow); + $location = $this->getHomeLocation($location).$this->yellow->system->get("coreContentSharedDirectory"); + foreach ($this->scanLocation($location) as $page) { + if ($page->get("status")=="shared") $pages->append($page); + } + return $pages; + } + + // Return root location + public function getRootLocation($location) { + $rootLocation = "root/"; + if ($this->yellow->system->get("coreMultiLanguageMode")) { + foreach ($this->scanLocation("") as $page) { + $token = substru($page->location, 4); + if ($token!="/" && substru($location, 0, strlenu($token))==$token) { + $rootLocation = "root$token"; + break; + } + } + } + return $rootLocation; + } + + // Return home location + public function getHomeLocation($location) { + return substru($this->getRootLocation($location), 4); + } + + // Return parent location + public function getParentLocation($location) { + $token = rtrim(substru($this->getRootLocation($location), 4), "/"); + if (preg_match("#^($token.*\/).+?$#", $location, $matches)) { + if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; + } + if (empty($parentLocation)) $parentLocation = "root$token/"; + return $parentLocation; + } + + // Return top-level location + public function getParentTopLocation($location) { + $token = rtrim(substru($this->getRootLocation($location), 4), "/"); + if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; + if (empty($parentTopLocation)) $parentTopLocation = "$token/"; + return $parentTopLocation; + } + + // TODO: remove later, for backwards compatibility + public function shared($name) { return null; } +} + +class YellowMedia { + public $yellow; // access to API + public $files; // scanned files + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->files = array(); + } + + // Scan file system on demand + public function scanLocation($location) { + if (!isset($this->files[$location])) { + if (defined("DEBUG") && DEBUG>=2) echo "YellowMedia::scanLocation location:$location<br/>\n"; + $this->files[$location] = array(); + $scheme = $this->yellow->page->scheme; + $address = $this->yellow->page->address; + $base = $this->yellow->system->get("coreServerBase"); + if (empty($location)) { + $fileNames = array($this->yellow->system->get("coreMediaDirectory")); + } else { + $fileNames = array(); + $path = substru($location, 1); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, true) as $entry) { + array_push($fileNames, $entry."/"); + } + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, false, true) as $entry) { + array_push($fileNames, $entry); + } + } + foreach ($fileNames as $fileName) { + $file = new YellowPage($this->yellow); + $file->setRequestInformation($scheme, $address, $base, "/".$fileName, $fileName); + $file->parseData(null, false, 0); + array_push($this->files[$location], $file); + } + } + return $this->files[$location]; + } + + // Return page with media file information, null if not found + public function find($location, $absoluteLocation = false) { + $found = false; + if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->system->get("coreServerBase"))); + foreach ($this->scanLocation($this->getParentLocation($location)) as $file) { + if ($file->location==$location) { + if ($this->yellow->lookup->isFileLocation($file->location)) { + $found = true; + break; + } + } + } + return $found ? $file : null; + } + + // Return page collection with all media files + public function index($showInvisible = false, $multiPass = false, $levelMax = 0) { + return $this->getChildrenRecursive("", $showInvisible, $levelMax); + } + + // Return page collection that's empty + public function clean() { + return new YellowPageCollection($this->yellow); + } + + // Return child files + public function getChildren($location, $showInvisible = false) { + $files = new YellowPageCollection($this->yellow); + foreach ($this->scanLocation($location) as $file) { + if ($file->isAvailable() && ($file->isVisible() || $showInvisible)) { + if ($this->yellow->lookup->isFileLocation($file->location)) $files->append($file); + } + } + return $files; + } + + // Return child files recursively + public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) { + --$levelMax; + $files = new YellowPageCollection($this->yellow); + foreach ($this->scanLocation($location) as $file) { + if ($file->isAvailable() && ($file->isVisible() || $showInvisible)) { + if ($this->yellow->lookup->isFileLocation($file->location)) $files->append($file); + } + if (!$this->yellow->lookup->isFileLocation($file->location) && $levelMax!=0) { + $files->merge($this->getChildrenRecursive($file->location, $showInvisible, $levelMax)); + } + } + return $files; + } + + // Return home location + public function getHomeLocation($location) { + return $this->yellow->system->get("coreMediaLocation"); + } + + // Return parent location + public function getParentLocation($location) { + $token = rtrim($this->yellow->system->get("coreMediaLocation"), "/"); + if (preg_match("#^($token.*\/).+?$#", $location, $matches)) { + if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; + } + if (empty($parentLocation)) $parentLocation = ""; + return $parentLocation; + } + + // Return top-level location + public function getParentTopLocation($location) { + $token = rtrim($this->yellow->system->get("coreMediaLocation"), "/"); + if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; + if (empty($parentTopLocation)) $parentTopLocation = "$token/"; + return $parentTopLocation; + } +} + +class YellowSystem { + public $yellow; // access to API + public $modified; // system modification date + public $settings; // system settings + public $settingsDefaults; // system settings defaults + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->modified = 0; + $this->settings = new YellowArray(); + $this->settingsDefaults = new YellowArray(); + } + + // Load system settings from file + public function load($fileName) { + if (defined("DEBUG") && DEBUG>=2) echo "YellowSystem::load file:$fileName<br/>\n"; + $this->modified = $this->yellow->toolbox->getFileModified($fileName); + $fileData = $this->yellow->toolbox->readFile($fileName); + $this->settings = $this->yellow->toolbox->getTextSettings($fileData, ""); + if (defined("DEBUG") && DEBUG>=3) { + foreach ($this->settings as $key=>$value) { + echo "YellowSystem::load ".ucfirst($key).":$value<br/>\n"; + } + } + } + + // Save system settings to file + public function save($fileName, $settings) { + $this->modified = time(); + $settingsNew = new YellowArray(); + foreach ($settings as $key=>$value) { + if (!empty($key) && !strempty($value)) { + $this->set($key, $value); + $settingsNew[$key] = $value; + } + } + $fileData = $this->yellow->toolbox->readFile($fileName); + $fileData = $this->yellow->toolbox->setTextSettings($fileData, "", "", $settingsNew); + return $this->yellow->toolbox->createFile($fileName, $fileData); + } + + // Set default system setting + public function setDefault($key, $value) { + $this->settingsDefaults[$key] = $value; + } + + // Set system setting + public function set($key, $value) { + $this->settings[$key] = $value; + } + + // Return system setting + public function get($key) { + if (isset($this->settings[$key])) { + $value = $this->settings[$key]; + } else { + $value = isset($this->settingsDefaults[$key]) ? $this->settingsDefaults[$key] : ""; + } + return $value; + } + + // Return system setting, HTML encoded + public function getHtml($key) { + return htmlspecialchars($this->get($key)); + } + + // Return system settings + public function getSettings($filterStart = "", $filterEnd = "") { + $settings = array(); + if (empty($filterStart) && empty($filterEnd)) { + $settings = array_merge($this->settingsDefaults->getArrayCopy(), $this->settings->getArrayCopy()); + } else { + foreach (array_merge($this->settingsDefaults->getArrayCopy(), $this->settings->getArrayCopy()) as $key=>$value) { + if (!empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value; + if (!empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $settings[$key] = $value; + } + } + return $settings; + } + + // Return supported values for system setting, empty if not known + public function getValues($key) { + $values = array(); + if ($key=="email") { + foreach ($this->yellow->user->settings as $userKey=>$userValue) { + array_push($values, $userKey); + } + } elseif ($key=="language") { + foreach ($this->yellow->language->settings as $languageKey=>$languageValue) { + array_push($values, $languageKey); + } + } elseif ($key=="layout") { + $path = $this->yellow->system->get("coreLayoutDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.html$/", true, false, false) as $entry) { + array_push($values, lcfirst(substru($entry, 0, -5))); + } + } elseif ($key=="theme") { + $path = $this->yellow->system->get("coreThemeDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.css$/", true, false, false) as $entry) { + array_push($values, lcfirst(substru($entry, 0, -4))); + } + } + return $values; + } + + // Return system settings modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; + } + + // Check if system setting exists + public function isExisting($key) { + return isset($this->settings[$key]); + } +} + +class YellowUser { + public $yellow; // access to API + public $modified; // user modification date + public $settings; // user settings + public $email; // current email + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->modified = 0; + $this->settings = new YellowArray(); + $this->email = ""; + } + + // Load user settings from file + public function load($fileName) { + if (defined("DEBUG") && DEBUG>=2) echo "YellowUser::load file:$fileName<br/>\n"; + $this->modified = $this->yellow->toolbox->getFileModified($fileName); + $fileData = $this->yellow->toolbox->readFile($fileName); + $this->settings = $this->yellow->toolbox->getTextSettings($fileData, "email"); + } + + // Save user settings to file + public function save($fileName, $email, $settings) { + $this->modified = time(); + $settingsNew = new YellowArray(); + $settingsNew["email"] = $email; + foreach ($settings as $key=>$value) { + if (!empty($key) && !strempty($value)) { + $this->setUser($key, $value, $email); + $settingsNew[$key] = $value; + } + } + $fileData = $this->yellow->toolbox->readFile($fileName); + $fileData = $this->yellow->toolbox->setTextSettings($fileData, "email", $email, $settingsNew); + return $this->yellow->toolbox->createFile($fileName, $fileData); + } + + // Remove user settings from file + public function remove($fileName, $email) { + $this->modified = time(); + if (isset($this->settings[$email])) unset($this->settings[$email]); + $fileData = $this->yellow->toolbox->readFile($fileName); + $fileData = $this->yellow->toolbox->unsetTextSettings($fileData, "email", $email); + return $this->yellow->toolbox->createFile($fileName, $fileData); + } + + // Set current email + public function set($email) { + $this->email = $email; + } + + // Set user setting + public function setUser($key, $value, $email) { + if (!isset($this->settings[$email])) $this->settings[$email] = new YellowArray(); + $this->settings[$email][$key] = $value; + } + + // Return user setting + public function getUser($key, $email = "") { + if (empty($email)) $email = $this->email; + return isset($this->settings[$email]) && isset($this->settings[$email][$key]) ? $this->settings[$email][$key] : ""; + } + + // Return user setting, HTML encoded + public function getUserHtml($key, $email = "") { + return htmlspecialchars($this->getUser($key, $email)); + } + + // Return user settings + public function getSettings($email = "") { + $settings = array(); + if (empty($email)) $email = $this->email; + if (isset($this->settings[$email])) $settings = $this->settings[$email]->getArrayCopy(); + return $settings; + } + + // Return user settings modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; + } + + // Check if user setting exists + public function isUser($key, $email = "") { + if (empty($email)) $email = $this->email; + return isset($this->settings[$email]) && isset($this->settings[$email][$key]); + } + + // Check if user exists + public function isExisting($email) { + return isset($this->settings[$email]); + } +} + +class YellowLanguage { + public $yellow; // access to API + public $modified; // language modification date + public $settings; // language settings + public $settingsDefaults; // language settings defaults + public $language; // current language + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->modified = 0; + $this->settings = new YellowArray(); + $this->settingsDefaults = new YellowArray(); + $this->language = ""; + } + + // Load language settings from file + public function load($fileName) { + $path = dirname($fileName); + $regex = "/^".basename($fileName)."$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false) as $entry) { + if (defined("DEBUG") && DEBUG>=2) echo "YellowLanguage::load file:$entry<br/>\n"; + $this->modified = max($this->modified, filemtime($entry)); + $fileData = $this->yellow->toolbox->readFile($entry); + $settings = $this->yellow->toolbox->getTextSettings($fileData, "language"); + foreach($settings as $language=>$block) { + if (!isset($this->settings[$language])) { + $this->settings[$language] = $block; + } else { + foreach ($block as $key=>$value) { + $this->settings[$language][$key] = $value; + } + } + } + } + } + + // Set current language + public function set($language) { + $this->language = $language; + } + + // Set default language setting + public function setDefault($key) { + $this->settingsDefaults[$key] = true; + } + + // Set language setting + public function setText($key, $value, $language) { + if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); + $this->settings[$language][$key] = $value; + } + + // Return language setting + public function getText($key, $language = "") { + if (empty($language)) $language = $this->language; + return $this->isText($key, $language) ? $this->settings[$language][$key] : "[$key]"; + } + + // Return language setting, HTML encoded + public function getTextHtml($key, $language = "") { + return htmlspecialchars($this->getText($key, $language)); + } + + // Return human readable date + public function getDateFormatted($timestamp, $format, $language = "") { + $dateMonths = preg_split("/\s*,\s*/", $this->getText("coreDateMonths", $language)); + $dateWeekdays = preg_split("/\s*,\s*/", $this->getText("coreDateWeekdays", $language)); + $month = $dateMonths[date("n", $timestamp) - 1]; + $weekday = $dateWeekdays[date("N", $timestamp) - 1]; + $timeZone = $this->yellow->system->get("coreServerTimezone"); + $timeZoneHelper = new DateTime(null, new DateTimeZone($timeZone)); + $timeZoneOffset = $timeZoneHelper->getOffset(); + $timeZoneAbbreviation = "GMT".($timeZoneOffset<0 ? "-" : "+").abs(intval($timeZoneOffset/3600)); + $format = preg_replace("/(?<!\\\)F/", addcslashes($month, "A..Za..z"), $format); + $format = preg_replace("/(?<!\\\)M/", addcslashes(substru($month, 0, 3), "A..Za..z"), $format); + $format = preg_replace("/(?<!\\\)D/", addcslashes(substru($weekday, 0, 3), "A..Za..z"), $format); + $format = preg_replace("/(?<!\\\)l/", addcslashes($weekday, "A..Za..z"), $format); + $format = preg_replace("/(?<!\\\)T/", addcslashes($timeZoneAbbreviation, "A..Za..z"), $format); + return date($format, $timestamp); + } + + // Return human readable date, relative to today + public function getDateRelative($timestamp, $format, $daysLimit, $language = "") { + $timeDifference = time() - $timestamp; + $days = abs(intval($timeDifference / 86400)); + $key = $timeDifference>=0 ? "coreDatePast" : "coreDateFuture"; + $tokens = preg_split("/\s*,\s*/", $this->getText($key, $language)); + if (count($tokens)>=8) { + if ($days<=$daysLimit || $daysLimit==0) { + if ($days==0) { + $output = $tokens[0]; + } elseif ($days==1) { + $output = $tokens[1]; + } elseif ($days>=2 && $days<=29) { + $output = preg_replace("/@x/i", $days, $tokens[2]); + } elseif ($days>=30 && $days<=59) { + $output = $tokens[3]; + } elseif ($days>=60 && $days<=364) { + $output = preg_replace("/@x/i", intval($days/30), $tokens[4]); + } elseif ($days>=365 && $days<=729) { + $output = $tokens[5]; + } else { + $output = preg_replace("/@x/i", intval($days/365.25), $tokens[6]); + } + } else { + $output = preg_replace("/@x/i", $this->getDateFormatted($timestamp, $format, $language), $tokens[7]); + } + } else { + $output = "[$key]"; + } + return $output; + } + + // Return language settings + public function getSettings($filterStart = "", $filterEnd = "", $language = "") { + $settings = array(); + if (empty($language)) $language = $this->language; + if (isset($this->settings[$language])) { + if (empty($filterStart) && empty($filterEnd)) { + $settings = $this->settings[$language]->getArrayCopy(); + } else { + foreach ($this->settings[$language] as $key=>$value) { + if (!empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value; + if (!empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $settings[$key] = $value; + } + } + } + return $settings; + } + + // Return language settings modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; + } + + // Normalise date into known format + public function normaliseDate($text, $language = "") { + if (preg_match("/^\d+\-\d+$/", $text)) { + $output = $this->getDateFormatted(strtotime($text), $this->getText("coreDateFormatShort", $language), $language); + } elseif (preg_match("/^\d+\-\d+\-\d+$/", $text)) { + $output = $this->getDateFormatted(strtotime($text), $this->getText("coreDateFormatMedium", $language), $language); + } elseif (preg_match("/^\d+\-\d+\-\d+ \d+\:\d+$/", $text)) { + $output = $this->getDateFormatted(strtotime($text), $this->getText("coreDateFormatLong", $language), $language); + } else { + $output = $text; + } + return $output; + } + + // Check if language setting exists + public function isText($key, $language = "") { + if (empty($language)) $language = $this->language; + return isset($this->settings[$language]) && isset($this->settings[$language][$key]); + } + + // Check if language exists + public function isExisting($language) { + return isset($this->settings[$language]); + } +} + +class YellowExtension { + public $yellow; // access to API + public $modified; // extension modification date + public $data; // extension data + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->modified = 0; + $this->data = array(); + } + + // Load extensions + public function load($path) { + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.php$/", true, false) as $entry) { + if (defined("DEBUG") && DEBUG>=3) echo "YellowExtension::load file:$entry<br/>\n"; + $this->modified = max($this->modified, filemtime($entry)); + require_once($entry); + $name = $this->yellow->lookup->normaliseName(basename($entry), true, true); + $this->register(lcfirst($name), "Yellow".ucfirst($name)); + } + $callback = function ($a, $b) { + return $a["priority"] - $b["priority"]; + }; + uasort($this->data, $callback); + foreach ($this->data as $key=>$value) { + if (method_exists($this->data[$key]["object"], "onLoad")) $this->data[$key]["object"]->onLoad($this->yellow); + } + $this->yellow->system->set("mediaLocation", "/media/"); // TODO: remove later, for backwards compatibility + $this->yellow->system->set("downloadLocation", "/media/downloads/"); + $this->yellow->system->set("imageLocation", "/media/images/"); + $this->yellow->system->set("extensionLocation", "/media/extensions/"); + $this->yellow->system->set("resourceLocation", "/media/themes/"); + $this->yellow->system->set("mediaDir", "media/"); + $this->yellow->system->set("downloadDir", "media/downloads/"); + $this->yellow->system->set("imageDir", "media/images/"); + $this->yellow->system->set("systemDir", "system/"); + $this->yellow->system->set("extensionDir", "system/extensions/"); + $this->yellow->system->set("layoutDir", "system/layouts/"); + $this->yellow->system->set("resourceDir", "system/themes/"); + $this->yellow->system->set("settingDir", "system/extensions/"); + $this->yellow->system->set("trashDir", "system/trash/"); + $this->yellow->system->set("contentDir", "content/"); + $this->yellow->system->set("contentPagination", "page"); + $this->yellow->system->set("coreStaticDir", "public/"); + $this->yellow->system->set("coreCacheDir", "cache/"); + $this->yellow->system->set("coreTrashDir", "system/trash/"); + $this->yellow->system->set("coreMediaDir", "media/"); + $this->yellow->system->set("coreDownloadDir", "media/downloads/"); + $this->yellow->system->set("coreImageDir", "media/images/"); + $this->yellow->system->set("coreSystemDir", "system/"); + $this->yellow->system->set("coreExtensionDir", "system/extensions/"); + $this->yellow->system->set("coreLayoutDir", "system/layouts/"); + $this->yellow->system->set("coreResourceDir", "system/themes/"); + $this->yellow->system->set("coreSettingDir", "system/extensions/"); + $this->yellow->system->set("coreContentDir", "content/"); + $this->yellow->system->set("coreContentRootDir", "default/"); + $this->yellow->system->set("coreContentHomeDir", "home/"); + $this->yellow->system->set("coreContentSharedDir", "shared/"); + $this->yellow->system->set("coreResourceLocation", "/media/themes/"); + $this->yellow->system->set("coreResourceDirectory", "system/themes/"); + } + + // Register extension + public function register($key, $class) { + if (!$this->isExisting($key) && class_exists($class)) { + $this->data[$key] = array(); + $this->data[$key]["object"] = $class=="YellowCore" ? new stdClass : new $class; + $this->data[$key]["class"] = $class; + $this->data[$key]["version"] = defined("$class::VERSION") ? $class::VERSION : 0; + $this->data[$key]["priority"] = defined("$class::PRIORITY") ? $class::PRIORITY : count($this->data) + 10; + } + } + + // Return extension + public function get($key) { + return $this->data[$key]["object"]; + } + + // Return extensions modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; + } + + // Check if extension exists + public function isExisting($key) { + return isset($this->data[$key]); + } +} + +class YellowLookup { + public $yellow; // access to API + public $requestHandler; // request handler name + public $commandHandler; // command handler name + public $layoutArguments; // layout arguments + + public function __construct($yellow) { + $this->yellow = $yellow; + } + + // Detect file system + public function detectFileSystem() { + list($pathRoot, $pathHome) = $this->findFileSystemInformation(); + $this->yellow->system->set("coreContentRootDirectory", $pathRoot); + $this->yellow->system->set("coreContentHomeDirectory", $pathHome); + date_default_timezone_set($this->yellow->system->get("coreServerTimezone")); + } + + // Return file system information + public function findFileSystemInformation() { + $path = $this->yellow->system->get("coreContentDirectory"); + $pathRoot = $this->yellow->system->get("coreContentRootDirectory"); + $pathHome = $this->yellow->system->get("coreContentHomeDirectory"); + if (!$this->yellow->system->get("coreMultiLanguageMode")) $pathRoot = ""; + if (!empty($pathRoot)) { + $token = $root = rtrim($pathRoot, "/"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) { + if (empty($firstRoot)) $firstRoot = $token = $entry; + if ($this->normaliseToken($entry)==$root) { + $token = $entry; + break; + } + } + $pathRoot = $this->normaliseToken($token)."/"; + $path .= "$firstRoot/"; + } + if (!empty($pathHome)) { + $token = $home = rtrim($pathHome, "/"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) { + if (empty($firstHome)) $firstHome = $token = $entry; + if ($this->normaliseToken($entry)==$home) { + $token = $entry; + break; + } + } + $pathHome = $this->normaliseToken($token)."/"; + } + return array($pathRoot, $pathHome); + } + + // Return root locations + public function findRootLocations($includePath = true) { + $locations = array(); + $pathBase = $this->yellow->system->get("coreContentDirectory"); + $pathRoot = $this->yellow->system->get("coreContentRootDirectory"); + if (!empty($pathRoot)) { + foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) { + $token = $this->normaliseToken($entry)."/"; + if ($token==$pathRoot) $token = ""; + array_push($locations, $includePath ? "root/$token $pathBase$entry/" : "root/$token"); + if (defined("DEBUG") && DEBUG>=2) echo "YellowLookup::findRootLocations root/$token<br/>\n"; + } + } else { + array_push($locations, $includePath ? "root/ $pathBase" : "root/"); + } + return $locations; + } + + // Return location from file path + public function findLocationFromFile($fileName) { + $invalid = false; + $location = "/"; + $pathBase = $this->yellow->system->get("coreContentDirectory"); + $pathRoot = $this->yellow->system->get("coreContentRootDirectory"); + $pathHome = $this->yellow->system->get("coreContentHomeDirectory"); + $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); + $fileExtension = $this->yellow->system->get("coreContentExtension"); + if (substru($fileName, 0, strlenu($pathBase))==$pathBase && mb_check_encoding($fileName, "UTF-8")) { + $fileName = substru($fileName, strlenu($pathBase)); + $tokens = explode("/", $fileName); + if (!empty($pathRoot)) { + $token = $this->normaliseToken($tokens[0])."/"; + if ($token!=$pathRoot) $location .= $token; + array_shift($tokens); + } + for ($i=0; $i<count($tokens)-1; ++$i) { + $token = $this->normaliseToken($tokens[$i])."/"; + if ($i || $token!=$pathHome) $location .= $token; + } + $token = $this->normaliseToken($tokens[$i], $fileExtension); + $fileFolder = $this->normaliseToken($tokens[$i-1], $fileExtension); + if ($token!=$fileDefault && $token!=$fileFolder) { + $location .= $this->normaliseToken($tokens[$i], $fileExtension, true); + } + $extension = ($pos = strrposu($fileName, ".")) ? substru($fileName, $pos) : ""; + if ($extension!=$fileExtension) $invalid = true; + } else { + $invalid = true; + } + if (defined("DEBUG") && DEBUG>=2) { + $debug = ($invalid ? "INVALID" : $location)." <- $pathBase$fileName"; + echo "YellowLookup::findLocationFromFile $debug<br/>\n"; + } + return $invalid ? "" : $location; + } + + // Return file path from location + public function findFileFromLocation($location, $directory = false) { + $found = $invalid = false; + $path = $this->yellow->system->get("coreContentDirectory"); + $pathRoot = $this->yellow->system->get("coreContentRootDirectory"); + $pathHome = $this->yellow->system->get("coreContentHomeDirectory"); + $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); + $fileExtension = $this->yellow->system->get("coreContentExtension"); + $tokens = explode("/", $location); + if ($this->isRootLocation($location)) { + if (!empty($pathRoot)) { + $token = (count($tokens)>2) ? $tokens[1] : rtrim($pathRoot, "/"); + $path .= $this->findFileDirectory($path, $token, "", true, true, $found, $invalid); + } + } else { + if (!empty($pathRoot)) { + if (count($tokens)>2) { + if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathRoot, "/"))) $invalid = true; + $path .= $this->findFileDirectory($path, $tokens[1], "", true, false, $found, $invalid); + if ($found) array_shift($tokens); + } + if (!$found) { + $path .= $this->findFileDirectory($path, rtrim($pathRoot, "/"), "", true, true, $found, $invalid); + } + } + if (count($tokens)>2) { + if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathHome, "/"))) $invalid = true; + for ($i=1; $i<count($tokens)-1; ++$i) { + $path .= $this->findFileDirectory($path, $tokens[$i], "", true, true, $found, $invalid); + } + } else { + $i = 1; + $tokens[0] = rtrim($pathHome, "/"); + $path .= $this->findFileDirectory($path, $tokens[0], "", true, true, $found, $invalid); + } + if (!$directory) { + if (!strempty($tokens[$i])) { + $token = $tokens[$i].$fileExtension; + $fileFolder = $tokens[$i-1].$fileExtension; + if ($token==$fileDefault || $token==$fileFolder) $invalid = true; + $path .= $this->findFileDirectory($path, $token, $fileExtension, false, true, $found, $invalid); + } else { + $path .= $this->findFileDefault($path, $fileDefault, $fileExtension, false); + } + if (defined("DEBUG") && DEBUG>=2) { + $debug = "$location -> ".($invalid ? "INVALID" : $path); + echo "YellowLookup::findFileFromLocation $debug<br/>\n"; + } + } + } + return $invalid ? "" : $path; + } + + // Return file or directory that matches token + public function findFileDirectory($path, $token, $fileExtension, $directory, $default, &$found, &$invalid) { + if ($this->normaliseToken($token, $fileExtension)!=$token) $invalid = true; + if (!$invalid) { + $regex = "/^[\d\-\_\.]*".str_replace("-", ".", $token)."$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, $directory, false) as $entry) { + if ($this->normaliseToken($entry, $fileExtension)==$token) { + $token = $entry; + $found = true; + break; + } + } + } + if ($directory) $token .= "/"; + return ($default || $found) ? $token : ""; + } + + // Return default file in directory + public function findFileDefault($path, $fileDefault, $fileExtension, $includePath = true) { + $token = $fileDefault; + if (!is_file($path."/".$fileDefault)) { + $fileFolder = $this->normaliseToken(basename($path), $fileExtension); + $regex = "/^[\d\-\_\.]*($fileDefault|$fileFolder)$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) { + if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) { + $token = $entry; + break; + } + if ($this->normaliseToken($entry, $fileExtension)==$fileFolder) { + $token = $entry; + break; + } + } + } + return $includePath ? "$path/$token" : $token; + } + + // Return children from location + public function findChildrenFromLocation($location) { + $fileNames = array(); + $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); + $fileExtension = $this->yellow->system->get("coreContentExtension"); + if (!$this->isFileLocation($location)) { + $path = $this->findFileFromLocation($location, true); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) { + $token = $this->findFileDefault($path.$entry, $fileDefault, $fileExtension, false); + array_push($fileNames, $path.$entry."/".$token); + } + if (!$this->isRootLocation($location)) { + $fileFolder = $this->normaliseToken(basename($path), $fileExtension); + $regex = "/^.*\\".$fileExtension."$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) { + if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) continue; + if ($this->normaliseToken($entry, $fileExtension)==$fileFolder) continue; + array_push($fileNames, $path.$entry); + } + } + } + return $fileNames; + } + + // Return language from file path + public function findLanguageFromFile($fileName, $languageDefault) { + $language = $languageDefault; + $pathBase = $this->yellow->system->get("coreContentDirectory"); + $pathRoot = $this->yellow->system->get("coreContentRootDirectory"); + if (!empty($pathRoot)) { + $fileName = substru($fileName, strlenu($pathBase)); + if (preg_match("/^(.+?)\//", $fileName, $matches)) { + $name = $this->normaliseToken($matches[1]); + if (strlenu($name)==2) $language = $name; + } + } + return $language; + } + + // Return file path from media location + public function findFileFromMedia($location) { + $fileName = null; + if ($this->isFileLocation($location)) { + $mediaLocationLength = strlenu($this->yellow->system->get("coreMediaLocation")); + if (substru($location, 0, $mediaLocationLength)==$this->yellow->system->get("coreMediaLocation")) { + $fileName = $this->yellow->system->get("coreMediaDirectory").substru($location, 7); + } + } + return $fileName; + } + + // Return file path from system location + public function findFileFromSystem($location) { + $fileName = null; + if (preg_match("/\.(css|gif|ico|js|jpg|png|svg|woff|woff2)$/", $location)) { + $extensionLocationLength = strlenu($this->yellow->system->get("coreExtensionLocation")); + $themeLocationLength = strlenu($this->yellow->system->get("coreThemeLocation")); + if (substru($location, 0, $extensionLocationLength)==$this->yellow->system->get("coreExtensionLocation")) { + $fileName = $this->yellow->system->get("coreExtensionDirectory").substru($location, $extensionLocationLength); + } elseif (substru($location, 0, $themeLocationLength)==$this->yellow->system->get("coreThemeLocation")) { + $fileName = $this->yellow->system->get("coreThemeDirectory").substru($location, $themeLocationLength); + } + } + return $fileName; + } + + // Normalise file/directory token + public function normaliseToken($text, $fileExtension = "", $removeExtension = false) { + if (!empty($fileExtension)) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; + if (preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !empty($matches[1])) $text = $matches[1]; + return preg_replace("/[^\pL\d\-\_]/u", "-", $text).($removeExtension ? "" : $fileExtension); + } + + // Normalise name + public function normaliseName($text, $removePrefix = false, $removeExtension = false, $filterStrict = false) { + if ($removeExtension) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; + if ($removePrefix && preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !empty($matches[1])) $text = $matches[1]; + if ($filterStrict) $text = strtoloweru($text); + return preg_replace("/[^\pL\d\-\_]/u", "-", $text); + } + + // Normalise prefix + public function normalisePrefix($text) { + $prefix = ""; + if (preg_match("/^([\d\-\_\.]*)(.*)$/", $text, $matches)) $prefix = $matches[1]; + if (!empty($prefix) && !preg_match("/[\-\_\.]$/", $prefix)) $prefix .= "-"; + return $prefix; + } + + // Normalise array, make keys with same upper/lower case + public function normaliseUpperLower($input) { + $array = array(); + foreach ($input as $key=>$value) { + if (empty($key) || strempty($value)) continue; + $keySearch = strtoloweru($key); + foreach ($array as $keyNew=>$valueNew) { + if (strtoloweru($keyNew)==$keySearch) { + $key = $keyNew; + break; + } + } + $array[$key] += $value; + } + return $array; + } + + // Normalise location, make absolute location + public function normaliseLocation($location, $pageLocation, $filterStrict = true) { + if (!preg_match("/^\w+:/", trim(html_entity_decode($location, ENT_QUOTES, "UTF-8")))) { + $pageBase = $this->yellow->page->base; + $mediaBase = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreMediaLocation"); + if (!preg_match("/^\#/", $location)) { + if (!preg_match("/^\//", $location)) { + $location = $this->getDirectoryLocation($pageBase.$pageLocation).$location; + } elseif (!preg_match("#^($pageBase|$mediaBase)#", $location)) { + $location = $pageBase.$location; + } + } + $location = str_replace("/./", "/", $location); + $location = str_replace(":", $this->yellow->toolbox->getLocationArgumentsSeparator(), $location); + } else { + if ($filterStrict && !preg_match("/^(http|https|ftp|mailto):/", $location)) $location = "error-xss-filter"; + } + return $location; + } + + // Normalise URL, make absolute URL + public function normaliseUrl($scheme, $address, $base, $location, $filterStrict = true) { + if (!preg_match("/^\w+:/", $location)) { + $url = "$scheme://$address$base$location"; + } else { + if ($filterStrict && !preg_match("/^(http|https|ftp|mailto):/", $location)) $location = "error-xss-filter"; + $url = $location; + } + return $url; + } + + // Return URL information + public function getUrlInformation($url) { + $scheme = $address = $base = ""; + if (preg_match("#^(\w+)://([^/]+)(.*)$#", rtrim($url, "/"), $matches)) { + $scheme = $matches[1]; + $address = $matches[2]; + $base = $matches[3]; + } + return array($scheme, $address, $base); + } + + // Return directory location + public function getDirectoryLocation($location) { + return ($pos = strrposu($location, "/")) ? substru($location, 0, $pos+1) : "/"; + } + + // Return redirect location + public function getRedirectLocation($location) { + if ($this->isFileLocation($location)) { + $location = "$location/"; + } else { + $languageDefault = $this->yellow->system->get("language"); + $language = $this->yellow->toolbox->detectBrowserLanguage($this->yellow->content->getLanguages(), $languageDefault); + $location = "/$language/"; + } + return $location; + } + + // Check if clean URL is requested + public function isRequestCleanUrl($location) { + return isset($_REQUEST["clean-url"]) && substru($location, -1, 1)=="/"; + } + + // Check if location is specifying root + public function isRootLocation($location) { + return substru($location, 0, 1)!="/"; + } + + // Check if location is specifying file or directory + public function isFileLocation($location) { + return substru($location, -1, 1)!="/"; + } + + // Check if location can be redirected into directory + public function isRedirectLocation($location) { + $redirect = false; + if ($this->isFileLocation($location)) { + $redirect = is_dir($this->findFileFromLocation("$location/", true)); + } elseif ($location=="/") { + $redirect = $this->yellow->system->get("coreMultiLanguageMode"); + } + return $redirect; + } + + // Check if location contains nested directories + public function isNestedLocation($location, $fileName, $checkHomeLocation = false) { + $nested = false; + if (!$checkHomeLocation || $location==$this->yellow->content->getHomeLocation($location)) { + $path = dirname($fileName); + if (count($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false))) $nested = true; + } + return $nested; + } + + // Check if location is available + public function isAvailableLocation($location, $fileName) { + $available = true; + $pathBase = $this->yellow->system->get("coreContentDirectory"); + if (substru($fileName, 0, strlenu($pathBase))==$pathBase) { + $sharedLocation = $this->yellow->content->getHomeLocation($location).$this->yellow->system->get("coreContentSharedDirectory"); + if (substru($location, 0, strlenu($sharedLocation))==$sharedLocation) $available = false; + } + return $available; + } + + // Check if location is within current HTTP request + public function isActiveLocation($location, $currentLocation) { + if ($this->isFileLocation($location)) { + $active = $currentLocation==$location; + } else { + if ($location==$this->yellow->content->getHomeLocation($location)) { + $active = $this->getDirectoryLocation($currentLocation)==$location; + } else { + $active = substru($currentLocation, 0, strlenu($location))==$location; + } + } + return $active; + } + + // Check if file is valid + public function isValidFile($fileName) { + $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); + $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); + $systemDirectoryLength = strlenu($this->yellow->system->get("coreSystemDirectory")); + return substru($fileName, 0, $contentDirectoryLength)==$this->yellow->system->get("coreContentDirectory") || + substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory") || + substru($fileName, 0, $systemDirectoryLength)==$this->yellow->system->get("coreSystemDirectory"); + } + + // Check if content file + public function isContentFile($fileName) { + $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); + return substru($fileName, 0, $contentDirectoryLength)==$this->yellow->system->get("coreContentDirectory"); + } + + // Check if media file + public function isMediaFile($fileName) { + $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); + return substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory"); + } + + // Check if system file + public function isSystemFile($fileName) { + $systemDirectoryLength = strlenu($this->yellow->system->get("coreSystemDirectory")); + return substru($fileName, 0, $systemDirectoryLength)==$this->yellow->system->get("coreSystemDirectory"); + } +} + +class YellowToolbox { + + // Return browser cookie from from current HTTP request + public function getCookie($key) { + return isset($_COOKIE[$key]) ? $_COOKIE[$key] : ""; + } + + // Return server argument from current HTTP request + public function getServer($key) { + return isset($_SERVER[$key]) ? $_SERVER[$key] : ""; + } + + // Return location arguments from current HTTP request + public function getLocationArguments() { + return $this->getServer("LOCATION_ARGUMENTS"); + } + + // Return location arguments from current HTTP request, modify existing arguments + public function getLocationArgumentsNew($key, $value) { + $locationArguments = ""; + $found = false; + $separator = $this->getLocationArgumentsSeparator(); + foreach (explode("/", $this->getServer("LOCATION_ARGUMENTS")) as $token) { + if (preg_match("/^(.*?)$separator(.*)$/", $token, $matches)) { + if ($matches[1]==$key) { + $matches[2] = $value; + $found = true; + } + if (!empty($matches[1]) && !strempty($matches[2])) { + if (!empty($locationArguments)) $locationArguments .= "/"; + $locationArguments .= "$matches[1]:$matches[2]"; + } + } + } + if (!$found && !empty($key) && !strempty($value)) { + if (!empty($locationArguments)) $locationArguments .= "/"; + $locationArguments .= "$key:$value"; + } + if (!empty($locationArguments)) { + $locationArguments = $this->normaliseArguments($locationArguments, false, false); + if (!$this->isLocationArgumentsPagination($locationArguments)) $locationArguments .= "/"; + } + return $locationArguments; + } + + // Return location arguments from current HTTP request, convert form parameters + public function getLocationArgumentsCleanUrl() { + $locationArguments = ""; + foreach (array_merge($_GET, $_POST) as $key=>$value) { + if (!empty($key) && !strempty($value)) { + if (!empty($locationArguments)) $locationArguments .= "/"; + $key = str_replace(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $key); + $value = str_replace(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $value); + $locationArguments .= "$key:$value"; + } + } + if (!empty($locationArguments)) { + $locationArguments = $this->normaliseArguments($locationArguments, false, false); + if (!$this->isLocationArgumentsPagination($locationArguments)) $locationArguments .= "/"; + } + return $locationArguments; + } + + // Return location arguments separator + public function getLocationArgumentsSeparator() { + return (strtoupperu(substru(PHP_OS, 0, 3))!="WIN") ? ":" : "="; + } + + // Return human readable HTTP date + public function getHttpDateFormatted($timestamp) { + return gmdate("D, d M Y H:i:s", $timestamp)." GMT"; + } + + // Return human readable HTTP server status + public function getHttpStatusFormatted($statusCode, $shortFormat = false) { + switch ($statusCode) { + case 0: $text = "No data"; break; + case 200: $text = "OK"; break; + case 301: $text = "Moved permanently"; break; + case 302: $text = "Moved temporarily"; break; + case 303: $text = "Reload please"; break; + case 304: $text = "Not modified"; break; + case 400: $text = "Bad request"; break; + case 403: $text = "Forbidden"; break; + case 404: $text = "Not found"; break; + case 430: $text = "Login failed"; break; + case 434: $text = "Not existing"; break; + case 500: $text = "Server error"; break; + case 503: $text = "Service unavailable"; break; + default: $text = "Error $statusCode"; + } + $serverProtocol = $this->getServer("SERVER_PROTOCOL"); + if (!preg_match("/^HTTP\//", $serverProtocol)) $serverProtocol = "HTTP/1.1"; + return $shortFormat ? $text : "$serverProtocol $statusCode $text"; + } + + // Return MIME content type + public function getMimeContentType($fileName) { + $contentType = ""; + $contentTypes = array( + "css" => "text/css", + "gif" => "image/gif", + "html" => "text/html; charset=utf-8", + "ico" => "image/x-icon", + "js" => "application/javascript", + "json" => "application/json", + "jpg" => "image/jpeg", + "md" => "text/markdown", + "png" => "image/png", + "svg" => "image/svg+xml", + "txt" => "text/plain", + "woff" => "application/font-woff", + "woff2" => "application/font-woff2", + "xml" => "text/xml; charset=utf-8"); + $fileType = $this->getFileType($fileName); + if (empty($fileType)) { + $contentType = $contentTypes["html"]; + } elseif (array_key_exists($fileType, $contentTypes)) { + $contentType = $contentTypes[$fileType]; + } + return $contentType; + } + + // Return number of bytes + public function getNumberBytes($string) { + $bytes = intval($string); + switch (strtoupperu(substru($string, -1))) { + case "G": $bytes *= 1024*1024*1024; break; + case "M": $bytes *= 1024*1024; break; + case "K": $bytes *= 1024; break; + } + return $bytes; + } + + // Return files and directories + public function getDirectoryEntries($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true) { + $entries = array(); + $directoryHandle = @opendir($path); + if ($directoryHandle) { + $path = rtrim($path, "/"); + while (($entry = readdir($directoryHandle))!==false) { + if (substru($entry, 0, 1)==".") continue; + $entry = $this->normaliseUnicode($entry); + if (preg_match($regex, $entry)) { + if ($directories) { + if (is_dir("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry); + } else { + if (is_file("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry); + } + } + } + if ($sort) natcasesort($entries); + closedir($directoryHandle); + } + return $entries; + } + + // Return files and directories recursively + public function getDirectoryEntriesRecursive($path, $regex = "/.*/", $sort = true, $directories = true, $levelMax = 0) { + --$levelMax; + $entries = $this->getDirectoryEntries($path, $regex, $sort, $directories); + if ($levelMax!=0) { + foreach ($this->getDirectoryEntries($path, "/.*/", $sort, true) as $entry) { + $entries = array_merge($entries, $this->getDirectoryEntriesRecursive($entry, $regex, $sort, $directories, $levelMax)); + } + } + return $entries; + } + + // Read file, empty string if not found + public function readFile($fileName, $sizeMax = 0) { + $fileData = ""; + $fileHandle = @fopen($fileName, "rb"); + if ($fileHandle) { + clearstatcache(true, $fileName); + $fileSize = $sizeMax ? $sizeMax : filesize($fileName); + if ($fileSize) $fileData = fread($fileHandle, $fileSize); + fclose($fileHandle); + } + return $fileData; + } + + // Create file + public function createFile($fileName, $fileData, $mkdir = false) { + $ok = false; + if ($mkdir) { + $path = dirname($fileName); + if (!empty($path) && !is_dir($path)) @mkdir($path, 0777, true); + } + $fileHandle = @fopen($fileName, "wb"); + if ($fileHandle) { + clearstatcache(true, $fileName); + if (flock($fileHandle, LOCK_EX)) { + ftruncate($fileHandle, 0); + fwrite($fileHandle, $fileData); + flock($fileHandle, LOCK_UN); + } + fclose($fileHandle); + $ok = true; + } + return $ok; + } + + // Append file + public function appendFile($fileName, $fileData, $mkdir = false) { + $ok = false; + if ($mkdir) { + $path = dirname($fileName); + if (!empty($path) && !is_dir($path)) @mkdir($path, 0777, true); + } + $fileHandle = @fopen($fileName, "ab"); + if ($fileHandle) { + clearstatcache(true, $fileName); + if (flock($fileHandle, LOCK_EX)) { + fwrite($fileHandle, $fileData); + flock($fileHandle, LOCK_UN); + } + fclose($fileHandle); + $ok = true; + } + return $ok; + } + + // Copy file + public function copyFile($fileNameSource, $fileNameDestination, $mkdir = false) { + clearstatcache(); + if ($mkdir) { + $path = dirname($fileNameDestination); + if (!empty($path) && !is_dir($path)) @mkdir($path, 0777, true); + } + return @copy($fileNameSource, $fileNameDestination); + } + + // Rename file + public function renameFile($fileNameSource, $fileNameDestination, $mkdir = false) { + clearstatcache(); + if ($mkdir) { + $path = dirname($fileNameDestination); + if (!empty($path) && !is_dir($path)) @mkdir($path, 0777, true); + } + return @rename($fileNameSource, $fileNameDestination); + } + + // Rename directory + public function renameDirectory($pathSource, $pathDestination, $mkdir = false) { + return $pathSource==$pathDestination || $this->renameFile($pathSource, $pathDestination, $mkdir); + } + + // Delete file + public function deleteFile($fileName, $pathTrash = "") { + clearstatcache(); + if (empty($pathTrash)) { + $ok = @unlink($fileName); + } else { + if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true); + $fileNameDestination = $pathTrash; + $fileNameDestination .= pathinfo($fileName, PATHINFO_FILENAME); + $fileNameDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s", filemtime($fileName))); + $fileNameDestination .= ".".pathinfo($fileName, PATHINFO_EXTENSION); + $ok = @rename($fileName, $fileNameDestination); + } + return $ok; + } + + // Delete directory + public function deleteDirectory($path, $pathTrash = "") { + clearstatcache(); + if (empty($pathTrash)) { + $iterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS); + $files = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::CHILD_FIRST); + foreach ($files as $file) { + if ($file->getType()=="dir") { + @rmdir($file->getPathname()); + } else { + @unlink($file->getPathname()); + } + } + $ok = @rmdir($path); + } else { + if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true); + $pathDestination = $pathTrash; + $pathDestination .= basename($path); + $pathDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s", filemtime($path))); + $ok = @rename($path, $pathDestination); + } + return $ok; + } + + // Set file modification date, Unix time + public function modifyFile($fileName, $modified) { + clearstatcache(true, $fileName); + return @touch($fileName, $modified); + } + + // Return file modification date, Unix time + public function getFileModified($fileName) { + return is_file($fileName) ? filemtime($fileName) : 0; + } + + // Return file type + public function getFileType($fileName) { + return strtoloweru(($pos = strrposu($fileName, ".")) ? substru($fileName, $pos+1) : ""); + } + + // Return file group + public function getFileGroup($fileName, $path) { + $group = "none"; + if (preg_match("#^$path(.+?)\/#", $fileName, $matches)) $group = strtoloweru($matches[1]); + return $group; + } + + // Return lines from text, including newline + public function getTextLines($text) { + $lines = preg_split("/\n/", $text); + foreach ($lines as &$line) { + $line = $line."\n"; + } + if (strempty($text) || substru($text, -1, 1)=="\n") array_pop($lines); + return $lines; + } + + // Return settings from text + function getTextSettings($text, $blockStart) { + $settings = new YellowArray(); + if (empty($blockStart)) { + foreach ($this->getTextLines($text) as $line) { + if (preg_match("/^\#/", $line)) continue; + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!empty($matches[1]) && !strempty($matches[2])) { + $settings[$matches[1]] = $matches[2]; + + } + } + } + } else { + $blockKey = ""; + foreach ($this->getTextLines($text) as $line) { + if (preg_match("/^\#/", $line)) continue; + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$blockStart && !strempty($matches[2])) { + $blockKey = $matches[2]; + $settings[$blockKey] = new YellowArray(); + } + if (!empty($blockKey) && !empty($matches[1]) && !strempty($matches[2])) { + $settings[$blockKey][$matches[1]] = $matches[2]; + } + } + } + } + return $settings; + } + + // Set settings in text + function setTextSettings($text, $blockStart, $blockKey, $settings) { + $textNew = ""; + if (empty($blockStart)) { + foreach ($this->getTextLines($text) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!empty($matches[1]) && isset($settings[$matches[1]])) { + $textNew .= "$matches[1]: ".$settings[$matches[1]]."\n"; + unset($settings[$matches[1]]); + continue; + } + } + $textNew .= $line; + } + foreach ($settings as $key=>$value) { + $textNew .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + } + } else { + $scan = false; + $textStart = $textMiddle = $textEnd = ""; + foreach ($this->getTextLines($text) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$blockStart && !strempty($matches[2])) { + $scan = lcfirst($matches[2])==lcfirst($blockKey); + } + } + if (!$scan && empty($textMiddle)) { + $textStart .= $line; + } elseif ($scan) { + $textMiddle .= $line; + } else { + $textEnd .= $line; + } + } + $textSettings = ""; + foreach ($this->getTextLines($textMiddle) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!empty($matches[1]) && isset($settings[$matches[1]])) { + $textSettings .= "$matches[1]: ".$settings[$matches[1]]."\n"; + unset($settings[$matches[1]]); + continue; + } + $textSettings .= $line; + } + } + foreach ($settings as $key=>$value) { + $textSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + } + if (!empty($textMiddle)) { + $textMiddle = $textSettings; + if (!empty($textEnd)) $textMiddle .= "\n"; + } else { + if (!empty($textStart)) $textEnd .= "\n"; + $textEnd .= $textSettings; + } + $textNew = $textStart.$textMiddle.$textEnd; + } + return $textNew; + } + + // Remove settings from text + function unsetTextSettings($text, $blockStart, $blockKey) { + $textNew = ""; + if (!empty($blockStart)) { + $scan = false; + $textStart = $textMiddle = $textEnd = ""; + foreach ($this->getTextLines($text) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$blockStart && !strempty($matches[2])) { + $scan = lcfirst($matches[2])==lcfirst($blockKey); + } + } + if (!$scan && empty($textMiddle)) { + $textStart .= $line; + } elseif ($scan) { + $textMiddle .= $line; + } else { + $textEnd .= $line; + } + } + $textNew = rtrim($textStart.$textEnd)."\n"; + } + return $textNew; + } + + // Return attributes from text + public function getTextAttributes($text) { + $tokens = array(); + $posStart = $posQuote = 0; + $textLength = strlenb($text); + for ($pos=0; $pos<$textLength; ++$pos) { + if ($text[$pos]==" " && !$posQuote) { + if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart)); + $posStart = $pos+1; + } + if ($text[$pos]=="=" && !$posQuote) { + if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart)); + array_push($tokens, "="); + $posStart = $pos+1; + } + if ($text[$pos]=="\"") { + if ($posQuote) { + if ($pos>$posQuote) array_push($tokens, substrb($text, $posQuote+1, $pos-$posQuote-1)); + $posQuote = 0; + $posStart = $pos+1; + } else { + if ($pos==$posStart) $posQuote = $pos; + } + } + } + if ($pos>$posStart && !$posQuote) { + array_push($tokens, substrb($text, $posStart, $pos-$posStart)); + } + $attributes = array(); + for ($i=0; $i<count($tokens); ++$i) { + if ($i+2<count($tokens) && $tokens[$i+1]=="=") { + $key = $tokens[$i]; + $value = $tokens[$i+2]; + $i += 2; + } else { + $key = $value = $tokens[$i]; + } + if (!strempty($key) && !strempty($value)) { + $attributes[$key] = $value; + } + } + return $attributes; + } + + // Return array of specific size from text + public function getTextList($text, $separator, $size) { + $tokens = explode($separator, $text, $size); + return array_pad($tokens, $size, null); + } + + // Return array from text, space separated + public function getTextArguments($text, $optional = "-", $sizeMin = 9) { + $text = preg_replace("/\s+/s", " ", trim($text)); + $tokens = str_getcsv($text, " ", "\""); + foreach ($tokens as $key=>$value) { + if ($value==$optional) $tokens[$key] = ""; + } + return array_pad($tokens, $sizeMin, null); + } + + // Return text from array, space separated + public function getTextString($tokens, $optional = "-") { + $text = ""; + foreach ($tokens as $token) { + if (preg_match("/\s/", $token)) $token = "\"$token\""; + if (empty($token)) $token = $optional; + if (!empty($text)) $text .= " "; + $text .= $token; + } + return $text; + } + + // Return number of words in text + public function getTextWords($text) { + $text = preg_replace("/([\p{Han}\p{Hiragana}\p{Katakana}]{3})/u", "$1 ", $text); + $text = preg_replace("/(\pL|\p{N})/u", "x", $text); + return str_word_count($text); + } + + // Return text truncated at word boundary + public function getTextTruncated($text, $lengthMax) { + if (strlenu($text)>$lengthMax-1) { + $text = substru($text, 0, $lengthMax); + $pos = strrposu($text, " "); + $text = substru($text, 0, $pos ? $pos : $lengthMax-1)."…"; + } + return $text; + } + + // Create text description, with or without HTML + public function createTextDescription($text, $lengthMax = 0, $removeHtml = true, $endMarker = "", $endMarkerText = "") { + $output = ""; + $elementsBlock = array("blockquote", "br", "div", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "li", "ol", "p", "pre", "ul"); + $elementsVoid = array("area", "br", "col", "embed", "hr", "img", "input", "param", "source", "wbr"); + if ($lengthMax==0) $lengthMax = strlenu($text); + if ($removeHtml) { + $offsetBytes = 0; + while (true) { + $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); + $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); + $elementRawData = isset($matches[0][0]) ? $matches[0][0] : ""; + $elementStart = isset($matches[1][0]) ? $matches[1][0] : ""; + $elementName = isset($matches[2][0]) ? $matches[2][0] : ""; + if (!strempty($elementBefore)) { + $rawText = preg_replace("/\s+/s", " ", html_entity_decode($elementBefore, ENT_QUOTES, "UTF-8")); + if (empty($elementStart) && in_array(strtolower($elementName), $elementsBlock)) $rawText = rtrim($rawText)." "; + if (substru($rawText, 0, 1)==" " && (empty($output) || substru($output, -1)==" ")) $rawText = ltrim($rawText); + $output .= $this->getTextTruncated($rawText, $lengthMax); + $lengthMax -= strlenu($rawText); + } + if (!empty($elementRawData) && $elementRawData==$endMarker) { + $output .= $endMarkerText; + $lengthMax = 0; + } + if ($lengthMax<=0 || !$elementFound) break; + $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); + } + $output = preg_replace("/\s+\…$/s", "…", $output); + } else { + $elementsOpen = array(); + $offsetBytes = 0; + while (true) { + $elementFound = preg_match("/&.*?\;|<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); + $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); + $elementRawData = isset($matches[0][0]) ? $matches[0][0] : ""; + $elementStart = isset($matches[1][0]) ? $matches[1][0] : ""; + $elementName = isset($matches[2][0]) ? $matches[2][0] : ""; + $elementEnd = isset($matches[4][0]) ? $matches[4][0] : ""; + if (!strempty($elementBefore)) { + $output .= $this->getTextTruncated($elementBefore, $lengthMax); + $lengthMax -= strlenu($elementBefore); + } + if (!empty($elementRawData) && $elementRawData==$endMarker) { + $output .= $endMarkerText; + $lengthMax = 0; + } + if ($lengthMax<=0 || !$elementFound) break; + if (!empty($elementName) && empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) { + if (empty($elementStart)) { + array_push($elementsOpen, $elementName); + } else { + array_pop($elementsOpen); + } + } + $output .= $elementRawData; + if ($elementRawData[0]=="&") --$lengthMax; + $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); + } + $output = preg_replace("/\s+\…$/s", "…", $output); + for ($i=count($elementsOpen)-1; $i>=0; --$i) { + $output .= "</".$elementsOpen[$i].">"; + } + } + return trim($output); + } + + // Create title from text + public function createTextTitle($text) { + if (preg_match("/^.*\/([\pL\d\-\_]+)/u", $text, $matches)) $text = str_replace("-", " ", ucfirst($matches[1])); + return $text; + } + + // Create random text for cryptography + public function createSalt($length, $bcryptFormat = false) { + $dataBuffer = $salt = ""; + $dataBufferSize = $bcryptFormat ? intval(ceil($length/4) * 3) : intval(ceil($length/2)); + if (empty($dataBuffer) && function_exists("random_bytes")) { + $dataBuffer = @random_bytes($dataBufferSize); + } + if (empty($dataBuffer) && function_exists("mcrypt_create_iv")) { + $dataBuffer = @mcrypt_create_iv($dataBufferSize, MCRYPT_DEV_URANDOM); + } + if (empty($dataBuffer) && function_exists("openssl_random_pseudo_bytes")) { + $dataBuffer = @openssl_random_pseudo_bytes($dataBufferSize); + } + if (strlenb($dataBuffer)==$dataBufferSize) { + if ($bcryptFormat) { + $salt = substrb(base64_encode($dataBuffer), 0, $length); + $base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + $bcrypt64Chars = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + $salt = strtr($salt, $base64Chars, $bcrypt64Chars); + } else { + $salt = substrb(bin2hex($dataBuffer), 0, $length); + } + } + return $salt; + } + + // Create hash with random salt, bcrypt or sha256 + public function createHash($text, $algorithm, $cost = 0) { + $hash = ""; + switch ($algorithm) { + case "bcrypt": $prefix = sprintf("$2y$%02d$", $cost); + $salt = $this->createSalt(22, true); + $hash = crypt($text, $prefix.$salt); + if (empty($salt) || strlenb($hash)!=60) $hash = ""; + break; + case "sha256": $prefix = "$5y$"; + $salt = $this->createSalt(32); + $hash = "$prefix$salt".hash("sha256", $salt.$text); + if (empty($salt) || strlenb($hash)!=100) $hash = ""; + break; + } + return $hash; + } + + // Verify that text matches hash + public function verifyHash($text, $algorithm, $hash) { + $hashCalculated = ""; + switch ($algorithm) { + case "bcrypt": if (substrb($hash, 0, 4)=="$2y$" || substrb($hash, 0, 4)=="$2a$") { + $hashCalculated = crypt($text, $hash); + } + break; + case "sha256": if (substrb($hash, 0, 4)=="$5y$") { + $prefix = "$5y$"; + $salt = substrb($hash, 4, 32); + $hashCalculated = "$prefix$salt".hash("sha256", $salt.$text); + } + break; + } + return $this->verifyToken($hashCalculated, $hash); + } + + // Verify that token is not empty and identical, timing attack safe string comparison + public function verifyToken($tokenExpected, $tokenReceived) { + $ok = false; + $lengthExpected = strlenb($tokenExpected); + $lengthReceived = strlenb($tokenReceived); + if ($lengthExpected!=0 && $lengthReceived!=0) { + $ok = $lengthExpected==$lengthReceived; + for ($i=0; $i<$lengthReceived; ++$i) { + $ok &= $tokenExpected[$i<$lengthExpected ? $i : 0]==$tokenReceived[$i]; + } + } + return $ok; + } + + // Return meta data from raw data + public function getMetaData($rawData, $key) { + $value = ""; + if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) { + $key = lcfirst($key); + foreach ($this->getTextLines($parts[2]) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$key && !strempty($matches[2])) { + $value = $matches[2]; + break; + } + } + } + } + return $value; + } + + // Set meta data in raw data + public function setMetaData($rawData, $key, $value) { + if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) { + $found = false; + $key = lcfirst($key); + $rawDataMiddle = ""; + foreach ($this->getTextLines($parts[2]) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$key) { + $rawDataMiddle .= "$matches[1]: $value\n"; + $found = true; + continue; + } + } + $rawDataMiddle .= $line; + } + if (!$found) $rawDataMiddle .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + $rawDataNew = $parts[1]."---\n".$rawDataMiddle."---\n".$parts[3]; + } else { + $rawDataNew = $rawData; + } + return $rawDataNew; + } + + // Detect server URL + public function detectServerUrl() { + $scheme = "http"; + if ($this->getServer("REQUEST_SCHEME")=="https" || $this->getServer("HTTPS")=="on") $scheme = "https"; + if ($this->getServer("HTTP_X_FORWARDED_PROTO")=="https") $scheme = "https"; + $address = $this->getServer("SERVER_NAME"); + $port = $this->getServer("SERVER_PORT"); + if ($port!=80 && $port!=443) $address .= ":$port"; + $base = ""; + if (preg_match("/^(.*)\/.*\.php$/", $this->getServer("SCRIPT_NAME"), $matches)) $base = $matches[1]; + return "$scheme://$address$base/"; + } + + // Detect server location + public function detectServerLocation() { + if (isset($_SERVER["REQUEST_URI"])) { + $location = $_SERVER["REQUEST_URI"]; + $location = rawurldecode(($pos = strposu($location, "?")) ? substru($location, 0, $pos) : $location); + $location = $this->normaliseTokens($location, true); + $separator = $this->getLocationArgumentsSeparator(); + if (preg_match("/^(.*?\/)([^\/]+$separator.*)$/", $location, $matches)) { + $_SERVER["LOCATION"] = $location = $matches[1]; + $_SERVER["LOCATION_ARGUMENTS"] = $matches[2]; + foreach (explode("/", $matches[2]) as $token) { + if (preg_match("/^(.*?)$separator(.*)$/", $token, $matches)) { + if (!empty($matches[1]) && !strempty($matches[2])) { + $matches[1] = str_replace(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[1]); + $matches[2] = str_replace(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[2]); + $_REQUEST[$matches[1]] = $matches[2]; + } + } + } + } else { + $_SERVER["LOCATION"] = $location; + $_SERVER["LOCATION_ARGUMENTS"] = ""; + } + } + return $this->getServer("LOCATION"); + } + + // Detect server timezone + public function detectServerTimezone() { + $timezone = @date_default_timezone_get(); + if (PHP_OS=="Darwin" && $timezone=="UTC") { + if (preg_match("#zoneinfo/(.*)#", @readlink("/etc/localtime"), $matches)) $timezone = $matches[1]; + } + return $timezone; + } + + // Detect server name and version + public function detectServerInformation() { + if (preg_match("/^(\S+)\/(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) { + $name = $matches[1]; + $version = $matches[2]; + } elseif (preg_match("/^(\pL+)/u", $this->getServer("SERVER_SOFTWARE"), $matches)) { + $name = $matches[1]; + $version = "x.x.x"; + } else { + $name = "CLI"; + $version = PHP_VERSION; + } + return array($name, $version); + } + + // Detect browser language + public function detectBrowserLanguage($languages, $languageDefault) { + $languageFound = $languageDefault; + foreach (preg_split("/\s*,\s*/", $this->getServer("HTTP_ACCEPT_LANGUAGE")) as $string) { + list($language, $dummy) = $this->getTextList($string, ";", 2); + if (!empty($language) && in_array($language, $languages)) { + $languageFound = $language; + break; + } + } + return $languageFound; + } + + // Detect image dimensions and type for gif/jpg/png/svg + public function detectImageInformation($fileName, $fileType = "") { + $width = $height = 0; + $type = ""; + $fileHandle = @fopen($fileName, "rb"); + if ($fileHandle) { + if (empty($fileType)) $fileType = $this->getFileType($fileName); + if ($fileType=="gif") { + $dataSignature = fread($fileHandle, 6); + $dataHeader = fread($fileHandle, 7); + if (!feof($fileHandle) && ($dataSignature=="GIF87a" || $dataSignature=="GIF89a")) { + $width = (ord($dataHeader[1])<<8) + ord($dataHeader[0]); + $height = (ord($dataHeader[3])<<8) + ord($dataHeader[2]); + $type = $fileType; + } + } elseif ($fileType=="jpg") { + $dataBufferSizeMax = filesize($fileName); + $dataBufferSize = min($dataBufferSizeMax, 4096); + if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize); + $dataSignature = substrb($dataBuffer, 0, 4); + if (!feof($fileHandle) && ($dataSignature=="\xff\xd8\xff\xe0" || $dataSignature=="\xff\xd8\xff\xe1")) { + for ($pos=2; $pos+8<$dataBufferSize; $pos+=$length) { + if ($dataBuffer[$pos]!="\xff") break; + if ($dataBuffer[$pos+1]=="\xc0" || $dataBuffer[$pos+1]=="\xc2") { + $width = (ord($dataBuffer[$pos+7])<<8) + ord($dataBuffer[$pos+8]); + $height = (ord($dataBuffer[$pos+5])<<8) + ord($dataBuffer[$pos+6]); + $type = $fileType; + break; + } + $length = (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]) + 2; + while ($pos+$length+8>=$dataBufferSize) { + if ($dataBufferSize==$dataBufferSizeMax) break; + $dataBufferDiff = min($dataBufferSizeMax, $dataBufferSize*2) - $dataBufferSize; + $dataBufferSize += $dataBufferDiff; + $dataBufferChunk = fread($fileHandle, $dataBufferDiff); + if (feof($fileHandle) || $dataBufferChunk===false) { + $dataBufferSize = 0; + break; + } + $dataBuffer .= $dataBufferChunk; + } + } + } + } elseif ($fileType=="png") { + $dataSignature = fread($fileHandle, 8); + $dataHeader = fread($fileHandle, 16); + if (!feof($fileHandle) && $dataSignature=="\x89PNG\r\n\x1a\n") { + $width = (ord($dataHeader[10])<<8) + ord($dataHeader[11]); + $height = (ord($dataHeader[14])<<8) + ord($dataHeader[15]); + $type = $fileType; + } + } elseif ($fileType=="svg") { + $dataBufferSizeMax = filesize($fileName); + $dataBufferSize = min($dataBufferSizeMax, 4096); + if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize); + if (!feof($fileHandle) && preg_match("/<svg(\s.*?)>/s", $dataBuffer, $matches)) { + if (preg_match("/\swidth=\"(\d+)\"/s", $matches[1], $tokens)) $width = $tokens[1]; + if (preg_match("/\sheight=\"(\d+)\"/s", $matches[1], $tokens)) $height = $tokens[1]; + $type = $fileType; + } + } + fclose($fileHandle); + } + return array($width, $height, $type); + } + + // Normalise location arguments + public function normaliseArguments($text, $appendSlash = true, $filterStrict = true) { + if ($appendSlash) $text .= "/"; + if ($filterStrict) $text = str_replace(" ", "-", strtoloweru($text)); + $text = str_replace(":", $this->getLocationArgumentsSeparator(), $text); + return str_replace(array("%2F","%3A","%3D"), array("/",":","="), rawurlencode($text)); + } + + // Normalise path or location, take care of relative path tokens + public function normaliseTokens($text, $prependSlash = false) { + $textFiltered = ""; + if ($prependSlash && substru($text, 0, 1)!="/") $textFiltered .= "/"; + $textLength = strlenb($text); + for ($pos=0; $pos<$textLength; ++$pos) { + if (($text[$pos]=="/" || $pos==0) && $pos+1<$textLength) { + if ($text[$pos+1]=="/") continue; + if ($text[$pos+1]==".") { + $posNew = $pos+1; + while ($text[$posNew]==".") { + ++$posNew; + } + if ($text[$posNew]=="/" || $text[$posNew]=="") { + $pos = $posNew-1; + continue; + } + } + } + $textFiltered .= $text[$pos]; + } + return $textFiltered; + } + + // Normalise elements and attributes in HTML/SVG data + public function normaliseData($text, $type = "html", $filterStrict = true) { + $output = ""; + $elementsHtml = array( + "a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "iframe", "image", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meta", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "shadow", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"); + $elementsSvg = array( + "svg", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "feblend", "fecolormatrix", "fecomponenttransfer", "fecomposite", "feconvolvematrix", "fediffuselighting", "fedisplacementmap", "fedistantlight", "feflood", "fefunca", "fefuncb", "fefuncg", "fefuncr", "fegaussianblur", "femerge", "femergenode", "femorphology", "feoffset", "fepointlight", "fespecularlighting", "fespotlight", "fetile", "feturbulence", "filter", "font", "g", "glyph", "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "use", "view", "vkern"); + $attributesHtml = array( + "accept", "action", "align", "allowfullscreen", "alt", "autocomplete", "background", "bgcolor", "border", "cellpadding", "cellspacing", "charset", "checked", "cite", "class", "clear", "color", "cols", "colspan", "content", "controls", "coords", "crossorigin", "datetime", "default", "dir", "disabled", "download", "enctype", "face", "for", "frameborder", "headers", "height", "hidden", "high", "href", "hreflang", "id", "integrity", "ismap", "label", "lang", "list", "loop", "low", "max", "maxlength", "media", "method", "min", "multiple", "name", "noshade", "novalidate", "nowrap", "open", "optimum", "pattern", "placeholder", "poster", "prefix", "preload", "property", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "spellcheck", "scope", "selected", "shape", "size", "sizes", "span", "srclang", "start", "src", "srcset", "step", "style", "summary", "tabindex", "target", "title", "type", "usemap", "valign", "value", "width", "xmlns"); + $attributesSvg = array( + "accent-height", "accumulate", "additivive", "alignment-baseline", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "datenstrom", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "fill", "fill-opacity", "fill-rule", "filter", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "points", "preservealpha", "preserveaspectratio", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "specularconstant", "specularexponent", "spreadmethod", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "tabindex", "targetx", "targety", "transform", "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "viewbox", "visibility", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xlink:href", "xml:id", "xml:space", "xmlns", "y", "y1", "y2", "z", "zoomandpan"); + $elementsSafe = $elementsHtml; + $attributesSafe = $attributesHtml; + if ($type=="svg") { + $elementsSafe = array_merge($elementsHtml, $elementsSvg); + $attributesSafe = array_merge($attributesHtml, $attributesSvg); + } + $offsetBytes = 0; + while (true) { + $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); + $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); + $elementStart = $elementFound ? $matches[1][0] : ""; + $elementName = $elementFound ? $matches[2][0]: ""; + $elementMiddle = $elementFound ? $matches[3][0]: ""; + $elementEnd = $elementFound ? $matches[4][0]: ""; + $output .= $elementBefore; + if (substrb($elementName, 0, 1)=="!") { + $output .= "<$elementName$elementMiddle>"; + } elseif (in_array(strtolower($elementName), $elementsSafe)) { + $elementAttributes = $this->getTextAttributes($elementMiddle); + foreach ($elementAttributes as $key=>$value) { + if (!in_array(strtolower($key), $attributesSafe) && !preg_match("/^(aria|data)-/i", $key)) { + unset($elementAttributes[$key]); + } + } + if ($filterStrict) { + $href = isset($elementAttributes["href"]) ? $elementAttributes["href"] : ""; + if (preg_match("/^\w+:/", $href) && !preg_match("/^(http|https|ftp|mailto):/", $href)) { + $elementAttributes["href"] = "error-xss-filter"; + } + $href = isset($elementAttributes["xlink:href"]) ? $elementAttributes["xlink:href"] : ""; + if (preg_match("/^\w+:/", $href) && !preg_match("/^(http|https|ftp|mailto):/", $href)) { + $elementAttributes["xlink:href"] = "error-xss-filter"; + } + } + $output .= "<$elementStart$elementName"; + foreach ($elementAttributes as $key=>$value) $output .= " $key=\"$value\""; + if (!empty($elementEnd)) $output .= " "; + $output .= "$elementEnd>"; + } + if (!$elementFound) break; + $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); + } + return $output; + } + + // Normalise text lines, convert line endings + public function normaliseLines($text, $endOfLine = "lf") { + if ($endOfLine=="lf") { + $text = preg_replace("/\R/u", "\n", $text); + } else { + $text = preg_replace("/\R/u", "\r\n", $text); + } + return $text; + } + + // Normalise text into UTF-8 NFC + public function normaliseUnicode($text) { + if (PHP_OS=="Darwin" && !mb_check_encoding($text, "ASCII")) { + $utf8nfc = preg_match("//u", $text) && !preg_match("/[^\\x00-\\x{2FF}]/u", $text); + if (!$utf8nfc) $text = iconv("UTF-8-MAC", "UTF-8", $text); + } + return $text; + } + + // Start timer + public function timerStart(&$time) { + $time = microtime(true); + } + + // Stop timer and calculate elapsed time in milliseconds + public function timerStop(&$time) { + $time = intval((microtime(true)-$time) * 1000); + } + + // Check if there are location arguments in current HTTP request + public function isLocationArguments($location = "") { + if (empty($location)) $location = $this->getServer("LOCATION").$this->getServer("LOCATION_ARGUMENTS"); + $separator = $this->getLocationArgumentsSeparator(); + return preg_match("/[^\/]+$separator.*$/", $location); + } + + // Check if there are pagination arguments in current HTTP request + public function isLocationArgumentsPagination($location) { + $separator = $this->getLocationArgumentsSeparator(); + return preg_match("/^(.*\/)?page$separator.*$/", $location); + } + + // Check if unmodified since last HTTP request + public function isNotModified($lastModifiedFormatted) { + return $this->getServer("HTTP_IF_MODIFIED_SINCE")==$lastModifiedFormatted; + } + + // TODO: remove later, for backwards compatibility + public function getLocationArgs() { return $this->getLocationArguments(); } + public function getLocationArgsNew($key, $value) { return $this->getLocationArgumentsNew($key, $value); } + public function getLocationArgsCleanUrl() { return $this->getLocationArgumentsCleanUrl(); } + public function getLocationArgsSeparator() { return $this->getLocationArgumentsSeparator(); } + public function getTextArgs($text, $optional = "-", $sizeMin = 9) { return $this->getTextArguments($text, $optional, $sizeMin); } + public function normaliseArgs($text, $appendSlash = true, $filterStrict = true) { return $this->normaliseArguments($text, $appendSlash, $filterStrict); } + public function isLocationArgs($location = "") { return $this->isLocationArguments($location); } + public function isLocationArgsPagination($location) { return $this->isLocationArgumentsPagination($location); } +} + +function strreplaceu() { // TODO: remove later, for backwards compatibility + return call_user_func_array("str_replace", func_get_args()); +} + +class YellowText { // TODO: remove later, for backwards compatibility + public $yellow; + public function __construct($yellow) { $this->yellow = $yellow; } + public function load($fileName) { $this->yellow->language->load($fileName); } + public function setLanguage($language) { $this->yellow->language->set($language); } + public function setText($key, $value, $language) { $this->yellow->language->setText($key, $value, $language); } + public function get($key) { return $this->yellow->language->getText($key); } + public function getHtml($key) { return $this->yellow->language->getTextHtml($key); } + public function getText($key, $language) { return $this->yellow->language->getText($key, $language); } + public function getTextHtml($key, $language) { return $this->yellow->language->getTextHtml($key, $language); } + public function getData($filterStart = "", $language = "") { return $this->yellow->language->getSettings($filterStart, "", $language); } + public function getDateFormatted($timestamp, $format) { return $this->yellow->language->getDateFormatted($timestamp, $format); } + public function getDateRelative($timestamp, $format, $daysLimit) { return $this->yellow->language->getDateRelative($timestamp, $format, $daysLimit); } + public function getModified($httpFormat = false) { return $this->yellow->language->getModified($httpFormat); } + public function getLanguages() { return $this->yellow->system->getValues("language"); } + public function normaliseDate($text) { return $this->yellow->language->normaliseDate($text); } + public function isLanguage($language) { return $this->yellow->language->isExisting($language); } + public function isExisting($key, $language = "") { return $this->yellow->language->isText($key, $language); } +} + +class YellowExtensions { // TODO: remove later, for backwards compatibility + public $yellow; + public function __construct($yellow) { $this->yellow = $yellow; } + public function load($path) { $this->yellow->extension->load($path); } + public function register($name, $class) { $this->yellow->extension->register($name, $class); } + public function get($name) { return $this->yellow->extension->get($name); } + public function getData($type = "") { return array(); } + public function getModified($httpFormat = false) { return $this->yellow->extension->getModified($httpFormat); } + public function getExtensions($type = "") { return array(); } + public function isExisting($name) { return $this->yellow->extension->isExisting($name); } +} + +class YellowArray extends ArrayObject { + public function __construct() { + parent::__construct(array()); + } + + // Set array element + public function set($key, $value) { + $this->offsetSet($key, $value); + } + + // Return array element + public function get($key) { + return $this->offsetExists($key) ? $this->offsetGet($key) : ""; + } + + // Check if array element exists + public function isExisting($key) { + return $this->offsetExists($key); + } + + // Return array element + public function offsetGet($key) { + if (is_string($key)) $key = lcfirst($key); + return parent::offsetGet($key); + } + + // Set array element + public function offsetSet($key, $value) { + if (is_string($key)) $key = lcfirst($key); + parent::offsetSet($key, $value); + } + + // Remove array element + public function offsetUnset($key) { + if (is_string($key)) $key = lcfirst($key); + parent::offsetUnset($key); + } + + // Check if array element exists + public function offsetExists($key) { + if (is_string($key)) $key = lcfirst($key); + return parent::offsetExists($key); + } +} + +// Make string lowercase, UTF-8 compatible +function strtoloweru() { + return call_user_func_array("mb_strtolower", func_get_args()); +} + +// Make string uppercase, UTF-8 compatible +function strtoupperu() { + return call_user_func_array("mb_strtoupper", func_get_args()); +} + +// Return string length, UTF-8 characters +function strlenu() { + return call_user_func_array("mb_strlen", func_get_args()); +} + +// Return string length, bytes +function strlenb() { + return call_user_func_array("strlen", func_get_args()); +} + +// Return string position of first match, UTF-8 characters +function strposu() { + return call_user_func_array("mb_strpos", func_get_args()); +} + +// Return string position of first match, bytes +function strposb() { + return call_user_func_array("strpos", func_get_args()); +} + +// Return string position of last match, UTF-8 characters +function strrposu() { + return call_user_func_array("mb_strrpos", func_get_args()); +} + +// Return string position of last match, bytes +function strrposb() { + return call_user_func_array("strrpos", func_get_args()); +} + +// Return part of a string, UTF-8 characters +function substru() { + return call_user_func_array("mb_substr", func_get_args()); +} + +// Return part of a string, bytes +function substrb() { + return call_user_func_array("substr", func_get_args()); +} + +// Check if string is empty +function strempty($string) { + return is_null($string) || $string===""; +} diff --git a/system/extensions/edit.css b/system/extensions/edit.css new file mode 100644 index 0000000..f85c7e4 --- /dev/null +++ b/system/extensions/edit.css @@ -0,0 +1,579 @@ +/* Edit extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/edit */ + +.yellow-bar { + position: relative; +} +.yellow-bar-left { + display: block; + float: left; +} +.yellow-bar-right { + display: block; + float: right; +} +.yellow-bar-right a { + margin-left: 1em; +} +.yellow-bar-banner { + clear: both; +} +.yellow-body-modal-open { + overflow: hidden; +} +.yellow-pane { + position: absolute; + display: none; + z-index: 100; + padding: 10px; + background-color: #fff; + color: #000; + border: 1px solid #bbb; + border-radius: 4px; + box-shadow: 2px 4px 10px rgba(0, 0, 0, 0.2); + text-align: center; +} +.yellow-pane h1 { + color: #000; + font-size: 2em; + margin: 0 1em; + overflow: hidden; + text-overflow: ellipsis; +} +.yellow-pane p { + margin: 0.5em 0; +} +.yellow-pane .yellow-status { + margin-bottom: 1em; +} +.yellow-pane .yellow-fields { + width: 14em; + margin: 0 auto; + text-align: left; +} +.yellow-pane .yellow-fields .yellow-center { + width: 14em; + display: inline-block; + text-align: center; +} +.yellow-pane .yellow-fields .yellow-form-control { + width: 15em; + box-sizing: border-box; +} +.yellow-pane .yellow-fields .yellow-btn { + width: 15em; + margin: 1em 0 0.5em 0; +} +.yellow-pane .yellow-buttons .yellow-btn { + width: 15em; + margin: 0.5em 0; +} +.yellow-close { + position: absolute; + top: 0.8em; + right: 1em; + cursor: pointer; + font-size: 0.9em; + color: #bbb; + text-decoration: none; +} +.yellow-close:hover { + color: #000; + text-decoration: none; +} +.yellow-arrow { + position: absolute; + top: 0; + left: 0; +} +.yellow-arrow:after, +.yellow-arrow:before { + position: absolute; + pointer-events: none; + bottom: 100%; + height: 0; + width: 0; + border: solid transparent; + content: ""; +} +.yellow-arrow:after { + border-color: rgba(255, 255, 255, 0); + border-bottom-color: #fff; + border-width: 10px; + margin-left: -10px; +} +.yellow-arrow:before { + border-color: rgba(187, 187, 187, 0); + border-bottom-color: #bbb; + border-width: 11px; + margin-left: -11px; +} +.yellow-settings { + text-align: left; +} +.yellow-settings-left { + float: left; + padding: 0 0.5em; +} +.yellow-settings-right { + float: left; +} +.yellow-settings-separator { + visibility: hidden; + padding: 20px; +} +.yellow-settings-banner { + clear: both; +} +.yellow-popup { + position: absolute; + display: none; + z-index: 200; + padding: 10px 0; + background-color: #fff; + color: #000; + border: 1px solid #bbb; + border-radius: 4px; + box-shadow: 2px 4px 10px rgba(0, 0, 0, 0.2); +} +.yellow-dropdown { + list-style: none; + margin: 0; + padding: 0; +} +.yellow-dropdown span { + display: block; + margin: 0; + padding: 0.25em 1em; +} +.yellow-dropdown a { + display: block; + padding: 0.2em 1em; + text-decoration: none; +} +.yellow-dropdown a:hover { + color: #fff; + background-color: #18e; + text-decoration: none; +} +.yellow-dropdown-menu a { + color: #000; +} +.yellow-toolbar { + list-style: none; + margin: 0; + padding: 0; +} +.yellow-toolbar-left { + display: inline-block; + float: left; +} +.yellow-toolbar-right { + display: inline-block; + float: right; +} +.yellow-toolbar-banner { + clear: both; +} +.yellow-toolbar h1 { + margin: -5px 0 0 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.yellow-toolbar li { + display: inline-block; + vertical-align: top; +} +.yellow-toolbar a { + display: inline-block; + padding: 6px 16px; + text-decoration: none; + background-color: #fff; + color: #000; + font-size: 0.9em; + font-weight: normal; + border: 1px solid #bbb; + border-radius: 4px; +} +.yellow-toolbar a:hover { + background-color: #18e; + background-image: none; + border-color: #18e; + color: #fff; + text-decoration: none; +} +.yellow-toolbar-left a { + margin-right: 4px; + margin-bottom: 10px; +} +.yellow-toolbar-right a { + margin-left: 4px; + margin-bottom: 10px; +} +.yellow-toolbar .yellow-icon { + font-size: 0.9em; + min-width: 1em; + text-align: center; +} +.yellow-toolbar .yellow-toolbar-btn { + padding: 6px 10px; + min-width: 4em; + text-align: center; +} +.yellow-toolbar .yellow-toolbar-btn-edit { + background-color: #29f; + border-color: #29f; + color: #fff; +} +.yellow-toolbar .yellow-toolbar-btn-create { + background-color: #29f; + border-color: #29f; + color: #fff; +} +.yellow-toolbar .yellow-toolbar-btn-delete { + background-color: #e55; + border-color: #e55; + color: #fff; +} +.yellow-toolbar .yellow-toolbar-btn-delete:hover { + background-color: #d44; + border-color: #d44; +} +.yellow-toolbar .yellow-toolbar-btn-separator { + visibility: hidden; + padding: 6px; +} +.yellow-toolbar .yellow-toolbar-checked { + background-color: #666; + border-color: #666; + color: #fff; +} +.yellow-toolbar-tooltip { + position: relative; +} +.yellow-toolbar-tooltip::after, +.yellow-toolbar-tooltip::before { + position: absolute; + z-index: 300; + display: none; + pointer-events: none; +} +.yellow-toolbar-tooltip::after { + padding: 2px 9px; + font-weight: normal; + font-size: 0.9em; + text-align: center; + white-space: nowrap; + content: attr(aria-label); + background-color: #111; + color: #ddd; + border-radius: 3px; + top: 100%; + right: 50%; + margin-top: 6px; + transform: translateX(50%); +} +.yellow-toolbar-tooltip::before { + width: 0; + height: 0; + content: ""; + border: 4px solid transparent; + top: auto; + right: 50%; + bottom: -6px; + margin-right: -4px; + border-bottom-color: #111; +} +.yellow-toolbar-tooltip:hover::before, +.yellow-toolbar-tooltip:hover::after { + display: inline-block; +} +.yellow-toolbar-selected.yellow-toolbar-tooltip::before, +.yellow-toolbar-selected.yellow-toolbar-tooltip::after { + display: none; +} +.yellow-edit-text { + margin: 0; + padding: 0 2px; + outline: none; + resize: none; + border: none; + font-size: 0.9em; + font-family: inherit; + font-weight: normal; + line-height: normal; +} +.yellow-edit-preview { + padding: 0; + overflow: auto; +} +.yellow-edit-preview h1 { + margin: 0.67em 0; +} +.yellow-edit-preview p { + margin: 1em 0; +} +.yellow-edit-preview .content { + margin: 0; + padding: 0; +} +.yellow-form-control { + margin: 0; + padding: 2px 4px; + display: inline-block; + background-color: #fff; + color: #000; + background-image: linear-gradient(to bottom, #fff, #fff); + border: 1px solid #bbb; + border-radius: 4px; + font-size: 0.9em; + font-family: inherit; + font-weight: normal; + line-height: normal; +} +.yellow-btn { + margin: 0; + padding: 4px 22px; + display: inline-block; + min-width: 8em; + background-color: #eaeaea; + color: #333333; + background-image: linear-gradient(to bottom, #f8f8f8, #e1e1e1); + border: 1px solid #bbb; + border-color: #c1c1c1 #c1c1c1 #aaaaaa; + border-radius: 4px; + outline-offset: -2px; + font-size: 0.9em; + font-family: inherit; + font-weight: normal; + line-height: 1; + text-align: center; + text-decoration: none; + box-sizing: border-box; +} +.yellow-btn:hover, +.yellow-btn:focus, +.yellow-btn:active { + color: #333333; + background-image: none; + text-decoration: none; +} +.yellow-btn:active { + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Specific panes */ + +#yellow-pane-create-bar { + padding: 0 0.5em; +} +#yellow-pane-delete-bar { + padding: 0 0.5em; +} +#yellow-pane-create, +#yellow-pane-edit, +#yellow-pane-delete { + text-align: left; +} +#yellow-pane-menu { + padding: 10px 0; + text-align: left; +} + +/* Specific popups */ + +#yellow-popup-format, +#yellow-popup-heading, +#yellow-popup-list { + width: 16em; +} +#yellow-popup-format a, +#yellow-popup-heading a { + padding: 0.25em 16px; +} +#yellow-popup-format #yellow-popup-format-h1, +#yellow-popup-heading #yellow-popup-heading-h1 { + font-size: 2em; + font-weight: bold; +} +#yellow-popup-format #yellow-popup-format-h2, +#yellow-popup-heading #yellow-popup-heading-h2 { + font-size: 1.6em; + font-weight: bold; +} +#yellow-popup-format #yellow-popup-format-h3, +#yellow-popup-heading #yellow-popup-heading-h3 { + font-size: 1.3em; + font-weight: bold; +} +#yellow-popup-format #yellow-popup-format-notice { + font-weight: bold; +} +#yellow-popup-format #yellow-popup-format-quote { + font-style: italic; +} +#yellow-popup-format #yellow-popup-format-pre { + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 0.9em; + line-height: 1.8; +} +#yellow-popup-emojiawesome { + padding: 10px; + width: 14em; +} +#yellow-popup-emojiawesome a { + padding: 0.2em; +} +#yellow-popup-emojiawesome .yellow-dropdown li { + display: inline-block; +} +#yellow-popup-fontawesome { + padding: 10px; + width: 13em; +} +#yellow-popup-fontawesome a { + padding: 0.18em 0.3em; + min-width: 1em; + text-align: center; +} +#yellow-popup-fontawesome .yellow-dropdown li { + display: inline-block; +} + +/* Icons */ + +@font-face { + font-family: "Edit"; + font-weight: normal; + font-style: normal; + src: url("edit.woff") format("woff"); +} +.yellow-icon { + display: inline-block; + font-family: Edit; + font-style: normal; + font-weight: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.yellow-spin { + -webkit-animation: yellow-spin 1s infinite steps(16); + animation: yellow-spin 1s infinite steps(16); +} +@-webkit-keyframes yellow-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes yellow-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} + +.yellow-icon-preview:before { + content: "\f100"; +} +.yellow-icon-format:before { + content: "\f101"; +} +.yellow-icon-paragraph:before { + content: "\f101"; +} +.yellow-icon-heading:before { + content: "\f102"; +} +.yellow-icon-h1:before { + content: "\f103"; +} +.yellow-icon-h2:before { + content: "\f104"; +} +.yellow-icon-h3:before { + content: "\f105"; +} +.yellow-icon-bold:before { + content: "\f106"; +} +.yellow-icon-italic:before { + content: "\f0f7"; +} +.yellow-icon-strikethrough:before { + content: "\f108"; +} +.yellow-icon-quote:before { + content: "\f109"; +} +.yellow-icon-code:before { + content: "\f10a"; +} +.yellow-icon-pre:before { + content: "\f10a"; +} +.yellow-icon-link:before { + content: "\f10b"; +} +.yellow-icon-file:before { + content: "\f10c"; +} +.yellow-icon-list:before { + content: "\f10d"; +} +.yellow-icon-ul:before { + content: "\f10d"; +} +.yellow-icon-ol:before { + content: "\f10e"; +} +.yellow-icon-tl:before { + content: "\f10f"; +} +.yellow-icon-hr:before { + content: "\f110"; +} +.yellow-icon-table:before { + content: "\f111"; +} +.yellow-icon-emojiawesome:before { + content: "\f112"; +} +.yellow-icon-fontawesome:before { + content: "\f113"; +} +.yellow-icon-status:before { + content: "\f114"; +} +.yellow-icon-undo:before { + content: "\f115"; +} +.yellow-icon-redo:before { + content: "\f116"; +} +.yellow-icon-spinner:before { + content: "\f200"; +} +.yellow-icon-search:before { + content: "\f201"; +} +.yellow-icon-close:before { + content: "\f202"; +} +.yellow-icon-help:before { + content: "\f203"; +} +.yellow-icon-markdown:before { + content: "\f203"; +} +.yellow-icon-logo:before { + content: "\f8ff"; +} diff --git a/system/extensions/edit.js b/system/extensions/edit.js new file mode 100644 index 0000000..fafecaf --- /dev/null +++ b/system/extensions/edit.js @@ -0,0 +1,1500 @@ +// Edit extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/edit + +var yellow = { + onLoad: function(e) { yellow.edit.load(e); }, + onKeydown: function(e) { yellow.edit.keydown(e); }, + onDrag: function(e) { yellow.edit.drag(e); }, + onDrop: function(e) { yellow.edit.drop(e); }, + onClick: function(e) { yellow.edit.click(e); }, + onClickAction: function(e) { yellow.edit.clickAction(e); }, + onPageShow: function(e) { yellow.edit.pageShow(e); }, + onUpdatePane: function() { yellow.edit.updatePane(yellow.edit.paneId, yellow.edit.paneAction, yellow.edit.paneStatus); }, + onResizePane: function() { yellow.edit.resizePane(yellow.edit.paneId, yellow.edit.paneAction, yellow.edit.paneStatus); }, + action: function(action, status, arguments) { yellow.edit.processAction(action, status, arguments); } +}; + +yellow.edit = { + paneId: 0, // visible pane ID + paneAction: 0, // current pane action + paneStatus: 0, // current pane status + popupId: 0, // visible popup ID + intervalId: 0, // timer interval ID + + // Handle initialisation + load: function(e) { + var body = document.getElementsByTagName("body")[0]; + if (body && body.firstChild && !document.getElementById("yellow-bar")) { + this.createBar("yellow-bar"); + this.processAction(yellow.page.action, yellow.page.status); + clearInterval(this.intervalId); + } + if (e.type=="DOMContentLoaded") { + var page = document.getElementsByClassName("page")[0]; + if (page) this.bindActions(page); + } + }, + + // Handle keyboard + keydown: function(e) { + if (this.paneId=="yellow-pane-create" || this.paneId=="yellow-pane-edit" || this.paneId=="yellow-pane-delete") this.processShortcut(e); + if (this.paneId && e.keyCode==27) this.hidePane(this.paneId); + }, + + // Handle drag + drag: function(e) { + e.stopPropagation(); + e.preventDefault(); + }, + + // Handle drop + drop: function(e) { + e.stopPropagation(); + e.preventDefault(); + var elementText = document.getElementById(this.paneId+"-text"); + var files = e.dataTransfer ? e.dataTransfer.files : e.target.files; + for (var i=0; i<files.length; i++) this.uploadFile(elementText, files[i]); + }, + + // Handle mouse clicked + click: function(e) { + if (this.popupId && !document.getElementById(this.popupId).contains(e.target)) this.hidePopup(this.popupId, true); + if (this.paneId && !document.getElementById(this.paneId).contains(e.target)) this.hidePane(this.paneId, true); + }, + + // Handle action clicked + clickAction: function(e) { + e.stopPropagation(); + e.preventDefault(); + var element = e.target; + for (; element; element=element.parentNode) { + if (element.tagName=="A") break; + } + this.processAction(element.getAttribute("data-action"), element.getAttribute("data-status"), element.getAttribute("data-arguments")); + }, + + // Handle page cache + pageShow: function(e) { + if (e.persisted && yellow.user.email && !this.getCookie("csrftoken")) { + window.location.reload(); + } + }, + + // Create bar + createBar: function(barId) { + var elementBar = document.createElement("div"); + elementBar.className = "yellow-bar"; + elementBar.setAttribute("id", barId); + if (barId=="yellow-bar") { + yellow.toolbox.addEvent(document, "click", yellow.onClick); + yellow.toolbox.addEvent(document, "keydown", yellow.onKeydown); + yellow.toolbox.addEvent(window, "pageshow", yellow.onPageShow); + yellow.toolbox.addEvent(window, "resize", yellow.onResizePane); + } + var elementDiv = document.createElement("div"); + elementDiv.setAttribute("id", barId+"-content"); + if (yellow.user.name) { + elementDiv.innerHTML = + "<div class=\"yellow-bar-left\">"+ + this.getRawDataPaneAction("edit")+ + "</div>"+ + "<div class=\"yellow-bar-right\">"+ + this.getRawDataPaneAction("create")+ + this.getRawDataPaneAction("delete")+ + this.getRawDataPaneAction("menu", yellow.user.name, true)+ + "</div>"+ + "<div class=\"yellow-bar-banner\"></div>"; + } else { + elementDiv.innerHTML = " "; + } + elementBar.appendChild(elementDiv); + yellow.toolbox.insertBefore(elementBar, document.getElementsByTagName("body")[0].firstChild); + this.bindActions(elementBar); + }, + + // Update bar + updateBar: function(paneId, name) { + if (paneId) { + var element = document.getElementById(paneId+"-bar"); + if (element) { + if (name.indexOf("selected")!=-1) element.setAttribute("aria-expanded", "true"); + yellow.toolbox.addClass(element, name); + } + } else { + var elements = document.getElementsByClassName(name); + for (var i=0, l=elements.length; i<l; i++) { + if (name.indexOf("selected")!=-1) elements[i].setAttribute("aria-expanded", "false"); + yellow.toolbox.removeClass(elements[i], name); + } + } + }, + + // Create pane + createPane: function(paneId, paneAction, paneStatus) { + if (yellow.system.debug) console.log("yellow.edit.createPane id:"+paneId); + var elementPane = document.createElement("div"); + elementPane.className = "yellow-pane"; + elementPane.setAttribute("id", paneId); + elementPane.style.display = "none"; + if (paneId=="yellow-pane-create" || paneId=="yellow-pane-edit") { + yellow.toolbox.addEvent(elementPane, "input", yellow.onUpdatePane); + yellow.toolbox.addEvent(elementPane, "dragenter", yellow.onDrag); + yellow.toolbox.addEvent(elementPane, "dragover", yellow.onDrag); + yellow.toolbox.addEvent(elementPane, "drop", yellow.onDrop); + } + if (paneId=="yellow-pane-create" || paneId=="yellow-pane-edit" || paneId=="yellow-pane-delete" || paneId=="yellow-pane-menu") { + var elementArrow = document.createElement("span"); + elementArrow.className = "yellow-arrow"; + elementArrow.setAttribute("id", paneId+"-arrow"); + elementPane.appendChild(elementArrow); + } + var elementDiv = document.createElement("div"); + elementDiv.className = "yellow-content"; + elementDiv.setAttribute("id", paneId+"-content"); + switch (paneId) { + case "yellow-pane-login": + elementDiv.innerHTML = + "<form method=\"post\">"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<div class=\"yellow-title\"><h1>"+this.getText("LoginTitle")+"</h1></div>"+ + "<div class=\"yellow-fields\">"+ + "<input type=\"hidden\" name=\"action\" value=\"login\" />"+ + "<p><label for=\"yellow-pane-login-email\">"+this.getText("LoginEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-login-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(yellow.system.editLoginEmail)+"\" /></p>"+ + "<p><label for=\"yellow-pane-login-password\">"+this.getText("LoginPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-login-password\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(yellow.system.editLoginPassword)+"\" /></p>"+ + "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("LoginButton")+"\" /></p>"+ + "<p><a href=\"#\" id=\"yellow-pane-login-forgot\" class=\"yellow-center\" data-action=\"forgot\">"+this.getText("LoginForgot")+"</a><br /><a href=\"#\" id=\"yellow-pane-login-signup\" class=\"yellow-center\" data-action=\"signup\">"+this.getText("LoginSignup")+"</a></p>"+ + "</div>"+ + "</form>"; + break; + case "yellow-pane-signup": + elementDiv.innerHTML = + "<form method=\"post\">"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<div class=\"yellow-title\"><h1>"+this.getText("SignupTitle")+"</h1></div>"+ + "<div class=\"yellow-status\"><p id=\"yellow-pane-signup-status\" class=\""+paneStatus+"\">"+this.getText("SignupStatus", "", paneStatus)+"</p></div>"+ + "<div class=\"yellow-fields\">"+ + "<input type=\"hidden\" name=\"action\" value=\"signup\" />"+ + "<p><label for=\"yellow-pane-signup-name\">"+this.getText("SignupName")+"</label><br /><input class=\"yellow-form-control\" name=\"name\" id=\"yellow-pane-signup-name\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("name"))+"\" /></p>"+ + "<p><label for=\"yellow-pane-signup-email\">"+this.getText("SignupEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-signup-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+ + "<p><label for=\"yellow-pane-signup-password\">"+this.getText("SignupPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-signup-password\" maxlength=\"64\" value=\"\" /></p>"+ + "<p><input type=\"checkbox\" name=\"consent\" value=\"consent\" id=\"yellow-pane-signup-consent\""+(this.getRequest("consent") ? " checked=\"checked\"" : "")+"> <label for=\"yellow-pane-signup-consent\">"+this.getText("SignupConsent")+"</label></p>"+ + "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("SignupButton")+"\" /></p>"+ + "</div>"+ + "</form>"; + break; + case "yellow-pane-forgot": + elementDiv.innerHTML = + "<form method=\"post\">"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<div class=\"yellow-title\"><h1>"+this.getText("ForgotTitle")+"</h1></div>"+ + "<div class=\"yellow-status\"><p id=\"yellow-pane-forgot-status\" class=\""+paneStatus+"\">"+this.getText("ForgotStatus", "", paneStatus)+"</p></div>"+ + "<div class=\"yellow-fields\">"+ + "<input type=\"hidden\" name=\"action\" value=\"forgot\" />"+ + "<p><label for=\"yellow-pane-forgot-email\">"+this.getText("ForgotEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-forgot-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+ + "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("OkButton")+"\" /></p>"+ + "</div>"+ + "</form>"; + break; + case "yellow-pane-recover": + elementDiv.innerHTML = + "<form method=\"post\">"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<div class=\"yellow-title\"><h1>"+this.getText("RecoverTitle")+"</h1></div>"+ + "<div class=\"yellow-status\"><p id=\"yellow-pane-recover-status\" class=\""+paneStatus+"\">"+this.getText("RecoverStatus", "", paneStatus)+"</p></div>"+ + "<div class=\"yellow-fields\">"+ + "<p><label for=\"yellow-pane-recover-password\">"+this.getText("RecoverPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-recover-password\" maxlength=\"64\" value=\"\" /></p>"+ + "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("OkButton")+"\" /></p>"+ + "</div>"+ + "</form>"; + break; + case "yellow-pane-quit": + elementDiv.innerHTML = + "<form method=\"post\">"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<div class=\"yellow-title\"><h1>"+this.getText("QuitTitle")+"</h1></div>"+ + "<div class=\"yellow-status\"><p id=\"yellow-pane-quit-status\" class=\""+paneStatus+"\">"+this.getText("QuitStatus", "", paneStatus)+"</p></div>"+ + "<div class=\"yellow-fields\">"+ + "<input type=\"hidden\" name=\"action\" value=\"quit\" />"+ + "<input type=\"hidden\" name=\"csrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("csrftoken"))+"\" />"+ + "<p><label for=\"yellow-pane-quit-name\">"+this.getText("SignupName")+"</label><br /><input class=\"yellow-form-control\" name=\"name\" id=\"yellow-pane-quit-name\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("name"))+"\" /></p>"+ + "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("DeleteButton")+"\" /></p>"+ + "</div>"+ + "</form>"; + break; + case "yellow-pane-account": + elementDiv.innerHTML = + "<form method=\"post\">"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<div class=\"yellow-title\"><h1 id=\"yellow-pane-account-title\">"+this.getText("AccountTitle")+"</h1></div>"+ + "<div class=\"yellow-status\"><p id=\"yellow-pane-account-status\" class=\""+paneStatus+"\">"+this.getText("AccountStatus", "", paneStatus)+"</p></div>"+ + "<div class=\"yellow-settings\">"+ + "<div id=\"yellow-pane-account-settings-actions\" class=\"yellow-settings-left\"><p>"+this.getRawDataSettingsActions(paneAction)+"</p></div>"+ + "<div id=\"yellow-pane-account-settings-separator\" class=\"yellow-settings-left yellow-settings-separator\"> </div>"+ + "<div id=\"yellow-pane-account-settings-fields\" class=\"yellow-settings-right yellow-fields\">"+ + "<input type=\"hidden\" name=\"action\" value=\"account\" />"+ + "<input type=\"hidden\" name=\"csrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("csrftoken"))+"\" />"+ + "<p><label for=\"yellow-pane-account-name\">"+this.getText("SignupName")+"</label><br /><input class=\"yellow-form-control\" name=\"name\" id=\"yellow-pane-account-name\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("name"))+"\" /></p>"+ + "<p><label for=\"yellow-pane-account-email\">"+this.getText("SignupEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-account-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+ + "<p><label for=\"yellow-pane-account-password\">"+this.getText("SignupPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-account-password\" maxlength=\"64\" value=\"\" /></p>"+ + "<p>"+this.getRawDataLanguages(paneId)+"</p>"+ + "<p>"+this.getText("AccountInformation")+" <a href=\"#\" data-action=\"quit\">"+this.getText("AccountMore")+"</a></p>"+ + "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("ChangeButton")+"\" /></p>"+ + "</div>"+ + "<div class=\"yellow-settings yellow-settings-banner\"></div>"+ + "</div>"+ + "</form>"; + break; + case "yellow-pane-system": + elementDiv.innerHTML = + "<form method=\"post\">"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<div class=\"yellow-title\"><h1 id=\"yellow-pane-system-title\">"+this.getText("SystemTitle")+"</h1></div>"+ + "<div class=\"yellow-status\"><p id=\"yellow-pane-system-status\" class=\""+paneStatus+"\">"+this.getText("SystemStatus", "", paneStatus)+"</p></div>"+ + "<div class=\"yellow-settings\">"+ + "<div id=\"yellow-pane-system-settings-actions\" class=\"yellow-settings-left\"><p>"+this.getRawDataSettingsActions(paneAction)+"</p></div>"+ + "<div id=\"yellow-pane-system-settings-separator\" class=\"yellow-settings-left yellow-settings-separator\"> </div>"+ + "<div id=\"yellow-pane-system-settings-fields\" class=\"yellow-settings-right yellow-fields\">"+ + "<input type=\"hidden\" name=\"action\" value=\"system\" />"+ + "<input type=\"hidden\" name=\"csrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("csrftoken"))+"\" />"+ + "<p><label for=\"yellow-pane-system-sitename\">"+this.getText("SystemSitename")+"</label><br /><input class=\"yellow-form-control\" name=\"sitename\" id=\"yellow-pane-system-sitename\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("sitename"))+"\" /></p>"+ + "<p><label for=\"yellow-pane-system-author\">"+this.getText("SystemAuthor")+"</label><br /><input class=\"yellow-form-control\" name=\"author\" id=\"yellow-pane-system-author\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("author"))+"\" /></p>"+ + "<p><label for=\"yellow-pane-system-email\">"+this.getText("SystemEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-system-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+ + "<p>"+this.getText("SystemInformation")+"</p>"+ + "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("ChangeButton")+"\" /></p>"+ + "</div>"+ + "<div class=\"yellow-settings yellow-settings-banner\"></div>"+ + "</div>"+ + "</form>"; + break; + case "yellow-pane-update": + elementDiv.innerHTML = + "<form method=\"post\">"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<div class=\"yellow-title\"><h1 id=\"yellow-pane-update-title\">"+yellow.toolbox.encodeHtml(yellow.system.coreProductRelease)+"</h1></div>"+ + "<div class=\"yellow-status\"><p id=\"yellow-pane-update-status\" class=\""+paneStatus+"\">"+this.getText("UpdateStatus", "", paneStatus)+"</p></div>"+ + "<div class=\"yellow-output\" id=\"yellow-pane-update-output\">"+yellow.page.rawDataOutput+"</div>"+ + "<div class=\"yellow-buttons\" id=\"yellow-pane-update-buttons\">"+ + "<p><a href=\"#\" id=\"yellow-pane-update-submit\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ + "</div>"+ + "</form>"; + break; + case "yellow-pane-create": + elementDiv.innerHTML = + "<form method=\"post\">"+ + "<div id=\"yellow-pane-create-toolbar\">"+ + "<div class=\"yellow-toolbar yellow-toolbar-left\"><h1 id=\"yellow-pane-create-toolbar-title\">"+this.getText("Create")+"</h1></div>"+ + "<ul id=\"yellow-pane-create-toolbar-buttons\" class=\"yellow-toolbar yellow-toolbar-left\">"+this.getRawDataButtons(paneId)+"</ul>"+ + "<ul id=\"yellow-pane-create-toolbar-main\" class=\"yellow-toolbar yellow-toolbar-right\">"+ + "<li><a href=\"#\" id=\"yellow-pane-create-cancel\" class=\"yellow-toolbar-btn\" data-action=\"close\">"+this.getText("CancelButton")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-pane-create-submit\" class=\"yellow-toolbar-btn\" data-action=\"submit\">"+this.getText("CreateButton")+"</a></li>"+ + "</ul>"+ + "<ul class=\"yellow-toolbar yellow-toolbar-banner\"></ul>"+ + "</div>"+ + "<textarea id=\"yellow-pane-create-text\" class=\"yellow-edit-text\"></textarea>"+ + "<div id=\"yellow-pane-create-preview\" class=\"yellow-edit-preview\"></div>"+ + "</form>"; + break; + case "yellow-pane-edit": + elementDiv.innerHTML = + "<form method=\"post\">"+ + "<div id=\"yellow-pane-edit-toolbar\">"+ + "<div class=\"yellow-toolbar yellow-toolbar-left\"><h1 id=\"yellow-pane-edit-toolbar-title\">"+this.getText("Edit")+"</h1></div>"+ + "<ul id=\"yellow-pane-edit-toolbar-buttons\" class=\"yellow-toolbar yellow-toolbar-left\">"+this.getRawDataButtons(paneId)+"</ul>"+ + "<ul id=\"yellow-pane-edit-toolbar-main\" class=\"yellow-toolbar yellow-toolbar-right\">"+ + "<li><a href=\"#\" id=\"yellow-pane-edit-cancel\" class=\"yellow-toolbar-btn\" data-action=\"close\">"+this.getText("CancelButton")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-pane-edit-submit\" class=\"yellow-toolbar-btn\" data-action=\"submit\">"+this.getText("EditButton")+"</a></li>"+ + "</ul>"+ + "<ul class=\"yellow-toolbar yellow-toolbar-banner\"></ul>"+ + "</div>"+ + "<textarea id=\"yellow-pane-edit-text\" class=\"yellow-edit-text\"></textarea>"+ + "<div id=\"yellow-pane-edit-preview\" class=\"yellow-edit-preview\"></div>"+ + "</form>"; + break; + case "yellow-pane-delete": + elementDiv.innerHTML = + "<form method=\"post\">"+ + "<div id=\"yellow-pane-delete-toolbar\">"+ + "<div class=\"yellow-toolbar yellow-toolbar-left\"><h1 id=\"yellow-pane-delete-toolbar-title\">"+this.getText("Delete")+"</h1></div>"+ + "<ul id=\"yellow-pane-delete-toolbar-buttons\" class=\"yellow-toolbar yellow-toolbar-left\">"+this.getRawDataButtons(paneId)+"</ul>"+ + "<ul id=\"yellow-pane-delete-toolbar-main\" class=\"yellow-toolbar yellow-toolbar-right\">"+ + "<li><a href=\"#\" id=\"yellow-pane-delete-cancel\" class=\"yellow-toolbar-btn\" data-action=\"close\">"+this.getText("CancelButton")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-pane-delete-submit\" class=\"yellow-toolbar-btn\" data-action=\"submit\">"+this.getText("DeleteButton")+"</a></li>"+ + "</ul>"+ + "<ul class=\"yellow-toolbar yellow-toolbar-banner\"></ul>"+ + "</div>"+ + "<textarea id=\"yellow-pane-delete-text\" class=\"yellow-edit-text\"></textarea>"+ + "<div id=\"yellow-pane-delete-preview\" class=\"yellow-edit-preview\"></div>"+ + "</form>"; + break; + case "yellow-pane-menu": + elementDiv.innerHTML = + "<ul class=\"yellow-dropdown\">"+ + "<li><span>"+yellow.toolbox.encodeHtml(yellow.user.email)+"</span></li>"+ + "<li><a href=\"#\" data-action=\"settings\">"+this.getText("MenuSettings")+"</a></li>" + + "<li><a href=\"#\" data-action=\"help\">"+this.getText("MenuHelp")+"</a></li>" + + "<li><a href=\"#\" data-action=\"submit\" data-arguments=\"action:logout\">"+this.getText("MenuLogout")+"</a></li>"+ + "</ul>"; + break; + case "yellow-pane-information": + elementDiv.innerHTML = + "<form method=\"post\">"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<div class=\"yellow-title\"><h1 id=\"yellow-pane-information-title\">"+this.getText(paneAction+"Title")+"</h1></div>"+ + "<div class=\"yellow-status\"><p id=\"yellow-pane-information-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+ + "<div class=\"yellow-buttons\" id=\"yellow-pane-information-buttons\">"+ + "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ + "</div>"+ + "</form>"; + break; + default: elementDiv.innerHTML = + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<div class=\"yellow-error\">Pane '"+paneId+"' was not found. Oh no...</div>"; + } + elementPane.appendChild(elementDiv); + yellow.toolbox.insertAfter(elementPane, document.getElementsByTagName("body")[0].firstChild); + this.bindActions(elementPane); + }, + + // Update pane + updatePane: function(paneId, paneAction, paneStatus, paneInit) { + switch (paneId) { + case "yellow-pane-login": + if (paneInit && yellow.system.editLoginRestriction) { + yellow.toolbox.setVisible(document.getElementById("yellow-pane-login-signup"), false); + } + break; + case "yellow-pane-quit": + if (paneStatus=="none") { + document.getElementById("yellow-pane-quit-status").innerHTML = this.getText("QuitStatusNone"); + document.getElementById("yellow-pane-quit-name").value = ""; + } + break; + case "yellow-pane-account": + if (paneInit && yellow.system.editSettingsActions=="none") { + document.getElementById("yellow-pane-account-title").innerHTML = this.getText("MenuSettings"); + } + if (paneStatus=="none") { + document.getElementById("yellow-pane-account-status").innerHTML = this.getText("AccountStatusNone"); + document.getElementById("yellow-pane-account-name").value = yellow.user.name; + document.getElementById("yellow-pane-account-email").value = yellow.user.email; + document.getElementById("yellow-pane-account-password").value = ""; + document.getElementById("yellow-pane-account-"+yellow.user.language).checked = true; + } + break; + case "yellow-pane-system": + if (paneStatus=="none") { + document.getElementById("yellow-pane-system-status").innerHTML = this.getText("SystemStatusNone"); + document.getElementById("yellow-pane-system-sitename").value = yellow.system.sitename; + document.getElementById("yellow-pane-system-author").value = yellow.system.author; + document.getElementById("yellow-pane-system-email").value = yellow.system.email; + } + break; + case "yellow-pane-update": + if (paneStatus=="none") { + document.getElementById("yellow-pane-update-status").innerHTML = this.getText("UpdateStatusCheck"); + document.getElementById("yellow-pane-update-output").innerHTML = ""; + setTimeout("yellow.action('submit', '', 'action:update/option:check/');", 500); + } + if (paneStatus=="updates") { + document.getElementById(paneId+"-submit").innerHTML = this.getText("UpdateButton"); + document.getElementById(paneId+"-submit").setAttribute("data-action", "submit"); + document.getElementById(paneId+"-submit").setAttribute("data-arguments", "action:update"); + } + break; + case "yellow-pane-create": + case "yellow-pane-edit": + case "yellow-pane-delete": + document.getElementById(paneId+"-text").focus(); + if (paneInit) { + yellow.toolbox.setVisible(document.getElementById(paneId+"-text"), true); + yellow.toolbox.setVisible(document.getElementById(paneId+"-preview"), false); + document.getElementById(paneId+"-toolbar-title").innerHTML = yellow.toolbox.encodeHtml(yellow.page.title); + document.getElementById(paneId+"-text").value = paneId=="yellow-pane-create" ? yellow.page.rawDataNew : yellow.page.rawDataEdit; + var matches = document.getElementById(paneId+"-text").value.match(/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+/); + var position = document.getElementById(paneId+"-text").value.indexOf("\n", matches ? matches[0].length : 0); + document.getElementById(paneId+"-text").setSelectionRange(position, position); + if (yellow.system.editToolbarButtons!="none") { + yellow.toolbox.setVisible(document.getElementById(paneId+"-toolbar-title"), false); + this.updateToolbar(0, "yellow-toolbar-checked"); + } + if (!this.isUserAccess(paneAction, yellow.page.location) || (yellow.page.rawDataReadonly && paneId!="yellow-pane-create")) { + yellow.toolbox.setVisible(document.getElementById(paneId+"-submit"), false); + document.getElementById(paneId+"-text").readOnly = true; + } + } + if (!document.getElementById(paneId+"-text").readOnly) { + paneAction = this.paneAction = this.getPaneAction(paneId); + var className = "yellow-toolbar-btn yellow-toolbar-btn-"+paneAction; + if (document.getElementById(paneId+"-submit").className != className) { + document.getElementById(paneId+"-submit").className = className; + document.getElementById(paneId+"-submit").innerHTML = this.getText(paneAction+"Button"); + document.getElementById(paneId+"-submit").setAttribute("data-arguments", "action:"+paneAction); + this.resizePane(paneId, paneAction, paneStatus); + } + } + break; + } + this.bindActions(document.getElementById(paneId)); + }, + + // Resize pane + resizePane: function(paneId, paneAction, paneStatus) { + var elementBar = document.getElementById("yellow-bar-content"); + var paneLeft = yellow.toolbox.getOuterLeft(elementBar); + var paneTop = yellow.toolbox.getOuterTop(elementBar) + yellow.toolbox.getOuterHeight(elementBar) + 10; + var paneWidth = yellow.toolbox.getOuterWidth(elementBar); + var paneHeight = yellow.toolbox.getWindowHeight() - paneTop - Math.min(yellow.toolbox.getOuterHeight(elementBar) + 10, (yellow.toolbox.getWindowWidth()-yellow.toolbox.getOuterWidth(elementBar))/2); + switch (paneId) { + case "yellow-pane-account": + case "yellow-pane-system": + yellow.toolbox.setOuterLeft(document.getElementById(paneId), paneLeft); + yellow.toolbox.setOuterTop(document.getElementById(paneId), paneTop); + yellow.toolbox.setOuterWidth(document.getElementById(paneId), paneWidth); + var elementWidth = yellow.toolbox.getWidth(document.getElementById(paneId)); + var actionsWidth = yellow.toolbox.getOuterWidth(document.getElementById(paneId+"-settings-actions")); + var fieldsWidth = yellow.toolbox.getOuterWidth(document.getElementById(paneId+"-settings-fields")); + var separatorWidth = Math.max(10, ((elementWidth-fieldsWidth)/2)-actionsWidth); + yellow.toolbox.setOuterWidth(document.getElementById(paneId+"-settings-separator"), separatorWidth); + break; + case "yellow-pane-create": + case "yellow-pane-edit": + case "yellow-pane-delete": + yellow.toolbox.setOuterLeft(document.getElementById(paneId), paneLeft); + yellow.toolbox.setOuterTop(document.getElementById(paneId), paneTop); + yellow.toolbox.setOuterHeight(document.getElementById(paneId), paneHeight); + yellow.toolbox.setOuterWidth(document.getElementById(paneId), paneWidth); + var elementWidth = yellow.toolbox.getWidth(document.getElementById(paneId)); + yellow.toolbox.setOuterWidth(document.getElementById(paneId+"-text"), elementWidth); + yellow.toolbox.setOuterWidth(document.getElementById(paneId+"-preview"), elementWidth); + var buttonsWidth = 0; + var buttonsWidthMax = yellow.toolbox.getOuterWidth(document.getElementById(paneId+"-toolbar")) - + yellow.toolbox.getOuterWidth(document.getElementById(paneId+"-toolbar-main")) - 1; + var element = document.getElementById(paneId+"-toolbar-buttons").firstChild; + for (; element; element=element.nextSibling) { + element.removeAttribute("style"); + buttonsWidth += yellow.toolbox.getOuterWidth(element); + if (buttonsWidth>buttonsWidthMax) yellow.toolbox.setVisible(element, false); + } + yellow.toolbox.setOuterWidth(document.getElementById(paneId+"-toolbar-title"), buttonsWidthMax); + var height1 = yellow.toolbox.getHeight(document.getElementById(paneId)); + var height2 = yellow.toolbox.getOuterHeight(document.getElementById(paneId+"-toolbar")); + yellow.toolbox.setOuterHeight(document.getElementById(paneId+"-text"), height1 - height2); + yellow.toolbox.setOuterHeight(document.getElementById(paneId+"-preview"), height1 - height2); + var elementLink = document.getElementById(paneId+"-bar"); + var position = yellow.toolbox.getOuterLeft(elementLink) + yellow.toolbox.getOuterWidth(elementLink)/2; + position -= yellow.toolbox.getOuterLeft(document.getElementById(paneId)) + 1; + yellow.toolbox.setOuterLeft(document.getElementById(paneId+"-arrow"), position); + break; + case "yellow-pane-menu": + yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-menu"), paneLeft + paneWidth - yellow.toolbox.getOuterWidth(document.getElementById("yellow-pane-menu"))); + yellow.toolbox.setOuterTop(document.getElementById("yellow-pane-menu"), paneTop); + var elementLink = document.getElementById("yellow-pane-menu-bar"); + var position = yellow.toolbox.getOuterLeft(elementLink) + yellow.toolbox.getOuterWidth(elementLink)/2; + position -= yellow.toolbox.getOuterLeft(document.getElementById("yellow-pane-menu")); + yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-menu-arrow"), position); + break; + default: + yellow.toolbox.setOuterLeft(document.getElementById(paneId), paneLeft); + yellow.toolbox.setOuterTop(document.getElementById(paneId), paneTop); + yellow.toolbox.setOuterWidth(document.getElementById(paneId), paneWidth); + break; + } + }, + + // Show or hide pane + showPane: function(paneId, paneAction, paneStatus, paneModal) { + if (this.paneId!=paneId || this.paneAction!=paneAction) { + this.hidePane(this.paneId); + var paneInit = !document.getElementById(paneId); + if (!document.getElementById(paneId)) this.createPane(paneId, paneAction, paneStatus); + var element = document.getElementById(paneId); + if (!yellow.toolbox.isVisible(element)) { + if (yellow.system.debug) console.log("yellow.edit.showPane id:"+paneId); + yellow.toolbox.setVisible(element, true); + if (paneModal) { + yellow.toolbox.addClass(document.body, "yellow-body-modal-open"); + yellow.toolbox.addValue("meta[name=viewport]", "content", ", maximum-scale=1, user-scalable=0"); + } + this.paneId = paneId; + this.paneAction = paneAction; + this.paneStatus = paneStatus; + this.updatePane(paneId, paneAction, paneStatus, paneInit); + this.resizePane(paneId, paneAction, paneStatus); + this.updateBar(paneId, "yellow-bar-selected"); + } + } else { + this.hidePane(this.paneId, true); + } + }, + + // Hide pane + hidePane: function(paneId, fadeout) { + var element = document.getElementById(paneId); + if (yellow.toolbox.isVisible(element)) { + if (yellow.system.debug) console.log("yellow.edit.hidePane id:"+paneId); + yellow.toolbox.removeClass(document.body, "yellow-body-modal-open"); + yellow.toolbox.removeValue("meta[name=viewport]", "content", ", maximum-scale=1, user-scalable=0"); + yellow.toolbox.setVisible(element, false, fadeout); + this.paneId = 0; + this.paneAction = 0; + this.paneStatus = 0; + this.updateBar(0, "yellow-bar-selected"); + } + this.hidePopup(this.popupId); + }, + + // Process action + processAction: function(action, status, arguments) { + action = action ? action : "none"; + status = status ? status : "none"; + arguments = arguments ? arguments : "none"; + if (action!="none") { + if (yellow.system.debug) console.log("yellow.edit.processAction action:"+action+" status:"+status); + var paneId = (status!="next" && status!="done") ? "yellow-pane-"+action : "yellow-pane-information"; + switch(action) { + case "login": this.showPane(paneId, action, status); break; + case "signup": this.showPane(paneId, action, status); break; + case "confirm": this.showPane(paneId, action, status); break; + case "approve": this.showPane(paneId, action, status); break; + case "forgot": this.showPane(paneId, action, status); break; + case "recover": this.showPane(paneId, action, status); break; + case "reactivate": this.showPane(paneId, action, status); break; + case "verify": this.showPane(paneId, action, status); break; + case "change": this.showPane(paneId, action, status); break; + case "quit": this.showPane(paneId, action, status); break; + case "remove": this.showPane(paneId, action, status); break; + case "account": this.showPane(paneId, action, status); break; + case "system": this.showPane(paneId, action, status); break; + case "update": this.showPane(paneId, action, status); break; + case "create": this.showPane(paneId, action, status, true); break; + case "edit": this.showPane(paneId, action, status, true); break; + case "delete": this.showPane(paneId, action, status, true); break; + case "menu": this.showPane(paneId, action, status); break; + case "close": this.hidePane(this.paneId); break; + case "toolbar": this.processToolbar(status, arguments); break; + case "settings": this.processSettings(arguments); break; + case "submit": this.processSubmit(arguments); break; + case "help": this.processHelp(); break; + } + } + }, + + // Process toolbar + processToolbar: function(status, arguments) { + if (yellow.system.debug) console.log("yellow.edit.processToolbar status:"+status); + var elementText = document.getElementById(this.paneId+"-text"); + var elementPreview = document.getElementById(this.paneId+"-preview"); + if (!yellow.toolbox.isVisible(elementPreview) && !elementText.readOnly) { + switch (status) { + case "h1": yellow.editor.setMarkdown(elementText, "# ", "insert-multiline-block", true); break; + case "h2": yellow.editor.setMarkdown(elementText, "## ", "insert-multiline-block", true); break; + case "h3": yellow.editor.setMarkdown(elementText, "### ", "insert-multiline-block", true); break; + case "paragraph": yellow.editor.setMarkdown(elementText, "", "remove-multiline-block"); + yellow.editor.setMarkdown(elementText, "", "remove-fenced-block"); break; + case "notice": yellow.editor.setMarkdown(elementText, "! ", "insert-multiline-block", true); break; + case "quote": yellow.editor.setMarkdown(elementText, "> ", "insert-multiline-block", true); break; + case "pre": yellow.editor.setMarkdown(elementText, "```\n", "insert-fenced-block", true); break; + case "bold": yellow.editor.setMarkdown(elementText, "**", "insert-inline", true); break; + case "italic": yellow.editor.setMarkdown(elementText, "*", "insert-inline", true); break; + case "strikethrough": yellow.editor.setMarkdown(elementText, "~~", "insert-inline", true); break; + case "code": yellow.editor.setMarkdown(elementText, "`", "insert-autodetect", true); break; + case "ul": yellow.editor.setMarkdown(elementText, "* ", "insert-multiline-block", true); break; + case "ol": yellow.editor.setMarkdown(elementText, "1. ", "insert-multiline-block", true); break; + case "tl": yellow.editor.setMarkdown(elementText, "- [ ] ", "insert-multiline-block", true); break; + case "link": yellow.editor.setMarkdown(elementText, "[link](url)", "insert", false, yellow.editor.getMarkdownLink); break; + case "text": yellow.editor.setMarkdown(elementText, arguments, "insert"); break; + case "status": yellow.editor.setMetaData(elementText, "status", true); break; + case "file": this.showFileDialog(); break; + case "undo": yellow.editor.undo(); break; + case "redo": yellow.editor.redo(); break; + } + } + if (status=="preview" && !elementText.readOnly) this.showPreview(elementText, elementPreview); + if (status=="save" && !elementText.readOnly && this.paneAction!="delete") this.processSubmit("action:"+this.paneAction); + if (status=="help") window.open(this.getText("YellowHelpUrl"), "_blank"); + if (this.isExpandable(status)) { + this.showPopup("yellow-popup-"+status, status); + } else { + this.hidePopup(this.popupId); + } + }, + + // Update toolbar + updateToolbar: function(status, name) { + if (status) { + var element = document.getElementById(this.paneId+"-toolbar-"+status); + if (element) { + if (name.indexOf("selected")!=-1) element.setAttribute("aria-expanded", "true"); + yellow.toolbox.addClass(element, name); + } + } else { + var elements = document.getElementsByClassName(name); + for (var i=0, l=elements.length; i<l; i++) { + if (name.indexOf("selected")!=-1) elements[i].setAttribute("aria-expanded", "false"); + yellow.toolbox.removeClass(elements[i], name); + } + } + }, + + // Process shortcut + processShortcut: function(e) { + var shortcut = yellow.toolbox.getEventShortcut(e); + if (shortcut) { + var tokens = yellow.system.editKeyboardShortcuts.split(/\s*,\s*/); + for (var i=0; i<tokens.length; i++) { + var pair = tokens[i].split(" "); + if (shortcut==pair[0] || shortcut.replace("meta+", "ctrl+")==pair[0]) { + if (yellow.system.debug) console.log("yellow.edit.processShortcut shortcut:"+shortcut); + e.stopPropagation(); + e.preventDefault(); + this.processToolbar(pair[1]); + } + } + } + }, + + // Process settings + processSettings: function(arguments) { + var action = arguments!="none" ? arguments : "account"; + if (action!=this.paneAction && action!="settings") this.processAction(action); + }, + + // Process submit + processSubmit: function(arguments) { + var settings = { "action":"none", "csrftoken":this.getCookie("csrftoken") }; + var tokens = arguments.split("/"); + for (var i=0; i<tokens.length; i++) { + var pair = tokens[i].split(/[:=]/); + if (!pair[0] || !pair[1]) continue; + settings[pair[0]] = pair[1]; + } + if (settings["action"]=="create" || settings["action"]=="edit" || settings["action"]=="delete") { + settings.rawdatasource = yellow.page.rawDataSource; + settings.rawdataedit = document.getElementById(this.paneId+"-text").value; + settings.rawdataendofline = yellow.page.rawDataEndOfLine; + } + if (settings["action"]!="none") yellow.toolbox.submitForm(settings); + }, + + // Process help + processHelp: function() { + this.hidePane(this.paneId); + window.open(this.getText("YellowHelpUrl"), "_self"); + }, + + // Create popup + createPopup: function(popupId) { + if (yellow.system.debug) console.log("yellow.edit.createPopup id:"+popupId); + var elementPopup = document.createElement("div"); + elementPopup.className = "yellow-popup"; + elementPopup.setAttribute("id", popupId); + elementPopup.style.display = "none"; + var elementDiv = document.createElement("div"); + elementDiv.setAttribute("id", popupId+"-content"); + switch (popupId) { + case "yellow-popup-format": + elementDiv.innerHTML = + "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+ + "<li><a href=\"#\" id=\"yellow-popup-format-h1\" data-action=\"toolbar\" data-status=\"h1\">"+this.getText("ToolbarH1")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-format-h2\" data-action=\"toolbar\" data-status=\"h2\">"+this.getText("ToolbarH2")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-format-h3\" data-action=\"toolbar\" data-status=\"h3\">"+this.getText("ToolbarH3")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-format-paragraph\" data-action=\"toolbar\" data-status=\"paragraph\">"+this.getText("ToolbarParagraph")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-format-pre\" data-action=\"toolbar\" data-status=\"pre\">"+this.getText("ToolbarPre")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-format-notice\" data-action=\"toolbar\" data-status=\"notice\">"+this.getText("ToolbarNotice")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-format-quote\" data-action=\"toolbar\" data-status=\"quote\">"+this.getText("ToolbarQuote")+"</a></li>"+ + "</ul>"; + break; + case "yellow-popup-heading": + elementDiv.innerHTML = + "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+ + "<li><a href=\"#\" id=\"yellow-popup-heading-h1\" data-action=\"toolbar\" data-status=\"h1\">"+this.getText("ToolbarH1")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-heading-h2\" data-action=\"toolbar\" data-status=\"h2\">"+this.getText("ToolbarH2")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-heading-h3\" data-action=\"toolbar\" data-status=\"h3\">"+this.getText("ToolbarH3")+"</a></li>"+ + "</ul>"; + break; + case "yellow-popup-list": + elementDiv.innerHTML = + "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+ + "<li><a href=\"#\" id=\"yellow-popup-list-ul\" data-action=\"toolbar\" data-status=\"ul\">"+this.getText("ToolbarUl")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-list-ol\" data-action=\"toolbar\" data-status=\"ol\">"+this.getText("ToolbarOl")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-list-tl\" data-action=\"toolbar\" data-status=\"tl\">"+this.getText("ToolbarTl")+"</a></li>"+ + "</ul>"; + break; + case "yellow-popup-emojiawesome": + var rawDataEmojis = ""; + if (yellow.system.emojiawesomeToolbarButtons && yellow.system.emojiawesomeToolbarButtons!="none") { + var tokens = yellow.system.emojiawesomeToolbarButtons.split(" "); + for (var i=0; i<tokens.length; i++) { + var token = tokens[i].replace(/[\:]/g,""); + var className = token.replace("+1", "plus1").replace("-1", "minus1").replace(/_/g, "-"); + rawDataEmojis += "<li><a href=\"#\" id=\"yellow-popup-list-"+yellow.toolbox.encodeHtml(token)+"\" data-action=\"toolbar\" data-status=\"text\" data-arguments=\":"+yellow.toolbox.encodeHtml(token)+":\"><i class=\"ea ea-"+yellow.toolbox.encodeHtml(className)+"\"></i></a></li>"; + } + } + elementDiv.innerHTML = "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+rawDataEmojis+"</ul>"; + break; + case "yellow-popup-fontawesome": + var rawDataIcons = ""; + if (yellow.system.fontawesomeToolbarButtons && yellow.system.fontawesomeToolbarButtons!="none") { + var tokens = yellow.system.fontawesomeToolbarButtons.split(" "); + for (var i=0; i<tokens.length; i++) { + var token = tokens[i].replace(/[\:]/g,""); + rawDataIcons += "<li><a href=\"#\" id=\"yellow-popup-list-"+yellow.toolbox.encodeHtml(token)+"\" data-action=\"toolbar\" data-status=\"text\" data-arguments=\":"+yellow.toolbox.encodeHtml(token)+":\"><i class=\"fa "+yellow.toolbox.encodeHtml(token)+"\"></i></a></li>"; + } + } + elementDiv.innerHTML = "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+rawDataIcons+"</ul>"; + break; + } + elementPopup.appendChild(elementDiv); + yellow.toolbox.insertAfter(elementPopup, document.getElementsByTagName("body")[0].firstChild); + this.bindActions(elementPopup); + }, + + // Show or hide popup + showPopup: function(popupId, status) { + if (this.popupId!=popupId) { + this.hidePopup(this.popupId); + if (!document.getElementById(popupId)) this.createPopup(popupId); + var element = document.getElementById(popupId); + if (yellow.system.debug) console.log("yellow.edit.showPopup id:"+popupId); + yellow.toolbox.setVisible(element, true); + this.popupId = popupId; + this.updateToolbar(status, "yellow-toolbar-selected"); + var elementParent = document.getElementById(this.paneId+"-toolbar-"+status); + var popupLeft = yellow.toolbox.getOuterLeft(elementParent); + var popupTop = yellow.toolbox.getOuterTop(elementParent) + yellow.toolbox.getOuterHeight(elementParent) - 1; + yellow.toolbox.setOuterLeft(document.getElementById(popupId), popupLeft); + yellow.toolbox.setOuterTop(document.getElementById(popupId), popupTop); + } else { + this.hidePopup(this.popupId, true); + } + }, + + // Hide popup + hidePopup: function(popupId, fadeout) { + var element = document.getElementById(popupId); + if (yellow.toolbox.isVisible(element)) { + if (yellow.system.debug) console.log("yellow.edit.hidePopup id:"+popupId); + yellow.toolbox.setVisible(element, false, fadeout); + this.popupId = 0; + this.updateToolbar(0, "yellow-toolbar-selected"); + } + }, + + // Show or hide preview + showPreview: function(elementText, elementPreview) { + if (!yellow.toolbox.isVisible(elementPreview)) { + var thisObject = this; + var formData = new FormData(); + formData.append("action", "preview"); + formData.append("csrftoken", this.getCookie("csrftoken")); + formData.append("rawdataedit", elementText.value); + formData.append("rawdataendofline", yellow.page.rawDataEndOfLine); + var request = new XMLHttpRequest(); + request.open("POST", window.location.pathname, true); + request.onload = function() { if (this.status==200) thisObject.showPreviewDone.call(thisObject, elementText, elementPreview, this.responseText); }; + request.send(formData); + } else { + this.showPreviewDone(elementText, elementPreview, ""); + } + }, + + // Preview done + showPreviewDone: function(elementText, elementPreview, responseText) { + var showPreview = responseText.length!=0; + yellow.toolbox.setVisible(elementText, !showPreview); + yellow.toolbox.setVisible(elementPreview, showPreview); + if (showPreview) { + this.updateToolbar("preview", "yellow-toolbar-checked"); + elementPreview.innerHTML = responseText; + dispatchEvent(new Event("load")); + } else { + this.updateToolbar(0, "yellow-toolbar-checked"); + elementText.focus(); + } + }, + + // Show file dialog and trigger upload + showFileDialog: function() { + var element = document.createElement("input"); + element.setAttribute("id", "yellow-file-dialog"); + element.setAttribute("type", "file"); + element.setAttribute("accept", yellow.system.editUploadExtensions); + element.setAttribute("multiple", "multiple"); + yellow.toolbox.addEvent(element, "change", yellow.onDrop); + element.click(); + }, + + // Upload file + uploadFile: function(elementText, file) { + if (this.isUserAccess("upload", yellow.page.location)) { + var extension = (file.name.lastIndexOf(".")!=-1 ? file.name.substring(file.name.lastIndexOf("."), file.name.length) : "").toLowerCase(); + var extensions = yellow.system.editUploadExtensions.split(/\s*,\s*/); + if (file.size<=yellow.system.coreFileSizeMax && extensions.indexOf(extension)!=-1) { + var text = this.getText("UploadProgress")+"\u200b"; + yellow.editor.setMarkdown(elementText, text, "insert"); + var thisObject = this; + var formData = new FormData(); + formData.append("action", "upload"); + formData.append("csrftoken", this.getCookie("csrftoken")); + formData.append("file", file); + var request = new XMLHttpRequest(); + request.open("POST", window.location.pathname, true); + request.onload = function() { if (this.status==200) { thisObject.uploadFileDone.call(thisObject, elementText, this.responseText); } else { thisObject.uploadFileError.call(thisObject, elementText, this.responseText); } }; + request.send(formData); + } else { + var textError = extensions.indexOf(extension)!=-1 ? "file too big!" : "file format not supported!"; + var textNew = "[Can't upload file '"+file.name+"', "+textError+"]"; + yellow.editor.setMarkdown(elementText, textNew, "insert"); + } + } else { + var textNew = "[Can't upload file '"+file.name+"', access is restricted!]"; + yellow.editor.setMarkdown(elementText, textNew, "insert"); + } + }, + + // Upload done + uploadFileDone: function(elementText, responseText) { + var result = JSON.parse(responseText); + if (result) { + var textOld = this.getText("UploadProgress")+"\u200b"; + var textNew; + if (result.location.substring(0, yellow.system.coreImageLocation.length)==yellow.system.coreImageLocation) { + textNew = "[image "+result.location.substring(yellow.system.coreImageLocation.length)+"]"; + } else { + textNew = "[link]("+result.location+")"; + } + yellow.editor.replace(elementText, textOld, textNew); + } + }, + + // Upload error + uploadFileError: function(elementText, responseText) { + var result = JSON.parse(responseText); + if (result) { + var textOld = this.getText("UploadProgress")+"\u200b"; + var textNew = "["+result.error+"]"; + yellow.editor.replace(elementText, textOld, textNew); + } + }, + + // Bind actions to links + bindActions: function(element) { + var elements = element.getElementsByTagName("a"); + for (var i=0, l=elements.length; i<l; i++) { + if (elements[i].getAttribute("href") && elements[i].getAttribute("href").substring(0, 13)=="#data-action-") { + elements[i].setAttribute("data-action", elements[i].getAttribute("href").substring(13)); + } + if (elements[i].getAttribute("data-action")) elements[i].onclick = yellow.onClickAction; + if (elements[i].getAttribute("data-action")=="toolbar") elements[i].onmousedown = function(e) { e.preventDefault(); }; + } + }, + + // Return pane action + getPaneAction: function(paneId) { + var panePrefix = "yellow-pane-"; + var paneAction = paneId.substring(panePrefix.length); + if (paneAction=="edit") { + if (document.getElementById("yellow-pane-edit-text").value.length==0) paneAction = "delete"; + if (yellow.page.statusCode==434) paneAction = "create"; + } + return paneAction; + }, + + // Return raw data for pane action + getRawDataPaneAction: function(paneAction, text, important) { + var rawDataAction = ""; + if (this.isUserAccess(paneAction) || important) { + if (!text) text = this.getText(paneAction); + rawDataAction = "<a href=\"#\" id=\"yellow-pane-"+paneAction+"-bar\" data-action=\""+paneAction+"\" aria-expanded=\"false\">"+yellow.toolbox.encodeHtml(text)+"</a>"; + } + return rawDataAction; + }, + + // Return raw data for settings actions + getRawDataSettingsActions: function(paneAction) { + var rawDataActions = ""; + if (yellow.system.editSettingsActions && yellow.system.editSettingsActions!="none") { + var tokens = yellow.system.editSettingsActions.split(/\s*,\s*/); + for (var i=0; i<tokens.length; i++) { + var token = tokens[i]; + rawDataActions += "<a href=\"#\""+(token==paneAction ? "class=\"active\"": "")+" data-action=\"settings\" data-arguments=\""+yellow.toolbox.encodeHtml(token)+"\">"+this.getText(token+"Title")+"</a><br />"; + } + } + return rawDataActions; + }, + + // Return raw data for languages + getRawDataLanguages: function(paneId) { + var rawDataLanguages = ""; + if (yellow.system.coreLanguages && Object.keys(yellow.system.coreLanguages).length>1) { + for (var language in yellow.system.coreLanguages) { + var checked = language==this.getRequest("language") ? " checked=\"checked\"" : ""; + rawDataLanguages += "<label for=\""+paneId+"-"+language+"\"><input type=\"radio\" name=\"language\" id=\""+paneId+"-"+language+"\" value=\""+language+"\""+checked+"> "+yellow.toolbox.encodeHtml(yellow.system.coreLanguages[language])+"</label><br />"; + } + } + return rawDataLanguages + }, + + // Return raw data for buttons + getRawDataButtons: function(paneId) { + var rawDataButtons = ""; + if (yellow.system.editToolbarButtons && yellow.system.editToolbarButtons!="none") { + var tokens = yellow.system.editToolbarButtons.split(/\s*,\s*/); + for (var i=0; i<tokens.length; i++) { + var token = tokens[i]; + if (token!="separator") { + var shortcut = this.getShortcut(token); + var rawDataShortcut = shortcut ? "  "+yellow.toolbox.encodeHtml(shortcut) : ""; + var rawDataExpandable = this.isExpandable(token) ? " aria-expanded=\"false\"" : ""; + rawDataButtons += "<li><a href=\"#\" id=\""+paneId+"-toolbar-"+yellow.toolbox.encodeHtml(token)+"\" class=\"yellow-toolbar-btn-icon yellow-toolbar-tooltip\" data-action=\"toolbar\" data-status=\""+yellow.toolbox.encodeHtml(token)+"\" aria-label=\""+this.getText("Toolbar", "", token)+rawDataShortcut+"\""+rawDataExpandable+"><i class=\"yellow-icon yellow-icon-"+yellow.toolbox.encodeHtml(token)+"\"></i></a></li>"; + } else { + rawDataButtons += "<li><a href=\"#\" class=\"yellow-toolbar-btn-separator\"></a></li>"; + } + } + } + return rawDataButtons; + }, + + // Return request data + getRequest: function(key, prefix) { + if (!prefix) prefix = "request"; + key = prefix + yellow.toolbox.toUpperFirst(key); + return (key in yellow.page) ? yellow.page[key] : ""; + }, + + // Return shortcut setting + getShortcut: function(key) { + var shortcut = ""; + var tokens = yellow.system.editKeyboardShortcuts.split(/\s*,\s*/); + for (var i=0; i<tokens.length; i++) { + var pair = tokens[i].split(" "); + if (key==pair[1]) { + shortcut = pair[0]; + break; + } + } + var labels = yellow.language.editKeyboardLabels.split(/\s*,\s*/); + if (navigator.platform.indexOf("Mac")==-1) { + shortcut = shortcut.toUpperCase().replace("CTRL+", labels[0]).replace("ALT+", labels[1]).replace("SHIFT+", labels[2]); + } else { + shortcut = shortcut.toUpperCase().replace("CTRL+ALT+", "ALT+CTRL+").replace("CTRL+SHIFT+", "SHIFT+CTRL+"); + shortcut = shortcut.replace("CTRL+", labels[3]).replace("ALT+", labels[4]).replace("SHIFT+", labels[5]); + } + return shortcut; + }, + + // Return text setting + getText: function(key, prefix, postfix) { + if (!prefix) prefix = "edit"; + if (!postfix) postfix = ""; + key = prefix + yellow.toolbox.toUpperFirst(key) + yellow.toolbox.toUpperFirst(postfix); + return (key in yellow.language) ? yellow.language[key] : "["+key+"]"; + }, + + // Return browser cookie + getCookie: function(key) { + return yellow.toolbox.getCookie(key); + }, + + // Check if user with access + isUserAccess: function(action, location) { + var tokens = yellow.user.access.split(/\s*,\s*/); + return tokens.indexOf(action)!=-1 && (!location || location.substring(0, yellow.user.home.length)==yellow.user.home); + }, + + // Check if element is expandable + isExpandable: function(name) { + return (name=="format" || name=="heading" || name=="list" || name=="emojiawesome" || name=="fontawesome"); + }, + + // Check if extension exists + isExtension: function(name) { + return name in yellow.system.coreExtensions; + } +}; + +yellow.editor = { + + // Set Markdown formatting + setMarkdown: function(element, prefix, type, toggle, callback) { + var information = this.getMarkdownInformation(element, prefix, type); + var selectionStart = (information.type.indexOf("block")!=-1) ? information.top : information.start; + var selectionEnd = (information.type.indexOf("block")!=-1) ? information.bottom : information.end; + if (information.found && toggle) information.type = information.type.replace("insert", "remove"); + if (information.type=="remove-fenced-block" || information.type=="remove-inline") { + selectionStart -= information.prefix.length; selectionEnd += information.prefix.length; + } + var text = information.text; + var textSelectionBefore = text.substring(0, selectionStart); + var textSelection = text.substring(selectionStart, selectionEnd); + var textSelectionAfter = text.substring(selectionEnd, text.length); + var textSelectionNew, selectionStartNew, selectionEndNew; + switch (information.type) { + case "insert-multiline-block": + textSelectionNew = this.getMarkdownMultilineBlock(textSelection, information); + selectionStartNew = information.start + this.getMarkdownDifference(textSelection, textSelectionNew, true); + selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew); + if (information.start==information.top && information.start!=information.end) selectionStartNew = information.top; + if (information.end==information.top && information.start!=information.end) selectionEndNew = information.top; + break; + case "remove-multiline-block": + textSelectionNew = this.getMarkdownMultilineBlock(textSelection, information); + selectionStartNew = information.start + this.getMarkdownDifference(textSelection, textSelectionNew, true); + selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew); + if (selectionStartNew<=information.top) selectionStartNew = information.top; + if (selectionEndNew<=information.top) selectionEndNew = information.top; + break; + case "insert-fenced-block": + textSelectionNew = this.getMarkdownFencedBlock(textSelection, information); + selectionStartNew = information.start + information.prefix.length; + selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew) - information.prefix.length; + break; + case "remove-fenced-block": + textSelectionNew = this.getMarkdownFencedBlock(textSelection, information); + selectionStartNew = information.start - information.prefix.length; + selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew) + information.prefix.length; + break; + case "insert-inline": + textSelectionNew = information.prefix + textSelection + information.prefix; + selectionStartNew = information.start + information.prefix.length; + selectionEndNew = information.end + information.prefix.length; + break; + case "remove-inline": + textSelectionNew = text.substring(information.start, information.end); + selectionStartNew = information.start - information.prefix.length; + selectionEndNew = information.end - information.prefix.length; + break; + case "insert": + textSelectionNew = callback ? callback(textSelection, information) : information.prefix; + selectionStartNew = information.start + textSelectionNew.length; + selectionEndNew = selectionStartNew; + } + if (textSelection!=textSelectionNew || selectionStart!=selectionStartNew || selectionEnd!=selectionEndNew) { + element.focus(); + element.setSelectionRange(selectionStart, selectionEnd); + document.execCommand("insertText", false, textSelectionNew); + element.value = textSelectionBefore + textSelectionNew + textSelectionAfter; + element.setSelectionRange(selectionStartNew, selectionEndNew); + } + if (yellow.system.debug) console.log("yellow.editor.setMarkdown type:"+information.type); + }, + + // Return Markdown formatting information + getMarkdownInformation: function(element, prefix, type) { + var text = element.value; + var start = element.selectionStart; + var end = element.selectionEnd; + var top = start, bottom = end; + while (text.charAt(top-1)!="\n" && top>0) top--; + if (bottom==top && bottom<text.length) bottom++; + while (text.charAt(bottom-1)!="\n" && bottom<text.length) bottom++; + if (type=="insert-autodetect") { + if (text.substring(start, end).indexOf("\n")!=-1) { + type = "insert-fenced-block"; prefix = "```\n"; + } else { + type = "insert-inline"; prefix = "`"; + } + } + var found = false; + if (type.indexOf("multiline-block")!=-1) { + if (text.substring(top, top+prefix.length)==prefix) found = true; + } else if (type.indexOf("fenced-block")!=-1) { + if (text.substring(top-prefix.length, top)==prefix && text.substring(bottom, bottom+prefix.length)==prefix) { + found = true; + } + } else { + if (text.substring(start-prefix.length, start)==prefix && text.substring(end, end+prefix.length)==prefix) { + if (prefix=="*") { + var lettersBefore = 0, lettersAfter = 0; + for (var index=start-1; text.charAt(index)=="*"; index--) lettersBefore++; + for (var index=end; text.charAt(index)=="*"; index++) lettersAfter++; + found = lettersBefore!=2 && lettersAfter!=2; + } else { + found = true; + } + } + } + return { "text":text, "prefix":prefix, "type":type, "start":start, "end":end, "top":top, "bottom":bottom, "found":found }; + }, + + // Return Markdown length difference + getMarkdownDifference: function(textSelection, textSelectionNew, firstTextLine) { + var textSelectionLength, textSelectionLengthNew; + if (firstTextLine) { + var position = textSelection.indexOf("\n"); + var positionNew = textSelectionNew.indexOf("\n"); + textSelectionLength = position!=-1 ? position+1 : textSelection.length+1; + textSelectionLengthNew = positionNew!=-1 ? positionNew+1 : textSelectionNew.length+1; + } else { + var position = textSelection.indexOf("\n"); + var positionNew = textSelectionNew.indexOf("\n"); + textSelectionLength = position!=-1 ? textSelection.length : textSelection.length+1; + textSelectionLengthNew = positionNew!=-1 ? textSelectionNew.length : textSelectionNew.length+1; + } + return textSelectionLengthNew - textSelectionLength; + }, + + // Return Markdown for multiline block + getMarkdownMultilineBlock: function(textSelection, information) { + var textSelectionNew = ""; + var lines = yellow.toolbox.getTextLines(textSelection); + for (var i=0; i<lines.length; i++) { + var matches = lines[i].match(/^(\s*[\#\*\-\!\>\s]+)?(\s+\[.\]|\s*\d+\.)?[ \t]+/); + if (matches) { + textSelectionNew += lines[i].substring(matches[0].length); + } else { + textSelectionNew += lines[i]; + } + } + textSelection = textSelectionNew; + if (information.type.indexOf("remove")==-1) { + textSelectionNew = ""; + var linePrefix = information.prefix; + lines = yellow.toolbox.getTextLines(textSelection.length!=0 ? textSelection : "\n"); + for (var i=0; i<lines.length; i++) { + textSelectionNew += linePrefix+lines[i]; + if (information.prefix=="1. ") { + var matches = linePrefix.match(/^(\d+)\.\s/); + if (matches) linePrefix = (parseInt(matches[1])+1)+". "; + } + } + textSelection = textSelectionNew; + } + return textSelection; + }, + + // Return Markdown for fenced block + getMarkdownFencedBlock: function(textSelection, information) { + var textSelectionNew = ""; + var lines = yellow.toolbox.getTextLines(textSelection); + for (var i=0; i<lines.length; i++) { + var matches = lines[i].match(/^```/); + if (!matches) textSelectionNew += lines[i]; + } + textSelection = textSelectionNew; + if (information.type.indexOf("remove")==-1) { + if (textSelection.length==0) textSelection = "\n"; + textSelection = information.prefix + textSelection + information.prefix; + } + return textSelection; + }, + + // Return Markdown for link + getMarkdownLink: function(textSelection, information) { + return textSelection.length!=0 ? information.prefix.replace("link", textSelection) : information.prefix; + }, + + // Set meta data + setMetaData: function(element, key, toggle) { + var information = this.getMetaDataInformation(element, key); + if (information.bottom!=0) { + var value = ""; + if (key=="status") { + var tokens = yellow.system.editStatusValues.split(/\s*,\s*/); + var index = tokens.indexOf(information.value); + value = tokens[index+1<tokens.length ? index+1 : index]; + } + var selectionStart = information.found ? information.start : information.bottom; + var selectionEnd = information.found ? information.end : information.bottom; + var text = information.text; + var textSelectionBefore = text.substring(0, selectionStart); + var textSelection = text.substring(selectionStart, selectionEnd); + var textSelectionAfter = text.substring(selectionEnd, text.length); + var textSelectionNew = yellow.toolbox.toUpperFirst(key)+": "+value+"\n"; + if (information.found && information.value==value && toggle) textSelectionNew = ""; + var selectionStartNew = selectionStart; + var selectionEndNew = selectionStart + textSelectionNew.trim().length; + element.focus(); + element.setSelectionRange(selectionStart, selectionEnd); + document.execCommand("insertText", false, textSelectionNew); + element.value = textSelectionBefore + textSelectionNew + textSelectionAfter; + element.setSelectionRange(selectionStartNew, selectionEndNew); + element.scrollTop = 0; + if (yellow.system.debug) console.log("yellow.editor.setMetaData key:"+key); + } + }, + + // Return meta data information + getMetaDataInformation: function(element, key) { + var text = element.value; + var value = ""; + var start = 0, end = 0, top = 0, bottom = 0; + var found = false; + var parts = text.match(/^(\xEF\xBB\xBF)?(\-\-\-[\r\n]+)([\s\S]+?)\-\-\-[\r\n]+/); + if (parts) { + key = yellow.toolbox.toLowerFirst(key); + start = end = top = ((parts[1] ? parts[1] : "")+parts[2]).length; + bottom = ((parts[1] ? parts[1] : "")+parts[2]+parts[3]).length; + var lines = yellow.toolbox.getTextLines(parts[3]); + for (var i=0; i<lines.length; i++) { + var matches = lines[i].match(/^\s*(.*?)\s*:\s*(.*?)\s*$/); + if (matches && yellow.toolbox.toLowerFirst(matches[1])==key && matches[2].length!=0) { + value = matches[2]; + end = start + lines[i].length; + found = true; + break; + } + start = end = start + lines[i].length; + } + } + return { "text":text, "value":value, "start":start, "end":end, "top":top, "bottom":bottom, "found":found }; + }, + + // Replace text + replace: function(element, textOld, textNew) { + var text = element.value; + var selectionStart = element.selectionStart; + var selectionEnd = element.selectionEnd; + var selectionStartFound = text.indexOf(textOld); + var selectionEndFound = selectionStartFound + textOld.length; + if (selectionStartFound!=-1) { + var selectionStartNew = selectionStart<selectionStartFound ? selectionStart : selectionStart+textNew.length-textOld.length; + var selectionEndNew = selectionEnd<selectionEndFound ? selectionEnd : selectionEnd+textNew.length-textOld.length; + var textBefore = text.substring(0, selectionStartFound); + var textAfter = text.substring(selectionEndFound, text.length); + if (textOld!=textNew) { + element.focus(); + element.setSelectionRange(selectionStartFound, selectionEndFound); + document.execCommand("insertText", false, textNew); + element.value = textBefore + textNew + textAfter; + element.setSelectionRange(selectionStartNew, selectionEndNew); + } + } + }, + + // Undo changes + undo: function() { + document.execCommand("undo"); + }, + + // Redo changes + redo: function() { + document.execCommand("redo"); + } +}; + +yellow.toolbox = { + + // Insert element before reference element + insertBefore: function(element, elementReference) { + elementReference.parentNode.insertBefore(element, elementReference); + }, + + // Insert element after reference element + insertAfter: function(element, elementReference) { + elementReference.parentNode.insertBefore(element, elementReference.nextSibling); + }, + + // Add element class + addClass: function(element, name) { + element.classList.add(name); + }, + + // Remove element class + removeClass: function(element, name) { + element.classList.remove(name); + }, + + // Add attribute information + addValue: function(selector, name, value) { + var element = document.querySelector(selector); + element.setAttribute(name, element.getAttribute(name) + value); + }, + + // Remove attribute information + removeValue: function(selector, name, value) { + var element = document.querySelector(selector); + element.setAttribute(name, element.getAttribute(name).replace(value, "")); + }, + + // Add event handler + addEvent: function(element, type, handler) { + element.addEventListener(type, handler, false); + }, + + // Remove event handler + removeEvent: function(element, type, handler) { + element.removeEventListener(type, handler, false); + }, + + // Return shortcut from keyboard event, alphanumeric only + getEventShortcut: function(e) { + var shortcut = ""; + if (e.keyCode>=48 && e.keyCode<=90) { + shortcut += (e.ctrlKey ? "ctrl+" : "")+(e.metaKey ? "meta+" : "")+(e.altKey ? "alt+" : "")+(e.shiftKey ? "shift+" : ""); + shortcut += String.fromCharCode(e.keyCode).toLowerCase(); + } + return shortcut; + }, + + // Return element width in pixel + getWidth: function(element) { + return element.offsetWidth - this.getBoxSize(element).width; + }, + + // Return element height in pixel + getHeight: function(element) { + return element.offsetHeight - this.getBoxSize(element).height; + }, + + // Set element width in pixel, including padding and border + setOuterWidth: function(element, width) { + element.style.width = Math.max(0, width - this.getBoxSize(element).width) + "px"; + }, + + // Set element height in pixel, including padding and border + setOuterHeight: function(element, height) { + element.style.height = Math.max(0, height - this.getBoxSize(element).height) + "px"; + }, + + // Return element width in pixel, including padding and border + getOuterWidth: function(element, includeMargin) { + var width = element.offsetWidth; + if (includeMargin) width += this.getMarginSize(element).width; + return width; + }, + + // Return element height in pixel, including padding and border + getOuterHeight: function(element, includeMargin) { + var height = element.offsetHeight; + if (includeMargin) height += this.getMarginSize(element).height; + return height; + }, + + // Set element left position in pixel + setOuterLeft: function(element, left) { + element.style.left = Math.max(0, left) + "px"; + }, + + // Set element top position in pixel + setOuterTop: function(element, top) { + element.style.top = Math.max(0, top) + "px"; + }, + + // Return element left position in pixel + getOuterLeft: function(element) { + return element.getBoundingClientRect().left + window.pageXOffset; + }, + + // Return element top position in pixel + getOuterTop: function(element) { + return element.getBoundingClientRect().top + window.pageYOffset; + }, + + // Return window width in pixel + getWindowWidth: function() { + return window.innerWidth; + }, + + // Return window height in pixel + getWindowHeight: function() { + return window.innerHeight; + }, + + // Return element CSS property + getStyle: function(element, property) { + return window.getComputedStyle(element).getPropertyValue(property); + }, + + // Return element CSS padding and border + getBoxSize: function(element) { + var paddingLeft = parseFloat(this.getStyle(element, "padding-left")) || 0; + var paddingRight = parseFloat(this.getStyle(element, "padding-right")) || 0; + var borderLeft = parseFloat(this.getStyle(element, "border-left-width")) || 0; + var borderRight = parseFloat(this.getStyle(element, "border-right-width")) || 0; + var width = paddingLeft + paddingRight + borderLeft + borderRight; + var paddingTop = parseFloat(this.getStyle(element, "padding-top")) || 0; + var paddingBottom = parseFloat(this.getStyle(element, "padding-bottom")) || 0; + var borderTop = parseFloat(this.getStyle(element, "border-top-width")) || 0; + var borderBottom = parseFloat(this.getStyle(element, "border-bottom-width")) || 0; + var height = paddingTop + paddingBottom + borderTop + borderBottom; + return { "width":width, "height":height }; + }, + + // Return element CSS margin + getMarginSize: function(element) { + var marginLeft = parseFloat(this.getStyle(element, "margin-left")) || 0; + var marginRight = parseFloat(this.getStyle(element, "margin-right")) || 0; + var width = marginLeft + marginRight; + var marginTop = parseFloat(this.getStyle(element, "margin-top")) || 0; + var marginBottom = parseFloat(this.getStyle(element, "margin-bottom")) || 0; + var height = marginTop + marginBottom; + return { "width":width, "height":height }; + }, + + // Set element visibility + setVisible: function(element, show, fadeout) { + if (fadeout && !show) { + var opacity = 1; + function renderFrame() { + opacity -= .1; + if (opacity<=0) { + element.style.opacity = "initial"; + element.style.display = "none"; + } else { + element.style.opacity = opacity; + requestAnimationFrame(renderFrame); + } + } + renderFrame(); + } else { + element.style.display = show ? "block" : "none"; + } + }, + + // Check if element exists and is visible + isVisible: function(element) { + return element && element.style.display!="none"; + }, + + // Convert first letter to lowercase + toLowerFirst: function(string) { + return string.charAt(0).toLowerCase()+string.slice(1); + }, + + // Convert first letter to uppercase + toUpperFirst: function(string) { + return string.charAt(0).toUpperCase()+string.slice(1); + }, + + // Return lines from text string, including newline + getTextLines: function(string) { + var lines = string.split("\n"); + for (var i=0; i<lines.length; i++) lines[i] = lines[i]+"\n"; + if (string.length==0 || string.charAt(string.length-1)=="\n") lines.pop(); + return lines; + }, + + // Return browser cookie + getCookie: function(key) { + var matches = document.cookie.match("(^|; )"+key+"=([^;]+)"); + return matches ? unescape(matches[2]) : ""; + }, + + // Encode HTML special characters + encodeHtml: function(string) { + return string + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """); + }, + + // Submit form with post method + submitForm: function(arguments) { + var elementForm = document.createElement("form"); + elementForm.setAttribute("method", "post"); + for (var key in arguments) { + if (!arguments.hasOwnProperty(key)) continue; + var elementInput = document.createElement("input"); + elementInput.setAttribute("type", "hidden"); + elementInput.setAttribute("name", key); + elementInput.setAttribute("value", arguments[key]); + elementForm.appendChild(elementInput); + } + document.body.appendChild(elementForm); + elementForm.submit(); + } +}; + +yellow.edit.intervalId = setInterval("yellow.onLoad(new Event('DOMContentLoading'))", 1); +window.addEventListener("DOMContentLoaded", yellow.onLoad, false); diff --git a/system/extensions/edit.php b/system/extensions/edit.php new file mode 100644 index 0000000..2b26f25 --- /dev/null +++ b/system/extensions/edit.php @@ -0,0 +1,1863 @@ +<?php +// Edit extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/edit + +class YellowEdit { + const VERSION = "0.8.35"; + public $yellow; // access to API + public $response; // web response + public $merge; // text merge + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + $this->response = new YellowEditResponse($yellow); + $this->merge = new YellowEditMerge($yellow); + $this->yellow->system->setDefault("editLocation", "/edit/"); + $this->yellow->system->setDefault("editUploadNewLocation", "/media/@group/@filename"); + $this->yellow->system->setDefault("editUploadExtensions", ".gif, .jpg, .pdf, .png, .svg, .zip"); + $this->yellow->system->setDefault("editKeyboardShortcuts", "ctrl+b bold, ctrl+i italic, ctrl+k strikethrough, ctrl+e code, ctrl+s save, ctrl+alt+p preview"); + $this->yellow->system->setDefault("editToolbarButtons", "auto"); + $this->yellow->system->setDefault("editEndOfLine", "auto"); + $this->yellow->system->setDefault("editNewFile", "page-new-(.*).md"); + $this->yellow->system->setDefault("editUserPasswordMinLength", "8"); + $this->yellow->system->setDefault("editUserHashAlgorithm", "bcrypt"); + $this->yellow->system->setDefault("editUserHashCost", "10"); + $this->yellow->system->setDefault("editUserHome", "/"); + $this->yellow->system->setDefault("editUserAccess", "create, edit, delete, upload"); + $this->yellow->system->setDefault("editLoginRestriction", "0"); + $this->yellow->system->setDefault("editLoginSessionTimeout", "2592000"); + $this->yellow->system->setDefault("editBruteForceProtection", "25"); + $this->yellow->language->setDefault("editMailFooter"); + } + + // Handle request + public function onRequest($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->checkRequest($location)) { + $scheme = $this->yellow->system->get("coreServerScheme"); + $address = $this->yellow->system->get("coreServerAddress"); + $base = rtrim($this->yellow->system->get("coreServerBase").$this->yellow->system->get("editLocation"), "/"); + list($scheme, $address, $base, $location, $fileName) = $this->yellow->getRequestInformation($scheme, $address, $base); + $this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName); + $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName); + } + return $statusCode; + } + + // Handle page content of shortcut + public function onParseContentShortcut($page, $name, $text, $type) { + $output = null; + if ($name=="edit" && $type=="inline") { + $editText = "$name $text"; + if (substru($text, 0, 2)=="- ") $editText = trim(substru($text, 2)); + $output = "<a href=\"".$page->get("pageEdit")."\">".htmlspecialchars($editText)."</a>"; + } + return $output; + } + + // Handle page layout + public function onParsePageLayout($page, $name) { + if ($this->response->isActive()) { + $this->response->processPageData($page); + } + } + + // Handle page extra data + public function onParsePageExtra($page, $name) { + $output = null; + if ($name=="header" && $this->response->isActive()) { + $extensionLocation = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreExtensionLocation"); + $output = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"{$extensionLocation}edit.css\" />\n"; + $output .= "<script type=\"text/javascript\" src=\"{$extensionLocation}edit.js\"></script>\n"; + $output .= "<script type=\"text/javascript\">\n"; + $output .= "// <![CDATA[\n"; + $output .= "yellow.page = ".json_encode($this->response->getPageData($page)).";\n"; + $output .= "yellow.system = ".json_encode($this->response->getSystemData()).";\n"; + $output .= "yellow.user = ".json_encode($this->response->getUserData()).";\n"; + $output .= "yellow.language = ".json_encode($this->response->getLanguageData()).";\n"; + $output .= "// ]]>\n"; + $output .= "</script>\n"; + } + return $output; + } + + // Handle command + public function onCommand($command, $text) { + switch ($command) { + case "user": $statusCode = $this->processCommandUser($command, $text); break; + default: $statusCode = 0; + } + return $statusCode; + } + + // Handle command help + public function onCommandHelp() { + return "user [option email password name]\n"; + } + + // Handle update + public function onUpdate($action) { + if ($action=="update") { + $cleanup = false; + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $fileData = $this->yellow->toolbox->readFile($fileNameUser); + $fileDataNew = ""; + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])=="email" && !strempty($matches[2])) { + $status = $this->yellow->user->getUser("status", $matches[2]); + $cleanup = !empty($status) && $status!="active" && $status!="inactive"; + } + } + if (!$cleanup) $fileDataNew .= $line; + } + $fileDataNew = rtrim($fileDataNew)."\n"; + if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileNameUser, $fileDataNew)) { + $this->yellow->log("error", "Can't write file '$fileNameUser'!"); + } + } + } + + // Process command to update user account + public function processCommandUser($command, $text) { + list($option) = $this->yellow->toolbox->getTextArguments($text); + switch ($option) { + case "": $statusCode = $this->userShow($command, $text); break; + case "add": $statusCode = $this->userAdd($command, $text); break; + case "change": $statusCode = $this->userChange($command, $text); break; + case "remove": $statusCode = $this->userRemove($command, $text); break; + default: $statusCode = 400; echo "Yellow $command: Invalid arguments\n"; + } + return $statusCode; + } + + // Show user accounts + public function userShow($command, $text) { + $data = array(); + foreach ($this->yellow->user->settings as $key=>$value) { + $name = $value["name"]; + if (preg_match("/\s/", $name)) $name = "\"$name\""; + $data[$key] = "$value[email] $name $value[status]"; + } + uksort($data, "strnatcasecmp"); + foreach ($data as $line) echo "$line\n"; + if (count($data)==0) echo "Yellow $command: No user accounts\n"; + return 200; + } + + // Add user account + public function userAdd($command, $text) { + $status = "ok"; + list($option, $email, $password, $name) = $this->yellow->toolbox->getTextArguments($text); + if (empty($email) || empty($password)) $status = $this->response->status = "incomplete"; + if (empty($name)) $name = $this->yellow->system->get("sitename"); + if ($status=="ok") $status = $this->getUserAccount("add", $email, $password); + if ($status=="ok" && $this->isUserAccountTaken($email)) $status = "taken"; + switch ($status) { + case "incomplete": echo "ERROR updating settings: Please enter email and password!\n"; break; + case "invalid": echo "ERROR updating settings: Please enter a valid email!\n"; break; + case "taken": echo "ERROR updating settings: Please enter a different email!\n"; break; + case "weak": echo "ERROR updating settings: Please enter a different password!\n"; break; + case "short": echo "ERROR updating settings: Please enter a longer password!\n"; break; + } + if ($status=="ok") { + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array( + "name" => $name, + "language" => $this->yellow->system->get("language"), + "home" => $this->yellow->system->get("editUserHome"), + "access" => $this->yellow->system->get("editUserAccess"), + "hash" => $this->response->createHash($password), + "stamp" => $this->response->createStamp(), + "pending" => "none", + "failed" => "0", + "modified" => date("Y-m-d H:i:s", time()), + "status" => "active"); + $status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($status=="error") echo "ERROR updating settings: Can't write file '$fileNameUser'!\n"; + $this->yellow->log($status=="ok" ? "info" : "error", "Add user '".strtok($name, " ")."'"); + } + if ($status=="ok") { + $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); + $status = substru($this->yellow->user->getUser("hash", $email), 0, 10)!="error-hash" ? "ok" : "error"; + if ($status=="error") echo "ERROR updating settings: Hash algorithm '$algorithm' not supported!\n"; + } + $statusCode = $status=="ok" ? 200 : 500; + echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."added\n"; + return $statusCode; + } + + // Change user account + public function userChange($command, $text) { + $status = "ok"; + list($option, $email, $password, $name) = $this->yellow->toolbox->getTextArguments($text); + if (empty($email)) $status = $this->response->status = "invalid"; + if ($status=="ok") $status = $this->getUserAccount("change", $email, $password); + if ($status=="ok" && !$this->yellow->user->isExisting($email)) $status = "unknown"; + switch ($status) { + case "invalid": echo "ERROR updating settings: Please enter a valid email!\n"; break; + case "unknown": echo "ERROR updating settings: Can't find email '$email'!\n"; break; + case "weak": echo "ERROR updating settings: Please enter a different password!\n"; break; + case "short": echo "ERROR updating settings: Please enter a longer password!\n"; break; + } + if ($status=="ok") { + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array( + "name" => empty($name) ? $this->yellow->user->getUser("name", $email) : $name, + "hash" => empty($password) ? $this->yellow->user->getUser("hash", $email) : $this->response->createHash($password), + "failed" => "0", + "modified" => date("Y-m-d H:i:s", time())); + $status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($status=="error") echo "ERROR updating settings: Can't write file '$fileNameUser'!\n"; + } + $statusCode = $status=="ok" ? 200 : 500; + echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."changed\n"; + return $statusCode; + } + + // Remove user account + public function userRemove($command, $text) { + $status = "ok"; + list($option, $email) = $this->yellow->toolbox->getTextArguments($text); + $name = $this->yellow->user->getUser("name", $email); + if (empty($email)) $status = $this->response->status = "invalid"; + if (empty($name)) $name = $this->yellow->system->get("sitename"); + if ($status=="ok") $status = $this->getUserAccount("remove", $email, ""); + if ($status=="ok" && !$this->yellow->user->isExisting($email)) $status = "unknown"; + switch ($status) { + case "invalid": echo "ERROR updating settings: Please enter a valid email!\n"; break; + case "unknown": echo "ERROR updating settings: Can't find email '$email'!\n"; break; + } + if ($status=="ok") { + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $status = $this->yellow->user->remove($fileNameUser, $email) ? "ok" : "error"; + if ($status=="error") echo "ERROR updating settings: Can't write file '$fileNameUser'!\n"; + $this->yellow->log($status=="ok" ? "info" : "error", "Remove user '".strtok($name, " ")."'"); + } + $statusCode = $status=="ok" ? 200 : 500; + echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."removed\n"; + return $statusCode; + } + + // Process request + public function processRequest($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->checkUserAuth($scheme, $address, $base, $location, $fileName)) { + switch ($this->yellow->page->getRequest("action")) { + case "": $statusCode = $this->processRequestShow($scheme, $address, $base, $location, $fileName); break; + case "login": $statusCode = $this->processRequestLogin($scheme, $address, $base, $location, $fileName); break; + case "logout": $statusCode = $this->processRequestLogout($scheme, $address, $base, $location, $fileName); break; + case "quit": $statusCode = $this->processRequestQuit($scheme, $address, $base, $location, $fileName); break; + case "account": $statusCode = $this->processRequestAccount($scheme, $address, $base, $location, $fileName); break; + case "system": $statusCode = $this->processRequestSystem($scheme, $address, $base, $location, $fileName); break; + case "update": $statusCode = $this->processRequestUpdate($scheme, $address, $base, $location, $fileName); break; + case "create": $statusCode = $this->processRequestCreate($scheme, $address, $base, $location, $fileName); break; + case "edit": $statusCode = $this->processRequestEdit($scheme, $address, $base, $location, $fileName); break; + case "delete": $statusCode = $this->processRequestDelete($scheme, $address, $base, $location, $fileName); break; + case "preview": $statusCode = $this->processRequestPreview($scheme, $address, $base, $location, $fileName); break; + case "upload": $statusCode = $this->processRequestUpload($scheme, $address, $base, $location, $fileName); break; + } + } elseif ($this->checkUserUnauth($scheme, $address, $base, $location, $fileName)) { + $this->yellow->lookup->requestHandler = "core"; + switch ($this->yellow->page->getRequest("action")) { + case "": $statusCode = $this->processRequestShow($scheme, $address, $base, $location, $fileName); break; + case "signup": $statusCode = $this->processRequestSignup($scheme, $address, $base, $location, $fileName); break; + case "forgot": $statusCode = $this->processRequestForgot($scheme, $address, $base, $location, $fileName); break; + case "confirm": $statusCode = $this->processRequestConfirm($scheme, $address, $base, $location, $fileName); break; + case "approve": $statusCode = $this->processRequestApprove($scheme, $address, $base, $location, $fileName); break; + case "recover": $statusCode = $this->processRequestRecover($scheme, $address, $base, $location, $fileName); break; + case "reactivate": $statusCode = $this->processRequestReactivate($scheme, $address, $base, $location, $fileName); break; + case "verify": $statusCode = $this->processRequestVerify($scheme, $address, $base, $location, $fileName); break; + case "change": $statusCode = $this->processRequestChange($scheme, $address, $base, $location, $fileName); break; + case "remove": $statusCode = $this->processRequestRemove($scheme, $address, $base, $location, $fileName); break; + } + } + if ($statusCode==0) $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + $this->checkUserFailed($scheme, $address, $base, $location, $fileName); + return $statusCode; + } + + // Process request to show file + public function processRequestShow($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if (is_readable($fileName)) { + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } else { + if ($this->yellow->lookup->isRedirectLocation($location)) { + $location = $this->yellow->lookup->isFileLocation($location) ? "$location/" : "/".$this->yellow->getRequestLanguage()."/"; + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(301, $location); + } else { + $this->yellow->page->error($this->response->isUserAccess("create", $location) ? 434 : 404); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } + return $statusCode; + } + + // Process request for user login + public function processRequestLogin($scheme, $address, $base, $location, $fileName) { + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time())); + if ($this->yellow->user->save($fileNameUser, $this->response->userEmail, $settings)) { + $home = $this->yellow->user->getUser("home", $this->response->userEmail); + if (substru($location, 0, strlenu($home))==$home) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $home); + $statusCode = $this->yellow->sendStatus(302, $location); + } + } else { + $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + return $statusCode; + } + + // Process request for user logout + public function processRequestLogout($scheme, $address, $base, $location, $fileName) { + $this->response->userEmail = ""; + $this->response->destroyCookies($scheme, $address, $base); + $location = $this->yellow->lookup->normaliseUrl( + $this->yellow->system->get("coreServerScheme"), + $this->yellow->system->get("coreServerAddress"), + $this->yellow->system->get("coreServerBase"), + $location); + $statusCode = $this->yellow->sendStatus(302, $location); + return $statusCode; + } + + // Process request for user signup + public function processRequestSignup($scheme, $address, $base, $location, $fileName) { + $this->response->action = "signup"; + $this->response->status = "ok"; + $name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $this->yellow->page->getRequest("name"))); + $email = trim($this->yellow->page->getRequest("email")); + $password = trim($this->yellow->page->getRequest("password")); + $consent = trim($this->yellow->page->getRequest("consent")); + if (empty($name) || empty($email) || empty($password) || empty($consent)) $this->response->status = "incomplete"; + if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, $password); + if ($this->response->status=="ok" && $this->response->isLoginRestriction()) $this->response->status = "next"; + if ($this->response->status=="ok" && $this->isUserAccountTaken($email)) $this->response->status = "next"; + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array( + "name" => $name, + "language" => $this->yellow->lookup->findLanguageFromFile($fileName, $this->yellow->system->get("language")), + "home" => $this->yellow->system->get("editUserHome"), + "access" => $this->yellow->system->get("editUserAccess"), + "hash" => $this->response->createHash($password), + "stamp" => $this->response->createStamp(), + "pending" => "none", + "failed" => "0", + "modified" => date("Y-m-d H:i:s", time()), + "status" => "unconfirmed"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); + $this->response->status = substru($this->yellow->user->getUser("hash", $email), 0, 10)!="error-hash" ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Hash algorithm '$algorithm' not supported!"); + } + if ($this->response->status=="ok") { + $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "confirm") ? "next" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to confirm user signup + public function processRequestConfirm($scheme, $address, $base, $location, $fileName) { + $this->response->action = "confirm"; + $this->response->status = "ok"; + $email = $this->yellow->page->getRequest("email"); + $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "unapproved"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "approve") ? "done" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to approve user signup + public function processRequestApprove($scheme, $address, $base, $location, $fileName) { + $this->response->action = "approve"; + $this->response->status = "ok"; + $email = $this->yellow->page->getRequest("email"); + $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); + if ($this->response->status=="ok") { + $name = $this->yellow->user->getUser("name", $email); + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "active"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + $this->yellow->log($this->response->status=="ok" ? "info" : "error", "Add user '".strtok($name, " ")."'"); + } + if ($this->response->status=="ok") { + $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "welcome") ? "done" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request for forgotten password + public function processRequestForgot($scheme, $address, $base, $location, $fileName) { + $this->response->action = "forgot"; + $this->response->status = "ok"; + $email = trim($this->yellow->page->getRequest("email")); + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $this->response->status = "invalid"; + if ($this->response->status=="ok" && !$this->yellow->user->isExisting($email)) $this->response->status = "next"; + if ($this->response->status=="ok") { + $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "recover") ? "next" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to recover password + public function processRequestRecover($scheme, $address, $base, $location, $fileName) { + $this->response->action = "recover"; + $this->response->status = "ok"; + $email = trim($this->yellow->page->getRequest("email")); + $password = trim($this->yellow->page->getRequest("password")); + $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); + if ($this->response->status=="ok") { + if (empty($password)) $this->response->status = "password"; + if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, $password); + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("hash" => $this->response->createHash($password), "failed" => "0", "modified" => date("Y-m-d H:i:s", time())); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $this->response->destroyCookies($scheme, $address, $base); + $this->response->status = "done"; + } + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to reactivate account + public function processRequestReactivate($scheme, $address, $base, $location, $fileName) { + $this->response->action = "reactivate"; + $this->response->status = "ok"; + $email = $this->yellow->page->getRequest("email"); + $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "active"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "done" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to verify email + public function processRequestVerify($scheme, $address, $base, $location, $fileName) { + $this->response->action = "verify"; + $this->response->status = "ok"; + $email = $emailSource = $this->yellow->page->getRequest("email"); + $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); + if ($this->response->status=="ok") { + $emailSource = $this->yellow->user->getUser("pending", $email); + if ($this->yellow->user->getUser("status", $emailSource)!="active") $this->response->status = "done"; + } + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "unchanged"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $this->response->status = $this->response->sendMail($scheme, $address, $base, $emailSource, "change") ? "done" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to change email or password + public function processRequestChange($scheme, $address, $base, $location, $fileName) { + $this->response->action = "change"; + $this->response->status = "ok"; + $email = $emailSource = trim($this->yellow->page->getRequest("email")); + $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); + if ($this->response->status=="ok") { + list($email, $hash) = $this->yellow->toolbox->getTextList($this->yellow->user->getUser("pending", $email), ":", 2); + if (!$this->yellow->user->isExisting($email) || empty($hash)) $this->response->status = "done"; + } + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array( + "hash" => $hash, + "pending" => "none", + "failed" => "0", + "modified" => date("Y-m-d H:i:s", time()), + "status" => "active"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok" && $email!=$emailSource) { + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $this->response->status = $this->yellow->user->remove($fileNameUser, $emailSource) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $this->response->destroyCookies($scheme, $address, $base); + $this->response->status = "done"; + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to quit account + public function processRequestQuit($scheme, $address, $base, $location, $fileName) { + $this->response->action = "quit"; + $this->response->status = "ok"; + $name = trim($this->yellow->page->getRequest("name")); + $email = $this->response->userEmail; + if (empty($name)) $this->response->status = "none"; + if ($this->response->status=="ok" && $name!=$this->yellow->user->getUser("name", $email)) $this->response->status = "mismatch"; + if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, ""); + if ($this->response->status=="ok") { + $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "remove") ? "next" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to remove account + public function processRequestRemove($scheme, $address, $base, $location, $fileName) { + $this->response->action = "remove"; + $this->response->status = "ok"; + $email = $this->yellow->page->getRequest("email"); + $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); + if ($this->response->status=="ok") { + $name = $this->yellow->user->getUser("name", $email); + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "removed"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + $this->yellow->log($this->response->status=="ok" ? "info" : "error", "Remove user '".strtok($name, " ")."'"); + } + if ($this->response->status=="ok") { + $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "goodbye") ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $this->response->status = $this->yellow->user->remove($fileNameUser, $email) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $this->response->destroyCookies($scheme, $address, $base); + $this->response->status = "done"; + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to change account settings + public function processRequestAccount($scheme, $address, $base, $location, $fileName) { + $this->response->action = "account"; + $this->response->status = "ok"; + $email = trim($this->yellow->page->getRequest("email")); + $emailSource = $this->response->userEmail; + $password = trim($this->yellow->page->getRequest("password")); + $name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $this->yellow->page->getRequest("name"))); + $language = trim($this->yellow->page->getRequest("language")); + if ($email!=$emailSource || !empty($password)) { + if (empty($email)) $this->response->status = "invalid"; + if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, $password); + if ($this->response->status=="ok" && $email!=$emailSource && $this->isUserAccountTaken($email)) $this->response->status = "taken"; + if ($this->response->status=="ok" && $email!=$emailSource) { + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array( + "name" => $name, + "language" => $language, + "home" => $this->yellow->user->getUser("home", $emailSource), + "access" => $this->yellow->user->getUser("access", $emailSource), + "hash" => $this->response->createHash("none"), + "stamp" => $this->response->createStamp(), + "pending" => $emailSource, + "failed" => "0", + "modified" => date("Y-m-d H:i:s", time()), + "status" => "unverified"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array( + "name" => $name, + "language" => $language, + "pending" => $email.":".(empty($password) ? $this->yellow->user->getUser("hash", $emailSource) : $this->response->createHash($password)), + "failed" => "0", + "modified" => date("Y-m-d H:i:s", time())); + $this->response->status = $this->yellow->user->save($fileNameUser, $emailSource, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $action = $email!=$emailSource ? "verify" : "change"; + $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, $action) ? "next" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + } else { + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("name" => $name, "language" => $language, "failed" => "0", "modified" => date("Y-m-d H:i:s", time())); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "done" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + } + if ($this->response->status=="done") { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + return $statusCode; + } + + // Process request to change system settings + public function processRequestSystem($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->response->isUserAccess("system")) { + $this->response->action = "system"; + $this->response->status = "ok"; + $sitename = trim($this->yellow->page->getRequest("sitename")); + $author = trim($this->yellow->page->getRequest("author")); + $email = trim($this->yellow->page->getRequest("email")); + if ($email!=$this->yellow->system->get("email")) { + if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) $this->response->status = "invalid"; + } + if ($this->response->status=="ok") { + $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); + $settings = array("sitename" => $sitename, "author" => $author, "email" => $email); + $file = $this->response->getFileSystem($scheme, $address, $base, $location, $fileName, $settings); + $this->response->status = (!$file->isError() && $this->yellow->system->save($fileName, $settings)) ? "done" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileName'!"); + } + if ($this->response->status=="done") { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } + return $statusCode; + } + + // Process request to update website + public function processRequestUpdate($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->response->isUserAccess("update")) { + $this->response->action = "update"; + $this->response->status = "ok"; + if ($this->yellow->page->getRequest("option")=="check") { + list($statusCode, $rawData) = $this->response->getUpdateInformation(); + $this->response->status = empty($rawData) ? "ok" : "updates"; + $this->response->rawDataOutput = $rawData; + if ($statusCode!=200) { + $this->response->status = "error"; + $this->response->rawDataOutput = ""; + } + } else { + $this->response->status = $this->yellow->command("update")==0 ? "done" : "error"; + } + if ($this->response->status=="done") { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } + return $statusCode; + } + + // Process request to create page + public function processRequestCreate($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->response->isUserAccess("create", $location) && !empty($this->yellow->page->getRequest("rawdataedit"))) { + $this->response->rawDataSource = $this->yellow->page->getRequest("rawdatasource"); + $this->response->rawDataEdit = $this->yellow->page->getRequest("rawdatasource"); + $this->response->rawDataEndOfLine = $this->yellow->page->getRequest("rawdataendofline"); + $rawData = $this->yellow->page->getRequest("rawdataedit"); + $page = $this->response->getPageNew($scheme, $address, $base, $location, $fileName, + $rawData, $this->response->getEndOfLine()); + if (!$page->isError()) { + if ($this->yellow->toolbox->createFile($page->fileName, $page->rawData, true)) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $page->location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $this->yellow->page->error(500, "Can't write file '$page->fileName'!"); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } else { + $this->yellow->page->error(500, $page->get("pageError")); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } + return $statusCode; + } + + // Process request to edit page + public function processRequestEdit($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->response->isUserAccess("edit", $location) && !empty($this->yellow->page->getRequest("rawdataedit"))) { + $this->response->rawDataSource = $this->yellow->page->getRequest("rawdatasource"); + $this->response->rawDataEdit = $this->yellow->page->getRequest("rawdataedit"); + $this->response->rawDataEndOfLine = $this->yellow->page->getRequest("rawdataendofline"); + $rawDataFile = $this->yellow->toolbox->readFile($fileName); + $page = $this->response->getPageEdit($scheme, $address, $base, $location, $fileName, + $this->response->rawDataSource, $this->response->rawDataEdit, $rawDataFile, $this->response->rawDataEndOfLine); + if (!$page->isError()) { + if ($this->yellow->lookup->isFileLocation($location)) { + if ($this->yellow->toolbox->renameFile($fileName, $page->fileName, true) && + $this->yellow->toolbox->createFile($page->fileName, $page->rawData)) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $page->location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $this->yellow->page->error(500, "Can't write file '$page->fileName'!"); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } else { + if ($this->yellow->toolbox->renameDirectory(dirname($fileName), dirname($page->fileName), true) && + $this->yellow->toolbox->createFile($page->fileName, $page->rawData)) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $page->location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $this->yellow->page->error(500, "Can't write file '$page->fileName'!"); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } + } else { + $this->yellow->page->error(500, $page->get("pageError")); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } + return $statusCode; + } + + // Process request to delete page + public function processRequestDelete($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->response->isUserAccess("delete", $location) && is_file($fileName)) { + $this->response->rawDataSource = $this->yellow->page->getRequest("rawdatasource"); + $this->response->rawDataEdit = $this->yellow->page->getRequest("rawdatasource"); + $this->response->rawDataEndOfLine = $this->yellow->page->getRequest("rawdataendofline"); + $rawDataFile = $this->yellow->toolbox->readFile($fileName); + $page = $this->response->getPageDelete($scheme, $address, $base, $location, $fileName, + $rawDataFile, $this->response->rawDataEndOfLine); + if (!$page->isError()) { + $this->yellow->toolbox->createFile($fileName, $page->rawData); + if ($this->yellow->lookup->isFileLocation($location)) { + if ($this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory"))) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $this->yellow->page->error(500, "Can't delete file '$fileName'!"); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } else { + if ($this->yellow->toolbox->deleteDirectory(dirname($fileName), $this->yellow->system->get("coreTrashDirectory"))) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $this->yellow->page->error(500, "Can't delete file '$fileName'!"); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } + } else { + $this->yellow->page->error(500, $page->get("pageError")); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } + return $statusCode; + } + + // Process request to show preview + public function processRequestPreview($scheme, $address, $base, $location, $fileName) { + $page = $this->response->getPagePreview($scheme, $address, $base, $location, $fileName, + $this->yellow->page->getRequest("rawdataedit"), $this->yellow->page->getRequest("rawdataendofline")); + $statusCode = $this->yellow->sendData(200, $page->outputData, "", false); + if (defined("DEBUG") && DEBUG>=1) echo "YellowEdit::processRequestPreview file:$fileName<br/>\n"; + return $statusCode; + } + + // Process request to upload file + public function processRequestUpload($scheme, $address, $base, $location, $fileName) { + $data = array(); + $fileNameTemp = $_FILES["file"]["tmp_name"]; + $fileNameShort = preg_replace("/[^\pL\d\-\.]/u", "-", basename($_FILES["file"]["name"])); + $fileSizeMax = $this->yellow->toolbox->getNumberBytes(ini_get("upload_max_filesize")); + $extension = strtoloweru(($pos = strrposu($fileNameShort, ".")) ? substru($fileNameShort, $pos) : ""); + $extensions = preg_split("/\s*,\s*/", $this->yellow->system->get("editUploadExtensions")); + if ($this->response->isUserAccess("upload", $location) && is_uploaded_file($fileNameTemp) && + filesize($fileNameTemp)<=$fileSizeMax && in_array($extension, $extensions)) { + $file = $this->response->getFileUpload($scheme, $address, $base, $location, $fileNameTemp, $fileNameShort); + if (!$file->isError() && $this->yellow->toolbox->copyFile($fileNameTemp, $file->fileName, true)) { + $data["location"] = $file->getLocation(); + } else { + $data["error"] = "Can't write file '$file->fileName'!"; + } + } else { + $data["error"] = "Can't write file '$fileNameShort'!"; + } + $statusCode = $this->yellow->sendData(isset($data["error"]) ? 500 : 200, json_encode($data), "a.json", false); + return $statusCode; + } + + // Check request + public function checkRequest($location) { + $locationLength = strlenu($this->yellow->system->get("editLocation")); + $this->response->active = substru($location, 0, $locationLength)==$this->yellow->system->get("editLocation"); + return $this->response->isActive(); + } + + // Check user authentication + public function checkUserAuth($scheme, $address, $base, $location, $fileName) { + $action = $this->yellow->page->getRequest("action"); + $authToken = $this->yellow->toolbox->getCookie("authtoken"); + $csrfToken = $this->yellow->toolbox->getCookie("csrftoken"); + if (empty($action) || $this->isRequestSameSite("POST", $scheme, $address)) { + if ($action=="login") { + $email = $this->yellow->page->getRequest("email"); + $password = $this->yellow->page->getRequest("password"); + if ($this->response->checkAuthLogin($email, $password)) { + $this->response->createCookies($scheme, $address, $base, $email); + $this->response->userEmail = $email; + $this->response->language = $this->getUserLanguage($email); + } else { + $this->response->userFailedError = "login"; + $this->response->userFailedEmail = $email; + $this->response->userFailedExpire = PHP_INT_MAX; + } + } elseif (!empty($authToken) && !empty($csrfToken)) { + $csrfTokenReceived = isset($_POST["csrftoken"]) ? $_POST["csrftoken"] : ""; + $csrfTokenIrrelevant = empty($action); + if ($this->response->checkAuthToken($authToken, $csrfToken, $csrfTokenReceived, $csrfTokenIrrelevant)) { + $this->response->userEmail = $email = $this->response->getAuthEmail($authToken); + $this->response->language = $this->getUserLanguage($email); + } else { + $this->response->userFailedError = "auth"; + $this->response->userFailedEmail = $this->response->getAuthEmail($authToken); + $this->response->userFailedExpire = $this->response->getAuthExpire($authToken); + } + } + $this->yellow->user->set($this->response->userEmail); + } + return $this->response->isUser(); + } + + // Check user without authentication + public function checkUserUnauth($scheme, $address, $base, $location, $fileName) { + $ok = false; + $action = $this->yellow->page->getRequest("action"); + if (empty($action) || $action=="signup" || $action=="forgot") { + $ok = true; + } elseif ($this->yellow->page->isRequest("actiontoken")) { + $actionToken = $this->yellow->page->getRequest("actiontoken"); + $email = $this->yellow->page->getRequest("email"); + $action = $this->yellow->page->getRequest("action"); + $expire = $this->yellow->page->getRequest("expire"); + $language = $this->yellow->page->getRequest("language"); + if ($this->response->checkActionToken($actionToken, $email, $action, $expire)) { + $ok = true; + $this->response->language = $this->getActionLanguage($language); + } else { + $this->response->userFailedError = "action"; + $this->response->userFailedEmail = $email; + $this->response->userFailedExpire = $expire; + } + } + return $ok; + } + + // Check user failed + public function checkUserFailed($scheme, $address, $base, $location, $fileName) { + if (!empty($this->response->userFailedError)) { + if ($this->response->userFailedExpire>time() && $this->yellow->user->isExisting($this->response->userFailedEmail)) { + $email = $this->response->userFailedEmail; + $failed = $this->yellow->user->getUser("failed", $email)+1; + $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreUserFile"); + $status = $this->yellow->user->save($fileNameUser, $email, array("failed" => $failed)) ? "ok" : "error"; + if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + if ($failed==$this->yellow->system->get("editBruteForceProtection")) { + $statusBeforeProtection = $this->yellow->user->getUser("status", $email); + $statusAfterProtection = ($statusBeforeProtection=="active" || $statusBeforeProtection=="inactive") ? "inactive" : "failed"; + if ($status=="ok") { + $status = $this->yellow->user->save($fileNameUser, $email, array("status" => $statusAfterProtection)) ? "ok" : "error"; + if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($status=="ok" && $statusBeforeProtection=="active") { + $status = $this->response->sendMail($scheme, $address, $base, $email, "reactivate") ? "done" : "error"; + if ($status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + } + } + if ($this->response->userFailedError=="login" || $this->response->userFailedError=="auth") { + $this->response->destroyCookies($scheme, $address, $base); + $this->response->status = "error"; + $this->yellow->page->error(430); + } else { + $this->response->status = "error"; + $this->yellow->page->error(500, "Link has expired!"); + } + } + } + + // Return user status changes + public function getUserStatus($email, $action) { + switch ($action) { + case "confirm": $statusExpected = "unconfirmed"; break; + case "approve": $statusExpected = "unapproved"; break; + case "recover": $statusExpected = "active"; break; + case "reactivate": $statusExpected = "inactive"; break; + case "verify": $statusExpected = "unverified"; break; + case "change": $statusExpected = "active"; break; + case "remove": $statusExpected = "active"; break; + } + return $this->yellow->user->getUser("status", $email)==$statusExpected ? "ok" : "done"; + } + + // Return user account changes + public function getUserAccount($action, $email, $password) { + $status = null; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onEditUserAccount")) { + $status = $value["object"]->onEditUserAccount($action, $email, $password); + if (!is_null($status)) break; + } + } + if (is_null($status)) { + $status = "ok"; + if (!empty($password) && strlenu($password)<$this->yellow->system->get("editUserPasswordMinLength")) $status = "short"; + if (!empty($password) && $password==$email) $status = "weak"; + if (!empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) $status = "invalid"; + } + return $status; + } + + // Return user language + public function getUserLanguage($email) { + $language = $this->yellow->user->getUser("language", $email); + if (!$this->yellow->language->isExisting($language)) $language = $this->yellow->system->get("language"); + return $language; + } + + // Return action language + public function getActionLanguage($language) { + if (!$this->yellow->language->isExisting($language)) $language = $this->yellow->system->get("language"); + return $language; + } + + // Check if request came from same site + public function isRequestSameSite($method, $scheme, $address) { + $origin = ""; + if (preg_match("#^(\w+)://([^/]+)(.*)$#", $this->yellow->toolbox->getServer("HTTP_REFERER"), $matches)) $origin = "$matches[1]://$matches[2]"; + if ($this->yellow->toolbox->getServer("HTTP_ORIGIN")) $origin = $this->yellow->toolbox->getServer("HTTP_ORIGIN"); + return $this->yellow->toolbox->getServer("REQUEST_METHOD")==$method && $origin=="$scheme://$address"; + } + + // Check if user account is taken + public function isUserAccountTaken($email) { + $taken = false; + if ($this->yellow->user->isExisting($email)) { + $status = $this->yellow->user->getUser("status", $email); + $reserved = strtotime($this->yellow->user->getUser("modified", $email)) + 60*60*24; + if ($status=="active" || $status=="inactive" || $reserved>time()) $taken = true; + } + return $taken; + } +} + +class YellowEditResponse { + public $yellow; // access to API + public $extension; // access to extension + public $active; // location is active? (boolean) + public $userEmail; // user email + public $userFailedError; // error of failed authentication + public $userFailedEmail; // email of failed authentication + public $userFailedExpire; // expiration time of failed authentication + public $rawDataSource; // raw data of page for comparison + public $rawDataEdit; // raw data of page for editing + public $rawDataOutput; // raw data of dynamic output + public $rawDataReadonly; // raw data is read only? (boolean) + public $rawDataEndOfLine; // end of line format for raw data + public $language; // response language + public $action; // response action + public $status; // response status + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->extension = $yellow->extension->get("edit"); + } + + // Process page data + public function processPageData($page) { + if ($this->isUser()) { + if (empty($this->rawDataSource)) $this->rawDataSource = $page->rawData; + if (empty($this->rawDataEdit)) $this->rawDataEdit = $page->rawData; + if (empty($this->rawDataEndOfLine)) $this->rawDataEndOfLine = $this->getEndOfLine($page->rawData); + if ($page->statusCode==404 || $this->yellow->toolbox->isLocationArguments()) { + $this->rawDataEdit = $this->getRawDataGenerated($page); + $this->rawDataReadonly = true; + } + if ($page->statusCode==434) { + $this->rawDataEdit = $this->getRawDataNew($page, true); + $this->rawDataReadonly = false; + } + } + if (empty($this->language)) $this->language = $page->get("language"); + if (empty($this->action)) $this->action = $this->isUser() ? "none" : "login"; + if (empty($this->status)) $this->status = "none"; + if ($this->status=="error") $this->action = "error"; + } + + // Return new page + public function getPageNew($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { + $rawData = $this->yellow->toolbox->normaliseLines($rawData, $endOfLine); + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName); + $page->parseData($rawData, false, 0); + $this->editContentFile($page, "create", $this->userEmail); + if ($this->yellow->content->find($page->location)) { + $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation")); + $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); + while ($this->yellow->content->find($page->location) || empty($page->fileName)) { + $page->rawData = $this->yellow->toolbox->setMetaData($page->rawData, "title", $this->getTitleNext($page->rawData)); + $page->rawData = $this->yellow->toolbox->normaliseLines($page->rawData, $endOfLine); + $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation")); + $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); + if (++$pageCounter>999) break; + } + if ($this->yellow->content->find($page->location) || empty($page->fileName)) { + $page->error(500, "Page '".$page->get("title")."' is not possible!"); + } + } else { + $page->fileName = $this->getPageNewFile($page->location); + } + if (!$this->isUserAccess("create", $page->location)) { + $page->error(500, "Page '".$page->get("title")."' is restricted!"); + } + return $page; + } + + // Return modified page + public function getPageEdit($scheme, $address, $base, $location, $fileName, $rawDataSource, $rawDataEdit, $rawDataFile, $endOfLine) { + $rawDataSource = $this->yellow->toolbox->normaliseLines($rawDataSource, $endOfLine); + $rawDataEdit = $this->yellow->toolbox->normaliseLines($rawDataEdit, $endOfLine); + $rawDataFile = $this->yellow->toolbox->normaliseLines($rawDataFile, $endOfLine); + $rawData = $this->extension->merge->merge($rawDataSource, $rawDataEdit, $rawDataFile); + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName); + $page->parseData($rawData, false, 0); + $pageSource = new YellowPage($this->yellow); + $pageSource->setRequestInformation($scheme, $address, $base, $location, $fileName); + $pageSource->parseData(($rawDataSource), false, 0); + $this->editContentFile($page, "edit", $this->userEmail); + if ($this->isMetaModified($pageSource, $page) && $page->location!=$this->yellow->content->getHomeLocation($page->location)) { + $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation"), true); + $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); + if ($page->location!=$pageSource->location && ($this->yellow->content->find($page->location) || empty($page->fileName))) { + $page->error(500, "Page '".$page->get("title")."' is not possible!"); + } + } + if (empty($page->rawData)) $page->error(500, "Page has been modified by someone else!"); + if (!$this->isUserAccess("edit", $page->location) || + !$this->isUserAccess("edit", $pageSource->location)) { + $page->error(500, "Page '".$page->get("title")."' is restricted!"); + } + return $page; + } + + // Return deleted page + public function getPageDelete($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { + $rawData = $this->yellow->toolbox->normaliseLines($rawData, $endOfLine); + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName); + $page->parseData($rawData, false, 0); + $this->editContentFile($page, "delete", $this->userEmail); + $page->rawData = $this->yellow->toolbox->setMetaData($page->rawData, "fileNameOriginal", $fileName); + if (!$this->isUserAccess("delete", $page->location)) { + $page->error(500, "Page '".$page->get("title")."' is restricted!"); + } + return $page; + } + + // Return preview page + public function getPagePreview($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { + $rawData = $this->yellow->toolbox->normaliseLines($rawData, $endOfLine); + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName); + $page->parseData($rawData, false, 200); + $this->yellow->language->set($page->get("language")); + $class = "page-preview layout-".$page->get("layout"); + $output = "<div class=\"".htmlspecialchars($class)."\"><div class=\"content\"><div class=\"main\">"; + if ($this->yellow->system->get("editToolbarButtons")!="none") $output .= "<h1>".$page->getHtml("titleContent")."</h1>\n"; + $output .= $page->getContent(); + $output .= "</div></div></div>"; + $page->setOutput($output); + return $page; + } + + // Return uploaded file + public function getFileUpload($scheme, $address, $base, $pageLocation, $fileNameTemp, $fileNameShort) { + $file = new YellowPage($this->yellow); + $file->setRequestInformation($scheme, $address, $base, "/".$fileNameTemp, $fileNameTemp); + $file->parseData(null, false, 0); + $file->set("fileNameShort", $fileNameShort); + $file->set("type", $this->yellow->toolbox->getFileType($fileNameShort)); + if ($file->get("type")=="html" || $file->get("type")=="svg") { + $fileData = $this->yellow->toolbox->readFile($fileNameTemp); + $fileData = $this->yellow->toolbox->normaliseData($fileData, $file->get("type")); + if (empty($fileData) || !$this->yellow->toolbox->createFile($fileNameTemp, $fileData)) { + $file->error(500, "Can't write file '$fileNameTemp'!"); + } + } + $this->editMediaFile($file, "upload", $this->userEmail); + $file->location = $this->getFileNewLocation($fileNameShort, $pageLocation, $file->get("fileNewLocation")); + $file->fileName = substru($file->location, 1); + while (is_file($file->fileName)) { + $fileNameShort = $this->getFileNext(basename($file->fileName)); + $file->location = $this->getFileNewLocation($fileNameShort, $pageLocation, $file->get("fileNewLocation")); + $file->fileName = substru($file->location, 1); + if (++$fileCounter>999) break; + } + if (is_file($file->fileName)) $file->error(500, "File '".$file->get("fileNameShort")."' is not possible!"); + return $file; + } + + // Return system file + public function getFileSystem($scheme, $address, $base, $pageLocation, $fileName, $settings) { + $file = new YellowPage($this->yellow); + $file->setRequestInformation($scheme, $address, $base, "/".$fileName, $fileName); + $file->parseData(null, false, 0); + foreach ($settings as $key=>$value) $file->set($key, $value); + $this->editSystemFile($file, "system", $this->userEmail); + return $file; + } + + // Return page data including status information + public function getPageData($page) { + $data = array(); + if ($this->isUser()) { + $data["title"] = $this->yellow->toolbox->getMetaData($this->rawDataEdit, "title"); + $data["rawDataSource"] = $this->rawDataSource; + $data["rawDataEdit"] = $this->rawDataEdit; + $data["rawDataNew"] = $this->getRawDataNew($page); + $data["rawDataOutput"] = strval($this->rawDataOutput); + $data["rawDataReadonly"] = intval($this->rawDataReadonly); + $data["rawDataEndOfLine"] = $this->rawDataEndOfLine; + $data["scheme"] = $this->yellow->page->scheme; + $data["address"] = $this->yellow->page->address; + $data["base"] = $this->yellow->page->base; + $data["location"] = $this->yellow->page->location; + } + if ($this->action!="none") $data = array_merge($data, $this->getRequestData()); + $data["action"] = $this->action; + $data["status"] = $this->status; + $data["statusCode"] = $this->yellow->page->statusCode; + return $data; + } + + // Return system data + public function getSystemData() { + $data = array(); + $data["coreServerScheme"] = $this->yellow->system->get("coreServerScheme"); + $data["coreServerAddress"] = $this->yellow->system->get("coreServerAddress"); + $data["coreServerBase"] = $this->yellow->system->get("coreServerBase"); + $data = array_merge($data, $this->yellow->system->getSettings("", "Location")); + if ($this->isUser()) { + $data["coreFileSizeMax"] = $this->yellow->toolbox->getNumberBytes(ini_get("upload_max_filesize")); + $data["coreProductRelease"] = "Datenstrom Yellow ".YellowCore::RELEASE; + $data["coreExtensions"] = array(); + foreach ($this->yellow->extension->data as $key=>$value) { + $data["coreExtensions"][$key] = $value["class"]; + } + $data["coreLanguages"] = array(); + foreach ($this->yellow->system->getValues("language") as $language) { + $data["coreLanguages"][$language] = $this->yellow->language->getTextHtml("languageDescription", $language); + } + $data["editSettingsActions"] = $this->getSettingsActions(); + $data["editUploadExtensions"] = $this->yellow->system->get("editUploadExtensions"); + $data["editKeyboardShortcuts"] = $this->yellow->system->get("editKeyboardShortcuts"); + $data["editToolbarButtons"] = $this->getToolbarButtons(); + $data["editStatusValues"] = $this->getStatusValues(); + $data["emojiawesomeToolbarButtons"] = $this->yellow->system->get("emojiawesomeToolbarButtons"); + $data["fontawesomeToolbarButtons"] = $this->yellow->system->get("fontawesomeToolbarButtons"); + if ($this->isUserAccess("system")) { + $data["sitename"] = $this->yellow->system->get("sitename"); + $data["author"] = $this->yellow->system->get("author"); + $data["email"] = $this->yellow->system->get("email"); + } + } else { + $data["editLoginEmail"] = $this->yellow->page->get("editLoginEmail"); + $data["editLoginPassword"] = $this->yellow->page->get("editLoginPassword"); + $data["editLoginRestriction"] = intval($this->isLoginRestriction()); + } + if (defined("DEBUG") && DEBUG>=1) $data["debug"] = DEBUG; + return $data; + } + + // Return user data + public function getUserData() { + $data = array(); + if ($this->isUser()) { + $data["email"] = $this->userEmail; + $data["name"] = $this->yellow->user->getUser("name", $this->userEmail); + $data["language"] = $this->yellow->user->getUser("language", $this->userEmail); + $data["status"] = $this->yellow->user->getUser("status", $this->userEmail); + $data["home"] = $this->yellow->user->getUser("home", $this->userEmail); + $data["access"] = $this->yellow->user->getUser("access", $this->userEmail); + } + return $data; + } + + // Return language data + public function getLanguageData() { + $dataLanguage = $this->yellow->language->getSettings("language", "", $this->language); + $dataEdit = $this->yellow->language->getSettings("edit", "", $this->language); + return array_merge($dataLanguage, $dataEdit); + } + + // Return request data + public function getRequestData() { + $data = array(); + foreach ($_REQUEST as $key=>$value) { + if ($key=="password" || $key=="authtoken" || $key=="csrftoken" || $key=="actiontoken" || substru($key, 0, 7)=="rawdata") continue; + $data["request".ucfirst($key)] = trim($value); + } + return $data; + } + + // Return settings actions + public function getSettingsActions() { + $settingsActions = "account"; + if ($this->isUserAccess("system")) $settingsActions .= ", system"; + if ($this->isUserAccess("update")) $settingsActions .= ", update"; + return $settingsActions=="account" ? "" : $settingsActions; + } + + // Return toolbar buttons + public function getToolbarButtons() { + $toolbarButtons = $this->yellow->system->get("editToolbarButtons"); + if ($toolbarButtons=="auto") { + $toolbarButtons = ""; + if ($this->yellow->extension->isExisting("markdown")) $toolbarButtons = "format, bold, italic, strikethrough, code, separator, list, link, file"; + if ($this->yellow->extension->isExisting("emojiawesome")) $toolbarButtons .= ", emojiawesome"; + if ($this->yellow->extension->isExisting("fontawesome")) $toolbarButtons .= ", fontawesome"; + $toolbarButtons .= ", status, preview"; + } + return $toolbarButtons; + } + + // Return status values + public function getStatusValues() { + $statusValues = ""; + if ($this->yellow->extension->isExisting("private")) $statusValues .= ", private"; + if ($this->yellow->extension->isExisting("draft")) $statusValues .= ", draft"; + $statusValues .= ", unlisted"; + return ltrim($statusValues, ", "); + } + + // Return end of line format + public function getEndOfLine($rawData = "") { + $endOfLine = $this->yellow->system->get("editEndOfLine"); + if ($endOfLine=="auto") { + $rawData = empty($rawData) ? PHP_EOL : substru($rawData, 0, 4096); + $endOfLine = strposu($rawData, "\r")===false ? "lf" : "crlf"; + } + return $endOfLine; + } + + // Return update information + public function getUpdateInformation() { + $statusCode = 200; + $rawData = ""; + if ($this->yellow->extension->isExisting("update")) { + list($statusCodeCurrent, $settingsCurrent) = $this->yellow->extension->get("update")->getExtensionSettings(false); + list($statusCodeLatest, $settingsLatest) = $this->yellow->extension->get("update")->getExtensionSettings(true); + $statusCode = max($statusCodeCurrent, $statusCodeLatest); + foreach ($settingsCurrent as $key=>$value) { + if ($settingsLatest->isExisting($key)) { + $versionCurrent = $settingsCurrent[$key]->get("version"); + $versionLatest = $settingsLatest[$key]->get("version"); + if (strnatcasecmp($versionCurrent, $versionLatest)<0) { + $rawData .= htmlspecialchars(ucfirst($key)." $versionLatest")."<br />"; + } + } + } + if (!empty($rawData)) $rawData = "<p>$rawData</p>\n"; + } + return array($statusCode, $rawData); + } + + // Return raw data for generated page + public function getRawDataGenerated($page) { + $title = $page->get("title"); + $text = $this->yellow->language->getText("editDataGenerated", $page->get("language")); + return "---\nTitle: $title\n---\n$text"; + } + + // Return raw data for new page + public function getRawDataNew($page, $customTitle = false) { + $fileName = ""; + foreach ($this->yellow->content->path($page->location)->reverse() as $ancestor) { + if ($ancestor->isExisting("layoutNew")) { + $name = $this->yellow->lookup->normaliseName($ancestor->get("layoutNew")); + $location = $this->yellow->content->getHomeLocation($page->location).$this->yellow->system->get("coreContentSharedDirectory"); + $fileName = $this->yellow->lookup->findFileFromLocation($location, true).$this->yellow->system->get("editNewFile"); + $fileName = str_replace("(.*)", $name, $fileName); + if (is_file($fileName)) break; + } + } + if (!is_file($fileName)) { + $name = $this->yellow->lookup->normaliseName($this->yellow->system->get("layout")); + $location = $this->yellow->content->getHomeLocation($page->location).$this->yellow->system->get("coreContentSharedDirectory"); + $fileName = $this->yellow->lookup->findFileFromLocation($location, true).$this->yellow->system->get("editNewFile"); + $fileName = str_replace("(.*)", $name, $fileName); + } + if (is_file($fileName)) { + $rawData = $this->yellow->toolbox->readFile($fileName); + $rawData = preg_replace("/@timestamp/i", time(), $rawData); + $rawData = preg_replace("/@datetime/i", date("Y-m-d H:i:s"), $rawData); + $rawData = preg_replace("/@date/i", date("Y-m-d"), $rawData); + $rawData = preg_replace("/@usershort/i", strtok($this->yellow->user->getUser("name", $this->userEmail), " "), $rawData); + $rawData = preg_replace("/@username/i", $this->yellow->user->getUser("name", $this->userEmail), $rawData); + $rawData = preg_replace("/@userlanguage/i", $this->yellow->user->getUser("language", $this->userEmail), $rawData); + } else { + $rawData = "---\nTitle: Page\n---\n"; + } + if ($customTitle) { + $title = $this->yellow->toolbox->createTextTitle($page->location); + $rawData = $this->yellow->toolbox->setMetaData($rawData, "title", $title); + } + return $rawData; + } + + // Return location for new/modified page + public function getPageNewLocation($rawData, $pageLocation, $pageNewLocation, $pageMatchLocation = false) { + $location = empty($pageNewLocation) ? "@title" : $pageNewLocation; + $location = preg_replace("/@title/i", $this->getPageNewTitle($rawData), $location); + $location = preg_replace("/@timestamp/i", $this->getPageNewData($rawData, "published", true, "U"), $location); + $location = preg_replace("/@date/i", $this->getPageNewData($rawData, "published", true, "Y-m-d"), $location); + $location = preg_replace("/@year/i", $this->getPageNewData($rawData, "published", true, "Y"), $location); + $location = preg_replace("/@month/i", $this->getPageNewData($rawData, "published", true, "m"), $location); + $location = preg_replace("/@day/i", $this->getPageNewData($rawData, "published", true, "d"), $location); + $location = preg_replace("/@tag/i", $this->getPageNewData($rawData, "tag", true), $location); + $location = preg_replace("/@author/i", $this->getPageNewData($rawData, "author", true), $location); + if (!preg_match("/^\//", $location)) { + if ($this->yellow->lookup->isFileLocation($pageLocation) || !$pageMatchLocation) { + $location = $this->yellow->lookup->getDirectoryLocation($pageLocation).$location; + } else { + $location = $this->yellow->lookup->getDirectoryLocation(rtrim($pageLocation, "/")).$location; + } + } + if ($pageMatchLocation) { + $location = rtrim($location, "/").($this->yellow->lookup->isFileLocation($pageLocation) ? "" : "/"); + } + return $location; + } + + // Return title for new/modified page + public function getPageNewTitle($rawData) { + $title = $this->yellow->toolbox->getMetaData($rawData, "title"); + $titleSlug = $this->yellow->toolbox->getMetaData($rawData, "titleSlug"); + $value = empty($titleSlug) ? $title : $titleSlug; + $value = $this->yellow->lookup->normaliseName($value, true, false, true); + return trim(preg_replace("/-+/", "-", $value), "-"); + } + + // Return data for new/modified page + public function getPageNewData($rawData, $key, $filterFirst = false, $dateFormat = "") { + $value = $this->yellow->toolbox->getMetaData($rawData, $key); + if ($filterFirst && preg_match("/^(.*?)\,(.*)$/", $value, $matches)) $value = $matches[1]; + if (!empty($dateFormat)) $value = date($dateFormat, strtotime($value)); + if (strempty($value)) $value = "none"; + $value = $this->yellow->lookup->normaliseName($value, true, false, true); + return trim(preg_replace("/-+/", "-", $value), "-"); + } + + // Return file name for new/modified page + public function getPageNewFile($location, $pageFileName = "", $pagePrefix = "") { + $fileName = $this->yellow->lookup->findFileFromLocation($location); + if (!empty($fileName)) { + if (!is_dir(dirname($fileName))) { + $path = ""; + $tokens = explode("/", $fileName); + for ($i=0; $i<count($tokens)-1; ++$i) { + if (!is_dir($path.$tokens[$i])) { + if (!preg_match("/^[\d\-\_\.]+(.*)$/", $tokens[$i])) { + $number = 1; + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^[\d\-\_\.]+(.*)$/", true, true, false) as $entry) { + if ($number!=1 && $number!=intval($entry)) break; + $number = intval($entry)+1; + } + $tokens[$i] = "$number-".$tokens[$i]; + } + $tokens[$i] = $this->yellow->lookup->normaliseName($tokens[$i], false, false, true); + } + $path .= $tokens[$i]."/"; + } + $fileName = $path.$tokens[$i]; + $pageFileName = empty($pageFileName) ? $fileName : $pageFileName; + } + $prefix = $this->getPageNewPrefix($location, $pageFileName, $pagePrefix); + if ($this->yellow->lookup->isFileLocation($location)) { + if (preg_match("#^(.*)\/(.+?)$#", $fileName, $matches)) { + $path = $matches[1]; + $text = $this->yellow->lookup->normaliseName($matches[2], true, true); + if (preg_match("/^[\d\-\_\.]*$/", $text)) $prefix = ""; + $fileName = $path."/".$prefix.$text.$this->yellow->system->get("coreContentExtension"); + } + } else { + if (preg_match("#^(.*)\/(.+?)$#", dirname($fileName), $matches)) { + $path = $matches[1]; + $text = $this->yellow->lookup->normaliseName($matches[2], true, false); + if (preg_match("/^[\d\-\_\.]*$/", $text)) $prefix = ""; + $fileName = $path."/".$prefix.$text."/".$this->yellow->system->get("coreContentDefaultFile"); + } + } + } + return $fileName; + } + + // Return prefix for new/modified page + public function getPageNewPrefix($location, $pageFileName, $pagePrefix) { + if (empty($pagePrefix)) { + if ($this->yellow->lookup->isFileLocation($location)) { + if (preg_match("#^(.*)\/(.+?)$#", $pageFileName, $matches)) $pagePrefix = $matches[2]; + } else { + if (preg_match("#^(.*)\/(.+?)$#", dirname($pageFileName), $matches)) $pagePrefix = $matches[2]; + } + } + return $this->yellow->lookup->normalisePrefix($pagePrefix, true); + } + + // Return location for new file + public function getFileNewLocation($fileNameShort, $pageLocation, $fileNewLocation) { + $location = empty($fileNewLocation) ? $this->yellow->system->get("editUploadNewLocation") : $fileNewLocation; + $location = preg_replace("/@timestamp/i", time(), $location); + $location = preg_replace("/@type/i", $this->yellow->toolbox->getFileType($fileNameShort), $location); + $location = preg_replace("/@group/i", $this->getFileNewGroup($fileNameShort), $location); + $location = preg_replace("/@folder/i", $this->getFileNewFolder($pageLocation), $location); + $location = preg_replace("/@filename/i", strtoloweru($fileNameShort), $location); + if (!preg_match("/^\//", $location)) { + $location = $this->yellow->system->get("coreMediaLocation").$location; + } + return $location; + } + + // Return group for new file + public function getFileNewGroup($fileNameShort) { + $group = "none"; + $path = $this->yellow->system->get("coreMediaDirectory"); + $fileType = $this->yellow->toolbox->getFileType($fileNameShort); + $fileName = $this->yellow->system->get(preg_match("/(gif|jpg|png|svg)$/", $fileType) ? "coreImageDirectory" : "coreDownloadDirectory").$fileNameShort; + if (preg_match("#^$path(.+?)\/#", $fileName, $matches)) $group = strtoloweru($matches[1]); + return $group; + } + + // Return folder for new file + public function getFileNewFolder($pageLocation) { + $parentTopLocation = $this->yellow->content->getParentTopLocation($pageLocation); + if ($parentTopLocation==$this->yellow->content->getHomeLocation($pageLocation)) $parentTopLocation .= "home"; + return strtoloweru(trim($parentTopLocation, "/")); + } + + // Return next file name + public function getFileNext($fileNameShort) { + $fileText = $fileNumber = $fileExtension = ""; + if (preg_match("/^(.*?)(\d*)(\..*?)?$/", $fileNameShort, $matches)) { + $fileText = $matches[1]; + $fileNumber = strempty($matches[2]) ? "-2" : $matches[2]+1; + $fileExtension = $matches[3]; + } + return $fileText.$fileNumber.$fileExtension; + } + + // Return next title + public function getTitleNext($rawData) { + $titleText = $titleNumber = ""; + if (preg_match("/^(.*?)(\d*)$/", $this->yellow->toolbox->getMetaData($rawData, "title"), $matches)) { + $titleText = $matches[1]; + $titleNumber = strempty($matches[2]) ? " 2" : $matches[2]+1; + } + return $titleText.$titleNumber; + } + + // Send mail to user + public function sendMail($scheme, $address, $base, $email, $action) { + if ($action=="approve") { + $userName = $this->yellow->system->get("author"); + $userEmail = $this->yellow->system->get("email"); + $userLanguage = $this->extension->getUserLanguage($userEmail); + } else { + $userName = $this->yellow->user->getUser("name", $email); + $userEmail = $email; + $userLanguage = $this->extension->getUserLanguage($email); + } + if ($action=="welcome" || $action=="goodbye") { + $url = "$scheme://$address$base/"; + } else { + $expire = time() + 60*60*24; + $actionToken = $this->createActionToken($email, $action, $expire); + $url = "$scheme://$address$base"."/action:$action/email:$email/expire:$expire/language:$userLanguage/actiontoken:$actionToken/"; + } + $prefix = "edit".ucfirst($action); + $message = $this->yellow->language->getText("{$prefix}Message", $userLanguage); + $message = str_replace("\\n", "\r\n", $message); + $message = preg_replace("/@useraccount/i", $email, $message); + $message = preg_replace("/@usershort/i", strtok($userName, " "), $message); + $message = preg_replace("/@username/i", $userName, $message); + $message = preg_replace("/@userlanguage/i", $userLanguage, $message); + $sitename = $this->yellow->system->get("sitename"); + $footer = $this->yellow->language->getText("editMailFooter", $userLanguage); + $footer = str_replace("\\n", "\r\n", $footer); + $footer = preg_replace("/@sitename/i", $sitename, $footer); + $mailTo = mb_encode_mimeheader("$userName")." <$userEmail>"; + $mailSubject = mb_encode_mimeheader($this->yellow->language->getText("{$prefix}Subject", $userLanguage)); + $mailHeaders = mb_encode_mimeheader("From: $sitename")." <noreply>\r\n"; + $mailHeaders .= mb_encode_mimeheader("X-Request-Url: $scheme://$address$base")."\r\n"; + $mailHeaders .= "Mime-Version: 1.0\r\n"; + $mailHeaders .= "Content-Type: text/plain; charset=utf-8\r\n"; + $mailMessage = "$message\r\n\r\n$url\r\n-- \r\n$footer"; + return mail($mailTo, $mailSubject, $mailMessage, $mailHeaders); + } + + // Create browser cookies + public function createCookies($scheme, $address, $base, $email) { + $expire = time() + $this->yellow->system->get("editLoginSessionTimeout"); + $authToken = $this->createAuthToken($email, $expire); + $csrfToken = $this->createCsrfToken(); + setcookie("authtoken", $authToken, $expire, "$base/", "", $scheme=="https", true); + setcookie("csrftoken", $csrfToken, $expire, "$base/", "", $scheme=="https", false); + } + + // Destroy browser cookies + public function destroyCookies($scheme, $address, $base) { + setcookie("authtoken", "", 1, "$base/", "", $scheme=="https", true); + setcookie("csrftoken", "", 1, "$base/", "", $scheme=="https", false); + } + + // Create authentication token + public function createAuthToken($email, $expire) { + $hash = $this->yellow->user->getUser("hash", $email); + $signature = $this->yellow->toolbox->createHash($hash."auth".$expire, "sha256"); + if (empty($signature)) $signature = "padd"."error-hash-algorithm-sha256"; + return substrb($signature, 4).$this->yellow->user->getUser("stamp", $email).dechex($expire); + } + + // Create action token + public function createActionToken($email, $action, $expire) { + $hash = $this->yellow->user->getUser("hash", $email); + $signature = $this->yellow->toolbox->createHash($hash.$action.$expire, "sha256"); + if (empty($signature)) $signature = "padd"."error-hash-algorithm-sha256"; + return substrb($signature, 4); + } + + // Create CSRF token + public function createCsrfToken() { + return $this->yellow->toolbox->createSalt(64); + } + + // Create password hash + public function createHash($password) { + $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); + $cost = $this->yellow->system->get("editUserHashCost"); + $hash = $this->yellow->toolbox->createHash($password, $algorithm, $cost); + if (empty($hash)) $hash = "error-hash-algorithm-$algorithm"; + return $hash; + } + + // Create user stamp + public function createStamp() { + $stamp = $this->yellow->toolbox->createSalt(20); + while ($this->getAuthEmail("none", $stamp)) { + $stamp = $this->yellow->toolbox->createSalt(20); + } + return $stamp; + } + + // Check user authentication from email and password + public function checkAuthLogin($email, $password) { + $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); + $hash = $this->yellow->user->getUser("hash", $email); + return $this->yellow->user->getUser("status", $email)=="active" && + $this->yellow->toolbox->verifyHash($password, $algorithm, $hash); + } + + // Check user authentication from tokens + public function checkAuthToken($authToken, $csrfTokenExpected, $csrfTokenReceived, $csrfTokenIrrelevant) { + $signature = "$5y$".substrb($authToken, 0, 96); + $email = $this->getAuthEmail($authToken); + $expire = $this->getAuthExpire($authToken); + $hash = $this->yellow->user->getUser("hash", $email); + return $expire>time() && $this->yellow->user->getUser("status", $email)=="active" && + $this->yellow->toolbox->verifyHash($hash."auth".$expire, "sha256", $signature) && + ($this->yellow->toolbox->verifyToken($csrfTokenExpected, $csrfTokenReceived) || $csrfTokenIrrelevant); + } + + // Check action token + public function checkActionToken($actionToken, $email, $action, $expire) { + $signature = "$5y$".$actionToken; + $hash = $this->yellow->user->getUser("hash", $email); + return $expire>time() && $this->yellow->user->isExisting($email) && + $this->yellow->toolbox->verifyHash($hash.$action.$expire, "sha256", $signature); + } + + // Return user email from authentication, timing attack safe email lookup + public function getAuthEmail($authToken, $stamp = "") { + $email = ""; + if (empty($stamp)) $stamp = substrb($authToken, 96, 20); + foreach ($this->yellow->user->settings as $key=>$value) { + if ($this->yellow->toolbox->verifyToken($value["stamp"], $stamp)) $email = $key; + } + return $email; + } + + // Return expiration time from authentication + public function getAuthExpire($authToken) { + return hexdec(substrb($authToken, 96+20)); + } + + // Change content file + public function editContentFile($page, $action, $email) { + if (!$page->isError()) { + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onEditContentFile")) $value["object"]->onEditContentFile($page, $action, $email); + } + } + } + + // Change media file + public function editMediaFile($file, $action, $email) { + if (!$file->isError()) { + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onEditMediaFile")) $value["object"]->onEditMediaFile($file, $action, $email); + } + } + } + + // Change system file + public function editSystemFile($file, $action, $email) { + if (!$file->isError()) { + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onEditSystemFile")) $value["object"]->onEditSystemFile($file, $action, $email); + } + } + } + + // Check if meta data has been modified + public function isMetaModified($pageSource, $pageOther) { + return substrb($pageSource->rawData, 0, $pageSource->metaDataOffsetBytes) != + substrb($pageOther->rawData, 0, $pageOther->metaDataOffsetBytes); + } + + // Check if active + public function isActive() { + return $this->active; + } + + // Check if user is logged in + public function isUser() { + return !empty($this->userEmail); + } + + // Check if user with access + public function isUserAccess($action, $location = "") { + $userHome = $this->yellow->user->getUser("home", $this->userEmail); + $userAccess = preg_split("/\s*,\s*/", $this->yellow->user->getUser("access", $this->userEmail)); + return in_array($action, $userAccess) && (empty($location) || substru($location, 0, strlenu($userHome))==$userHome); + } + + // Check if login with restriction + public function isLoginRestriction() { + return $this->yellow->system->get("editLoginRestriction"); + } +} + +class YellowEditMerge { + public $yellow; // access to API + const ADD = "+"; // merge types + const MODIFY = "*"; + const REMOVE = "-"; + const SAME = " "; + + public function __construct($yellow) { + $this->yellow = $yellow; + } + + // Merge text, null if not possible + public function merge($textSource, $textMine, $textYours, $showDiff = false) { + if ($textMine!=$textYours) { + $diffMine = $this->buildDiff($textSource, $textMine); + $diffYours = $this->buildDiff($textSource, $textYours); + $diff = $this->mergeDiff($diffMine, $diffYours); + $output = $this->getOutput($diff, $showDiff); + } else { + $output = $textMine; + } + return $output; + } + + // Build differences to common source + public function buildDiff($textSource, $textOther) { + $diff = array(); + $lastRemove = -1; + $textStart = 0; + $textSource = $this->yellow->toolbox->getTextLines($textSource); + $textOther = $this->yellow->toolbox->getTextLines($textOther); + $sourceEnd = $sourceSize = count($textSource); + $otherEnd = $otherSize = count($textOther); + while ($textStart<$sourceEnd && $textStart<$otherEnd && $textSource[$textStart]==$textOther[$textStart]) { + ++$textStart; + } + while ($textStart<$sourceEnd && $textStart<$otherEnd && $textSource[$sourceEnd-1]==$textOther[$otherEnd-1]) { + --$sourceEnd; + --$otherEnd; + } + for ($pos=0; $pos<$textStart; ++$pos) { + array_push($diff, array(YellowEditMerge::SAME, $textSource[$pos], false)); + } + $lcs = $this->buildDiffLCS($textSource, $textOther, $textStart, $sourceEnd-$textStart, $otherEnd-$textStart); + for ($x=0,$y=0,$xEnd=$otherEnd-$textStart,$yEnd=$sourceEnd-$textStart; $x<$xEnd || $y<$yEnd;) { + $max = $lcs[$y][$x]; + if ($y<$yEnd && $lcs[$y+1][$x]==$max) { + array_push($diff, array(YellowEditMerge::REMOVE, $textSource[$textStart+$y], false)); + if ($lastRemove==-1) $lastRemove = count($diff)-1; + ++$y; + continue; + } + if ($x<$xEnd && $lcs[$y][$x+1]==$max) { + if ($lastRemove==-1 || $diff[$lastRemove][0]!=YellowEditMerge::REMOVE) { + array_push($diff, array(YellowEditMerge::ADD, $textOther[$textStart+$x], false)); + $lastRemove = -1; + } else { + $diff[$lastRemove] = array(YellowEditMerge::MODIFY, $textOther[$textStart+$x], false); + ++$lastRemove; + if (count($diff)==$lastRemove) $lastRemove = -1; + } + ++$x; + continue; + } + array_push($diff, array(YellowEditMerge::SAME, $textSource[$textStart+$y], false)); + $lastRemove = -1; + ++$x; + ++$y; + } + for ($pos=$sourceEnd;$pos<$sourceSize; ++$pos) { + array_push($diff, array(YellowEditMerge::SAME, $textSource[$pos], false)); + } + return $diff; + } + + // Build longest common subsequence + public function buildDiffLCS($textSource, $textOther, $textStart, $yEnd, $xEnd) { + $lcs = array_fill(0, $yEnd+1, array_fill(0, $xEnd+1, 0)); + for ($y=$yEnd-1; $y>=0; --$y) { + for ($x=$xEnd-1; $x>=0; --$x) { + if ($textSource[$textStart+$y]==$textOther[$textStart+$x]) { + $lcs[$y][$x] = $lcs[$y+1][$x+1]+1; + } else { + $lcs[$y][$x] = max($lcs[$y][$x+1], $lcs[$y+1][$x]); + } + } + } + return $lcs; + } + + // Merge differences + public function mergeDiff($diffMine, $diffYours) { + $diff = array(); + $posMine = $posYours = 0; + while ($posMine<count($diffMine) && $posYours<count($diffYours)) { + $typeMine = $diffMine[$posMine][0]; + $typeYours = $diffYours[$posYours][0]; + if ($typeMine==YellowEditMerge::SAME) { + array_push($diff, $diffYours[$posYours]); + } elseif ($typeYours==YellowEditMerge::SAME) { + array_push($diff, $diffMine[$posMine]); + } elseif ($typeMine==YellowEditMerge::ADD && $typeYours==YellowEditMerge::ADD) { + $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], false); + } elseif ($typeMine==YellowEditMerge::MODIFY && $typeYours==YellowEditMerge::MODIFY) { + $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], false); + } elseif ($typeMine==YellowEditMerge::REMOVE && $typeYours==YellowEditMerge::REMOVE) { + array_push($diff, $diffMine[$posMine]); + } elseif ($typeMine==YellowEditMerge::ADD) { + array_push($diff, $diffMine[$posMine]); + } elseif ($typeYours==YellowEditMerge::ADD) { + array_push($diff, $diffYours[$posYours]); + } else { + $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], true); + } + if (defined("DEBUG") && DEBUG>=2) echo "YellowEditMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n"; + if ($typeMine==YellowEditMerge::ADD || $typeYours==YellowEditMerge::ADD) { + if ($typeMine==YellowEditMerge::ADD) ++$posMine; + if ($typeYours==YellowEditMerge::ADD) ++$posYours; + } else { + ++$posMine; + ++$posYours; + } + } + for (;$posMine<count($diffMine); ++$posMine) { + array_push($diff, $diffMine[$posMine]); + $typeMine = $diffMine[$posMine][0]; + $typeYours = " "; + if (defined("DEBUG") && DEBUG>=2) echo "YellowEditMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n"; + } + for (;$posYours<count($diffYours); ++$posYours) { + array_push($diff, $diffYours[$posYours]); + $typeYours = $diffYours[$posYours][0]; + $typeMine = " "; + if (defined("DEBUG") && DEBUG>=2) echo "YellowEditMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n"; + } + return $diff; + } + + // Merge potential conflict + public function mergeConflict(&$diff, $diffMine, $diffYours, $conflict) { + if (!$conflict && $diffMine[1]==$diffYours[1]) { + array_push($diff, $diffMine); + } else { + array_push($diff, array($diffMine[0], $diffMine[1], true)); + array_push($diff, array($diffYours[0], $diffYours[1], true)); + } + } + + // Return merged text, null if not possible + public function getOutput($diff, $showDiff = false) { + $output = ""; + $conflict = false; + if (!$showDiff) { + for ($i=0; $i<count($diff); ++$i) { + if ($diff[$i][0]!=YellowEditMerge::REMOVE) $output .= $diff[$i][1]; + $conflict |= $diff[$i][2]; + } + } else { + for ($i=0; $i<count($diff); ++$i) { + $output .= $diff[$i][2] ? "! " : $diff[$i][0]." "; + $output .= $diff[$i][1]; + } + } + return !$conflict ? $output : null; + } +} diff --git a/system/extensions/edit.woff b/system/extensions/edit.woff new file mode 100644 index 0000000000000000000000000000000000000000..fca12f196fb6b3409ecac2dcbdd4e8cf923b2433 GIT binary patch literal 5688 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 literal 0 HcmV?d00001 diff --git a/system/extensions/english.php b/system/extensions/english.php new file mode 100644 index 0000000..fcebd14 --- /dev/null +++ b/system/extensions/english.php @@ -0,0 +1,23 @@ +<?php +// English extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/english + +class YellowEnglish { + const VERSION = "0.8.24"; + public $yellow; // access to API + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + } + + // Handle update + public function onUpdate($action) { + $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); + if ($action=="install") { + $this->yellow->system->save($fileName, array("language" => "en")); + } elseif ($action=="uninstall" && $this->yellow->system->get("language")=="en") { + $language = reset(array_diff($this->yellow->system->getValues("language"), array("en"))); + $this->yellow->system->save($fileName, array("language" => $language)); + } + } +} diff --git a/system/extensions/english.txt b/system/extensions/english.txt new file mode 100644 index 0000000..ff7da19 --- /dev/null +++ b/system/extensions/english.txt @@ -0,0 +1,253 @@ +# English extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/english + +Language: en +LanguageLocale: en_GB +LanguageDescription: English +LanguageTranslator: Mark Seuffert +BerlinDescription: Berlin is a beautiful theme inspired by Dieter Rams. +BlogDescription: Blog for your website. +BlogBy: by +BlogTag: Tags: +BlogMore: Read more… +BreadcrumbDescription: Breadcrumb navigation. +BundleDescription: Bundle website files. +ChineseDescription: Chinese/简体中文 with language 'zh-CN'. +CommandDescription: Command line of the website. +ContactDescription: Email contact page. +ContactName: Name: +ContactEmail: Email: +ContactMessage: Message: +ContactConsent: I consent that this website stores my message. +ContactButton: Send message +ContactMailSpam: [Spam] +ContactMailFooter: This email was sent via @sitename - @title +ContactStatusNone: Say hello. Your feedback is very welcome. +ContactStatusIncomplete: Please fill out all fields. +ContactStatusInvalid: Please enter a valid email. +ContactStatusReview: Please remove links from the message. +ContactStatusDone: You have sent an email. Thank you! +ContactStatusError: Email could not be sent, please try again later! +CoreDescription: Core functionality of the website. +CoreDatePast: today, yesterday, @x days ago, 1 month ago, @x months ago, 1 year ago, @x years ago, on @x +CoreDateFuture: soon, tomorrow, in @x days, in 1 month, in @x months, in 1 year, in @x years, on @x +CoreDateMonths: January, February, March, April, May, June, July, August, September, October, November, December +CoreDateWeekdays: Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday +CoreDateWeekstart: Monday +CoreDateFormatShort: F Y +CoreDateFormatMedium: Y-m-d +CoreDateFormatLong: Y-m-d H:i +CoreTimeFormatShort: H:i +CoreTimeFormatMedium: H:i:s +CoreTimeFormatLong: H:i:s T +CorePaginationPrevious: ← Previous +CorePaginationNext: Next → +CoreError404Title: File not found +CoreError404Text: The requested file was not found. Oh no... +CoreError430Title: Login failed +CoreError430Text: The email or password is incorrect. [Please try again](#data-action-login). +CoreError434Title: File not found +CoreError434Text: The requested file was not found. [You can create this page](#data-action-edit). +CoreError500Title: Server error +CoreError500Text: Something went wrong. [yellow error] +CzechDescription: Czech/Čeština with language 'cs'. +DanishDescription: Danish/Dansk with language 'da'. +DisqusDescription: Show Disqus comments on blog. +DraftDescription: Support for draft pages. +DraftStatusEmpty: No drafts found. +DutchDescription: Dutch/Nederlands (België) with language 'nl'. +EditDescription: Edit your website in a web browser. +EditLoginTitle: Welcome +EditLoginEmail: Email: +EditLoginPassword: Password: +EditLoginForgot: Forgot your password? +EditLoginSignup: Create user account? +EditLoginButton: Log in +EditSignupTitle: Create user account +EditSignupName: Name: +EditSignupEmail: Email: +EditSignupPassword: Password: +EditSignupConsent: I consent that this website stores my personal data. +EditSignupButton: Create +EditSignupStatusNone: Here you can create a new user account. +EditSignupStatusIncomplete: Please fill out all fields. +EditSignupStatusInvalid: Please enter a valid email. +EditSignupStatusWeak: Please enter a different password. +EditSignupStatusShort: Please enter a longer password. +EditSignupStatusNext: User account will be created, please check your emails. +EditForgotTitle: Forgot your password +EditForgotEmail: Email: +EditForgotStatusNone: No problem, you can create a new password. +EditForgotStatusInvalid: Please enter a valid email. +EditForgotStatusNext: User account will be recovered, please check your emails. +EditRecoverTitle: Forgot your password +EditRecoverPassword: Password: +EditRecoverStatusPassword: Please enter a new password. +EditRecoverStatusWeak: Please enter a different password. +EditRecoverStatusShort: Please enter a longer password. +EditRecoverStatusDone: User account recovered. Thank you! +EditQuitTitle: Delete user account +EditQuitStatusNone: Please enter your name to confirm. +EditQuitStatusMismatch: Please enter a different name. +EditQuitStatusNext: User account will be deleted, please check your emails. +EditConfirmSubject: Confirm user account +EditConfirmMessage: Hi @usershort,\n\nplease confirm your user account. Click the following link. +EditConfirmTitle: User account +EditConfirmStatusDone: User account confirmed and waiting for approval. Thank you! +EditApproveSubject: Approve user account +EditApproveMessage: Hi @usershort,\n\nplease approve a new user account for @useraccount. Click the following link. +EditApproveTitle: User account +EditApproveStatusDone: User account approved. Thank you! +EditReactivateSubject: Reactivate user account +EditReactivateMessage: Hi @usershort,\n\nplease reactivate your user account. There were too many failed login attempts. Click the following link. +EditReactivateTitle: User account +EditReactivateStatusDone: User account reactivated. Thank you! +EditVerifySubject: Confirm user account +EditVerifyMessage: Hi @usershort,\n\nplease confirm a new email for your user account. Click the following link. +EditVerifyTitle: User account +EditVerifyStatusDone: User account confirmed. Thank you! +EditChangeSubject: Change user account +EditChangeMessage: Hi @usershort,\n\nplease confirm that you want to change your user account. Click the following link. +EditChangeTitle: User account +EditChangeStatusDone: User account changed. Thank you! +EditRemoveSubject: Delete user account +EditRemoveMessage: Hi @usershort,\n\nplease confirm that you want to delete your user account. Click the following link. +EditRemoveTitle: User account +EditRemoveStatusDone: User account deleted. Thank you! +EditRecoverSubject: Recover user account +EditRecoverMessage: Hi @usershort,\n\nplease confirm that you forgot your password. Click the following link. +EditWelcomeSubject: Welcome +EditWelcomeMessage: Hi @usershort,\n\nyour user account has been created. Have fun editing the website. +EditGoodbyeSubject: Goodbye +EditGoodbyeMessage: Hi @usershort,\n\nyour user account has been deleted. Take care. +EditAccountTitle: User settings +EditAccountInformation: You can delete your user account anytime. +EditAccountMore: Read more… +EditAccountStatusNone: Here you can change your user account. +EditAccountStatusInvalid: Please enter a valid email. +EditAccountStatusTaken: Please enter a different email. +EditAccountStatusWeak: Please enter a different password. +EditAccountStatusShort: Please enter a longer password. +EditAccountStatusNext: User account will be changed, please check your emails. +EditSystemTitle: System settings +EditSystemSitename: Name of the website: +EditSystemAuthor: Name of the webmaster: +EditSystemEmail: Email of the webmaster: +EditSystemInformation: The webmaster can approve new user accounts. +EditSystemStatusNone: Here you can change system settings. +EditSystemStatusInvalid: Please enter a valid email. +EditUpdateTitle: Updates +EditUpdateStatusNone: Datenstrom Yellow is for people who make small websites. +EditUpdateStatusCheck: Checking for updates… +EditUpdateStatusUpdates: The following updates are available: +EditUpdateStatusOk: Your website is up to date. +EditOkButton: Ok +EditCancelButton: Cancel +EditChangeButton: Change +EditCreateButton: Create +EditEditButton: Save +EditDeleteButton: Delete +EditUpdateButton: Update +EditEdit: Edit page +EditCreate: + +EditDelete: - +EditKeyboardLabels: Ctrl+, Alt+, Shift+, ⌘, ⌥, ⇧ +EditToolbarFormat: Format +EditToolbarHeading: Heading +EditToolbarH1: Heading 1 +EditToolbarH2: Heading 2 +EditToolbarH3: Heading 3 +EditToolbarParagraph: Normal text +EditToolbarPre: Source code +EditToolbarNotice: Notice +EditToolbarQuote: Quote +EditToolbarBold: Bold +EditToolbarItalic: Italic +EditToolbarStrikethrough: Strikethrough +EditToolbarCode: Code +EditToolbarList: List +EditToolbarUl: • Unordered list +EditToolbarOl: 1. Ordered list +EditToolbarTl: ✓ Task list +EditToolbarLink: Link +EditToolbarFile: File +EditToolbarEmojiawesome: Emoji +EditToolbarFontawesome: Icon +EditToolbarStatus: Status +EditToolbarUndo: Undo +EditToolbarRedo: Redo +EditToolbarPreview: Preview +EditToolbarMarkdown: Markdown +EditToolbarHelp: Help +EditMailFooter: @sitename +EditDataGenerated: This page is generated automatically. +EditUploadProgress: [Uploading file…] +EditMenuSettings: Settings +EditMenuHelp: Help +EditMenuLogout: Logout +EditYellowUrl: https://datenstrom.se/yellow/ +EditYellowHelpUrl: https://datenstrom.se/yellow/help/ +EmojiawesomeDescription: Lots and lots of emoji. +EnglishDescription: English/English with language 'en'. +FeedDescription: Feed with recent changes. +FontawesomeDescription: Icons and symbols. +FrenchDescription: French/Français with language 'fr'. +GalleryDescription: Image gallery with popup. +GermanDescription: German/Deutsch with language 'de'. +GooglecalendarDescription: Embed Google calendar. +GooglemapDescription: Embed Google map. +HelpDescription: Help for your website. +HighlightDescription: Highlight source code. +HungarianDescription: Hungarian/Magyar with language 'hu'. +ImageDescription: Images and thumbnails. +ImageDefaultAlt: Image without description +InstagramDescription: Embed Instagram photos. +InstallTitle: Hello +InstallExtension: What do you want to make? +InstallExtensionWebsite: Website +InstallExtensionBlog: Blog +InstallExtensionWiki: Wiki +InstallHomeTitle: Home +InstallHomeTitleContent: Your website works! +InstallHomeText: [image photo.jpg Example rounded]\n\n[edit - You can edit this page]. The help gives you more information about how to create small web pages, blogs and wikis. [Learn more](https://datenstrom.se/yellow/help/). +InstallDefaultTitle: Page +InstallDefaultText: This is a new page. +InstallBlogTitle: Blog page +InstallBlogText: This is a new blog page. +InstallWikiTitle: Wiki page +InstallWikiText: This is a new wiki page. +InstallExampleImage: This is an example image +ItalianDescription: Italian/Italiano with language 'it'. +JapaneseDescription: Japanese/日本語 with language 'ja'. +MarkdownDescription: Text formatting for humans. +MetaDescription: Meta data for social media sites. +NorwegianDescription: Norwegian/Norsk Bokmål with language 'nb'. +ParisDescription: Paris is an elegant theme. +PolishDescription: Polish/Polski with language 'pl'. +PortugueseDescription: Portuguese/Português with language 'pt'. +PreviousnextDescription: Show links to previous/next page. +PreviousnextPagePrevious: ← Previous: @title +PreviousnextPageNext: Next: @title → +PublishDescription: Package and publish extensions. +RussianDescription: Russian/Русский with language 'ru'. +SearchDescription: Full-text search. +SearchResultsNone: Enter a search term. +SearchResultsEmpty: No results found. +SearchSpecialChanges: Recent changes +SearchButton: Search +SitemapDescription: Sitemap with all pages. +SliderDescription: Image gallery with slider. +SlovakDescription: Slovak/Slovenčina with language 'sk'. +SoundcloudDescription: Embed Soundcloud audio tracks. +SpanishDescription: Spanish/Español with language 'es'. +StockholmDescription: Stockholm is a clean theme. +SwedishDescription: Swedish/Svenska with language 'sv'. +TocDescription: Table of contents. +TrafficDescription: Create traffic analytics from web server log files. +TwitterDescription: Embed Twitter messages. +UpdateDescription: Keep your website up to date. +WikiDescription: Wiki for your website. +WikiModified: Last updated on +WikiTag: Tags: +WikiSpecialPages: All pages +WikiSpecialChanges: Recent changes +YoutubeDescription: Embed Youtube videos. diff --git a/system/extensions/french.php b/system/extensions/french.php new file mode 100644 index 0000000..7cd5750 --- /dev/null +++ b/system/extensions/french.php @@ -0,0 +1,23 @@ +<?php +// French extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/french + +class YellowFrench { + const VERSION = "0.8.24"; + public $yellow; // access to API + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + } + + // Handle update + public function onUpdate($action) { + $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); + if ($action=="install") { + $this->yellow->system->save($fileName, array("language" => "fr")); + } elseif ($action=="uninstall" && $this->yellow->system->get("language")=="fr") { + $language = reset(array_diff($this->yellow->system->getValues("language"), array("fr"))); + $this->yellow->system->save($fileName, array("language" => $language)); + } + } +} diff --git a/system/extensions/french.txt b/system/extensions/french.txt new file mode 100644 index 0000000..78a591a --- /dev/null +++ b/system/extensions/french.txt @@ -0,0 +1,253 @@ +# French extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/french + +Language: fr +LanguageLocale: fr_FR +LanguageDescription: Français +LanguageTranslator: Juh Nibreh +BerlinDescription: Berlin is a theme inspired by Dieter Rams. +BlogDescription: Blog pour votre site web. +BlogBy: par +BlogTag: Tags: +BlogMore: Lire la suite… +BreadcrumbDescription: Breadcrumb navigation. +BundleDescription: Bundle website files. +ChineseDescription: Chinese/简体中文 with language 'zh-CN'. +CommandDescription: Ligne de commande du site web. +ContactDescription: Email contact page. +ContactName: Nom: +ContactEmail: Email: +ContactMessage: Message: +ContactConsent: Je consens à ce que ce site stocke mon message. +ContactButton: Envoyer le message +ContactMailSpam: [Spam] +ContactMailFooter: Cet email a été envoyé via @sitename - @title +ContactStatusNone: Dites bonjour ! Vos commentaires sont les bienvenus. +ContactStatusIncomplete: S'il vous plaît, veuillez remplir tous les champs. +ContactStatusInvalid: S'il vous plaît, veuillez entrer une adresse email valide. +ContactStatusReview: S'il vous plaît, veuillez supprimer les liens du message. +ContactStatusDone: Votre message a bien été envoyé. Merci ! +ContactStatusError: Votre message n'a pas pu être envoyé, réessayez plus tard s'il vous plaît ! +CoreDescription: Fonctionnalité principale du site web. +CoreDatePast: aujourd'hui, hier, il y a @x jours, il ya 1 mois, il y a @x mois, il y a 1 an, il y a @x ans, le @x +CoreDateFuture: bientôt, demain, dans @x jours, en 1 mois, dans @x mois, en 1 an, dans @x ans, le @x +CoreDateMonths: janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre +CoreDateWeekdays: lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche +CoreDateWeekstart: lundi +CoreDateFormatShort: F Y +CoreDateFormatMedium: d/m/Y +CoreDateFormatLong: d/m/Y H:i +CoreTimeFormatShort: H:i +CoreTimeFormatMedium: H:i:s +CoreTimeFormatLong: H:i:s T +CorePaginationPrevious: ← Précédent +CorePaginationNext: Suivant → +CoreError404Title: Fichier non trouvé +CoreError404Text: Le fichier demandé n'a pas été trouvé. Oh non... +CoreError430Title: La connexion a échoué +CoreError430Text: L'email ou le mot de passe est incorrect. [Veuillez réessayer](#data-action-login). +CoreError434Title: Fichier non trouvé +CoreError434Text: Le fichier demandé n'a pas été trouvé. [Vous pouvez créer cette page](#data-action-edit). +CoreError500Title: Erreur du serveur +CoreError500Text: Une erreur s'est produite. [yellow error] +CzechDescription: Czech/Čeština with language 'cs'. +DanishDescription: Danish/Dansk with language 'da'. +DisqusDescription: Show Disqus comments on blog. +DraftDescription: Support for draft pages. +DraftStatusEmpty: Aucun brouillon trouvé. +DutchDescription: Dutch/Nederlands (België) with language 'nl'. +EditDescription: Modifiez votre site web dans un navigateur web. +EditLoginTitle: Bienvenue +EditLoginEmail: Email: +EditLoginPassword: Mot de passe: +EditLoginForgot: Mot de passe oublié ? +EditLoginSignup: Créer un compte utilisateur ? +EditLoginButton: Se connecter +EditSignupTitle: Créer un compte utilisateur +EditSignupName: Nom: +EditSignupEmail: Email: +EditSignupPassword: Mot de passe: +EditSignupConsent: Je consens à ce que ce site web stocke mes données personnelles. +EditSignupButton: Créer +EditSignupStatusNone: Ici, vous pouvez créer un nouveau compte utilisateur. +EditSignupStatusIncomplete: Veuillez remplir tous les champs. +EditSignupStatusInvalid: S'il vous plaît, veuillez entrer une adresse email valide. +EditSignupStatusWeak: S'il vous plaît, choisissez un mot de passe différent. +EditSignupStatusShort: S'il vous plaît, choisissez un mot de passe plus long. +EditSignupStatusNext: Votre compte a été créé, vérifiez vos emails. +EditForgotTitle: Mot de passe oublié +EditForgotEmail: Email: +EditForgotStatusNone: Pas de problème, vous pouvez créer un nouveau mot de passe. +EditForgotStatusInvalid: S'il vous plaît, veuillez entrer une adresse email valide. +EditForgotStatusNext: Votre compte est à nouveau disponible, vérifiez vos emails. +EditRecoverTitle: Mot de passe oublié +EditRecoverPassword: Mot de passe: +EditRecoverStatusPassword: S'il vous plaît, choisissez un nouveau mot de passe. +EditRecoverStatusWeak: S'il vous plaît, choisissez un mot de passe différent. +EditRecoverStatusShort: S'il vous plaît, choisissez un mot de passe plus long. +EditRecoverStatusDone: Compte utilisateur restauré. Merci ! +EditQuitTitle: Supprimer le compte d'utilisateur +EditQuitStatusNone: S'il vous plaît entrez votre nom pour confirmer. +EditQuitStatusMismatch: S'il vous plaît entrer un nom différent. +EditQuitStatusNext: Votre compte sera supprimé, vérifier vos emails. +EditConfirmSubject: Confirmation d'un compte utilisateur +EditConfirmMessage: Bonjour @usershort,\n\nveuillez confirmer votre compte utilisateur. Cliquez sur le lien suivant. +EditConfirmTitle: Compte d'utilisateur +EditConfirmStatusDone: Votre compte utilisateur est confirmé et en attente d'approbation. Merci ! +EditApproveSubject: Approuver un nouvel utilisateur +EditApproveMessage: Bonjour @usershort,\n\nveuillez approuver la création d'un nouveau compte utilisateur pour @useraccount. Cliquez sur le lien suivant. +EditApproveTitle: Compte d'utilisateur +EditApproveStatusDone: Compte utilisateur approuvé. Merci ! +EditReactivateSubject: Réactivation d'un compte utilisateur +EditReactivateMessage: Bonjour @usershort,\n\nveuillez réactivér votre compte utilisateur. Il y a eu trop de tentatives de connexion échouées. Cliquez sur le lien suivant. +EditReactivateTitle: Compte d'utilisateur +EditReactivateStatusDone: Compte d'utilisateur réactivé. Merci ! +EditVerifySubject: Confirmation d'un compte utilisateur +EditVerifyMessage: Bonjour @usershort,\n\nveuillez confirmer une nouvelle adresse email pour votre compte utilisateur. Cliquez sur le lien suivant. +EditVerifyTitle: Compte d'utilisateur +EditVerifyStatusDone: Votre compte utilisateur est confirmé. Merci ! +EditChangeSubject: Changement d'un compte utilisateur +EditChangeMessage: Bonjour @usershort,\n\nveuillez confirmer que vous souhaitez modifier votre compte utilisateur. Cliquez sur le lien suivant. +EditChangeTitle: Compte d'utilisateur +EditChangeStatusDone: Compte utilisateur changé. Merci ! +EditRemoveSubject: Supprimer le compte d'utilisateur +EditRemoveMessage: Bonjour @usershort,\n\nveuillez confirmer que vous souhaitez supprimer votre compte d'utilisateur. Cliquez sur le lien suivant. +EditRemoveTitle: Compte d'utilisateur +EditRemoveStatusDone: Compte d'utilisateur supprimé. Merci ! +EditRecoverSubject: Restauration d'un compte utilisateur +EditRecoverMessage: Bonjour @usershort,\n\nveuillez confirmer que vous avez oublié votre mot de passe. Cliquez sur le lien suivant. +EditWelcomeSubject: Bienvenue +EditWelcomeMessage: Bonjour @usershort,\n\nvotre compte utilisateur a bien été créé. Amusez-vous bien en éditant le site web. +EditGoodbyeSubject: Au revoir +EditGoodbyeMessage: Bonjour @usershort,\n\nvotre compte utilisateur a bien été supprimé. Prends soin. +EditAccountTitle: Paramètres utilisateur +EditAccountInformation: Vous pouvez supprimer votre compte d'utilisateur. +EditAccountMore: Lire la suite… +EditAccountStatusNone: Ici, vous pouvez changer votre compte utilisateur. +EditAccountStatusInvalid: S'il vous plaît, veuillez entrer une adresse email valide. +EditAccountStatusTaken: S'il vous plaît, veuillez entrer une adresse email différent. +EditAccountStatusWeak: S'il vous plaît, choisissez un mot de passe différent. +EditAccountStatusShort: S'il vous plaît, choisissez un mot de passe plus long. +EditAccountStatusNext: Votre compte a été changé, vérifiez vos emails. +EditSystemTitle: Paramètres du système +EditSystemSitename: Nom du site: +EditSystemAuthor: Nom du webmaster: +EditSystemEmail: Email du webmaster: +EditSystemInformation: Le webmaster peut approuver les nouveaux comptes d'utilisateurs. +EditSystemStatusNone: Ici, vous pouvez changer les paramètres du système. +EditSystemStatusInvalid: S'il vous plaît, veuillez entrer une adresse email valide. +EditUpdateTitle: Mises à jour +EditUpdateStatusNone: Datenstrom Yellow est fait pour les gens qui font de petits sites web. +EditUpdateStatusCheck: Vérification des mises à jour… +EditUpdateStatusUpdates: Les mises à jour suivantes sont disponibles: +EditUpdateStatusOk: Votre site web est à jour. +EditOkButton: Ok +EditCancelButton: Annuler +EditChangeButton: Modifier +EditCreateButton: Créer +EditEditButton: Sauvegarder +EditDeleteButton: Supprimer +EditUpdateButton: Mettre à jour +EditEdit: Éditer page +EditCreate: + +EditDelete: - +EditKeyboardLabels: Ctrl+, Alt+, Maj+, ⌘, ⌥, ⇧ +EditToolbarFormat: Format +EditToolbarHeading: Titre +EditToolbarH1: Titre 1 +EditToolbarH2: Titre 2 +EditToolbarH3: Titre 3 +EditToolbarParagraph: Texte normal +EditToolbarPre: Code source +EditToolbarNotice: Avis +EditToolbarQuote: Citation +EditToolbarBold: Gras +EditToolbarItalic: Italique +EditToolbarStrikethrough: Barré +EditToolbarCode: Code +EditToolbarList: Liste +EditToolbarUl: • Liste à puces +EditToolbarOl: 1. Liste numérotée +EditToolbarTl: ✓ Liste des tâches +EditToolbarLink: Lien +EditToolbarFile: Fichier +EditToolbarEmojiawesome: Emoji +EditToolbarFontawesome: Icone +EditToolbarStatus: Statut +EditToolbarUndo: Annuler +EditToolbarRedo: Refaire +EditToolbarPreview: Aperçu +EditToolbarMarkdown: Markdown +EditToolbarHelp: Aide +EditMailFooter: @sitename +EditDataGenerated: Cette page est créée automatiquement. +EditUploadProgress: [Téléchargement du fichier…] +EditMenuSettings: Paramètres +EditMenuHelp: Aide +EditMenuLogout: Déconnexion +EditYellowUrl: https://datenstrom.se/yellow/ +EditYellowHelpUrl: https://datenstrom.se/yellow/help/ +EmojiawesomeDescription: Lots and lots of emoji. +EnglishDescription: English/English with language 'en'. +FeedDescription: Feed with recent changes. +FontawesomeDescription: Icons and symbols. +FrenchDescription: French/Français with language 'fr'. +GalleryDescription: Image gallery with popup. +GermanDescription: German/Deutsch with language 'de'. +GooglecalendarDescription: Embed Google calendar. +GooglemapDescription: Embed Google map. +HelpDescription: Help for your website. +HighlightDescription: Highlight source code. +HungarianDescription: Hungarian/Magyar with language 'hu'. +ImageDescription: Images and thumbnails. +ImageDefaultAlt: Image sans description +InstagramDescription: Embed Instagram photos. +InstallTitle: Bonjour +InstallExtension: Que voulez-vous faire ? +InstallExtensionWebsite: Site web +InstallExtensionBlog: Blog +InstallExtensionWiki: Wiki +InstallHomeTitle: Accueil +InstallHomeTitleContent: Votre site web fonctionne ! +InstallHomeText: [image photo.jpg Exemple rounded]\n\n[edit - Vous pouvez modifier cette page]. L'aide vous donne plus d'informations sur les petits sites web, blogs et wikis. [Apprenez-en plus](https://datenstrom.se/yellow/help/). +InstallDefaultTitle: Page +InstallDefaultText: Ceci est une nouvelle page. +InstallBlogTitle: Page de blog +InstallBlogText: Ceci est une nouvelle page de blog. +InstallWikiTitle: Page de wiki +InstallWikiText: Ceci est une nouvelle page de wiki. +InstallExampleImage: Ceci est un exemple d'image +ItalianDescription: Italian/Italiano with language 'it'. +JapaneseDescription: Japanese/日本語 with language 'ja'. +MarkdownDescription: Formatage de texte pour les humains. +MetaDescription: Meta data for social media sites. +NorwegianDescription: Norwegian/Norsk Bokmål with language 'nb'. +ParisDescription: Paris is an elegant theme. +PolishDescription: Polish/Polski with language 'pl'. +PortugueseDescription: Portuguese/Português with language 'pt'. +PreviousnextDescription: Show links to previous/next page. +PreviousnextPagePrevious: ← Précédent: @title +PreviousnextPageNext: Suivant: @title → +PublishDescription: Package and publish extensions. +RussianDescription: Russian/Русский with language 'ru'. +SearchDescription: Full-text search. +SearchResultsNone: Entrez un mot dans le champ de recherche. +SearchResultsEmpty: Pas de résultats. +SearchSpecialChanges: Changements récents +SearchButton: Rechercher +SitemapDescription: Sitemap with all pages. +SliderDescription: Image gallery with slider. +SlovakDescription: Slovak/Slovenčina with language 'sk'. +SoundcloudDescription: Embed Soundcloud audio tracks. +SpanishDescription: Spanish/Español with language 'es'. +StockholmDescription: Stockholm is a clean theme. +SwedishDescription: Swedish/Svenska with language 'sv'. +TocDescription: Table of contents. +TrafficDescription: Create traffic analytics from web server log files. +TwitterDescription: Embed Twitter messages. +UpdateDescription: Gardez votre site web à jour. +WikiDescription: Wiki pour votre site web. +WikiModified: Dernière mise à jour le +WikiTag: Tags: +WikiSpecialPages: Toutes les pages +WikiSpecialChanges: Changements récents +YoutubeDescription: Embed Youtube videos. diff --git a/system/extensions/german.php b/system/extensions/german.php new file mode 100644 index 0000000..262e060 --- /dev/null +++ b/system/extensions/german.php @@ -0,0 +1,23 @@ +<?php +// German extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/german + +class YellowGerman { + const VERSION = "0.8.24"; + public $yellow; // access to API + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + } + + // Handle update + public function onUpdate($action) { + $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); + if ($action=="install") { + $this->yellow->system->save($fileName, array("language" => "de")); + } elseif ($action=="uninstall" && $this->yellow->system->get("language")=="de") { + $language = reset(array_diff($this->yellow->system->getValues("language"), array("de"))); + $this->yellow->system->save($fileName, array("language" => $language)); + } + } +} diff --git a/system/extensions/german.txt b/system/extensions/german.txt new file mode 100644 index 0000000..1f7f59d --- /dev/null +++ b/system/extensions/german.txt @@ -0,0 +1,253 @@ +# German extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/german + +Language: de +LanguageLocale: de_DE +LanguageDescription: Deutsch +LanguageTranslator: David Fehrmann +BerlinDescription: Berlin ist ein Thema inspiriert von Dieter Rams. +BlogDescription: Blog für deine Webseite. +BlogBy: von +BlogTag: Tags: +BlogMore: Lies mehr… +BreadcrumbDescription: Brotkrümel-Navigation. +BundleDescription: Webseiten-Dateien bündeln. +ChineseDescription: Chinesisch/简体中文 mit der Sprache "zh-CN". +CommandDescription: Befehlszeile der Webseite. +ContactDescription: E-Mail-Kontaktseite. +ContactName: Name: +ContactEmail: E-Mail: +ContactMessage: Nachricht: +ContactConsent: Ich stimme zu, dass diese Webseite meine Nachricht speichert. +ContactButton: Nachricht absenden +ContactMailSpam: [Spam] +ContactMailFooter: Diese E-Mail wurde über @sitename verschickt - @title +ContactStatusNone: Sag Hallo! Dein Feedback ist sehr willkommen. +ContactStatusIncomplete: Bitte alle Felder ausfüllen. +ContactStatusInvalid: Bitte eine gültige E-Mail angeben. +ContactStatusReview: Bitte entferne Links aus der Nachricht. +ContactStatusDone: Nachricht wurde versandt. Vielen Dank! +ContactStatusError: Nachricht konnte nicht versandt werden, versuche es später erneut! +CoreDescription: Kernfunktionalität der Webseite. +CoreDatePast: heute, gestern, vor @x Tagen, vor 1 Monat, vor @x Monaten, vor 1 Jahr, vor @x Jahren, am @x +CoreDateFuture: bald, morgen, in @x Tagen, in 1 Monat, in @x Monaten, in 1 Jahr, in @x Jahren, am @x +CoreDateMonths: Januar, Februar, März, April, Mai, Juni, Juli, August, September, Oktober, November, Dezember +CoreDateWeekdays: Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag, Sonntag +CoreDateWeekstart: Montag +CoreDateFormatShort: F Y +CoreDateFormatMedium: d.m.Y +CoreDateFormatLong: d.m.Y H:i +CoreTimeFormatShort: H:i +CoreTimeFormatMedium: H:i:s +CoreTimeFormatLong: H:i:s T +CorePaginationPrevious: ← Zurück +CorePaginationNext: Weiter → +CoreError404Title: Datei nicht gefunden +CoreError404Text: Die angeforderte Datei wurde nicht gefunden. Oh nein... +CoreError430Title: Anmeldung fehlgeschlagen +CoreError430Text: E-Mail oder Kennwort ist falsch. [Bitte erneut versuchen](#data-action-login). +CoreError434Title: Datei nicht gefunden +CoreError434Text: Die angeforderte Datei wurde nicht gefunden. [Du kannst diese Seite erstellen](#data-action-edit). +CoreError500Title: Serverfehler +CoreError500Text: Etwas ist schief gelaufen. [yellow error] +CzechDescription: Tschechisch/Čeština mit der Sprache "cs". +DanishDescription: Dänisch/Dansk mit der Sprache "da". +DisqusDescription: Disqus-Kommentare im Blog anzeigen. +DraftDescription: Unterstützung für Entwurfsseiten. +DraftStatusEmpty: Keine Entwürfe gefunden. +DutchDescription: Niederländisch/Nederlands (België) mit der Sprache "nl". +EditDescription: Webseite im Webbrowser bearbeiten. +EditLoginTitle: Willkommen +EditLoginEmail: E-Mail: +EditLoginPassword: Kennwort: +EditLoginForgot: Kennwort vergessen? +EditLoginSignup: Benutzerkonto erstellen? +EditLoginButton: Anmelden +EditSignupTitle: Benutzerkonto erstellen +EditSignupName: Name: +EditSignupEmail: E-Mail: +EditSignupPassword: Kennwort: +EditSignupConsent: Ich stimme zu, dass diese Webseite meine persönlichen Daten speichert. +EditSignupButton: Erstellen +EditSignupStatusNone: Hier kannst du ein neues Benutzerkonto erstellen. +EditSignupStatusIncomplete: Bitte alle Felder ausfüllen. +EditSignupStatusInvalid: Bitte eine gültige E-Mail angeben. +EditSignupStatusWeak: Bitte ein anderes Kennwort angeben. +EditSignupStatusShort: Bitte ein längeres Kennwort angeben. +EditSignupStatusNext: Benutzerkonto wird erstellt, bitte überprüfe deine E-Mails. +EditForgotTitle: Kennwort vergessen +EditForgotEmail: E-Mail: +EditForgotStatusNone: Kein Problem, du kannst ein neues Kennwort erstellen. +EditForgotStatusInvalid: Bitte eine gültige E-Mail angeben. +EditForgotStatusNext: Benutzerkonto wird wiederhergestellt, bitte überprüfe deine E-Mails. +EditRecoverTitle: Kennwort vergessen +EditRecoverPassword: Kennwort: +EditRecoverStatusPassword: Bitte ein neues Kennwort angeben. +EditRecoverStatusWeak: Bitte ein anderes Kennwort angeben. +EditRecoverStatusShort: Bitte ein längeres Kennwort angeben. +EditRecoverStatusDone: Benutzerkonto wurde wiederhergestellt. Vielen Dank! +EditQuitTitle: Benutzerkonto löschen +EditQuitStatusNone: Bitte gib deinen Namen zur Bestätigung ein. +EditQuitStatusMismatch: Bitte gib einen anderen Namen ein. +EditQuitStatusNext: Benutzerkonto wird gelöscht, bitte überprüfe deine E-Mails. +EditConfirmSubject: Benutzerkonto bestätigen +EditConfirmMessage: Hallo @usershort,\n\nbitte bestätige dein Benutzerkonto. Klicke auf den folgenden Link. +EditConfirmTitle: Benutzerkonto +EditConfirmStatusDone: Benutzerkonto wurde bestätigt und wartet auf Genehmigung. Vielen Dank! +EditApproveSubject: Benutzerkonto genehmigen +EditApproveMessage: Hallo @usershort,\n\nbitte genehmige ein neues Benutzerkonto für @useraccount. Klicke auf den folgenden Link. +EditApproveTitle: Benutzerkonto +EditApproveStatusDone: Benutzerkonto wurde genehmigt. Vielen Dank! +EditReactivateSubject: Benutzerkonto reaktivieren +EditReactivateMessage: Hallo @usershort,\n\nbitte reaktiviere dein Benutzerkonto. Es gab zu viele fehlgeschlagene Anmeldeversuche. Klicke auf den folgenden Link. +EditReactivateTitle: Benutzerkonto +EditReactivateStatusDone: Benutzerkonto wurde reaktiviert. Vielen Dank! +EditVerifySubject: Benutzerkonto bestätigen +EditVerifyMessage: Hallo @usershort,\n\nbitte bestätige eine neue E-Mail für dein Benutzerkonto. Klicke auf den folgenden Link. +EditVerifyTitle: Benutzerkonto +EditVerifyStatusDone: Benutzerkonto wurde bestätigt. Vielen Dank! +EditChangeSubject: Benutzerkonto ändern +EditChangeMessage: Hallo @usershort,\n\nbitte bestätige, dass du dein Benutzerkonto ändern möchtest. Klicke auf den folgenden Link. +EditChangeTitle: Benutzerkonto +EditChangeStatusDone: Benutzerkonto wurde geändert. Vielen Dank! +EditRemoveSubject: Benutzerkonto löschen +EditRemoveMessage: Hallo @usershort,\n\nbitte bestätige, dass du dein Benutzerkonto löschen möchtest. Klicke auf den folgenden Link. +EditRemoveTitle: Benutzerkonto +EditRemoveStatusDone: Benutzerkonto wurde gelöscht. Vielen Dank! +EditRecoverSubject: Benutzerkonto wiederherstellen +EditRecoverMessage: Hallo @usershort,\n\nbitte bestätige, dass du dein Kennwort vergessen hast. Klicke auf den folgenden Link. +EditWelcomeSubject: Willkommen +EditWelcomeMessage: Hallo @usershort,\n\ndein Benutzerkonto wurde erstellt. Viel Spass beim Bearbeiten der Webseite. +EditGoodbyeSubject: Auf Wiedersehen +EditGoodbyeMessage: Hallo @usershort,\n\ndein Benutzerkonto wurde gelöscht. Mach's gut. +EditAccountTitle: Benutzereinstellungen +EditAccountInformation: Du kannst dein Benutzerkonto jederzeit löschen. +EditAccountMore: Lies mehr… +EditAccountStatusNone: Hier kannst du dein Benutzerkonto ändern. +EditAccountStatusInvalid: Bitte eine gültige E-Mail angeben. +EditAccountStatusTaken: Bitte eine andere E-Mail angeben. +EditAccountStatusWeak: Bitte ein anderes Kennwort angeben. +EditAccountStatusShort: Bitte ein längeres Kennwort angeben. +EditAccountStatusNext: Benutzerkonto wird geändert, bitte überprüfe deine E-Mails. +EditSystemTitle: Systemeinstellungen +EditSystemSitename: Name der Webseite: +EditSystemAuthor: Name des Webmasters: +EditSystemEmail: E-Mail des Webmasters: +EditSystemInformation: Der Webmaster kann neue Benutzerkonten genehmigen. +EditSystemStatusNone: Hier kannst du Systemeinstellungen ändern. +EditSystemStatusInvalid: Bitte eine gültige E-Mail angeben. +EditUpdateTitle: Aktualisierungen +EditUpdateStatusNone: Datenstrom Yellow ist für Menschen die kleine Webseiten machen. +EditUpdateStatusCheck: Nach Aktualisierung suchen… +EditUpdateStatusUpdates: Die folgenden Aktualisierungen sind verfügbar: +EditUpdateStatusOk: Die Webseite ist auf dem neusten Stand. +EditOkButton: Ok +EditCancelButton: Abbruch +EditChangeButton: Ändern +EditCreateButton: Erzeugen +EditEditButton: Speichern +EditDeleteButton: Löschen +EditUpdateButton: Aktualisieren +EditEdit: Seite bearbeiten +EditCreate: + +EditDelete: - +EditKeyboardLabels: Strg+, Alt+, Umschalt+, ⌘, ⌥, ⇧ +EditToolbarFormat: Format +EditToolbarHeading: Überschrift +EditToolbarH1: Überschrift 1 +EditToolbarH2: Überschrift 2 +EditToolbarH3: Überschrift 3 +EditToolbarParagraph: Normaler Text +EditToolbarPre: Quellcode +EditToolbarNotice: Hinweis +EditToolbarQuote: Zitat +EditToolbarBold: Fettschrift +EditToolbarItalic: Kursiv +EditToolbarStrikethrough: Durchgestrichen +EditToolbarCode: Code +EditToolbarList: Liste +EditToolbarUl: • Unsortierte Liste +EditToolbarOl: 1. Sortierte Liste +EditToolbarTl: ✓ Aufgabenliste +EditToolbarLink: Link +EditToolbarFile: Datei +EditToolbarEmojiawesome: Emoji +EditToolbarFontawesome: Symbol +EditToolbarStatus: Status +EditToolbarUndo: Rückgängig +EditToolbarRedo: Wiederholen +EditToolbarPreview: Vorschau +EditToolbarMarkdown: Markdown +EditToolbarHelp: Hilfe +EditMailFooter: @sitename +EditDataGenerated: Diese Seite ist automatisch erstellt. +EditUploadProgress: [Datei hochladen…] +EditMenuSettings: Einstellungen +EditMenuHelp: Hilfe +EditMenuLogout: Abmelden +EditYellowUrl: https://datenstrom.se/de/yellow/ +EditYellowHelpUrl: https://datenstrom.se/de/yellow/help/ +EmojiawesomeDescription: Jede Menge Emoji. +EnglishDescription: Englisch/English mit der Sprache "en". +FeedDescription: Feed mit letzten Änderungen. +FontawesomeDescription: Icons und Symbole. +FrenchDescription: Französisch/Français mit der Sprache "fr". +GalleryDescription: Bildergalerie mit Popup. +GermanDescription: Deutsch/Deutsch mit der Sprache "de". +GooglecalendarDescription: Google-Kalender einbinden. +GooglemapDescription: Google-Karten einbinden. +HelpDescription: Hilfe für deine Webseite. +HighlightDescription: Quellcode hervorheben. +HungarianDescription: Ungarisch/Magyar mit der Sprache "hu". +ImageDescription: Bilder in unterschiedlichen Größen. +ImageDefaultAlt: Bild ohne Beschreibung +InstagramDescription: Instagram-Fotos einbinden. +InstallTitle: Hallo +InstallExtension: Was willst du machen? +InstallExtensionWebsite: Webseite +InstallExtensionBlog: Blog +InstallExtensionWiki: Wiki +InstallHomeTitle: Startseite +InstallHomeTitleContent: Deine Webseite funktioniert! +InstallHomeText: [image photo.jpg Beispiel rounded]\n\n[edit - Du kannst diese Seite bearbeiten]. Die Hilfe zeigt dir wie man kleine Webseiten, Blogs und Wikis erstellt. [Weitere Informationen](https://datenstrom.se/de/yellow/help/). +InstallDefaultTitle: Seite +InstallDefaultText: Dies ist eine neue Seite. +InstallBlogTitle: Blogseite +InstallBlogText: Dies ist eine neue Blogseite. +InstallWikiTitle: Wikiseite +InstallWikiText: Dies ist eine neue Wikiseite. +InstallExampleImage: Das ist ein Beispielbild +ItalianDescription: Italienisch/Italiano mit der Sprache "it". +JapaneseDescription: Japanisch/日本語 mit der Sprache "ja". +MarkdownDescription: Textformatierung für Menschen. +MetaDescription: Metadaten für soziale Medien. +NorwegianDescription: Norwegisch/Norsk Bokmål mit der Sprache "nb". +ParisDescription: Paris ist ein elegantes Thema. +PolishDescription: Polnisch/Polski mit der Sprache "pl". +PortugueseDescription: Portugiesisch/Português mit der Sprache "pt". +PreviousnextDescription: Links zu vorherigen/nächsten Seite anzeigen. +PreviousnextPagePrevious: ← Zurück: @title +PreviousnextPageNext: Weiter: @title → +PublishDescription: Erweiterungen verpacken und veröffentlichen. +RussianDescription: Russisch/Русский mit der Sprache "ru". +SearchDescription: Volltext-Suche. +SearchResultsNone: Bitte einen Suchbegriff eingeben. +SearchResultsEmpty: Keine Treffer für diese Suchanfrage. +SearchSpecialChanges: Letzte Änderungen +SearchButton: Suchen +SitemapDescription: Sitemap mit allen Seiten. +SliderDescription: Bildergalerie mit Schieber. +SlovakDescription: Slovak/Slovenčina with language 'sk'. +SoundcloudDescription: Soundcloud-Audio einbinden. +SpanishDescription: Spanish/Español with language 'es'. +StockholmDescription: Stockholm is a clean theme. +SwedishDescription: Swedish/Svenska with language 'sv'. +TocDescription: Inhaltsverzeichnis anzeigen. +TrafficDescription: Zugriffsanalysen aus Webserver-Logdateien erstellen. +TwitterDescription: Twitter-Nachrichten einbinden. +UpdateDescription: Webseite auf dem neusten Stand halten. +WikiDescription: Wiki für deine Webseite. +WikiModified: Zuletzt aktualisiert am +WikiTag: Tags: +WikiSpecialPages: Alle Seiten +WikiSpecialChanges: Letzte Änderungen +YoutubeDescription: Youtube-Videos einbinden. diff --git a/system/extensions/image.php b/system/extensions/image.php new file mode 100644 index 0000000..3b9a5c9 --- /dev/null +++ b/system/extensions/image.php @@ -0,0 +1,206 @@ +<?php +// Image extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/image + +class YellowImage { + const VERSION = "0.8.9"; + public $yellow; // access to API + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + $this->yellow->system->setDefault("imageUploadWidthMax", "1280"); + $this->yellow->system->setDefault("imageUploadHeightMax", "1280"); + $this->yellow->system->setDefault("imageUploadJpgQuality", "80"); + $this->yellow->system->setDefault("imageThumbnailLocation", "/media/thumbnails/"); + $this->yellow->system->setDefault("imageThumbnailDirectory", "media/thumbnails/"); + $this->yellow->system->setDefault("imageThumbnailJpgQuality", "80"); + $this->yellow->language->setDefault("imageDefaultAlt"); + } + + // Handle page content of shortcut + public function onParseContentShortcut($page, $name, $text, $type) { + $output = null; + if ($name=="image" && $type=="inline") { + list($name, $alt, $style, $width, $height) = $this->yellow->toolbox->getTextArguments($text); + if (!preg_match("/^\w+:/", $name)) { + if (empty($alt)) $alt = $this->yellow->language->getText("imageDefaultAlt"); + if (empty($width)) $width = "100%"; + if (empty($height)) $height = $width; + list($src, $width, $height) = $this->getImageInformation($this->yellow->system->get("coreImageDirectory").$name, $width, $height); + } else { + if (empty($alt)) $alt = $this->yellow->language->getText("imageDefaultAlt"); + $src = $this->yellow->lookup->normaliseUrl("", "", "", $name); + $width = $height = 0; + } + $output = "<img src=\"".htmlspecialchars($src)."\""; + if ($width && $height) $output .= " width=\"".htmlspecialchars($width)."\" height=\"".htmlspecialchars($height)."\""; + if (!empty($alt)) $output .= " alt=\"".htmlspecialchars($alt)."\" title=\"".htmlspecialchars($alt)."\""; + if (!empty($style)) $output .= " class=\"".htmlspecialchars($style)."\""; + $output .= " />"; + } + return $output; + } + + // Handle media file changes + public function onEditMediaFile($file, $action, $email) { + if ($action=="upload") { + $fileName = $file->fileName; + list($widthInput, $heightInput, $type) = $this->yellow->toolbox->detectImageInformation($fileName, $file->get("type")); + $widthMax = $this->yellow->system->get("imageUploadWidthMax"); + $heightMax = $this->yellow->system->get("imageUploadHeightMax"); + if (($widthInput>$widthMax || $heightInput>$heightMax) && ($type=="gif" || $type=="jpg" || $type=="png")) { + list($widthOutput, $heightOutput) = $this->getImageDimensionsFit($widthInput, $heightInput, $widthMax, $heightMax); + $image = $this->loadImage($fileName, $type); + $image = $this->resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput); + $image = $this->orientImage($image, $fileName, $type); + if (!$this->saveImage($image, $fileName, $type, $this->yellow->system->get("imageUploadJpgQuality"))) { + $file->error(500, "Can't write file '$fileName'!"); + } + } + } + } + + // Handle command + public function onCommand($command, $text) { + switch ($command) { + case "clean": $statusCode = $this->processCommandClean($command, $text); break; + default: $statusCode = 0; + } + return $statusCode; + } + + // Process command to clean thumbnails + public function processCommandClean($command, $text) { + $statusCode = 0; + if ($command=="clean" && $text=="all") { + $path = $this->yellow->system->get("imageThumbnailDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, false) as $entry) { + if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; + } + if ($statusCode==500) echo "ERROR cleaning thumbnails: Can't delete files in directory '$path'!\n"; + } + return $statusCode; + } + + // Return image information, create thumbnail on demand + public function getImageInformation($fileName, $widthOutput, $heightOutput) { + $fileNameShort = substru($fileName, strlenu($this->yellow->system->get("coreImageDirectory"))); + list($widthInput, $heightInput, $type) = $this->yellow->toolbox->detectImageInformation($fileName); + $widthOutput = $this->convertValueAndUnit($widthOutput, $widthInput); + $heightOutput = $this->convertValueAndUnit($heightOutput, $heightInput); + if (($widthInput==$widthOutput && $heightInput==$heightOutput) || $type=="svg") { + $src = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreImageLocation").$fileNameShort; + $width = $widthOutput; + $height = $heightOutput; + } else { + $fileNameThumb = ltrim(str_replace(array("/", "\\", "."), "-", dirname($fileNameShort)."/".pathinfo($fileName, PATHINFO_FILENAME)), "-"); + $fileNameThumb .= "-".$widthOutput."x".$heightOutput; + $fileNameThumb .= ".".pathinfo($fileName, PATHINFO_EXTENSION); + $fileNameOutput = $this->yellow->system->get("imageThumbnailDirectory").$fileNameThumb; + if ($this->isFileNotUpdated($fileName, $fileNameOutput)) { + $image = $this->loadImage($fileName, $type); + $image = $this->resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput); + $image = $this->orientImage($image, $fileName, $type); + if (is_file($fileNameOutput)) $this->yellow->toolbox->deleteFile($fileNameOutput); + if (!$this->saveImage($image, $fileNameOutput, $type, $this->yellow->system->get("imageThumbnailJpgQuality")) || + !$this->yellow->toolbox->modifyFile($fileNameOutput, $this->yellow->toolbox->getFileModified($fileName))) { + $this->yellow->page->error(500, "Can't write file '$fileNameOutput'!"); + } + } + $src = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("imageThumbnailLocation").$fileNameThumb; + list($width, $height) = $this->yellow->toolbox->detectImageInformation($fileNameOutput); + } + return array($src, $width, $height); + } + + // Return image dimensions that fit, scale proportional + public function getImageDimensionsFit($widthInput, $heightInput, $widthMax, $heightMax) { + $widthOutput = $widthMax; + $heightOutput = $widthMax * ($heightInput / $widthInput); + if ($heightOutput>$heightMax) { + $widthOutput = $widthOutput * ($heightMax / $heightOutput); + $heightOutput = $heightOutput * ($heightMax / $heightOutput); + } + return array(intval($widthOutput), intval($heightOutput)); + } + + // Load image from file + public function loadImage($fileName, $type) { + $image = false; + switch ($type) { + case "gif": $image = @imagecreatefromgif($fileName); break; + case "jpg": $image = @imagecreatefromjpeg($fileName); break; + case "png": $image = @imagecreatefrompng($fileName); break; + } + return $image; + } + + // Save image to file + public function saveImage($image, $fileName, $type, $quality) { + $ok = false; + switch ($type) { + case "gif": $ok = @imagegif($image, $fileName); break; + case "jpg": $ok = @imagejpeg($image, $fileName, $quality); break; + case "png": $ok = @imagepng($image, $fileName); break; + } + return $ok; + } + + // Create image from scratch + public function createImage($width, $height) { + $image = imagecreatetruecolor($width, $height); + imagealphablending($image, false); + imagesavealpha($image, true); + return $image; + } + + // Resize image + public function resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput) { + $widthFit = $widthInput * ($heightOutput / $heightInput); + $heightFit = $heightInput * ($widthOutput / $widthInput); + $widthDiff = abs($widthOutput - $widthFit); + $heightDiff = abs($heightOutput - $heightFit); + $imageOutput = $this->createImage($widthOutput, $heightOutput); + if ($heightFit>$heightOutput) { + imagecopyresampled($imageOutput, $image, 0, $heightDiff/-2, 0, 0, $widthOutput, $heightFit, $widthInput, $heightInput); + } else { + imagecopyresampled($imageOutput, $image, $widthDiff/-2, 0, 0, 0, $widthFit, $heightOutput, $widthInput, $heightInput); + } + return $imageOutput; + } + + // Orient image automatically + public function orientImage($image, $fileName, $type) { + if ($type=="jpg") { + $exif = @exif_read_data($fileName); + if ($exif && isset($exif["Orientation"])) { + switch ($exif["Orientation"]) { + case 2: imageflip($image, IMG_FLIP_HORIZONTAL); break; + case 3: $image = imagerotate($image, 180, 0); break; + case 4: imageflip($image, IMG_FLIP_VERTICAL); break; + case 5: $image = imagerotate($image, 90, 0); imageflip($image, IMG_FLIP_VERTICAL); break; + case 6: $image = imagerotate($image, -90, 0); break; + case 7: $image = imagerotate($image, 90, 0); imageflip($image, IMG_FLIP_HORIZONTAL); break; + case 8: $image = imagerotate($image, 90, 0); break; + } + } + } + return $image; + } + + // Return value according to unit + public function convertValueAndUnit($text, $valueBase) { + $value = $unit = ""; + if (preg_match("/([\d\.]+)(\S*)/", $text, $matches)) { + $value = $matches[1]; + $unit = $matches[2]; + if ($unit=="%") $value = $valueBase * $value / 100; + } + return intval($value); + } + + // Check if file needs to be updated + public function isFileNotUpdated($fileNameInput, $fileNameOutput) { + return $this->yellow->toolbox->getFileModified($fileNameInput)!=$this->yellow->toolbox->getFileModified($fileNameOutput); + } +} diff --git a/system/extensions/markdown.php b/system/extensions/markdown.php new file mode 100644 index 0000000..981e8a5 --- /dev/null +++ b/system/extensions/markdown.php @@ -0,0 +1,4033 @@ +<?php +// Markdown extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/markdown + +class YellowMarkdown { + const VERSION = "0.8.15"; + public $yellow; // access to API + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + } + + // Handle page content in raw format + public function onParseContentRaw($page, $text) { + $markdown = new YellowMarkdownParser($this->yellow, $page); + return $markdown->transform($text); + } +} + +// PHP Markdown Lib +// Copyright (c) 2004-2018 Michel Fortin +// <https://michelf.ca/> +// All rights reserved. +// +// Original Markdown +// Copyright (c) 2004-2006 John Gruber +// <https://daringfireball.net/> +// 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. +// +// * Neither the name "Markdown" nor the names of its contributors may +// be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// This software is provided by the copyright holders and contributors "as +// is" and any express or implied warranties, including, but not limited +// to, the implied warranties of merchantability and fitness for a +// particular purpose are disclaimed. In no event shall the copyright owner +// or contributors be liable for any direct, indirect, incidental, special, +// exemplary, or consequential damages (including, but not limited to, +// procurement of substitute goods or services; loss of use, data, or +// profits; or business interruption) however caused and on any theory of +// liability, whether in contract, strict liability, or tort (including +// negligence or otherwise) arising in any way out of the use of this +// software, even if advised of the possibility of such damage. + +class MarkdownParser { + /** + * Define the package version + * @var string + */ + const MARKDOWNLIB_VERSION = "1.9.0"; + + /** + * Simple function interface - Initialize the parser and return the result + * of its transform method. This will work fine for derived classes too. + * + * @api + * + * @param string $text + * @return string + */ + public static function defaultTransform($text) { + // Take parser class on which this function was called. + $parser_class = \get_called_class(); + + // Try to take parser from the static parser list + static $parser_list; + $parser =& $parser_list[$parser_class]; + + // Create the parser it not already set + if (!$parser) { + $parser = new $parser_class; + } + + // Transform text using parser. + return $parser->transform($text); + } + + /** + * Configuration variables + */ + + /** + * Change to ">" for HTML output. + * @var string + */ + public $empty_element_suffix = " />"; + + /** + * The width of indentation of the output markup + * @var int + */ + public $tab_width = 4; + + /** + * Change to `true` to disallow markup or entities. + * @var boolean + */ + public $no_markup = false; + public $no_entities = false; + + + /** + * Change to `true` to enable line breaks on \n without two trailling spaces + * @var boolean + */ + public $hard_wrap = false; + + /** + * Predefined URLs and titles for reference links and images. + * @var array + */ + public $predef_urls = array(); + public $predef_titles = array(); + + /** + * Optional filter function for URLs + * @var callable|null + */ + public $url_filter_func = null; + + /** + * Optional header id="" generation callback function. + * @var callable|null + */ + public $header_id_func = null; + + /** + * Optional function for converting code block content to HTML + * @var callable|null + */ + public $code_block_content_func = null; + + /** + * Optional function for converting code span content to HTML. + * @var callable|null + */ + public $code_span_content_func = null; + + /** + * Class attribute to toggle "enhanced ordered list" behaviour + * setting this to true will allow ordered lists to start from the index + * number that is defined first. + * + * For example: + * 2. List item two + * 3. List item three + * + * Becomes: + * <ol start="2"> + * <li>List item two</li> + * <li>List item three</li> + * </ol> + * + * @var bool + */ + public $enhanced_ordered_list = false; + + /** + * Parser implementation + */ + + /** + * Regex to match balanced [brackets]. + * Needed to insert a maximum bracked depth while converting to PHP. + * @var int + */ + protected $nested_brackets_depth = 6; + protected $nested_brackets_re; + + protected $nested_url_parenthesis_depth = 4; + protected $nested_url_parenthesis_re; + + /** + * Table of hash values for escaped characters: + * @var string + */ + protected $escape_chars = '\`*_{}[]()>#+-.!'; + protected $escape_chars_re; + + /** + * Constructor function. Initialize appropriate member variables. + * @return void + */ + public function __construct() { + $this->_initDetab(); + $this->prepareItalicsAndBold(); + + $this->nested_brackets_re = + str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). + str_repeat('\])*', $this->nested_brackets_depth); + + $this->nested_url_parenthesis_re = + str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). + str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); + + $this->escape_chars_re = '['.preg_quote($this->escape_chars).']'; + + // Sort document, block, and span gamut in ascendent priority order. + asort($this->document_gamut); + asort($this->block_gamut); + asort($this->span_gamut); + } + + + /** + * Internal hashes used during transformation. + * @var array + */ + protected $urls = array(); + protected $titles = array(); + protected $html_hashes = array(); + + /** + * Status flag to avoid invalid nesting. + * @var boolean + */ + protected $in_anchor = false; + + /** + * Status flag to avoid invalid nesting. + * @var boolean + */ + protected $in_emphasis_processing = false; + + /** + * Called before the transformation process starts to setup parser states. + * @return void + */ + protected function setup() { + // Clear global hashes. + $this->urls = $this->predef_urls; + $this->titles = $this->predef_titles; + $this->html_hashes = array(); + $this->in_anchor = false; + $this->in_emphasis_processing = false; + } + + /** + * Called after the transformation process to clear any variable which may + * be taking up memory unnecessarly. + * @return void + */ + protected function teardown() { + $this->urls = array(); + $this->titles = array(); + $this->html_hashes = array(); + } + + /** + * Main function. Performs some preprocessing on the input text and pass + * it through the document gamut. + * + * @api + * + * @param string $text + * @return string + */ + public function transform($text) { + $this->setup(); + + # Remove UTF-8 BOM and marker character in input, if present. + $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text); + + # Standardize line endings: + # DOS to Unix and Mac to Unix + $text = preg_replace('{\r\n?}', "\n", $text); + + # Make sure $text ends with a couple of newlines: + $text .= "\n\n"; + + # Convert all tabs to spaces. + $text = $this->detab($text); + + # Turn block-level HTML blocks into hash entries + $text = $this->hashHTMLBlocks($text); + + # Strip any lines consisting only of spaces and tabs. + # This makes subsequent regexen easier to write, because we can + # match consecutive blank lines with /\n+/ instead of something + # contorted like /[ ]*\n+/ . + $text = preg_replace('/^[ ]+$/m', '', $text); + + # Run document gamut methods. + foreach ($this->document_gamut as $method => $priority) { + $text = $this->$method($text); + } + + $this->teardown(); + + return $text . "\n"; + } + + /** + * Define the document gamut + * @var array + */ + protected $document_gamut = array( + // Strip link definitions, store in hashes. + "stripLinkDefinitions" => 20, + "runBasicBlockGamut" => 30, + ); + + /** + * Strips link definitions from text, stores the URLs and titles in + * hash references + * @param string $text + * @return string + */ + protected function stripLinkDefinitions($text) { + + $less_than_tab = $this->tab_width - 1; + + // Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (?: + <(.+?)> # url = $2 + | + (\S+?) # url = $3 + ) + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $4 + [")] + [ ]* + )? # title is optional + (?:\n+|\Z) + }xm', + array($this, '_stripLinkDefinitions_callback'), + $text + ); + return $text; + } + + /** + * The callback to strip link definitions + * @param array $matches + * @return string + */ + protected function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[$link_id] = $url; + $this->titles[$link_id] =& $matches[4]; + return ''; // String that will replace the block + } + + /** + * Hashify HTML blocks + * @param string $text + * @return string + */ + protected function hashHTMLBlocks($text) { + if ($this->no_markup) { + return $text; + } + + $less_than_tab = $this->tab_width - 1; + + /** + * Hashify HTML blocks: + * + * We only want to do this for block-level HTML tags, such as headers, + * lists, and tables. That's because we still want to wrap <p>s around + * "paragraphs" that are wrapped in non-block-level tags, such as + * anchors, phrase emphasis, and spans. The list of tags we're looking + * for is hard-coded: + * + * * List "a" is made of tags which can be both inline or block-level. + * These will be treated block-level when the start tag is alone on + * its line, otherwise they're not matched here and will be taken as + * inline later. + * * List "b" is made of tags which are always block-level; + */ + $block_tags_a_re = 'ins|del'; + $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. + 'script|noscript|style|form|fieldset|iframe|math|svg|'. + 'article|section|nav|aside|hgroup|header|footer|'. + 'figure'; + + // Regular expression for the content of a block tag. + $nested_tags_level = 4; + $attr = ' + (?> # optional tag attributes + \s # starts with whitespace + (?> + [^>"/]+ # text outside quotes + | + /+(?!>) # slash not followed by ">" + | + "[^"]*" # text inside double quotes (tolerate ">") + | + \'[^\']*\' # text inside single quotes (tolerate ">") + )* + )? + '; + $content = + str_repeat(' + (?> + [^<]+ # content without tag + | + <\2 # nested opening tag + '.$attr.' # attributes + (?> + /> + | + >', $nested_tags_level). // end of opening tag + '.*?'. // last level nested tag content + str_repeat(' + </\2\s*> # closing nested tag + ) + | + <(?!/\2\s*> # other tags with a different name + ) + )*', + $nested_tags_level); + $content2 = str_replace('\2', '\3', $content); + + /** + * First, look for nested blocks, e.g.: + * <div> + * <div> + * tags for inner block must be indented. + * </div> + * </div> + * + * The outermost tags must start at the left margin for this to match, + * and the inner nested divs must be indented. + * We need to do this before the next, more liberal match, because the + * next match will start at the first `<div>` and stop at the + * first `</div>`. + */ + $text = preg_replace_callback('{(?> + (?> + (?<=\n) # Starting on its own line + | # or + \A\n? # the at beginning of the doc + ) + ( # save in $1 + + # Match from `\n<tag>` to `</tag>\n`, handling nested tags + # in between. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_b_re.')# start tag = $2 + '.$attr.'> # attributes followed by > and \n + '.$content.' # content, support nesting + </\2> # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special version for tags of group a. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_a_re.')# start tag = $3 + '.$attr.'>[ ]*\n # attributes followed by > + '.$content2.' # content, support nesting + </\3> # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special case just for <hr />. It was easier to make a special + # case than to make the other regex more complicated. + + [ ]{0,'.$less_than_tab.'} + <(hr) # start tag = $2 + '.$attr.' # attributes + /?> # the matching end tag + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # Special case for standalone HTML comments: + + [ ]{0,'.$less_than_tab.'} + (?s: + <!-- .*? --> + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # PHP and ASP-style processor instructions (<? and <%) + + [ ]{0,'.$less_than_tab.'} + (?s: + <([?%]) # $2 + .*? + \2> + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + ) + )}Sxmi', + array($this, '_hashHTMLBlocks_callback'), + $text + ); + + return $text; + } + + /** + * The callback for hashing HTML blocks + * @param string $matches + * @return string + */ + protected function _hashHTMLBlocks_callback($matches) { + $text = $matches[1]; + $key = $this->hashBlock($text); + return "\n\n$key\n\n"; + } + + /** + * Called whenever a tag must be hashed when a function insert an atomic + * element in the text stream. Passing $text to through this function gives + * a unique text-token which will be reverted back when calling unhash. + * + * The $boundary argument specify what character should be used to surround + * the token. By convension, "B" is used for block elements that needs not + * to be wrapped into paragraph tags at the end, ":" is used for elements + * that are word separators and "X" is used in the general case. + * + * @param string $text + * @param string $boundary + * @return string + */ + protected function hashPart($text, $boundary = 'X') { + // Swap back any tag hash found in $text so we do not have to `unhash` + // multiple times at the end. + $text = $this->unhash($text); + + // Then hash the block. + static $i = 0; + $key = "$boundary\x1A" . ++$i . $boundary; + $this->html_hashes[$key] = $text; + return $key; // String that will replace the tag. + } + + /** + * Shortcut function for hashPart with block-level boundaries. + * @param string $text + * @return string + */ + protected function hashBlock($text) { + return $this->hashPart($text, 'B'); + } + + /** + * Define the block gamut - these are all the transformations that form + * block-level tags like paragraphs, headers, and list items. + * @var array + */ + protected $block_gamut = array( + "doHeaders" => 10, + "doHorizontalRules" => 20, + "doLists" => 40, + "doCodeBlocks" => 50, + "doBlockQuotes" => 60, + ); + + /** + * Run block gamut tranformations. + * + * We need to escape raw HTML in Markdown source before doing anything + * else. This need to be done for each block, and not only at the + * begining in the Markdown function since hashed blocks can be part of + * list items and could have been indented. Indented blocks would have + * been seen as a code block in a previous pass of hashHTMLBlocks. + * + * @param string $text + * @return string + */ + protected function runBlockGamut($text) { + $text = $this->hashHTMLBlocks($text); + return $this->runBasicBlockGamut($text); + } + + /** + * Run block gamut tranformations, without hashing HTML blocks. This is + * useful when HTML blocks are known to be already hashed, like in the first + * whole-document pass. + * + * @param string $text + * @return string + */ + protected function runBasicBlockGamut($text) { + + foreach ($this->block_gamut as $method => $priority) { + $text = $this->$method($text); + } + + // Finally form paragraph and restore hashed blocks. + $text = $this->formParagraphs($text); + + return $text; + } + + /** + * Convert horizontal rules + * @param string $text + * @return string + */ + protected function doHorizontalRules($text) { + return preg_replace( + '{ + ^[ ]{0,3} # Leading space + ([-*_]) # $1: First marker + (?> # Repeated marker group + [ ]{0,2} # Zero, one, or two spaces. + \1 # Marker character + ){2,} # Group repeated at least twice + [ ]* # Tailing spaces + $ # End of line. + }mx', + "\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n", + $text + ); + } + + /** + * These are all the transformations that occur *within* block-level + * tags like paragraphs, headers, and list items. + * @var array + */ + protected $span_gamut = array( + // Process character escapes, code spans, and inline HTML + // in one shot. + "parseSpan" => -30, + // Process anchor and image tags. Images must come first, + // because ![foo][f] looks like an anchor. + "doImages" => 10, + "doAnchors" => 20, + // Make links out of things like `<https://example.com/>` + // Must come after doAnchors, because you can use < and > + // delimiters in inline links like [this](<url>). + "doAutoLinks" => 30, + "encodeAmpsAndAngles" => 40, + "doItalicsAndBold" => 50, + "doHardBreaks" => 60, + ); + + /** + * Run span gamut transformations + * @param string $text + * @return string + */ + protected function runSpanGamut($text) { + foreach ($this->span_gamut as $method => $priority) { + $text = $this->$method($text); + } + + return $text; + } + + /** + * Do hard breaks + * @param string $text + * @return string + */ + protected function doHardBreaks($text) { + if ($this->hard_wrap) { + return preg_replace_callback('/ *\n/', + array($this, '_doHardBreaks_callback'), $text); + } else { + return preg_replace_callback('/ {2,}\n/', + array($this, '_doHardBreaks_callback'), $text); + } + } + + /** + * Trigger part hashing for the hard break (callback method) + * @param array $matches + * @return string + */ + protected function _doHardBreaks_callback($matches) { + return $this->hashPart("<br$this->empty_element_suffix\n"); + } + + /** + * Turn Markdown link shortcuts into XHTML <a> tags. + * @param string $text + * @return string + */ + protected function doAnchors($text) { + if ($this->in_anchor) { + return $text; + } + $this->in_anchor = true; + + // First, handle reference-style links: [link text] [id] + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + // Next, inline-style links: [link text](url "optional title") + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + \( # literal paren + [ \n]* + (?: + <(.+?)> # href = $3 + | + ('.$this->nested_url_parenthesis_re.') # href = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ \n]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + ) + }xs', + array($this, '_doAnchors_inline_callback'), $text); + + // Last, handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link text][1] + // or [link text](/foo) + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ([^\[\]]+) # link text = $2; can\'t contain [ or ] + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + + /** + * Callback method to parse referenced anchors + * @param string $matches + * @return string + */ + protected function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + // for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + // lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if ( isset( $this->titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + $result = $this->hashPart($result); + } else { + $result = $whole_match; + } + return $result; + } + + /** + * Callback method to parse inline anchors + * @param string $matches + * @return string + */ + protected function _doAnchors_inline_callback($matches) { + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] === '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + // If the URL was of the form <s p a c e s> it got caught by the HTML + // tag parser and hashed. Need to reverse the process before using + // the URL. + $unhashed = $this->unhash($url); + if ($unhashed !== $url) + $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); + + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + + return $this->hashPart($result); + } + + /** + * Turn Markdown image shortcuts into <img> tags. + * @param string $text + * @return string + */ + protected function doImages($text) { + // First, handle reference-style labeled images: ![alt text][id] + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array($this, '_doImages_reference_callback'), $text); + + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + ('.$this->nested_url_parenthesis_re.') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + ) + }xs', + array($this, '_doImages_inline_callback'), $text); + + return $text; + } + + /** + * Callback to parse references image tags + * @param array $matches + * @return string + */ + protected function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); // for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeURLAttribute($this->urls[$link_id]); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($this->titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } else { + // If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + + /** + * Callback to parse inline image tags + * @param array $matches + * @return string + */ + protected function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeURLAttribute($url); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; // $title already quoted + } + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + /** + * Parse Markdown heading elements to HTML + * @param string $text + * @return string + */ + protected function doHeaders($text) { + /** + * Setext-style headers: + * Header 1 + * ======== + * + * Header 2 + * -------- + */ + $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', + array($this, '_doHeaders_callback_setext'), $text); + + /** + * atx-style headers: + * # Header 1 + * ## Header 2 + * ## Header 2 with closing hashes ## + * ... + * ###### Header 6 + */ + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + \n+ + }xm', + array($this, '_doHeaders_callback_atx'), $text); + + return $text; + } + + /** + * Setext header parsing callback + * @param array $matches + * @return string + */ + protected function _doHeaders_callback_setext($matches) { + // Terrible hack to check we haven't found an empty list item. + if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) { + return $matches[0]; + } + + $level = $matches[2][0] == '=' ? 1 : 2; + + // ID attribute generation + $idAtt = $this->_generateIdFromHeaderValue($matches[1]); + + $block = "<h$level$idAtt>".$this->runSpanGamut($matches[1])."</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + /** + * ATX header parsing callback + * @param array $matches + * @return string + */ + protected function _doHeaders_callback_atx($matches) { + // ID attribute generation + $idAtt = $this->_generateIdFromHeaderValue($matches[2]); + + $level = strlen($matches[1]); + $block = "<h$level$idAtt>".$this->runSpanGamut($matches[2])."</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + /** + * If a header_id_func property is set, we can use it to automatically + * generate an id attribute. + * + * This method returns a string in the form id="foo", or an empty string + * otherwise. + * @param string $headerValue + * @return string + */ + protected function _generateIdFromHeaderValue($headerValue) { + if (!is_callable($this->header_id_func)) { + return ""; + } + + $idValue = call_user_func($this->header_id_func, $headerValue); + if (!$idValue) { + return ""; + } + + return ' id="' . $this->encodeAttribute($idValue) . '"'; + } + + /** + * Form HTML ordered (numbered) and unordered (bulleted) lists. + * @param string $text + * @return string + */ + protected function doLists($text) { + $less_than_tab = $this->tab_width - 1; + + // Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + + $markers_relist = array( + $marker_ul_re => $marker_ol_re, + $marker_ol_re => $marker_ul_re, + ); + + foreach ($markers_relist as $marker_re => $other_marker_re) { + // Re-usable pattern to match any entirel ul or ol list: + $whole_list_re = ' + ( # $1 = whole list + ( # $2 + ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces + ('.$marker_re.') # $4 = first list item marker + [ ]+ + ) + (?s:.+?) + ( # $5 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another list item marker + [ ]* + '.$marker_re.'[ ]+ + ) + | + (?= # Lookahead for another kind of list + \n + \3 # Must have the same indentation + '.$other_marker_re.'[ ]+ + ) + ) + ) + '; // mx + + // We use a different prefix before nested lists than top-level lists. + //See extended comment in _ProcessListItems(). + + if ($this->list_level) { + $text = preg_replace_callback('{ + ^ + '.$whole_list_re.' + }mx', + array($this, '_doLists_callback'), $text); + } else { + $text = preg_replace_callback('{ + (?:(?<=\n)\n|\A\n?) # Must eat the newline + '.$whole_list_re.' + }mx', + array($this, '_doLists_callback'), $text); + } + } + + return $text; + } + + /** + * List parsing callback + * @param array $matches + * @return string + */ + protected function _doLists_callback($matches) { + // Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + $marker_ol_start_re = '[0-9]+'; + + $list = $matches[1]; + $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol"; + + $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); + + $list .= "\n"; + $result = $this->processListItems($list, $marker_any_re); + + $ol_start = 1; + if ($this->enhanced_ordered_list) { + // Get the start number for ordered list. + if ($list_type == 'ol') { + $ol_start_array = array(); + $ol_start_check = preg_match("/$marker_ol_start_re/", $matches[4], $ol_start_array); + if ($ol_start_check){ + $ol_start = $ol_start_array[0]; + } + } + } + + if ($ol_start > 1 && $list_type == 'ol'){ + $result = $this->hashBlock("<$list_type start=\"$ol_start\">\n" . $result . "</$list_type>"); + } else { + $result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>"); + } + return "\n". $result ."\n\n"; + } + + /** + * Nesting tracker for list levels + * @var integer + */ + protected $list_level = 0; + + /** + * Process the contents of a single ordered or unordered list, splitting it + * into individual list items. + * @param string $list_str + * @param string $marker_any_re + * @return string + */ + protected function processListItems($list_str, $marker_any_re) { + /** + * The $this->list_level global keeps track of when we're inside a list. + * Each time we enter a list, we increment it; when we leave a list, + * we decrement. If it's zero, we're not in a list anymore. + * + * We do this because when we're not inside a list, we want to treat + * something like this: + * + * I recommend upgrading to version + * 8. Oops, now this line is treated + * as a sub-list. + * + * As a single paragraph, despite the fact that the second line starts + * with a digit-period-space sequence. + * + * Whereas when we're inside a list (or sub-list), that line will be + * treated as the start of a sub-list. What a kludge, huh? This is + * an aspect of Markdown's syntax that's hard to parse perfectly + * without resorting to mind-reading. Perhaps the solution is to + * change the syntax rules such that sub-lists must start with a + * starting cardinal number; e.g. "1." or "a.". + */ + $this->list_level++; + + // Trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + $list_str = preg_replace_callback('{ + (\n)? # leading line = $1 + (^[ ]*) # leading whitespace = $2 + ('.$marker_any_re.' # list marker and space = $3 + (?:[ ]+|(?=\n)) # space only required if item is not empty + ) + ((?s:.*?)) # list item text = $4 + (?:(\n+(?=\n))|\n) # tailing blank line = $5 + (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) + }xm', + array($this, '_processListItems_callback'), $list_str); + + $this->list_level--; + return $list_str; + } + + /** + * List item parsing callback + * @param array $matches + * @return string + */ + protected function _processListItems_callback($matches) { + $item = $matches[4]; + $leading_line =& $matches[1]; + $leading_space =& $matches[2]; + $marker_space = $matches[3]; + $tailing_blank_line =& $matches[5]; + + if ($leading_line || $tailing_blank_line || + preg_match('/\n{2,}/', $item)) + { + // Replace marker with the appropriate whitespace indentation + $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; + $item = $this->runBlockGamut($this->outdent($item)."\n"); + } else { + // Recursion for sub-lists: + $item = $this->doLists($this->outdent($item)); + $item = $this->formParagraphs($item, false); + } + + return "<li>" . $item . "</li>\n"; + } + + /** + * Process Markdown `<pre><code>` blocks. + * @param string $text + * @return string + */ + protected function doCodeBlocks($text) { + $text = preg_replace_callback('{ + (?:\n\n|\A\n?) + ( # $1 = the code block -- one or more lines, starting with a space/tab + (?> + [ ]{'.$this->tab_width.'} # Lines must start with a tab or a tab-width of spaces + .*\n+ + )+ + ) + ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc + }xm', + array($this, '_doCodeBlocks_callback'), $text); + + return $text; + } + + /** + * Code block parsing callback + * @param array $matches + * @return string + */ + protected function _doCodeBlocks_callback($matches) { + $codeblock = $matches[1]; + + $codeblock = $this->outdent($codeblock); + if (is_callable($this->code_block_content_func)) { + $codeblock = call_user_func($this->code_block_content_func, $codeblock, ""); + } else { + $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); + } + + # trim leading newlines and trailing newlines + $codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock); + + $codeblock = "<pre><code>$codeblock\n</code></pre>"; + return "\n\n" . $this->hashBlock($codeblock) . "\n\n"; + } + + /** + * Create a code span markup for $code. Called from handleSpanToken. + * @param string $code + * @return string + */ + protected function makeCodeSpan($code) { + if (is_callable($this->code_span_content_func)) { + $code = call_user_func($this->code_span_content_func, $code); + } else { + $code = htmlspecialchars(trim($code), ENT_NOQUOTES); + } + return $this->hashPart("<code>$code</code>"); + } + + /** + * Define the emphasis operators with their regex matches + * @var array + */ + protected $em_relist = array( + '' => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?![\.,:;]?\s)', + '*' => '(?<![\s*])\*(?!\*)', + '_' => '(?<![\s_])_(?!_)', + ); + + /** + * Define the strong operators with their regex matches + * @var array + */ + protected $strong_relist = array( + '' => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?![\.,:;]?\s)', + '**' => '(?<![\s*])\*\*(?!\*)', + '__' => '(?<![\s_])__(?!_)', + ); + + /** + * Define the emphasis + strong operators with their regex matches + * @var array + */ + protected $em_strong_relist = array( + '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?![\.,:;]?\s)', + '***' => '(?<![\s*])\*\*\*(?!\*)', + '___' => '(?<![\s_])___(?!_)', + ); + + /** + * Container for prepared regular expressions + * @var array + */ + protected $em_strong_prepared_relist; + + /** + * Prepare regular expressions for searching emphasis tokens in any + * context. + * @return void + */ + protected function prepareItalicsAndBold() { + foreach ($this->em_relist as $em => $em_re) { + foreach ($this->strong_relist as $strong => $strong_re) { + // Construct list of allowed token expressions. + $token_relist = array(); + if (isset($this->em_strong_relist["$em$strong"])) { + $token_relist[] = $this->em_strong_relist["$em$strong"]; + } + $token_relist[] = $em_re; + $token_relist[] = $strong_re; + + // Construct master expression from list. + $token_re = '{(' . implode('|', $token_relist) . ')}'; + $this->em_strong_prepared_relist["$em$strong"] = $token_re; + } + } + } + + /** + * Convert Markdown italics (emphasis) and bold (strong) to HTML + * @param string $text + * @return string + */ + protected function doItalicsAndBold($text) { + if ($this->in_emphasis_processing) { + return $text; // avoid reentrency + } + $this->in_emphasis_processing = true; + + $token_stack = array(''); + $text_stack = array(''); + $em = ''; + $strong = ''; + $tree_char_em = false; + + while (1) { + // Get prepared regular expression for seraching emphasis tokens + // in current context. + $token_re = $this->em_strong_prepared_relist["$em$strong"]; + + // Each loop iteration search for the next emphasis token. + // Each token is then passed to handleSpanToken. + $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + $text_stack[0] .= $parts[0]; + $token =& $parts[1]; + $text =& $parts[2]; + + if (empty($token)) { + // Reached end of text span: empty stack without emitting. + // any more emphasis. + while ($token_stack[0]) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + break; + } + + $token_len = strlen($token); + if ($tree_char_em) { + // Reached closing marker while inside a three-char emphasis. + if ($token_len == 3) { + // Three-char closing marker, close em and strong. + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<strong><em>$span</em></strong>"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + $strong = ''; + } else { + // Other closing marker: close one em or strong and + // change current token state to match the other + $token_stack[0] = str_repeat($token[0], 3-$token_len); + $tag = $token_len == 2 ? "strong" : "em"; + $span = $text_stack[0]; + $span = $this->runSpanGamut($span); + $span = "<$tag>$span</$tag>"; + $text_stack[0] = $this->hashPart($span); + $$tag = ''; // $$tag stands for $em or $strong + } + $tree_char_em = false; + } else if ($token_len == 3) { + if ($em) { + // Reached closing marker for both em and strong. + // Closing strong marker: + for ($i = 0; $i < 2; ++$i) { + $shifted_token = array_shift($token_stack); + $tag = strlen($shifted_token) == 2 ? "strong" : "em"; + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<$tag>$span</$tag>"; + $text_stack[0] .= $this->hashPart($span); + $$tag = ''; // $$tag stands for $em or $strong + } + } else { + // Reached opening three-char emphasis marker. Push on token + // stack; will be handled by the special condition above. + $em = $token[0]; + $strong = "$em$em"; + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $tree_char_em = true; + } + } else if ($token_len == 2) { + if ($strong) { + // Unwind any dangling emphasis marker: + if (strlen($token_stack[0]) == 1) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + $em = ''; + } + // Closing strong marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<strong>$span</strong>"; + $text_stack[0] .= $this->hashPart($span); + $strong = ''; + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $strong = $token; + } + } else { + // Here $token_len == 1 + if ($em) { + if (strlen($token_stack[0]) == 1) { + // Closing emphasis marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<em>$span</em>"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + } else { + $text_stack[0] .= $token; + } + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $em = $token; + } + } + } + $this->in_emphasis_processing = false; + return $text_stack[0]; + } + + /** + * Parse Markdown blockquotes to HTML + * @param string $text + * @return string + */ + protected function doBlockQuotes($text) { + $text = preg_replace_callback('/ + ( # Wrap whole match in $1 + (?> + ^[ ]*>[ ]? # ">" at the start of a line + .+\n # rest of the first line + (.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + ) + /xm', + array($this, '_doBlockQuotes_callback'), $text); + + return $text; + } + + /** + * Blockquote parsing callback + * @param array $matches + * @return string + */ + protected function _doBlockQuotes_callback($matches) { + $bq = $matches[1]; + // trim one level of quoting - trim whitespace-only lines + $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); + $bq = $this->runBlockGamut($bq); // recurse + + $bq = preg_replace('/^/m', " ", $bq); + // These leading spaces cause problem with <pre> content, + // so we need to fix that: + $bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx', + array($this, '_doBlockQuotes_callback2'), $bq); + + return "\n" . $this->hashBlock("<blockquote>\n$bq\n</blockquote>") . "\n\n"; + } + + /** + * Blockquote parsing callback + * @param array $matches + * @return string + */ + protected function _doBlockQuotes_callback2($matches) { + $pre = $matches[1]; + $pre = preg_replace('/^ /m', '', $pre); + return $pre; + } + + /** + * Parse paragraphs + * + * @param string $text String to process in paragraphs + * @param boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags + * @return string + */ + protected function formParagraphs($text, $wrap_in_p = true) { + // Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + // Wrap <p> tags and unhashify HTML blocks + foreach ($grafs as $key => $value) { + if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { + // Is a paragraph. + $value = $this->runSpanGamut($value); + if ($wrap_in_p) { + $value = preg_replace('/^([ ]*)/', "<p>", $value); + $value .= "</p>"; + } + $grafs[$key] = $this->unhash($value); + } else { + // Is a block. + // Modify elements of @grafs in-place... + $graf = $value; + $block = $this->html_hashes[$graf]; + $graf = $block; +// if (preg_match('{ +// \A +// ( # $1 = <div> tag +// <div \s+ +// [^>]* +// \b +// markdown\s*=\s* ([\'"]) # $2 = attr quote char +// 1 +// \2 +// [^>]* +// > +// ) +// ( # $3 = contents +// .* +// ) +// (</div>) # $4 = closing tag +// \z +// }xs', $block, $matches)) +// { +// list(, $div_open, , $div_content, $div_close) = $matches; +// +// // We can't call Markdown(), because that resets the hash; +// // that initialization code should be pulled into its own sub, though. +// $div_content = $this->hashHTMLBlocks($div_content); +// +// // Run document gamut methods on the content. +// foreach ($this->document_gamut as $method => $priority) { +// $div_content = $this->$method($div_content); +// } +// +// $div_open = preg_replace( +// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); +// +// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; +// } + $grafs[$key] = $graf; + } + } + + return implode("\n\n", $grafs); + } + + /** + * Encode text for a double-quoted HTML attribute. This function + * is *not* suitable for attributes enclosed in single quotes. + * @param string $text + * @return string + */ + protected function encodeAttribute($text) { + $text = $this->encodeAmpsAndAngles($text); + $text = str_replace('"', '"', $text); + return $text; + } + + /** + * Encode text for a double-quoted HTML attribute containing a URL, + * applying the URL filter if set. Also generates the textual + * representation for the URL (removing mailto: or tel:) storing it in $text. + * This function is *not* suitable for attributes enclosed in single quotes. + * + * @param string $url + * @param string $text Passed by reference + * @return string URL + */ + protected function encodeURLAttribute($url, &$text = null) { + if (is_callable($this->url_filter_func)) { + $url = call_user_func($this->url_filter_func, $url); + } + + if (preg_match('{^mailto:}i', $url)) { + $url = $this->encodeEntityObfuscatedAttribute($url, $text, 7); + } else if (preg_match('{^tel:}i', $url)) { + $url = $this->encodeAttribute($url); + $text = substr($url, 4); + } else { + $url = $this->encodeAttribute($url); + $text = $url; + } + + return $url; + } + + /** + * Smart processing for ampersands and angle brackets that need to + * be encoded. Valid character entities are left alone unless the + * no-entities mode is set. + * @param string $text + * @return string + */ + protected function encodeAmpsAndAngles($text) { + if ($this->no_entities) { + $text = str_replace('&', '&', $text); + } else { + // Ampersand-encoding based entirely on Nat Irons's Amputator + // MT plugin: <http://bumppo.net/projects/amputator/> + $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', + '&', $text); + } + // Encode remaining <'s + $text = str_replace('<', '<', $text); + + return $text; + } + + /** + * Parse Markdown automatic links to anchor HTML tags + * @param string $text + * @return string + */ + protected function doAutoLinks($text) { + $text = preg_replace_callback('{<((https?|ftp|dict|tel):[^\'">\s]+)>}i', + array($this, '_doAutoLinks_url_callback'), $text); + + // Email addresses: <address@domain.foo> + $text = preg_replace_callback('{ + < + (?:mailto:)? + ( + (?: + [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+ + | + ".*?" + ) + \@ + (?: + [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ + | + \[[\d.a-fA-F:]+\] # IPv4 & IPv6 + ) + ) + > + }xi', + array($this, '_doAutoLinks_email_callback'), $text); + + return $text; + } + + /** + * Parse URL callback + * @param array $matches + * @return string + */ + protected function _doAutoLinks_url_callback($matches) { + $url = $this->encodeURLAttribute($matches[1], $text); + $link = "<a href=\"$url\">$text</a>"; + return $this->hashPart($link); + } + + /** + * Parse email address callback + * @param array $matches + * @return string + */ + protected function _doAutoLinks_email_callback($matches) { + $addr = $matches[1]; + $url = $this->encodeURLAttribute("mailto:$addr", $text); + $link = "<a href=\"$url\">$text</a>"; + return $this->hashPart($link); + } + + /** + * Input: some text to obfuscate, e.g. "mailto:foo@example.com" + * + * Output: the same text but with most characters encoded as either a + * decimal or hex entity, in the hopes of foiling most address + * harvesting spam bots. E.g.: + * + * mailto:foo + * @example.co + * m + * + * Note: the additional output $tail is assigned the same value as the + * ouput, minus the number of characters specified by $head_length. + * + * Based by a filter by Matthew Wickline, posted to BBEdit-Talk. + * With some optimizations by Milian Wolff. Forced encoding of HTML + * attribute special characters by Allan Odgaard. + * + * @param string $text + * @param string $tail Passed by reference + * @param integer $head_length + * @return string + */ + protected function encodeEntityObfuscatedAttribute($text, &$tail = null, $head_length = 0) { + if ($text == "") { + return $tail = ""; + } + + $chars = preg_split('/(?<!^)(?!$)/', $text); + $seed = (int)abs(crc32($text) / strlen($text)); // Deterministic seed. + + foreach ($chars as $key => $char) { + $ord = ord($char); + // Ignore non-ascii chars. + if ($ord < 128) { + $r = ($seed * (1 + $key)) % 100; // Pseudo-random function. + // roughly 10% raw, 45% hex, 45% dec + // '@' *must* be encoded. I insist. + // '"' and '>' have to be encoded inside the attribute + if ($r > 90 && strpos('@"&>', $char) === false) { + /* do nothing */ + } else if ($r < 45) { + $chars[$key] = '&#x'.dechex($ord).';'; + } else { + $chars[$key] = '&#'.$ord.';'; + } + } + } + + $text = implode('', $chars); + $tail = $head_length ? implode('', array_slice($chars, $head_length)) : $text; + + return $text; + } + + /** + * Take the string $str and parse it into tokens, hashing embeded HTML, + * escaped characters and handling code spans. + * @param string $str + * @return string + */ + protected function parseSpan($str) { + $output = ''; + + $span_re = '{ + ( + \\\\'.$this->escape_chars_re.' + | + (?<![`\\\\]) + `+ # code span marker + '.( $this->no_markup ? '' : ' + | + <!-- .*? --> # comment + | + <\?.*?\?> | <%.*?%> # processing instruction + | + <[!$]?[-a-zA-Z0-9:_]+ # regular tags + (?> + \s + (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* + )? + > + | + <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag + | + </[-a-zA-Z0-9:_]+\s*> # closing tag + ').' + ) + }xs'; + + while (1) { + // Each loop iteration seach for either the next tag, the next + // openning code span marker, or the next escaped character. + // Each token is then passed to handleSpanToken. + $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE); + + // Create token from text preceding tag. + if ($parts[0] != "") { + $output .= $parts[0]; + } + + // Check if we reach the end. + if (isset($parts[1])) { + $output .= $this->handleSpanToken($parts[1], $parts[2]); + $str = $parts[2]; + } else { + break; + } + } + + return $output; + } + + /** + * Handle $token provided by parseSpan by determining its nature and + * returning the corresponding value that should replace it. + * @param string $token + * @param string $str Passed by reference + * @return string + */ + protected function handleSpanToken($token, &$str) { + switch ($token[0]) { + case "\\": + return $this->hashPart("&#". ord($token[1]). ";"); + case "`": + // Search for end marker in remaining text. + if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm', + $str, $matches)) + { + $str = $matches[2]; + $codespan = $this->makeCodeSpan($matches[1]); + return $this->hashPart($codespan); + } + return $token; // Return as text since no ending marker found. + default: + return $this->hashPart($token); + } + } + + /** + * Remove one level of line-leading tabs or spaces + * @param string $text + * @return string + */ + protected function outdent($text) { + return preg_replace('/^(\t|[ ]{1,' . $this->tab_width . '})/m', '', $text); + } + + + /** + * String length function for detab. `_initDetab` will create a function to + * handle UTF-8 if the default function does not exist. + * @var string + */ + protected $utf8_strlen = 'mb_strlen'; + + /** + * Replace tabs with the appropriate amount of spaces. + * + * For each line we separate the line in blocks delemited by tab characters. + * Then we reconstruct every line by adding the appropriate number of space + * between each blocks. + * + * @param string $text + * @return string + */ + protected function detab($text) { + $text = preg_replace_callback('/^.*\t.*$/m', + array($this, '_detab_callback'), $text); + + return $text; + } + + /** + * Replace tabs callback + * @param string $matches + * @return string + */ + protected function _detab_callback($matches) { + $line = $matches[0]; + $strlen = $this->utf8_strlen; // strlen function for UTF-8. + + // Split in blocks. + $blocks = explode("\t", $line); + // Add each blocks to the line. + $line = $blocks[0]; + unset($blocks[0]); // Do not add first block twice. + foreach ($blocks as $block) { + // Calculate amount of space, insert spaces, insert block. + $amount = $this->tab_width - + $strlen($line, 'UTF-8') % $this->tab_width; + $line .= str_repeat(" ", $amount) . $block; + } + return $line; + } + + /** + * Check for the availability of the function in the `utf8_strlen` property + * (initially `mb_strlen`). If the function is not available, create a + * function that will loosely count the number of UTF-8 characters with a + * regular expression. + * @return void + */ + protected function _initDetab() { + + if (function_exists($this->utf8_strlen)) { + return; + } + + $this->utf8_strlen = function($text) { + return preg_match_all('/[\x00-\xBF]|[\xC0-\xFF][\x80-\xBF]*/', $text, $m); + }; + } + + /** + * Swap back in all the tags hashed by _HashHTMLBlocks. + * @param string $text + * @return string + */ + protected function unhash($text) { + return preg_replace_callback('/(.)\x1A[0-9]+\1/', + array($this, '_unhash_callback'), $text); + } + + /** + * Unhashing callback + * @param array $matches + * @return string + */ + protected function _unhash_callback($matches) { + return $this->html_hashes[$matches[0]]; + } +} + +class MarkdownExtraParser extends MarkdownParser { + /** + * Configuration variables + */ + + /** + * Prefix for footnote ids. + * @var string + */ + public $fn_id_prefix = ""; + + /** + * Optional title attribute for footnote links. + * @var string + */ + public $fn_link_title = ""; + + /** + * Optional class attribute for footnote links and backlinks. + * @var string + */ + public $fn_link_class = "footnote-ref"; + public $fn_backlink_class = "footnote-backref"; + + /** + * Content to be displayed within footnote backlinks. The default is '↩'; + * the U+FE0E on the end is a Unicode variant selector used to prevent iOS + * from displaying the arrow character as an emoji. + * Optionally use '^^' and '%%' to refer to the footnote number and + * reference number respectively. {@see parseFootnotePlaceholders()} + * @var string + */ + public $fn_backlink_html = '↩︎'; + + /** + * Optional title and aria-label attributes for footnote backlinks for + * added accessibility (to ensure backlink uniqueness). + * Use '^^' and '%%' to refer to the footnote number and reference number + * respectively. {@see parseFootnotePlaceholders()} + * @var string + */ + public $fn_backlink_title = ""; + public $fn_backlink_label = ""; + + /** + * Class name for table cell alignment (%% replaced left/center/right) + * For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center' + * If empty, the align attribute is used instead of a class name. + * @var string + */ + public $table_align_class_tmpl = ''; + + /** + * Optional class prefix for fenced code block. + * @var string + */ + public $code_class_prefix = ""; + + /** + * Class attribute for code blocks goes on the `code` tag; + * setting this to true will put attributes on the `pre` tag instead. + * @var boolean + */ + public $code_attr_on_pre = false; + + /** + * Predefined abbreviations. + * @var array + */ + public $predef_abbr = array(); + + /** + * Only convert atx-style headers if there's a space between the header and # + * @var boolean + */ + public $hashtag_protection = false; + + /** + * Determines whether footnotes should be appended to the end of the document. + * If true, footnote html can be retrieved from $this->footnotes_assembled. + * @var boolean + */ + public $omit_footnotes = false; + + + /** + * After parsing, the HTML for the list of footnotes appears here. + * This is available only if $omit_footnotes == true. + * + * Note: when placing the content of `footnotes_assembled` on the page, + * consider adding the attribute `role="doc-endnotes"` to the `div` or + * `section` that will enclose the list of footnotes so they are + * reachable to accessibility tools the same way they would be with the + * default HTML output. + * @var null|string + */ + public $footnotes_assembled = null; + + /** + * Parser implementation + */ + + /** + * Constructor function. Initialize the parser object. + * @return void + */ + public function __construct() { + // Add extra escapable characters before parent constructor + // initialize the table. + $this->escape_chars .= ':|'; + + // Insert extra document, block, and span transformations. + // Parent constructor will do the sorting. + $this->document_gamut += array( + "doFencedCodeBlocks" => 5, + "stripFootnotes" => 15, + "stripAbbreviations" => 25, + "appendFootnotes" => 50, + ); + $this->block_gamut += array( + "doFencedCodeBlocks" => 5, + "doTables" => 15, + "doDefLists" => 45, + ); + $this->span_gamut += array( + "doFootnotes" => 5, + "doAbbreviations" => 70, + ); + + $this->enhanced_ordered_list = true; + parent::__construct(); + } + + + /** + * Extra variables used during extra transformations. + * @var array + */ + protected $footnotes = array(); + protected $footnotes_ordered = array(); + protected $footnotes_ref_count = array(); + protected $footnotes_numbers = array(); + protected $abbr_desciptions = array(); + /** @var string */ + protected $abbr_word_re = ''; + + /** + * Give the current footnote number. + * @var integer + */ + protected $footnote_counter = 1; + + /** + * Ref attribute for links + * @var array + */ + protected $ref_attr = array(); + + /** + * Setting up Extra-specific variables. + */ + protected function setup() { + parent::setup(); + + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->footnotes_ref_count = array(); + $this->footnotes_numbers = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + $this->footnote_counter = 1; + $this->footnotes_assembled = null; + + foreach ($this->predef_abbr as $abbr_word => $abbr_desc) { + if ($this->abbr_word_re) + $this->abbr_word_re .= '|'; + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + } + } + + /** + * Clearing Extra-specific variables. + */ + protected function teardown() { + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->footnotes_ref_count = array(); + $this->footnotes_numbers = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + + if ( ! $this->omit_footnotes ) + $this->footnotes_assembled = null; + + parent::teardown(); + } + + + /** + * Extra attribute parser + */ + + /** + * Expression to use to catch attributes (includes the braces) + * @var string + */ + protected $id_class_attr_catch_re = '\{((?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,})[ ]*\}'; + + /** + * Expression to use when parsing in a context when no capture is desired + * @var string + */ + protected $id_class_attr_nocatch_re = '\{(?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,}[ ]*\}'; + + /** + * Parse attributes caught by the $this->id_class_attr_catch_re expression + * and return the HTML-formatted list of attributes. + * + * Currently supported attributes are .class and #id. + * + * In addition, this method also supports supplying a default Id value, + * which will be used to populate the id attribute in case it was not + * overridden. + * @param string $tag_name + * @param string $attr + * @param mixed $defaultIdValue + * @param array $classes + * @return string + */ + protected function doExtraAttributes($tag_name, $attr, $defaultIdValue = null, $classes = array()) { + if (empty($attr) && !$defaultIdValue && empty($classes)) { + return ""; + } + + // Split on components + preg_match_all('/[#.a-z][-_:a-zA-Z0-9=]+/', $attr, $matches); + $elements = $matches[0]; + + // Handle classes and IDs (only first ID taken into account) + $attributes = array(); + $id = false; + foreach ($elements as $element) { + if ($element[0] === '.') { + $classes[] = substr($element, 1); + } else if ($element[0] === '#') { + if ($id === false) $id = substr($element, 1); + } else if (strpos($element, '=') > 0) { + $parts = explode('=', $element, 2); + $attributes[] = $parts[0] . '="' . $parts[1] . '"'; + } + } + + if ($id === false || $id === '') { + $id = $defaultIdValue; + } + + // Compose attributes as string + $attr_str = ""; + if (!empty($id)) { + $attr_str .= ' id="'.$this->encodeAttribute($id) .'"'; + } + if (!empty($classes)) { + $attr_str .= ' class="'. implode(" ", $classes) . '"'; + } + if (!$this->no_markup && !empty($attributes)) { + $attr_str .= ' '.implode(" ", $attributes); + } + return $attr_str; + } + + /** + * Strips link definitions from text, stores the URLs and titles in + * hash references. + * @param string $text + * @return string + */ + protected function stripLinkDefinitions($text) { + $less_than_tab = $this->tab_width - 1; + + // Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (?: + <(.+?)> # url = $2 + | + (\S+?) # url = $3 + ) + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $4 + [")] + [ ]* + )? # title is optional + (?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr + (?:\n+|\Z) + }xm', + array($this, '_stripLinkDefinitions_callback'), + $text); + return $text; + } + + /** + * Strip link definition callback + * @param array $matches + * @return string + */ + protected function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[$link_id] = $url; + $this->titles[$link_id] =& $matches[4]; + $this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]); + return ''; // String that will replace the block + } + + + /** + * HTML block parser + */ + + /** + * Tags that are always treated as block tags + * @var string + */ + protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption|figure'; + + /** + * Tags treated as block tags only if the opening tag is alone on its line + * @var string + */ + protected $context_block_tags_re = 'script|noscript|style|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video'; + + /** + * Tags where markdown="1" default to span mode: + * @var string + */ + protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address'; + + /** + * Tags which must not have their contents modified, no matter where + * they appear + * @var string + */ + protected $clean_tags_re = 'script|style|math|svg'; + + /** + * Tags that do not need to be closed. + * @var string + */ + protected $auto_close_tags_re = 'hr|img|param|source|track'; + + /** + * Hashify HTML Blocks and "clean tags". + * + * We only want to do this for block-level HTML tags, such as headers, + * lists, and tables. That's because we still want to wrap <p>s around + * "paragraphs" that are wrapped in non-block-level tags, such as anchors, + * phrase emphasis, and spans. The list of tags we're looking for is + * hard-coded. + * + * This works by calling _HashHTMLBlocks_InMarkdown, which then calls + * _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1" + * attribute is found within a tag, _HashHTMLBlocks_InHTML calls back + * _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag. + * These two functions are calling each other. It's recursive! + * @param string $text + * @return string + */ + protected function hashHTMLBlocks($text) { + if ($this->no_markup) { + return $text; + } + + // Call the HTML-in-Markdown hasher. + list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text); + + return $text; + } + + /** + * Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags. + * + * * $indent is the number of space to be ignored when checking for code + * blocks. This is important because if we don't take the indent into + * account, something like this (which looks right) won't work as expected: + * + * <div> + * <div markdown="1"> + * Hello World. <-- Is this a Markdown code block or text? + * </div> <-- Is this a Markdown code block or a real tag? + * <div> + * + * If you don't like this, just don't indent the tag on which + * you apply the markdown="1" attribute. + * + * * If $enclosing_tag_re is not empty, stops at the first unmatched closing + * tag with that name. Nested tags supported. + * + * * If $span is true, text inside must treated as span. So any double + * newline will be replaced by a single newline so that it does not create + * paragraphs. + * + * Returns an array of that form: ( processed text , remaining text ) + * + * @param string $text + * @param integer $indent + * @param string $enclosing_tag_re + * @param boolean $span + * @return array + */ + protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0, + $enclosing_tag_re = '', $span = false) + { + + if ($text === '') return array('', ''); + + // Regex to check for the presense of newlines around a block tag. + $newline_before_re = '/(?:^\n?|\n\n)*$/'; + $newline_after_re = + '{ + ^ # Start of text following the tag. + (?>[ ]*<!--.*?-->)? # Optional comment. + [ ]*\n # Must be followed by newline. + }xs'; + + // Regex to match any tag. + $block_tag_re = + '{ + ( # $2: Capture whole tag. + </? # Any opening or closing tag. + (?> # Tag name. + ' . $this->block_tags_re . ' | + ' . $this->context_block_tags_re . ' | + ' . $this->clean_tags_re . ' | + (?!\s)'.$enclosing_tag_re . ' + ) + (?: + (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. + (?> + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + <!-- .*? --> # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + <!\[CDATA\[.*?\]\]> # CData Block + ' . ( !$span ? ' # If not in span. + | + # Indented code block + (?: ^[ ]*\n | ^ | \n[ ]*\n ) + [ ]{' . ($indent + 4) . '}[^\n]* \n + (?> + (?: [ ]{' . ($indent + 4) . '}[^\n]* | [ ]* ) \n + )* + | + # Fenced code block marker + (?<= ^ | \n ) + [ ]{0,' . ($indent + 3) . '}(?:~{3,}|`{3,}) + [ ]* + (?: \.?[-_:a-zA-Z0-9]+ )? # standalone class name + [ ]* + (?: ' . $this->id_class_attr_nocatch_re . ' )? # extra attributes + [ ]* + (?= \n ) + ' : '' ) . ' # End (if not is span). + | + # Code span marker + # Note, this regex needs to go after backtick fenced + # code blocks but it should also be kept outside of the + # "if not in span" condition adding backticks to the parser + `+ + ) + }xs'; + + + $depth = 0; // Current depth inside the tag tree. + $parsed = ""; // Parsed text that will be returned. + + // Loop through every tag until we find the closing tag of the parent + // or loop until reaching the end of text if no parent tag specified. + do { + // Split the text using the first $tag_match pattern found. + // Text before pattern will be first in the array, text after + // pattern will be at the end, and between will be any catches made + // by the pattern. + $parts = preg_split($block_tag_re, $text, 2, + PREG_SPLIT_DELIM_CAPTURE); + + // If in Markdown span mode, add a empty-string span-level hash + // after each newline to prevent triggering any block element. + if ($span) { + $void = $this->hashPart("", ':'); + $newline = "\n$void"; + $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void; + } + + $parsed .= $parts[0]; // Text before current tag. + + // If end of $text has been reached. Stop loop. + if (count($parts) < 3) { + $text = ""; + break; + } + + $tag = $parts[1]; // Tag to handle. + $text = $parts[2]; // Remaining text after current tag. + + // Check for: Fenced code block marker. + // Note: need to recheck the whole tag to disambiguate backtick + // fences from code spans + if (preg_match('{^\n?([ ]{0,' . ($indent + 3) . '})(~{3,}|`{3,})[ ]*(?:\.?[-_:a-zA-Z0-9]+)?[ ]*(?:' . $this->id_class_attr_nocatch_re . ')?[ ]*\n?$}', $tag, $capture)) { + // Fenced code block marker: find matching end marker. + $fence_indent = strlen($capture[1]); // use captured indent in re + $fence_re = $capture[2]; // use captured fence in re + if (preg_match('{^(?>.*\n)*?[ ]{' . ($fence_indent) . '}' . $fence_re . '[ ]*(?:\n|$)}', $text, + $matches)) + { + // End marker found: pass text unchanged until marker. + $parsed .= $tag . $matches[0]; + $text = substr($text, strlen($matches[0])); + } + else { + // No end marker: just skip it. + $parsed .= $tag; + } + } + // Check for: Indented code block. + else if ($tag[0] === "\n" || $tag[0] === " ") { + // Indented code block: pass it unchanged, will be handled + // later. + $parsed .= $tag; + } + // Check for: Code span marker + // Note: need to check this after backtick fenced code blocks + else if ($tag[0] === "`") { + // Find corresponding end marker. + $tag_re = preg_quote($tag); + if (preg_match('{^(?>.+?|\n(?!\n))*?(?<!`)' . $tag_re . '(?!`)}', + $text, $matches)) + { + // End marker found: pass text unchanged until marker. + $parsed .= $tag . $matches[0]; + $text = substr($text, strlen($matches[0])); + } + else { + // Unmatched marker: just skip it. + $parsed .= $tag; + } + } + // Check for: Opening Block level tag or + // Opening Context Block tag (like ins and del) + // used as a block tag (tag is alone on it's line). + else if (preg_match('{^<(?:' . $this->block_tags_re . ')\b}', $tag) || + ( preg_match('{^<(?:' . $this->context_block_tags_re . ')\b}', $tag) && + preg_match($newline_before_re, $parsed) && + preg_match($newline_after_re, $text) ) + ) + { + // Need to parse tag and following text using the HTML parser. + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true); + + // Make sure it stays outside of any paragraph by adding newlines. + $parsed .= "\n\n$block_text\n\n"; + } + // Check for: Clean tag (like script, math) + // HTML Comments, processing instructions. + else if (preg_match('{^<(?:' . $this->clean_tags_re . ')\b}', $tag) || + $tag[1] === '!' || $tag[1] === '?') + { + // Need to parse tag and following text using the HTML parser. + // (don't check for markdown attribute) + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false); + + $parsed .= $block_text; + } + // Check for: Tag with same name as enclosing tag. + else if ($enclosing_tag_re !== '' && + // Same name as enclosing tag. + preg_match('{^</?(?:' . $enclosing_tag_re . ')\b}', $tag)) + { + // Increase/decrease nested tag count. + if ($tag[1] === '/') { + $depth--; + } else if ($tag[strlen($tag)-2] !== '/') { + $depth++; + } + + if ($depth < 0) { + // Going out of parent element. Clean up and break so we + // return to the calling function. + $text = $tag . $text; + break; + } + + $parsed .= $tag; + } + else { + $parsed .= $tag; + } + } while ($depth >= 0); + + return array($parsed, $text); + } + + /** + * Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags. + * + * * Calls $hash_method to convert any blocks. + * * Stops when the first opening tag closes. + * * $md_attr indicate if the use of the `markdown="1"` attribute is allowed. + * (it is not inside clean tags) + * + * Returns an array of that form: ( processed text , remaining text ) + * @param string $text + * @param string $hash_method + * @param bool $md_attr Handle `markdown="1"` attribute + * @return array + */ + protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) { + if ($text === '') return array('', ''); + + // Regex to match `markdown` attribute inside of a tag. + $markdown_attr_re = ' + { + \s* # Eat whitespace before the `markdown` attribute + markdown + \s*=\s* + (?> + (["\']) # $1: quote delimiter + (.*?) # $2: attribute value + \1 # matching delimiter + | + ([^\s>]*) # $3: unquoted attribute value + ) + () # $4: make $3 always defined (avoid warnings) + }xs'; + + // Regex to match any tag. + $tag_re = '{ + ( # $2: Capture whole tag. + </? # Any opening or closing tag. + [\w:$]+ # Tag name. + (?: + (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. + (?> + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + <!-- .*? --> # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + <!\[CDATA\[.*?\]\]> # CData Block + ) + }xs'; + + $original_text = $text; // Save original text in case of faliure. + + $depth = 0; // Current depth inside the tag tree. + $block_text = ""; // Temporary text holder for current text. + $parsed = ""; // Parsed text that will be returned. + $base_tag_name_re = ''; + + // Get the name of the starting tag. + // (This pattern makes $base_tag_name_re safe without quoting.) + if (preg_match('/^<([\w:$]*)\b/', $text, $matches)) + $base_tag_name_re = $matches[1]; + + // Loop through every tag until we find the corresponding closing tag. + do { + // Split the text using the first $tag_match pattern found. + // Text before pattern will be first in the array, text after + // pattern will be at the end, and between will be any catches made + // by the pattern. + $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + + if (count($parts) < 3) { + // End of $text reached with unbalenced tag(s). + // In that case, we return original text unchanged and pass the + // first character as filtered to prevent an infinite loop in the + // parent function. + return array($original_text[0], substr($original_text, 1)); + } + + $block_text .= $parts[0]; // Text before current tag. + $tag = $parts[1]; // Tag to handle. + $text = $parts[2]; // Remaining text after current tag. + + // Check for: Auto-close tag (like <hr/>) + // Comments and Processing Instructions. + if (preg_match('{^</?(?:' . $this->auto_close_tags_re . ')\b}', $tag) || + $tag[1] === '!' || $tag[1] === '?') + { + // Just add the tag to the block as if it was text. + $block_text .= $tag; + } + else { + // Increase/decrease nested tag count. Only do so if + // the tag's name match base tag's. + if (preg_match('{^</?' . $base_tag_name_re . '\b}', $tag)) { + if ($tag[1] === '/') { + $depth--; + } else if ($tag[strlen($tag)-2] !== '/') { + $depth++; + } + } + + // Check for `markdown="1"` attribute and handle it. + if ($md_attr && + preg_match($markdown_attr_re, $tag, $attr_m) && + preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3])) + { + // Remove `markdown` attribute from opening tag. + $tag = preg_replace($markdown_attr_re, '', $tag); + + // Check if text inside this tag must be parsed in span mode. + $mode = $attr_m[2] . $attr_m[3]; + $span_mode = $mode === 'span' || ($mode !== 'block' && + preg_match('{^<(?:' . $this->contain_span_tags_re . ')\b}', $tag)); + + // Calculate indent before tag. + if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) { + $strlen = $this->utf8_strlen; + $indent = $strlen($matches[1], 'UTF-8'); + } else { + $indent = 0; + } + + // End preceding block with this tag. + $block_text .= $tag; + $parsed .= $this->$hash_method($block_text); + + // Get enclosing tag name for the ParseMarkdown function. + // (This pattern makes $tag_name_re safe without quoting.) + preg_match('/^<([\w:$]*)\b/', $tag, $matches); + $tag_name_re = $matches[1]; + + // Parse the content using the HTML-in-Markdown parser. + list ($block_text, $text) + = $this->_hashHTMLBlocks_inMarkdown($text, $indent, + $tag_name_re, $span_mode); + + // Outdent markdown text. + if ($indent > 0) { + $block_text = preg_replace("/^[ ]{1,$indent}/m", "", + $block_text); + } + + // Append tag content to parsed text. + if (!$span_mode) { + $parsed .= "\n\n$block_text\n\n"; + } else { + $parsed .= (string) $block_text; + } + + // Start over with a new block. + $block_text = ""; + } + else $block_text .= $tag; + } + + } while ($depth > 0); + + // Hash last block text that wasn't processed inside the loop. + $parsed .= $this->$hash_method($block_text); + + return array($parsed, $text); + } + + /** + * Called whenever a tag must be hashed when a function inserts a "clean" tag + * in $text, it passes through this function and is automaticaly escaped, + * blocking invalid nested overlap. + * @param string $text + * @return string + */ + protected function hashClean($text) { + return $this->hashPart($text, 'C'); + } + + /** + * Turn Markdown link shortcuts into XHTML <a> tags. + * @param string $text + * @return string + */ + protected function doAnchors($text) { + if ($this->in_anchor) { + return $text; + } + $this->in_anchor = true; + + // First, handle reference-style links: [link text] [id] + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + (' . $this->nested_brackets_re . ') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + // Next, inline-style links: [link text](url "optional title") + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + (' . $this->nested_brackets_re . ') # link text = $2 + \] + \( # literal paren + [ \n]* + (?: + <(.+?)> # href = $3 + | + (' . $this->nested_url_parenthesis_re . ') # href = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ \n]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + (?:[ ]? ' . $this->id_class_attr_catch_re . ' )? # $8 = id/class attributes + ) + }xs', + array($this, '_doAnchors_inline_callback'), $text); + + // Last, handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link text][1] + // or [link text](/foo) + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ([^\[\]]+) # link text = $2; can\'t contain [ or ] + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + + /** + * Callback for reference anchors + * @param array $matches + * @return string + */ + protected function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + // for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + // lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if ( isset( $this->titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + if (isset($this->ref_attr[$link_id])) + $result .= $this->ref_attr[$link_id]; + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + $result = $this->hashPart($result); + } + else { + $result = $whole_match; + } + return $result; + } + + /** + * Callback for inline anchors + * @param array $matches + * @return string + */ + protected function _doAnchors_inline_callback($matches) { + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] === '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); + + // if the URL was of the form <s p a c e s> it got caught by the HTML + // tag parser and hashed. Need to reverse the process before using the URL. + $unhashed = $this->unhash($url); + if ($unhashed !== $url) + $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); + + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $attr; + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + + return $this->hashPart($result); + } + + /** + * Turn Markdown image shortcuts into <img> tags. + * @param string $text + * @return string + */ + protected function doImages($text) { + // First, handle reference-style labeled images: ![alt text][id] + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + (' . $this->nested_brackets_re . ') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array($this, '_doImages_reference_callback'), $text); + + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + (' . $this->nested_brackets_re . ') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + (' . $this->nested_url_parenthesis_re . ') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + (?:[ ]? ' . $this->id_class_attr_catch_re . ' )? # $8 = id/class attributes + ) + }xs', + array($this, '_doImages_inline_callback'), $text); + + return $text; + } + + /** + * Callback for referenced images + * @param array $matches + * @return string + */ + protected function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id === "") { + $link_id = strtolower($alt_text); // for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeURLAttribute($this->urls[$link_id]); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($this->titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + if (isset($this->ref_attr[$link_id])) { + $result .= $this->ref_attr[$link_id]; + } + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } + else { + // If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + + /** + * Callback for inline images + * @param array $matches + * @return string + */ + protected function _doImages_inline_callback($matches) { + $alt_text = $matches[2]; + $url = $matches[3] === '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeURLAttribute($url); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; // $title already quoted + } + $result .= $attr; + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + /** + * Process markdown headers. Redefined to add ID and class attribute support. + * @param string $text + * @return string + */ + protected function doHeaders($text) { + // Setext-style headers: + // Header 1 {#header1} + // ======== + // + // Header 2 {#header2 .class1 .class2} + // -------- + // + $text = preg_replace_callback( + '{ + (^.+?) # $1: Header text + (?:[ ]+ ' . $this->id_class_attr_catch_re . ' )? # $3 = id/class attributes + [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer + }mx', + array($this, '_doHeaders_callback_setext'), $text); + + // atx-style headers: + // # Header 1 {#header1} + // ## Header 2 {#header2} + // ## Header 2 with closing hashes ## {#header3.class1.class2} + // ... + // ###### Header 6 {.class2} + // + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]'.($this->hashtag_protection ? '+' : '*').' + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + (?:[ ]+ ' . $this->id_class_attr_catch_re . ' )? # $3 = id/class attributes + [ ]* + \n+ + }xm', + array($this, '_doHeaders_callback_atx'), $text); + + return $text; + } + + /** + * Callback for setext headers + * @param array $matches + * @return string + */ + protected function _doHeaders_callback_setext($matches) { + if ($matches[3] === '-' && preg_match('{^- }', $matches[1])) { + return $matches[0]; + } + + $level = $matches[3][0] === '=' ? 1 : 2; + + $defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[1]) : null; + + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2], $defaultId); + $block = "<h$level$attr>" . $this->runSpanGamut($matches[1]) . "</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + /** + * Callback for atx headers + * @param array $matches + * @return string + */ + protected function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + + $defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[2]) : null; + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3], $defaultId); + $block = "<h$level$attr>" . $this->runSpanGamut($matches[2]) . "</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + /** + * Form HTML tables. + * @param string $text + * @return string + */ + protected function doTables($text) { + $less_than_tab = $this->tab_width - 1; + // Find tables with leading pipe. + // + // | Header 1 | Header 2 + // | -------- | -------- + // | Cell 1 | Cell 2 + // | Cell 3 | Cell 4 + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,' . $less_than_tab . '} # Allowed whitespace. + [|] # Optional leading pipe (present) + (.+) \n # $1: Header row (at least one pipe) + + [ ]{0,' . $less_than_tab . '} # Allowed whitespace. + [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + [ ]* # Allowed whitespace. + [|] .* \n # Row content. + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array($this, '_doTable_leadingPipe_callback'), $text); + + // Find tables without leading pipe. + // + // Header 1 | Header 2 + // -------- | -------- + // Cell 1 | Cell 2 + // Cell 3 | Cell 4 + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,' . $less_than_tab . '} # Allowed whitespace. + (\S.*[|].*) \n # $1: Header row (at least one pipe) + + [ ]{0,' . $less_than_tab . '} # Allowed whitespace. + ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + .* [|] .* \n # Row content + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array($this, '_DoTable_callback'), $text); + + return $text; + } + + /** + * Callback for removing the leading pipe for each row + * @param array $matches + * @return string + */ + protected function _doTable_leadingPipe_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + $content = preg_replace('/^ *[|]/m', '', $content); + + return $this->_doTable_callback(array($matches[0], $head, $underline, $content)); + } + + /** + * Make the align attribute in a table + * @param string $alignname + * @return string + */ + protected function _doTable_makeAlignAttr($alignname) { + if (empty($this->table_align_class_tmpl)) { + return " align=\"$alignname\""; + } + + $classname = str_replace('%%', $alignname, $this->table_align_class_tmpl); + return " class=\"$classname\""; + } + + /** + * Calback for processing tables + * @param array $matches + * @return string + */ + protected function _doTable_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + // Remove any tailing pipes for each line. + $head = preg_replace('/[|] *$/m', '', $head); + $underline = preg_replace('/[|] *$/m', '', $underline); + $content = preg_replace('/[|] *$/m', '', $content); + + // Reading alignement from header underline. + $separators = preg_split('/ *[|] */', $underline); + foreach ($separators as $n => $s) { + if (preg_match('/^ *-+: *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('right'); + else if (preg_match('/^ *:-+: *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('center'); + else if (preg_match('/^ *:-+ *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('left'); + else + $attr[$n] = ''; + } + + // Parsing span elements, including code spans, character escapes, + // and inline HTML tags, so that pipes inside those gets ignored. + $head = $this->parseSpan($head); + $headers = preg_split('/ *[|] */', $head); + $col_count = count($headers); + $attr = array_pad($attr, $col_count, ''); + + // Write column headers. + $text = "<table>\n"; + $text .= "<thead>\n"; + $text .= "<tr>\n"; + foreach ($headers as $n => $header) { + $text .= " <th$attr[$n]>" . $this->runSpanGamut(trim($header)) . "</th>\n"; + } + $text .= "</tr>\n"; + $text .= "</thead>\n"; + + // Split content by row. + $rows = explode("\n", trim($content, "\n")); + + $text .= "<tbody>\n"; + foreach ($rows as $row) { + // Parsing span elements, including code spans, character escapes, + // and inline HTML tags, so that pipes inside those gets ignored. + $row = $this->parseSpan($row); + + // Split row by cell. + $row_cells = preg_split('/ *[|] */', $row, $col_count); + $row_cells = array_pad($row_cells, $col_count, ''); + + $text .= "<tr>\n"; + foreach ($row_cells as $n => $cell) { + $text .= " <td$attr[$n]>" . $this->runSpanGamut(trim($cell)) . "</td>\n"; + } + $text .= "</tr>\n"; + } + $text .= "</tbody>\n"; + $text .= "</table>"; + + return $this->hashBlock($text) . "\n"; + } + + /** + * Form HTML definition lists. + * @param string $text + * @return string + */ + protected function doDefLists($text) { + $less_than_tab = $this->tab_width - 1; + + // Re-usable pattern to match any entire dl list: + $whole_list_re = '(?> + ( # $1 = whole list + ( # $2 + [ ]{0,' . $less_than_tab . '} + ((?>.*\S.*\n)+) # $3 = defined term + \n? + [ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition + ) + (?s:.+?) + ( # $4 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another term + [ ]{0,' . $less_than_tab . '} + (?: \S.*\n )+? # defined term + \n? + [ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition + ) + (?! # Negative lookahead for another definition + [ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition + ) + ) + ) + )'; // mx + + $text = preg_replace_callback('{ + (?>\A\n?|(?<=\n\n)) + ' . $whole_list_re . ' + }mx', + array($this, '_doDefLists_callback'), $text); + + return $text; + } + + /** + * Callback for processing definition lists + * @param array $matches + * @return string + */ + protected function _doDefLists_callback($matches) { + // Re-usable patterns to match list item bullets and number markers: + $list = $matches[1]; + + // Turn double returns into triple returns, so that we can make a + // paragraph for the last item in a list, if necessary: + $result = trim($this->processDefListItems($list)); + $result = "<dl>\n" . $result . "\n</dl>"; + return $this->hashBlock($result) . "\n\n"; + } + + /** + * Process the contents of a single definition list, splitting it + * into individual term and definition list items. + * @param string $list_str + * @return string + */ + protected function processDefListItems($list_str) { + + $less_than_tab = $this->tab_width - 1; + + // Trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + // Process definition terms. + $list_str = preg_replace_callback('{ + (?>\A\n?|\n\n+) # leading line + ( # definition terms = $1 + [ ]{0,' . $less_than_tab . '} # leading whitespace + (?!\:[ ]|[ ]) # negative lookahead for a definition + # mark (colon) or more whitespace. + (?> \S.* \n)+? # actual term (not whitespace). + ) + (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed + # with a definition mark. + }xm', + array($this, '_processDefListItems_callback_dt'), $list_str); + + // Process actual definitions. + $list_str = preg_replace_callback('{ + \n(\n+)? # leading line = $1 + ( # marker space = $2 + [ ]{0,' . $less_than_tab . '} # whitespace before colon + \:[ ]+ # definition mark (colon) + ) + ((?s:.+?)) # definition text = $3 + (?= \n+ # stop at next definition mark, + (?: # next term or end of text + [ ]{0,' . $less_than_tab . '} \:[ ] | + <dt> | \z + ) + ) + }xm', + array($this, '_processDefListItems_callback_dd'), $list_str); + + return $list_str; + } + + /** + * Callback for <dt> elements in definition lists + * @param array $matches + * @return string + */ + protected function _processDefListItems_callback_dt($matches) { + $terms = explode("\n", trim($matches[1])); + $text = ''; + foreach ($terms as $term) { + $term = $this->runSpanGamut(trim($term)); + $text .= "\n<dt>" . $term . "</dt>"; + } + return $text . "\n"; + } + + /** + * Callback for <dd> elements in definition lists + * @param array $matches + * @return string + */ + protected function _processDefListItems_callback_dd($matches) { + $leading_line = $matches[1]; + $marker_space = $matches[2]; + $def = $matches[3]; + + if ($leading_line || preg_match('/\n{2,}/', $def)) { + // Replace marker with the appropriate whitespace indentation + $def = str_repeat(' ', strlen($marker_space)) . $def; + $def = $this->runBlockGamut($this->outdent($def . "\n\n")); + $def = "\n". $def ."\n"; + } + else { + $def = rtrim($def); + $def = $this->runSpanGamut($this->outdent($def)); + } + + return "\n<dd>" . $def . "</dd>\n"; + } + + /** + * Adding the fenced code block syntax to regular Markdown: + * + * ~~~ + * Code block + * ~~~ + * + * @param string $text + * @return string + */ + protected function doFencedCodeBlocks($text) { + + $text = preg_replace_callback('{ + (?:\n|\A) + # 1: Opening marker + ( + (?:~{3,}|`{3,}) # 3 or more tildes/backticks. + ) + [ ]* + (?: + \.?([-_:a-zA-Z0-9]+) # 2: standalone class name + )? + [ ]* + (?: + ' . $this->id_class_attr_catch_re . ' # 3: Extra attributes + )? + [ ]* \n # Whitespace and newline following marker. + + # 4: Content + ( + (?> + (?!\1 [ ]* \n) # Not a closing marker. + .*\n+ + )+ + ) + + # Closing marker. + \1 [ ]* (?= \n ) + }xm', + array($this, '_doFencedCodeBlocks_callback'), $text); + + return $text; + } + + /** + * Callback to process fenced code blocks + * @param array $matches + * @return string + */ + protected function _doFencedCodeBlocks_callback($matches) { + $classname =& $matches[2]; + $attrs =& $matches[3]; + $codeblock = $matches[4]; + + if ($this->code_block_content_func) { + $codeblock = call_user_func($this->code_block_content_func, $codeblock, $classname); + } else { + $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); + } + + $codeblock = preg_replace_callback('/^\n+/', + array($this, '_doFencedCodeBlocks_newlines'), $codeblock); + + $classes = array(); + if ($classname !== "") { + if ($classname[0] === '.') { + $classname = substr($classname, 1); + } + $classes[] = $this->code_class_prefix . $classname; + } + $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs, null, $classes); + $pre_attr_str = $this->code_attr_on_pre ? $attr_str : ''; + $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str; + $codeblock = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>"; + + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + + /** + * Replace new lines in fenced code blocks + * @param array $matches + * @return string + */ + protected function _doFencedCodeBlocks_newlines($matches) { + return str_repeat("<br$this->empty_element_suffix", + strlen($matches[0])); + } + + /** + * Redefining emphasis markers so that emphasis by underscore does not + * work in the middle of a word. + * @var array + */ + protected $em_relist = array( + '' => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?![\.,:;]?\s)', + '*' => '(?<![\s*])\*(?!\*)', + '_' => '(?<![\s_])_(?![a-zA-Z0-9_])', + ); + protected $strong_relist = array( + '' => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?![\.,:;]?\s)', + '**' => '(?<![\s*])\*\*(?!\*)', + '__' => '(?<![\s_])__(?![a-zA-Z0-9_])', + ); + protected $em_strong_relist = array( + '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?![\.,:;]?\s)', + '***' => '(?<![\s*])\*\*\*(?!\*)', + '___' => '(?<![\s_])___(?![a-zA-Z0-9_])', + ); + + /** + * Parse text into paragraphs + * @param string $text String to process in paragraphs + * @param boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags + * @return string HTML output + */ + protected function formParagraphs($text, $wrap_in_p = true) { + // Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + // Wrap <p> tags and unhashify HTML blocks + foreach ($grafs as $key => $value) { + $value = trim($this->runSpanGamut($value)); + + // Check if this should be enclosed in a paragraph. + // Clean tag hashes & block tag hashes are left alone. + $is_p = $wrap_in_p && !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value); + + if ($is_p) { + $value = "<p>$value</p>"; + } + $grafs[$key] = $value; + } + + // Join grafs in one text, then unhash HTML tags. + $text = implode("\n\n", $grafs); + + // Finish by removing any tag hashes still present in $text. + $text = $this->unhash($text); + + return $text; + } + + + /** + * Footnotes - Strips link definitions from text, stores the URLs and + * titles in hash references. + * @param string $text + * @return string + */ + protected function stripFootnotes($text) { + $less_than_tab = $this->tab_width - 1; + + // Link defs are in the form: [^id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,' . $less_than_tab . '}\[\^(.+?)\][ ]?: # note_id = $1 + [ ]* + \n? # maybe *one* newline + ( # text = $2 (no blank lines allowed) + (?: + .+ # actual text + | + \n # newlines but + (?!\[.+?\][ ]?:\s)# negative lookahead for footnote or link definition marker. + (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed + # by non-indented content + )* + ) + }xm', + array($this, '_stripFootnotes_callback'), + $text); + return $text; + } + + /** + * Callback for stripping footnotes + * @param array $matches + * @return string + */ + protected function _stripFootnotes_callback($matches) { + $note_id = $this->fn_id_prefix . $matches[1]; + $this->footnotes[$note_id] = $this->outdent($matches[2]); + return ''; // String that will replace the block + } + + /** + * Replace footnote references in $text [^id] with a special text-token + * which will be replaced by the actual footnote marker in appendFootnotes. + * @param string $text + * @return string + */ + protected function doFootnotes($text) { + if (!$this->in_anchor) { + $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text); + } + return $text; + } + + /** + * Append footnote list to text + * @param string $text + * @return string + */ + protected function appendFootnotes($text) { + $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array($this, '_appendFootnotes_callback'), $text); + + if ( ! empty( $this->footnotes_ordered ) ) { + $this->_doFootnotes(); + if ( ! $this->omit_footnotes ) { + $text .= "\n\n"; + $text .= "<div class=\"footnotes\" role=\"doc-endnotes\">\n"; + $text .= "<hr" . $this->empty_element_suffix . "\n"; + $text .= $this->footnotes_assembled; + $text .= "</div>"; + } + } + return $text; + } + + + /** + * Generates the HTML for footnotes. Called by appendFootnotes, even if + * footnotes are not being appended. + * @return void + */ + protected function _doFootnotes() { + $attr = array(); + if ($this->fn_backlink_class !== "") { + $class = $this->fn_backlink_class; + $class = $this->encodeAttribute($class); + $attr['class'] = " class=\"$class\""; + } + $attr['role'] = " role=\"doc-backlink\""; + $num = 0; + + $text = "<ol>\n\n"; + while (!empty($this->footnotes_ordered)) { + $footnote = reset($this->footnotes_ordered); + $note_id = key($this->footnotes_ordered); + unset($this->footnotes_ordered[$note_id]); + $ref_count = $this->footnotes_ref_count[$note_id]; + unset($this->footnotes_ref_count[$note_id]); + unset($this->footnotes[$note_id]); + + $footnote .= "\n"; // Need to append newline before parsing. + $footnote = $this->runBlockGamut("$footnote\n"); + $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array($this, '_appendFootnotes_callback'), $footnote); + + $num++; + $note_id = $this->encodeAttribute($note_id); + + // Prepare backlink, multiple backlinks if multiple references + // Do not create empty backlinks if the html is blank + $backlink = ""; + if (!empty($this->fn_backlink_html)) { + for ($ref_num = 1; $ref_num <= $ref_count; ++$ref_num) { + if (!empty($this->fn_backlink_title)) { + $attr['title'] = ' title="' . $this->encodeAttribute($this->fn_backlink_title) . '"'; + } + if (!empty($this->fn_backlink_label)) { + $attr['label'] = ' aria-label="' . $this->encodeAttribute($this->fn_backlink_label) . '"'; + } + $parsed_attr = $this->parseFootnotePlaceholders( + implode('', $attr), + $num, + $ref_num + ); + $backlink_text = $this->parseFootnotePlaceholders( + $this->fn_backlink_html, + $num, + $ref_num + ); + $ref_count_mark = $ref_num > 1 ? $ref_num : ''; + $backlink .= " <a href=\"#fnref$ref_count_mark:$note_id\"$parsed_attr>$backlink_text</a>"; + } + $backlink = trim($backlink); + } + + // Add backlink to last paragraph; create new paragraph if needed. + if (!empty($backlink)) { + if (preg_match('{</p>$}', $footnote)) { + $footnote = substr($footnote, 0, -4) . " $backlink</p>"; + } else { + $footnote .= "\n\n<p>$backlink</p>"; + } + } + + $text .= "<li id=\"fn:$note_id\" role=\"doc-endnote\">\n"; + $text .= $footnote . "\n"; + $text .= "</li>\n\n"; + } + $text .= "</ol>\n"; + + $this->footnotes_assembled = $text; + } + + /** + * Callback for appending footnotes + * @param array $matches + * @return string + */ + protected function _appendFootnotes_callback($matches) { + $node_id = $this->fn_id_prefix . $matches[1]; + + // Create footnote marker only if it has a corresponding footnote *and* + // the footnote hasn't been used by another marker. + if (isset($this->footnotes[$node_id])) { + $num =& $this->footnotes_numbers[$node_id]; + if (!isset($num)) { + // Transfer footnote content to the ordered list and give it its + // number + $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id]; + $this->footnotes_ref_count[$node_id] = 1; + $num = $this->footnote_counter++; + $ref_count_mark = ''; + } else { + $ref_count_mark = $this->footnotes_ref_count[$node_id] += 1; + } + + $attr = ""; + if ($this->fn_link_class !== "") { + $class = $this->fn_link_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_link_title !== "") { + $title = $this->fn_link_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + $attr .= " role=\"doc-noteref\""; + + $attr = str_replace("%%", $num, $attr); + $node_id = $this->encodeAttribute($node_id); + + return + "<sup id=\"fnref$ref_count_mark:$node_id\">". + "<a href=\"#fn:$node_id\"$attr>$num</a>". + "</sup>"; + } + + return "[^" . $matches[1] . "]"; + } + + /** + * Build footnote label by evaluating any placeholders. + * - ^^ footnote number + * - %% footnote reference number (Nth reference to footnote number) + * @param string $label + * @param int $footnote_number + * @param int $reference_number + * @return string + */ + protected function parseFootnotePlaceholders($label, $footnote_number, $reference_number) { + return str_replace( + array('^^', '%%'), + array($footnote_number, $reference_number), + $label + ); + } + + + /** + * Abbreviations - strips abbreviations from text, stores titles in hash + * references. + * @param string $text + * @return string + */ + protected function stripAbbreviations($text) { + $less_than_tab = $this->tab_width - 1; + + // Link defs are in the form: [id]*: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,' . $less_than_tab . '}\*\[(.+?)\][ ]?: # abbr_id = $1 + (.*) # text = $2 (no blank lines allowed) + }xm', + array($this, '_stripAbbreviations_callback'), + $text); + return $text; + } + + /** + * Callback for stripping abbreviations + * @param array $matches + * @return string + */ + protected function _stripAbbreviations_callback($matches) { + $abbr_word = $matches[1]; + $abbr_desc = $matches[2]; + if ($this->abbr_word_re) { + $this->abbr_word_re .= '|'; + } + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + return ''; // String that will replace the block + } + + /** + * Find defined abbreviations in text and wrap them in <abbr> elements. + * @param string $text + * @return string + */ + protected function doAbbreviations($text) { + if ($this->abbr_word_re) { + // cannot use the /x modifier because abbr_word_re may + // contain significant spaces: + $text = preg_replace_callback('{' . + '(?<![\w\x1A])' . + '(?:' . $this->abbr_word_re . ')' . + '(?![\w\x1A])' . + '}', + array($this, '_doAbbreviations_callback'), $text); + } + return $text; + } + + /** + * Callback for processing abbreviations + * @param array $matches + * @return string + */ + protected function _doAbbreviations_callback($matches) { + $abbr = $matches[0]; + if (isset($this->abbr_desciptions[$abbr])) { + $desc = $this->abbr_desciptions[$abbr]; + if (empty($desc)) { + return $this->hashPart("<abbr>$abbr</abbr>"); + } + $desc = $this->encodeAttribute($desc); + return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>"); + } + return $matches[0]; + } +} + +// Markdown parser, Copyright Datenstrom, License GPLv2 + +class YellowMarkdownParser extends MarkdownExtraParser { + public $yellow; // access to API + public $page; // access to page + public $idAttributes; // id attributes + public $noticeLevel; // recursive level + + public function __construct($yellow, $page) { + $this->yellow = $yellow; + $this->page = $page; + $this->idAttributes = array(); + $this->noticeLevel = 0; + $this->url_filter_func = function($url) use ($yellow, $page) { + return $yellow->lookup->normaliseLocation($url, $page->location); + }; + $this->span_gamut += array("doStrikethrough" => 55); + $this->block_gamut += array("doNoticeBlocks" => 65); + $this->escape_chars .= "~"; + parent::__construct(); + } + + // Handle striketrough + public function doStrikethrough($text) { + $parts = preg_split("/(?<![~])(~~)(?![~])/", $text, null, PREG_SPLIT_DELIM_CAPTURE); + if (count($parts)>3) { + $text = ""; + $open = false; + foreach ($parts as $part) { + if ($part=="~~") { + $text .= $open ? "</del>" : "<del>"; + $open = !$open; + } else { + $text .= $part; + } + } + if ($open) $text .= "</del>"; + } + return $text; + } + + // Handle links + public function doAutoLinks($text) { + $text = preg_replace_callback("/<(\w+:[^\'\">\s]+)>/", array($this, "_doAutoLinks_url_callback"), $text); + $text = preg_replace_callback("/<([\w\+\-\.]+@[\w\-\.]+)>/", array($this, "_doAutoLinks_email_callback"), $text); + $text = preg_replace_callback("/^\s*\[(\w+)(.*?)\]\s*$/", array($this, "_doAutoLinks_shortcutBlock_callback"), $text); + $text = preg_replace_callback("/\[(\w+)(.*?)\]/", array($this, "_doAutoLinks_shortcutInline_callback"), $text); + $text = preg_replace_callback("/\[\-\-(.*?)\-\-\]/", array($this, "_doAutoLinks_shortcutComment_callback"), $text); + $text = preg_replace_callback("/\:([\w\+\-\_]+)\:/", array($this, "_doAutoLinks_shortcutSymbol_callback"), $text); + $text = preg_replace_callback("/((http|https|ftp):\/\/\S+[^\'\"\,\.\;\:\*\~\s]+)/", array($this, "_doAutoLinks_url_callback"), $text); + $text = preg_replace_callback("/([\w\+\-\.]+@[\w\-\.]+\.[\w]{2,4})/", array($this, "_doAutoLinks_email_callback"), $text); + return $text; + } + + // Handle shortcuts, block style + public function _doAutoLinks_shortcutBlock_callback($matches) { + $output = $this->page->parseContentShortcut($matches[1], trim($matches[2]), "block"); + return is_null($output) ? $matches[0] : $this->hashBlock($output); + } + + // Handle shortcuts, inline style + public function _doAutoLinks_shortcutInline_callback($matches) { + $output = $this->page->parseContentShortcut($matches[1], trim($matches[2]), "inline"); + return is_null($output) ? $matches[0] : $this->hashPart($output); + } + + // Handle shortcuts, comment style + public function _doAutoLinks_shortcutComment_callback($matches) { + $output = "<!--".htmlspecialchars($matches[1], ENT_NOQUOTES)."-->"; + return $this->hashBlock($output); + } + + // Handle shortcuts, symbol style + public function _doAutoLinks_shortcutSymbol_callback($matches) { + $output = $this->page->parseContentShortcut("", $matches[1], "symbol"); + return is_null($output) ? $matches[0] : $this->hashPart($output); + } + + // Handle fenced code blocks + public function _doFencedCodeBlocks_callback($matches) { + $text = $matches[4]; + $name = empty($matches[2]) ? "" : trim("$matches[2] $matches[3]"); + $output = $this->page->parseContentShortcut($name, $text, "code"); + if (is_null($output)) { + $attr = $this->doExtraAttributes("pre", ".$matches[2] $matches[3]"); + $output = "<pre$attr><code>".htmlspecialchars($text, ENT_NOQUOTES)."</code></pre>"; + } + return "\n\n".$this->hashBlock($output)."\n\n"; + } + + // Handle headers, text style + public function _doHeaders_callback_setext($matches) { + if ($matches[3]=="-" && preg_match('{^- }', $matches[1])) return $matches[0]; + $text = $matches[1]; + $level = $matches[3][0]=="=" ? 1 : 2; + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]); + if (empty($attr) && $level>=2 && $level<=3) $attr = $this->getIdAttribute($text); + $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; + return "\n".$this->hashBlock($output)."\n\n"; + } + + // Handle headers, atx style + public function _doHeaders_callback_atx($matches) { + $text = $matches[2]; + $level = strlen($matches[1]); + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3]); + if (empty($attr) && $level>=2 && $level<=3) $attr = $this->getIdAttribute($text); + $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; + return "\n".$this->hashBlock($output)."\n\n"; + } + + // Handle inline links + public function _doAnchors_inline_callback($matches) { + $url = $matches[3]=="" ? $matches[4] : $matches[3]; + $text = $matches[2]; + $title = isset($matches[7]) ? $matches[7] : ""; + $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); + $output = "<a href=\"".$this->encodeURLAttribute($url)."\""; + if (!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; + $output .= $attr; + $output .= ">".$this->runSpanGamut($text)."</a>"; + return $this->hashPart($output); + } + + // Handle inline images + public function _doImages_inline_callback($matches) { + $src = $matches[3]=="" ? $matches[4] : $matches[3]; + if (!preg_match("/^\w+:/", $src)) { + $src = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreImageLocation").$src; + } + $alt = $matches[2]; + $title = isset($matches[7]) ? $matches[7] : $matches[2]; + $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); + $output = "<img src=\"".$this->encodeURLAttribute($src)."\""; + if (!empty($alt)) $output .= " alt=\"".$this->encodeAttribute($alt)."\""; + if (!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; + $output .= $attr; + $output .= $this->empty_element_suffix; + return $this->hashPart($output); + } + + // Handle lists, task list + public function _processListItems_callback($matches) { + $attr = ""; + $item = $matches[4]; + $leadingLine = $matches[1]; + $tailingLine = $matches[5]; + if ($leadingLine || $tailingLine || preg_match('/\n{2,}/', $item)) + { + $item = $matches[2].str_repeat(' ', strlen($matches[3])).$item; + $item = $this->runBlockGamut($this->outdent($item)."\n"); + } else { + $item = $this->doLists($this->outdent($item)); + $item = $this->formParagraphs($item, false); + $token = substr($item, 0, 4); + if ($token=="[ ] " || $token=="[x] ") { + $attr = " class=\"task-list-item\""; + $item = ($token=='[ ] ' ? "<input type=\"checkbox\" disabled=\"disabled\" /> " : + "<input type=\"checkbox\" disabled=\"disabled\" checked=\"checked\" /> ").substr($item, 4); + } + } + return "<li$attr>".$item."</li>\n"; + } + + // Handle notice blocks + public function doNoticeBlocks($text) { + return preg_replace_callback("/((?>^[ ]*!(?!\[)[ ]?.+\n(.+\n)*)+)/m", array($this, "_doNoticeBlocks_callback"), $text); + } + + // Handle notice blocks over multiple lines + public function _doNoticeBlocks_callback($matches) { + $lines = $matches[1]; + $attr = ""; + $text = preg_replace("/^[ ]*![ ]?/m", "", $lines); + if (preg_match("/^[ ]*".$this->id_class_attr_catch_re."[ ]*\n([\S\s]*)$/m", $text, $matches)) { + $attr = $this->doExtraAttributes("div", $dummy =& $matches[1]); + $text = $matches[2]; + } elseif ($this->noticeLevel==0) { + $level = strspn(str_replace(array("![", " "), "", $lines), "!"); + $attr = " class=\"notice$level\""; + } + if (!empty($text)) { + ++$this->noticeLevel; + $output = "<div$attr>\n".$this->runBlockGamut($text)."\n</div>"; + --$this->noticeLevel; + } else { + $output = "<div$attr></div>"; + } + return "\n".$this->hashBlock($output)."\n\n"; + } + + // Return unique id attribute + public function getIdAttribute($text) { + $attr = ""; + $text = $this->yellow->lookup->normaliseName($text, true, false, true); + $text = trim(preg_replace("/-+/", "-", $text), "-"); + if (!isset($this->idAttributes[$text])) { + $this->idAttributes[$text] = $text; + $attr = " id=\"$text\""; + } + return $attr; + } +} diff --git a/system/extensions/meta.php b/system/extensions/meta.php new file mode 100644 index 0000000..11cd730 --- /dev/null +++ b/system/extensions/meta.php @@ -0,0 +1,65 @@ +<?php +// Meta extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/meta + +class YellowMeta { + const VERSION = "0.8.14"; + public $yellow; // access to API + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + $this->yellow->system->setDefault("metaDefaultImage", "favicon"); + } + + // Handle page extra data + public function onParsePageExtra($page, $name) { + $output = null; + if ($name=="header" && !$page->isError()) { + list($imageUrl, $imageAlt) = $this->getImageInformation($page); + $locale = $this->yellow->language->getText("languageLocale", $page->get("language")); + $output .= "<meta property=\"og:url\" content=\"".htmlspecialchars($page->getUrl().$this->yellow->toolbox->getLocationArguments())."\" />\n"; + $output .= "<meta property=\"og:locale\" content=\"".htmlspecialchars($locale)."\" />\n"; + $output .= "<meta property=\"og:type\" content=\"website\" />\n"; + $output .= "<meta property=\"og:title\" content=\"".$page->getHtml("title")."\" />\n"; + $output .= "<meta property=\"og:site_name\" content=\"".$page->getHtml("sitename")."\" />\n"; + $output .= "<meta property=\"og:description\" content=\"".$page->getHtml("description")."\" />\n"; + $output .= "<meta property=\"og:image\" content=\"".htmlspecialchars($imageUrl)."\" />\n"; + $output .= "<meta property=\"og:image:alt\" content=\"".htmlspecialchars($imageAlt)."\" />\n"; + } + return $output; + } + + // Handle page output data + public function onParsePageOutput($page, $text) { + $output = null; + if ($text && preg_match("/^(.*?)<html(.*?)>(.*)$/s", $text, $matches)) { + $output = $matches[1]."<html".$matches[2]." prefix=\"og: http://ogp.me/ns#\">".$matches[3]; + } + return $output; + } + + // Return image information for page + public function getImageInformation($page) { + if ($page->isExisting("image")) { + $name = $page->get("image"); + $alt = $page->isExisting("imageAlt") ? $page->get("imageAlt") : $page->get("title"); + } elseif (preg_match("/\[image(\s.*?)\]/", $page->getContent(true), $matches)) { + list($name, $alt) = $this->yellow->toolbox->getTextArguments(trim($matches[1])); + if (empty($alt)) $alt = $page->get("title"); + } else { + $name = $this->yellow->system->get("metaDefaultImage"); + $alt = $page->isExisting("imageAlt") ? $page->get("imageAlt") : $page->get("title"); + } + if (!preg_match("/^\w+:/", $name)) { + $location = $name!="favicon" ? $this->yellow->system->get("coreImageLocation").$name : + $this->yellow->system->get("coreThemeLocation").$this->yellow->lookup->normaliseName($page->get("theme")).".png"; + $url = $this->yellow->lookup->normaliseUrl( + $this->yellow->system->get("coreServerScheme"), + $this->yellow->system->get("coreServerAddress"), + $this->yellow->system->get("coreServerBase"), $location); + } else { + $url = $this->yellow->lookup->normaliseUrl("", "", "", $name); + } + return array($url, $alt); + } +} diff --git a/system/extensions/stockholm.php b/system/extensions/stockholm.php new file mode 100644 index 0000000..528898c --- /dev/null +++ b/system/extensions/stockholm.php @@ -0,0 +1,23 @@ +<?php +// Stockholm extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/stockholm + +class YellowStockholm { + const VERSION = "0.8.9"; + public $yellow; // access to API + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + } + + // Handle update + public function onUpdate($action) { + $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); + if ($action=="install") { + $this->yellow->system->save($fileName, array("theme" => "stockholm")); + } elseif ($action=="uninstall" && $this->yellow->system->get("theme")=="stockholm") { + $theme = reset(array_diff($this->yellow->system->getValues("theme"), array("stockholm"))); + $this->yellow->system->save($fileName, array("theme" => $theme)); + } + } +} diff --git a/system/extensions/update-current.ini b/system/extensions/update-current.ini new file mode 100644 index 0000000..e4dffa8 --- /dev/null +++ b/system/extensions/update-current.ini @@ -0,0 +1,133 @@ +# Datenstrom Yellow update settings + +Extension: Bundle +Version: 0.8.15 +Description: Bundle website files. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/bundle +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/bundle.zip +Published: 2020-07-29 10:13:34 +Developer: Datenstrom +Tag: feature +system/extensions/bundle.php: bundle.php,create,update + +Extension: Command +Version: 0.8.22 +Description: Command line of the website. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/command +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/command.zip +Published: 2020-08-08 16:53:28 +Developer: Datenstrom +Tag: feature +system/extensions/command.php: command.php,create,update + +Extension: Core +Version: 0.8.19 +Description: Core functionality of the website. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/core +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/core.zip +Published: 2020-08-08 17:14:56 +Developer: Datenstrom +Tag: feature +system/extensions/core.php: core.php,create,update + +Extension: Edit +Version: 0.8.35 +Description: Edit your website in a web browser. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/edit +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/edit.zip +Published: 2020-08-06 23:26:40 +Developer: Datenstrom +Tag: feature +system/extensions/edit.php: edit.php,create,update +system/extensions/edit.css: edit.css,create,update +system/extensions/edit.js: edit.js,create,update +system/extensions/edit.woff: edit.woff,create,update + +Extension: Image +Version: 0.8.9 +Description: Images and thumbnails. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/image +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/image.zip +Published: 2020-07-26 16:01:58 +Developer: Datenstrom +Tag: feature +system/extensions/image.php: image.php,create,update + +Extension: Markdown +Version: 0.8.15 +Description: Text formatting for humans. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/markdown +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/markdown.zip +Published: 2020-07-26 16:03:56 +Developer: Datenstrom +Tag: feature +system/extensions/markdown.php: markdown.php,create,update +system/extensions/markdownx.php: markdownx.php,update + +Extension: Meta +Version: 0.8.14 +Description: Meta data for social media sites. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/meta +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/meta.zip +Published: 2020-07-26 16:03:37 +Developer: Datenstrom, Steffen Schultz +Tag: feature +system/extensions/meta.php: meta.php,create,update + +Extension: Stockholm +Version: 0.8.9 +Description: Stockholm is a clean theme. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/stockholm +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/stockholm.zip +Published: 2020-07-26 15:14:23 +Designer: Datenstrom +Tag: theme +system/extensions/stockholm.php: stockholm.php,create,update +system/themes/stockholm.css: stockholm.css,create,update,careful +system/themes/stockholm.png: stockholm.png,create +system/themes/stockholm-opensans-bold.woff: stockholm-opensans-bold.woff,create,update,careful +system/themes/stockholm-opensans-light.woff: stockholm-opensans-light.woff,create,update,careful +system/themes/stockholm-opensans-regular.woff: stockholm-opensans-regular.woff,create,update,careful + +Extension: Update +Version: 0.8.31 +Description: Keep your website up to date. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/update +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/update.zip +Published: 2020-08-08 16:53:43 +Developer: Datenstrom +Tag: feature +system/extensions/update.php: update.php,create,update + +Extension: English +Version: 0.8.24 +Description: English/English with language 'en'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/english +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/english.zip +Published: 2020-08-12 10:00:22 +Translator: Mark Seuffert +Tag: language +system/extensions/english.php: english.php,create,update +system/extensions/english.txt: english.txt,create,update + +Extension: French +Version: 0.8.24 +Description: French/Français with language 'fr'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/french +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/french.zip +Published: 2020-08-12 10:00:23 +Translator: Juh Nibreh +Tag: language +system/extensions/french.php: french.php,create,update +system/extensions/french.txt: french.txt,create,update + +Extension: German +Version: 0.8.24 +Description: German/Deutsch with language 'de'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/german +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/german.zip +Published: 2020-08-12 10:00:24 +Translator: David Fehrmann +Tag: language +system/extensions/german.php: german.php,create,update +system/extensions/german.txt: german.txt,create,update diff --git a/system/extensions/update-latest.ini b/system/extensions/update-latest.ini new file mode 100644 index 0000000..c159f66 --- /dev/null +++ b/system/extensions/update-latest.ini @@ -0,0 +1,787 @@ +# Datenstrom Yellow update settings + +Extension: About +Version: 0.8.7 +Description: Author profile for blog pages. +HelpUrl: https://github.com/schulle4u/yellow-extensions-schulle4u/tree/master/about +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/about.zip +Published: 2020-08-12 14:53:26 +Developer: Steffen Schultz +Tag: feature +system/extensions/about.php: about.php,create,update +content/about/page.md: page.md,create,optional + +Extension: Antispam +Version: 0.8.6 +HelpUrl: https://github.com/schulle4u/yellow-extensions-schulle4u/tree/master/antispam +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/antispam.zip +Tag: feature, email, antispam +Description: Alternative email address obfuscator. +Published: 2020-07-29 11:52:15 +Developer: Steffen Schultz +system/extensions/antispam.php: antispam.php,create,update +system/extensions/antispam.js: antispam.js,create,update + +Extension: Audio +Version: 0.8.7 +Tag: feature, audio, streaming +Description: HTML5 audio player. +HelpUrl: https://github.com/schulle4u/yellow-extensions-schulle4u/tree/master/audio +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/audio.zip +Published: 2020-07-29 12:04:07 +Developer: Steffen Schultz +system/extensions/audio.php: audio.php,create,update +system/layouts/audio.html: audio.html,create,update,careful +content/audio/page.md: page.md,create,optional + +Extension: Berlin +Version: 0.8.9 +Description: Berlin is a theme inspired by Dieter Rams. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/berlin +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/berlin.zip +Published: 2020-07-26 15:14:48 +Designer: Datenstrom +Tag: theme +system/extensions/berlin.php: berlin.php,create,update +system/layouts/berlin-default.html: berlin-default.html,create,update,careful +system/themes/berlin.css: berlin.css,create,update,careful +system/themes/berlin.png: berlin.png,create +system/themes/berlin-opensans-bold.woff: berlin-opensans-bold.woff,create,update,careful +system/themes/berlin-opensans-light.woff: berlin-opensans-light.woff,create,update,careful +system/themes/berlin-opensans-regular.woff: berlin-opensans-regular.woff,create,update,careful + +Extension: Blog +Version: 0.8.10 +Description: Blog for your website. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/blog +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/blog.zip +Published: 2020-08-16 10:03:12 +Developer: Datenstrom +Tag: feature +system/extensions/blog.php: blog.php,create,update +system/layouts/blog.html: blog.html,create,update,careful +system/layouts/blogpages.html: blogpages.html,create,update,careful +content/shared/page-new-blog.md: page-new-blog.md,create,optional +content/2-blog/page.md: page.md,create,optional +content/2-blog/2013-04-07-blog-example.md: 2013-04-07-blog-example.md,create,optional +content/2-blog/2018-04-22-made-for-people.md: 2018-04-22-made-for-people.md,create,optional + +Extension: Breadcrumb +Version: 0.8.6 +Description: Breadcrumb navigation. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/breadcrumb +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/breadcrumb.zip +Published: 2020-08-01 14:56:24 +Developer: Datenstrom +Tag: feature +system/extensions/breadcrumb.php: breadcrumb.php,create,update + +Extension: Bundle +Version: 0.8.15 +Description: Bundle website files. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/bundle +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/bundle.zip +Published: 2020-07-29 10:13:34 +Developer: Datenstrom +Tag: feature +system/extensions/bundle.php: bundle.php,create,update + +Extension: Chinese +Version: 0.8.24 +Description: Chinese/简体中文 with language 'zh-CN'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/chinese +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/chinese.zip +Published: 2020-08-12 10:43:35 +Translator: Hyson Lee +Tag: language +system/extensions/chinese.php: chinese.php,create,update +system/extensions/chinese.txt: chinese.txt,create,update + +Extension: Command +Version: 0.8.22 +Description: Command line of the website. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/command +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/command.zip +Published: 2020-08-11 09:43:50 +Developer: Datenstrom +Tag: feature +system/extensions/command.php: command.php,create,update + +Extension: Contact +Version: 0.8.13 +Description: Email contact page. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/contact +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/contact.zip +Published: 2020-07-26 20:45:31 +Developer: Datenstrom +Tag: feature +system/extensions/contact.php: contact.php,create,update +system/layouts/contact.html: contact.html,create,update,careful +content/contact/page.md: page.md,create,optional + +Extension: Core +Version: 0.8.20 +Description: Core functionality of the website. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/core +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/core.zip +Published: 2020-08-16 09:48:00 +Developer: Datenstrom +Tag: feature +system/extensions/core.php: core.php,create,update + +Extension: Csv +Version: 0.8.13 +Tag: feature, csv, data, tables +Description: CSV file parser. +HelpUrl: https://github.com/schulle4u/yellow-extensions-schulle4u/tree/master/csv +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/csv.zip +Published: 2020-08-01 19:23:00 +Developer: Steffen Schultz +system/extensions/csv.php: csv.php,create,update +system/extensions/csv.js: csv.js,create,update + +Extension: Czech +Version: 0.8.24 +Description: Czech/Čeština with language 'cs'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/czech +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/czech.zip +Published: 2020-08-12 10:43:36 +Translator: Ufo Vyhuleny +Tag: language +system/extensions/czech.php: czech.php,create,update +system/extensions/czech.txt: czech.txt,create,update + +Extension: Danish +Version: 0.8.24 +Description: Danish/Dansk with language 'da'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/danish +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/danish.zip +Published: 2020-08-12 10:43:34 +Translator: David Garcia +Tag: language +system/extensions/danish.php: danish.php,create,update +system/extensions/danish.txt: danish.txt,create,update + +Extension: Disqus +Version: 0.8.4 +Description: Show Disqus comments on blog. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/disqus +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/disqus.zip +Published: 2020-07-26 17:58:11 +Developer: Datenstrom +Tag: feature +system/extensions/disqus.php: disqus.php,create,update +system/extensions/disqus.js: disqus.js,create,update + +Extension: Draft +Version: 0.8.10 +Description: Support for draft pages. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/draft +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/draft.zip +Published: 2020-08-16 10:03:06 +Developer: Datenstrom +Tag: feature +system/extensions/draft.php: draft.php,create,update +system/layouts/draftpages.html: draftpages.html,create,update,careful +content/drafts/page.md: page.md,create,optional + +Extension: Dutch +Version: 0.8.24 +Description: Dutch/Nederlands (België) with language 'nl'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/dutch +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/dutch.zip +Published: 2020-08-12 10:43:37 +Translator: Robin Vannieuwenhuijse +Tag: language +system/extensions/dutch.php: dutch.php,create,update +system/extensions/dutch.txt: dutch.txt,create,update + +Extension: Edit +Version: 0.8.35 +Description: Edit your website in a web browser. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/edit +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/edit.zip +Published: 2020-08-10 13:12:52 +Developer: Datenstrom +Tag: feature +system/extensions/edit.php: edit.php,create,update +system/extensions/edit.css: edit.css,create,update +system/extensions/edit.js: edit.js,create,update +system/extensions/edit.woff: edit.woff,create,update + +Extension: Emojiawesome +Version: 0.8.6 +Description: Lots and lots of emoji. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/emojiawesome +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/emojiawesome.zip +Published: 2020-07-26 17:59:52 +Developer: Datenstrom +Tag: feature +system/extensions/emojiawesome.php: emojiawesome.php,create,update +system/extensions/emojiawesome.css: emojiawesome.css,create,update + +Extension: English +Version: 0.8.24 +Description: English/English with language 'en'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/english +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/english.zip +Published: 2020-08-12 10:00:22 +Translator: Mark Seuffert +Tag: language +system/extensions/english.php: english.php,create,update +system/extensions/english.txt: english.txt,create,update + +Extension: Feed +Version: 0.8.10 +Description: Feed with recent changes. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/feed +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/feed.zip +Published: 2020-08-16 10:03:48 +Developer: Datenstrom +Tag: feature +system/extensions/feed.php: feed.php,create,update +system/layouts/feed.html: feed.html,create,update,careful +content/feed/page.md: page.md,create,optional + +Extension: Fontawesome +Version: 0.8.6 +Description: Icons and symbols. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/fontawesome +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/fontawesome.zip +Published: 2020-07-26 17:59:58 +Developer: Datenstrom +Tag: feature +system/extensions/fontawesome.php: fontawesome.php,create,update +system/extensions/fontawesome.css: fontawesome.css,create,update +system/extensions/fontawesome.woff: fontawesome.woff,create,update + +Extension: French +Version: 0.8.24 +Description: French/Français with language 'fr'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/french +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/french.zip +Published: 2020-08-12 10:00:23 +Translator: Juh Nibreh +Tag: language +system/extensions/french.php: french.php,create,update +system/extensions/french.txt: french.txt,create,update + +Extension: Gallery +Version: 0.8.8 +Description: Image gallery with popup. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/gallery +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/gallery.zip +Published: 2020-08-05 08:48:35 +Developer: Datenstrom +Tag: feature +system/extensions/gallery.php: gallery.php,create,update +system/extensions/gallery.js: gallery.js,create,update +system/extensions/gallery.css: gallery.css,create,update +system/extensions/gallery-photoswipe.min.js: gallery-photoswipe.min.js,create,update +system/extensions/gallery-default-skin.png: gallery-default-skin.png,create,update +system/extensions/gallery-default-skin.svg: gallery-default-skin.svg,create,update + +Extension: German +Version: 0.8.24 +Description: German/Deutsch with language 'de'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/german +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/german.zip +Published: 2020-08-12 10:00:24 +Translator: David Fehrmann +Tag: language +system/extensions/german.php: german.php,create,update +system/extensions/german.txt: german.txt,create,update + +Extension: Googlecalendar +Version: 0.8.7 +Description: Embed Google calendar. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/googlecalendar +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/googlecalendar.zip +Published: 2020-07-26 18:01:25 +Developer: Datenstrom +Tag: feature +system/extensions/googlecalendar.php: googlecalendar.php,create,update +system/extensions/googlecalendar.js: googlecalendar.js,create,update +system/extensions/googlecalendar.css: googlecalendar.css,create,update + +Extension: Googlemap +Version: 0.8.7 +Description: Embed Google map. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/googlemap +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/googlemap.zip +Published: 2020-07-26 18:01:29 +Developer: Datenstrom +Tag: feature +system/extensions/googlemap.php: googlemap.php,create,update + +Extension: Help +Version: 0.8.14 +Description: Help for your website. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/help +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/help.zip +Published: 2020-07-26 20:53:08 +Developer: Datenstrom +Tag: feature +system/extensions/help.php: help.php,create,update +content/9-help/adjusting-content.md: adjusting-content.md,create,optional,multi-language +content/9-help/adjusting-media.md: adjusting-media.md,create,optional,multi-language +content/9-help/adjusting-system.md: adjusting-system.md,create,optional,multi-language +content/9-help/api-for-developers.md: api-for-developers.md,create,optional,multi-language +content/9-help/contributing-guidelines.md: contributing-guidelines.md,create,optional,multi-language +content/9-help/css-files.md: css-files.md,create,optional,multi-language +content/9-help/how-to-make-a-small-blog.md: how-to-make-a-small-blog.md,create,optional,multi-language +content/9-help/how-to-make-a-small-website.md: how-to-make-a-small-website.md,create,optional,multi-language +content/9-help/how-to-make-a-small-wiki.md: how-to-make-a-small-wiki.md,create,optional,multi-language +content/9-help/html-files.md: html-files.md,create,optional,multi-language +content/9-help/javascript-files.md: javascript-files.md,create,optional,multi-language +content/9-help/markdown-cheat-sheet.md: markdown-cheat-sheet.md,create,optional,multi-language +content/9-help/page.md: page.md,create,optional,multi-language +content/9-help/troubleshooting.md: troubleshooting.md,create,optional,multi-language +media/images/help-photo.jpg: help-photo.jpg,create,optional +media/images/language-en.png: language-en.png,create,optional +media/images/language-de.png: language-de.png,create,optional +media/images/language-fr.png: language-fr.png,create,optional +media/images/language-it.png: language-it.png,create,optional +media/images/language-sv.png: language-sv.png,create,optional + +Extension: Highlight +Version: 0.8.8 +Description: Highlight source code. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/highlight +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/highlight.zip +Published: 2020-07-26 18:02:41 +Developer: Datenstrom +Tag: feature +system/extensions/highlight.php: highlight.php,create,update +system/extensions/highlight.css: highlight.css,create,update +system/extensions/highlight-cpp.json: highlight-cpp.json,create,update +system/extensions/highlight-css.json: highlight-css.json,create,update +system/extensions/highlight-javascript.json: highlight-javascript.json,create,update +system/extensions/highlight-json.json: highlight-json.json,create,update +system/extensions/highlight-php.json: highlight-php.json,create,update +system/extensions/highlight-python.json: highlight-python.json,create,update +system/extensions/highlight-xml.json: highlight-xml.json,create,update +system/extensions/highlight-yaml.json: highlight-yaml.json,create,update + +Extension: Hungarian +Version: 0.8.24 +Description: Hungarian/Magyar with language 'hu'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/hungarian +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/hungarian.zip +Published: 2020-08-12 10:56:47 +Translator: Ádám Tuba +Tag: language +system/extensions/hungarian.php: hungarian.php,create,update +system/extensions/hungarian.txt: hungarian.txt,create,update + +Extension: Image +Version: 0.8.9 +Description: Images and thumbnails. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/image +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/image.zip +Published: 2020-07-26 16:01:58 +Developer: Datenstrom +Tag: feature +system/extensions/image.php: image.php,create,update + +Extension: Include +Version: 0.8.5 +Tag: feature, content, page +Description: Includes page content from other pages. +HelpUrl: https://github.com/schulle4u/yellow-extensions-schulle4u/tree/master/include +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/include.zip +Published: 2020-07-29 12:14:17 +Developer: Steffen Schultz +system/extensions/include.php: include.php,create,update + +Extension: Instagram +Version: 0.8.5 +Description: Embed Instagram photos. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/instagram +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/instagram.zip +Published: 2020-07-26 18:03:04 +Developer: Datenstrom +Tag: feature +system/extensions/instagram.php: instagram.php,create,update +system/extensions/instagram.js: instagram.js,create,update + +Extension: Italian +Version: 0.8.24 +Description: Italian/Italiano with language 'it'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/italian +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/italian.zip +Published: 2020-08-12 10:56:47 +Translator: Giovanni Salmeri +Tag: language +system/extensions/italian.php: italian.php,create,update +system/extensions/italian.txt: italian.txt,create,update + +Extension: Japanese +Version: 0.8.24 +Description: Japanese/日本語 with language 'ja'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/japanese +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/japanese.zip +Published: 2020-08-12 10:56:48 +Translator: Yuhko Senuma +Tag: language +system/extensions/japanese.php: japanese.php,create,update +system/extensions/japanese.txt: japanese.txt,create,update + +Extension: Markdown +Version: 0.8.15 +Description: Text formatting for humans. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/markdown +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/markdown.zip +Published: 2020-07-26 16:03:56 +Developer: Datenstrom +Tag: feature +system/extensions/markdown.php: markdown.php,create,update +system/extensions/markdownx.php: markdownx.php,update + +Extension: Meta +Version: 0.8.14 +Description: Meta data for social media sites. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/meta +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/meta.zip +Published: 2020-07-26 16:03:37 +Developer: Datenstrom, Steffen Schultz +Tag: feature +system/extensions/meta.php: meta.php,create,update + +Extension: Motd +Version: 0.8.4 +Tag: feature, message +Description: Message of the day. +HelpUrl: https://github.com/schulle4u/yellow-extensions-schulle4u/tree/master/motd +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/motd.zip +Published: 2020-07-29 12:15:03 +Developer: Steffen Schultz +system/extensions/motd.php: motd.php,create,update + +Extension: Norwegian +Version: 0.8.24 +Description: Norwegian/Norsk Bokmål with language 'nb'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/norwegian +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/norwegian.zip +Published: 2020-08-12 10:56:46 +Translator: Per Arne Solvik +Tag: language +system/extensions/norwegian.php: norwegian.php,create,update +system/extensions/norwegian.txt: norwegian.txt,create,update + +Extension: Pagesource +Version: 0.8.6 +Tag: feature, markdown, page, source +Description: View the markdown source of a page. +HelpUrl: https://github.com/schulle4u/yellow-extensions-schulle4u/tree/master/pagesource +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/pagesource.zip +Published: 2020-07-29 12:16:24 +Developer: Steffen Schultz +system/extensions/pagesource.php: pagesource.php,create,update + +Extension: Paris +Version: 0.8.9 +Description: Paris is an elegant theme. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/paris +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/paris.zip +Published: 2020-07-26 15:14:55 +Designer: Datenstrom +Tag: theme +system/extensions/paris.php: paris.php,create,update +system/layouts/paris-navigation.html: paris-navigation.html,create,update,careful +system/themes/paris.css: paris.css,create,update,careful +system/themes/paris.png: paris.png,create +system/themes/paris-logo.png: paris-logo.png,create +system/themes/paris-quote.png: paris-quote.png,create +system/themes/paris-opensans-bold.woff: paris-opensans-bold.woff,create,update,careful +system/themes/paris-opensans-light.woff: paris-opensans-light.woff,create,update,careful +system/themes/paris-opensans-regular.woff: paris-opensans-regular.woff,create,update,careful + +Extension: Podcast +Version: 0.8.9 +Tag: feature, feed, podcast +Description: Web feed optimized for podcast publishing. +HelpUrl: https://github.com/schulle4u/yellow-extensions-schulle4u/tree/master/podcast +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/podcast.zip +Published: 2020-08-16 14:07:43 +Developer: Steffen Schultz +system/extensions/podcast.php: podcast.php,create,update +system/layouts/podcast.html: podcast.html,create,update,careful +content/podcast/page.md: page.md,create,optional + +Extension: Polish +Version: 0.8.24 +Description: Polish/Polski with language 'pl'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/polish +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/polish.zip +Published: 2020-08-12 11:20:53 +Translator: Paweł Klockiewicz +Tag: language +system/extensions/polish.php: polish.php,create,update +system/extensions/polish.txt: polish.txt,create,update + +Extension: Portuguese +Version: 0.8.24 +Description: Portuguese/Português with language 'pt'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/portuguese +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/portuguese.zip +Published: 2020-08-12 11:07:16 +Translator: Al Garcia +Tag: language +system/extensions/portuguese.php: portuguese.php,create,update +system/extensions/portuguese.txt: portuguese.txt,create,update + +Extension: Previousnext +Version: 0.8.7 +Description: Show links to previous/next page. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/previousnext +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/previousnext.zip +Published: 2020-07-26 18:03:32 +Developer: Datenstrom +Tag: feature +system/extensions/previousnext.php: previousnext.php,create,update + +Extension: Private +Version: 0.8.7 +Tag: feature, page, private, security +Description: Support for password-protected pages. +HelpUrl: https://github.com/schulle4u/yellow-extensions-schulle4u/tree/master/private +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/private.zip +Published: 2020-07-29 12:20:44 +Developer: Steffen Schultz +system/extensions/private.php: private.php,create,update +system/extensions/private.txt: private.txt,create,update + +Extension: Publish +Version: 0.8.27 +Description: Package and publish extensions. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/publish +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/publish.zip +Published: 2020-08-16 11:08:28 +Developer: Datenstrom +Tag: feature +system/extensions/publish.php: publish.php,create,update + +Extension: Radioboss +Version: 0.8.9 +Tag: feature, radio, widgets +Description: Widgets for RadioBoss Cloud. +HelpUrl: https://github.com/schulle4u/yellow-extensions-schulle4u/tree/master/radioboss +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/radioboss.zip +Published: 2020-07-29 12:21:38 +Developer: Steffen Schultz +system/extensions/radioboss.php: radioboss.php,create,update + +Extension: Random +Version: 0.8.6 +Tag: feature, pages, random +Description: Display random pages from specified location. +HelpUrl: https://github.com/schulle4u/yellow-extensions-schulle4u/tree/master/random +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/random.zip +Published: 2020-08-17 21:15:55 +Developer: Steffen Schultz +system/extensions/random.php: random.php,create,update + +Extension: Redirect +Version: 0.8.3 +Tag: feature, page, redirect +Description: Alternative page redirection. +HelpUrl: https://github.com/schulle4u/yellow-extensions-schulle4u/tree/master/redirect +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/redirect.zip +Published: 2020-07-29 12:25:06 +Developer: Steffen Schultz +system/extensions/redirect.php: redirect.php,create,update + +Extension: Russian +Version: 0.8.24 +Description: Russian/Русский with language 'ru'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/russian +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/russian.zip +Published: 2020-08-12 11:07:15 +Translator: Сергей Ворон +Tag: language +system/extensions/russian.php: russian.php,create,update +system/extensions/russian.txt: russian.txt,create,update + +Extension: Search +Version: 0.8.10 +Description: Full-text search. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/search +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/search.zip +Published: 2020-08-16 10:03:54 +Developer: Datenstrom +Tag: feature +system/extensions/search.php: search.php,create,update +system/layouts/search.html: search.html,create,update,careful +content/search/page.md: page.md,create,optional + +Extension: Sitemap +Version: 0.8.10 +Description: Sitemap with all pages. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/sitemap +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/sitemap.zip +Published: 2020-08-16 10:04:05 +Developer: Datenstrom +Tag: feature +system/extensions/sitemap.php: sitemap.php,create,update +system/layouts/sitemap.html: sitemap.html,create,update,careful +content/sitemap/page.md: page.md,create,optional + +Extension: Slider +Version: 0.8.5 +Description: Image gallery with slider. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/slider +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/slider.zip +Published: 2020-07-26 18:06:59 +Developer: Datenstrom +Tag: feature +system/extensions/slider.php: slider.php,create,update +system/extensions/slider.js: slider.js,create,update +system/extensions/slider.css: slider.css,create,update +system/extensions/slider-flickity.min.js: slider-flickity.min.js,create,update + +Extension: Slovak +Version: 0.8.24 +Description: Slovak/Slovenčina with language 'sk'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/slovak +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/slovak.zip +Published: 2020-08-12 11:07:14 +Translator: Ádám Tuba +Tag: language +system/extensions/slovak.php: slovak.php,create,update +system/extensions/slovak.txt: slovak.txt,create,update + +Extension: Soundcloud +Version: 0.8.4 +Description: Embed Soundcloud audio tracks. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/soundcloud +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/soundcloud.zip +Published: 2020-07-26 18:07:35 +Developer: Datenstrom +Tag: feature +system/extensions/soundcloud.php: soundcloud.php,create,update + +Extension: Spanish +Version: 0.8.24 +Description: Spanish/Español with language 'es'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/spanish +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/spanish.zip +Published: 2020-08-12 11:07:13 +Translator: Al Garcia, David Garcia +Tag: language +system/extensions/spanish.php: spanish.php,create,update +system/extensions/spanish.txt: spanish.txt,create,update + +Extension: Spoiler +Version: 0.8.7 +Tag: feature, page, spoiler +Description: Hide certain page elements. +HelpUrl: https://github.com/schulle4u/yellow-extensions-schulle4u/tree/master/spoiler +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/spoiler.zip +Published: 2020-07-29 12:27:02 +Developer: Steffen Schultz +system/extensions/spoiler.php: spoiler.php,create,update +system/extensions/spoiler.js: spoiler.js,create,update + +Extension: Stockholm +Version: 0.8.9 +Description: Stockholm is a clean theme. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/stockholm +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/stockholm.zip +Published: 2020-07-26 15:14:23 +Designer: Datenstrom +Tag: theme +system/extensions/stockholm.php: stockholm.php,create,update +system/themes/stockholm.css: stockholm.css,create,update,careful +system/themes/stockholm.png: stockholm.png,create +system/themes/stockholm-opensans-bold.woff: stockholm-opensans-bold.woff,create,update,careful +system/themes/stockholm-opensans-light.woff: stockholm-opensans-light.woff,create,update,careful +system/themes/stockholm-opensans-regular.woff: stockholm-opensans-regular.woff,create,update,careful + +Extension: Swedish +Version: 0.8.24 +Description: Swedish/Svenska with language 'sv'. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/swedish +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/swedish.zip +Published: 2020-08-12 10:23:59 +Translator: Adam Engel +Tag: language +system/extensions/swedish.php: swedish.php,create,update +system/extensions/swedish.txt: swedish.txt,create,update + +Extension: Ticker +Version: 0.8.7 +Tag: feature, feed, rss, simplepie +Description: RSS feed parser. +HelpUrl: https://github.com/schulle4u/yellow-extensions-schulle4u/tree/master/ticker +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/ticker.zip +Published: 2020-07-29 12:29:17 +Developer: Steffen Schultz +system/extensions/ticker.php: ticker.php,create,update +system/extensions/ticker-simplepie.compiled.php: ticker-simplepie.compiled.php,create,update + +Extension: Toc +Version: 0.8.5 +Description: Table of contents. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/toc +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/toc.zip +Published: 2020-08-16 09:52:54 +Developer: Datenstrom +Tag: feature +system/extensions/toc.php: toc.php,create,update + +Extension: Traffic +Version: 0.8.7 +Description: Create traffic analytics from web server log files. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/traffic +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/traffic.zip +Published: 2020-07-26 18:08:29 +Developer: Datenstrom +Tag: feature +system/extensions/traffic.php: traffic.php,create,update + +Extension: Twitter +Version: 0.8.5 +Description: Embed Twitter messages. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/twitter +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/twitter.zip +Published: 2020-07-26 18:08:33 +Developer: Datenstrom, Steffen Schultz +Tag: feature +system/extensions/twitter.php: twitter.php,create,update +system/extensions/twitter.js: twitter.js,create,update + +Extension: Update +Version: 0.8.34 +Description: Keep your website up to date. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/update +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/update.zip +Published: 2020-08-17 11:16:25 +Developer: Datenstrom +Tag: feature +system/extensions/update.php: update.php,create,update + +Extension: Wiki +Version: 0.8.11 +Description: Wiki for your website. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/wiki +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/wiki.zip +Published: 2020-08-16 13:29:15 +Developer: Datenstrom +Tag: feature +system/extensions/wiki.php: wiki.php,create,update +system/layouts/wiki.html: wiki.html,create,update,careful +system/layouts/wikipages.html: wikipages.html,create,update,careful +content/shared/page-new-wiki.md: page-new-wiki.md,create,optional +content/2-wiki/page.md: page.md,create,optional +content/2-wiki/wiki-example.md: wiki-example.md,create,optional + +Extension: Youtube +Version: 0.8.4 +Description: Embed Youtube videos. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/youtube +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/youtube.zip +Published: 2020-07-26 18:08:39 +Developer: Datenstrom +Tag: feature +system/extensions/youtube.php: youtube.php,create,update diff --git a/system/extensions/update.php b/system/extensions/update.php new file mode 100644 index 0000000..693b05e --- /dev/null +++ b/system/extensions/update.php @@ -0,0 +1,760 @@ +<?php +// Update extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/update + +class YellowUpdate { + const VERSION = "0.8.34"; + const PRIORITY = "2"; + public $yellow; // access to API + public $updates; // number of updates + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + $this->yellow->system->setDefault("updateExtensionUrl", "https://github.com/datenstrom/yellow-extensions"); + $this->yellow->system->setDefault("updateExtensionFile", "extension.ini"); + $this->yellow->system->setDefault("updateCurrentFile", "update-current.ini"); + $this->yellow->system->setDefault("updateLatestFile", "update-latest.ini"); + $this->yellow->system->setDefault("updateNotification", "none"); + } + + // Handle request + public function onRequest($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->yellow->lookup->isContentFile($fileName) && $this->isExtensionPending()) { + $statusCode = $this->processRequestPending($scheme, $address, $base, $location, $fileName); + } + return $statusCode; + } + + // Handle command + public function onCommand($command, $text) { + $statusCode = 0; + if ($this->isExtensionPending()) $statusCode = $this->processCommandPending(); + if ($statusCode==0) { + switch ($command) { + case "about": $statusCode = $this->processCommandAbout($command, $text); break; + case "clean": $statusCode = $this->processCommandClean($command, $text); break; + case "install": $statusCode = $this->processCommandInstall($command, $text); break; + case "uninstall": $statusCode = $this->processCommandUninstall($command, $text); break; + case "update": $statusCode = $this->processCommandUpdate($command, $text); break; + default: $statusCode = 0; break; + } + } + return $statusCode; + } + + // Handle command help + public function onCommandHelp() { + $help = "about\n"; + $help .= "install [extension]\n"; + $help .= "uninstall [extension]\n"; + $help .= "update [extension]\n"; + return $help; + } + + // Handle update + public function onUpdate($action) { + if ($action=="update") { // TODO: remove later, converts old layout files + $path = $this->yellow->system->get("coreLayoutDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntriesRecursive($path, "/^.*\.html$/", true, false) as $entry) { + $key = str_replace("pages", "", $this->yellow->lookup->normaliseName(basename($entry), true, true)); + $fileData = $fileDataNew = $this->yellow->toolbox->readFile($entry); + $fileDataNew = str_replace("yellow->page->getPages()", "yellow->page->getPages(\"$key\")", $fileDataNew); + $fileDataNew = str_replace("\$this->yellow->content->shared(\"header\")", "null", $fileDataNew); + $fileDataNew = str_replace("\$this->yellow->content->shared(\"footer\")", "null", $fileDataNew); + $fileDataNew = str_replace("php if (\$page = null)", "php /* Remove this line */ if (\$page = null)", $fileDataNew); + if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($entry, $fileDataNew)) { + $this->yellow->log("error", "Can't write file '$entry'!"); + } + } + } + if ($action=="startup") { + if ($this->yellow->system->get("updateNotification")!="none") { + foreach (explode(",", $this->yellow->system->get("updateNotification")) as $token) { + list($extension, $action) = $this->yellow->toolbox->getTextList($token, "/", 2); + if ($this->yellow->extension->isExisting($extension) && ($action!="startup" && $action!="uninstall")) { + $value = $this->yellow->extension->data[$extension]; + if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate($action); + } + } + $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); + if (!$this->yellow->system->save($fileName, array("updateNotification" => "none"))) { + $this->yellow->log("error", "Can't write file '$fileName'!"); + } + $this->updateSystemSettings(); + $this->updateLanguageSettings(); + } + } + } + + // Update system settings after update notification + public function updateSystemSettings() { + $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); + $fileData = $this->yellow->toolbox->readFile($fileName); + $fileDataStart = $fileDataSettings = $fileDataComments = ""; + $settings = new YellowArray(); + $settings->exchangeArray($this->yellow->system->settingsDefaults->getArrayCopy()); + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if (empty($fileDataStart) && preg_match("/^\#/", $line)) { + $fileDataStart = $line; + } elseif (!empty($matches[1]) && isset($settings[$matches[1]])) { + $settings[$matches[1]] = $matches[2]; + } elseif (!empty($matches[1]) && substru($matches[1], 0, 1)!="#") { + $fileDataComments .= "# $line"; + } elseif (!empty($matches[1])) { + $fileDataComments .= $line; + } + } + unset($settings["coreSystemFile"]); + foreach ($settings as $key=>$value) { + $fileDataSettings .= ucfirst($key).(strempty($value) ? ":\n" : ": $value\n"); + } + if (!empty($fileDataStart)) $fileDataStart .= "\n"; + if (!empty($fileDataComments)) $fileDataSettings .= "\n"; + $fileDataNew = $fileDataStart.$fileDataSettings.$fileDataComments; + if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) { + $this->yellow->log("error", "Can't write file '$fileName'!"); + } + } + + // Update language settings after update notification + public function updateLanguageSettings() { + $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreLanguageFile"); + $fileData = $this->yellow->toolbox->readFile($fileName); + $fileDataStart = $fileDataSettings = $language = ""; + $settings = new YellowArray(); + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if (empty($fileDataStart) && preg_match("/^\#/", $line)) { + $fileDataStart = $line; + } elseif (!empty($matches[1]) && !empty($matches[2])) { + if (lcfirst($matches[1])=="language" && !strempty($matches[2])) { + if (!empty($settings)) { + if (!empty($fileDataSettings)) $fileDataSettings .= "\n"; + foreach ($settings as $key=>$value) { + $fileDataSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + } + } + $language = $matches[2]; + $settings = new YellowArray(); + $settings["language"] = $language; + foreach ($this->yellow->language->settingsDefaults as $key=>$value) { + if ($this->yellow->language->isText($key, $language)) { + $settings[$key] = $this->yellow->language->getText($key, $language); + } + } + } + if (!empty($language)) { + $settings[$matches[1]] = $matches[2]; + } + } + } + if (!empty($fileDataStart)) $fileDataStart .= "\n"; + if (!empty($fileDataSettings)) $fileDataSettings .= "\n"; + foreach ($settings as $key=>$value) { + $fileDataSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + } + $fileDataNew = $fileDataStart.$fileDataSettings; + if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) { + $this->yellow->log("error", "Can't write file '$fileName'!"); + } + } + + // Create extension settings from scratch + public function createExtensionSettings() { + $fileNameCurrent = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateCurrentFile"); + $fileNameLatest = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateLatestFile"); + $url = $this->yellow->system->get("updateExtensionUrl")."/raw/master/".$this->yellow->system->get("updateLatestFile"); + list($statusCode, $fileData) = $this->getExtensionFile($url); + if ($statusCode==200) { + $fileDataCurrent = $fileDataLatest = $fileData; + $settings = $this->yellow->toolbox->getTextSettings($fileDataCurrent, "extension"); + foreach ($settings as $key=>$value) { + if ($this->yellow->extension->isExisting($key)) { + $settingsNew = new YellowArray(); + $settingsNew["extension"] = ucfirst($key); + $settingsNew["version"] = $this->yellow->extension->data[$key]["version"]; + $fileDataCurrent = $this->yellow->toolbox->setTextSettings($fileDataCurrent, "extension", $key, $settingsNew); + } else { + $fileDataCurrent = $this->yellow->toolbox->unsetTextSettings($fileDataCurrent, "extension", $key); + } + } + if(!$this->yellow->toolbox->createFile($fileNameCurrent, $fileDataCurrent)) { + $this->yellow->log("error", "Can't write file '$fileNameCurrent'!"); + } + if(!$this->yellow->toolbox->createFile($fileNameLatest, $fileDataLatest)) { + $this->yellow->log("error", "Can't write file '$fileNameLatest'!"); + } + } + return $statusCode; + } + + // Update extension settings + public function updateExtensionSettings($extension, $settings, $action) { + $statusCode = 200; + $fileNameCurrent = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateCurrentFile"); + $fileData = $this->yellow->toolbox->readFile($fileNameCurrent); + if ($action=="install" || $action=="update") { + $settingsNew = new YellowArray(); + foreach($settings as $key=>$value) $settingsNew[$key] = $value; + $fileData = $this->yellow->toolbox->setTextSettings($fileData, "extension", $extension, $settingsNew); + } else { + $fileData = $this->yellow->toolbox->unsetTextSettings($fileData, "extension", $extension); + } + if (!$this->yellow->toolbox->createFile($fileNameCurrent, $fileData)) { + $statusCode = 500; + $this->yellow->page->error(500, "Can't write file '$fileNameCurrent'!"); + } + return $statusCode; + } + + // Process command to show website version and updates + public function processCommandAbout($command, $text) { + echo "Datenstrom Yellow ".YellowCore::RELEASE."\n"; + list($statusCode, $settingsCurrent) = $this->getExtensionSettings(false); + list($statusCode, $settingsLatest) = $this->getExtensionSettings(true); + foreach ($settingsCurrent as $key=>$value) { + $versionCurrent = $versionLatest = $settingsCurrent[$key]->get("version"); + if ($settingsLatest->isExisting($key)) $versionLatest = $settingsLatest[$key]->get("version"); + if (strnatcasecmp($versionCurrent, $versionLatest)<0) { + echo ucfirst($key)." $versionCurrent - Update available\n"; + } else { + echo ucfirst($key)." $versionCurrent\n"; + } + } + if ($statusCode!=200) echo "ERROR checking updates: ".$this->yellow->page->get("pageError")."\n"; + return $statusCode; + } + + // Process command to clean downloads + public function processCommandClean($command, $text) { + $statusCode = 0; + if ($command=="clean" && $text=="all") { + $path = $this->yellow->system->get("coreExtensionDirectory"); + $regex = "/^.*\\".$this->yellow->system->get("coreDownloadExtension")."$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, false) as $entry) { + if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; + } + if ($statusCode==500) echo "ERROR cleaning downloads: Can't delete files in directory '$path'!\n"; + } + return $statusCode; + } + + // Process command to install extensions + public function processCommandInstall($command, $text) { + $extensions = $this->getExtensionsFromText($text); + if (!empty($extensions)) { + $this->updates = 0; + list($statusCode, $settings) = $this->getExtensionInstallInformation($extensions); + if ($statusCode==200) $statusCode = $this->downloadExtensions($settings); + if ($statusCode==200) $statusCode = $this->updateExtensions("install"); + if ($statusCode>=400) echo "ERROR installing files: ".$this->yellow->page->get("pageError")."\n"; + echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; + echo ", $this->updates extension".($this->updates!=1 ? "s" : "")." installed\n"; + } else { + $statusCode = $this->showExtensions(); + } + return $statusCode; + } + + // Process command to uninstall extensions + public function processCommandUninstall($command, $text) { + $extensions = $this->getExtensionsFromText($text); + if (!empty($extensions)) { + $this->updates = 0; + list($statusCode, $settings) = $this->getExtensionUninstallInformation($extensions, "core, update"); + if ($statusCode==200) $statusCode = $this->removeExtensions($settings); + if ($statusCode>=400) echo "ERROR uninstalling files: ".$this->yellow->page->get("pageError")."\n"; + echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; + echo ", $this->updates extension".($this->updates!=1 ? "s" : "")." uninstalled\n"; + } else { + $statusCode = $this->showExtensions(); + } + return $statusCode; + } + + // Process command to update website + public function processCommandUpdate($command, $text) { + $extensions = $this->getExtensionsFromText($text); + list($statusCode, $settings) = $this->getExtensionUpdateInformation($extensions); + if ($statusCode!=200 || !empty($settings)) { + $this->updates = 0; + if ($statusCode==200) $statusCode = $this->downloadExtensions($settings); + if ($statusCode==200) $statusCode = $this->updateExtensions("update"); + if ($statusCode>=400) echo "ERROR updating files: ".$this->yellow->page->get("pageError")."\n"; + echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; + echo ", $this->updates update".($this->updates!=1 ? "s" : "")." installed\n"; + } else { + echo "Your website is up to date\n"; + } + return $statusCode; + } + + // Process command to install pending extension + public function processCommandPending() { + $statusCode = $this->updateExtensions("install"); + if ($statusCode!=200) echo "ERROR updating files: ".$this->yellow->page->get("pageError")."\n"; + echo "Your website has ".($statusCode!=200 ? "not " : "")."been updated: Please run command again\n"; + return $statusCode; + } + + // Process request to install pending extension + public function processRequestPending($scheme, $address, $base, $location, $fileName) { + $statusCode = $this->updateExtensions("install"); + if ($statusCode==200) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } + return $statusCode; + } + + // Process update notification + public function processUpdateNotification($extension, $action) { + $statusCode = 200; + if ($this->yellow->extension->isExisting($extension) && $action=="uninstall") { + $value = $this->yellow->extension->data[$extension]; + if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate($action); + } + $updateNotification = $this->yellow->system->get("updateNotification"); + if ($updateNotification=="none") $updateNotification = ""; + if (!empty($updateNotification)) $updateNotification .= ","; + $updateNotification .= "$extension/$action"; + $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); + if (!$this->yellow->system->save($fileName, array("updateNotification" => $updateNotification))) { + $statusCode = 500; + $this->yellow->page->error(500, "Can't write file '$fileName'!"); + } + return $statusCode; + } + + // Show extensions + public function showExtensions() { + list($statusCode, $settingsLatest) = $this->getExtensionSettings(true); + foreach ($settingsLatest as $key=>$value) { + $text = $description = $value->get("description"); + if ($value->isExisting("developer")) $text = "$description Developed by ".$value["developer"]."."; + if ($value->isExisting("translator")) $text = "$description Translated by ".$value["translator"]."."; + if ($value->isExisting("designer")) $text = "$description Designed by ".$value["designer"]."."; + echo ucfirst($key).": $text\n"; + } + if ($statusCode!=200) echo "ERROR checking extensions: ".$this->yellow->page->get("pageError")."\n"; + return $statusCode; + } + + // Download extensions + public function downloadExtensions($settings) { + $statusCode = 200; + $path = $this->yellow->system->get("coreExtensionDirectory"); + $fileExtension = $this->yellow->system->get("coreDownloadExtension"); + foreach ($settings as $key=>$value) { + $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip"; + list($statusCode, $fileData) = $this->getExtensionFile($value->get("downloadUrl")); + if (empty($fileData) || !$this->yellow->toolbox->createFile($fileName.$fileExtension, $fileData)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + break; + } + } + if ($statusCode==200) { + foreach ($settings as $key=>$value) { + $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip"; + if (!$this->yellow->toolbox->renameFile($fileName.$fileExtension, $fileName)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + } + } + return $statusCode; + } + + // Update extensions + public function updateExtensions($action) { + $statusCode = 200; + if (function_exists("opcache_reset")) opcache_reset(); + $path = $this->yellow->system->get("coreExtensionDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry) { + $statusCode = max($statusCode, $this->updateExtensionArchive($entry, $action)); + if (!$this->yellow->toolbox->deleteFile($entry)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't delete file '$entry'!"); + } + } + return $statusCode; + } + + // Update extension from archive + public function updateExtensionArchive($path, $action) { + $statusCode = 200; + $zip = new ZipArchive(); + if ($zip->open($path)===true) { + if (defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::updateExtensionArchive file:$path<br/>\n"; + $pathBase = ""; + if (preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1]; + $languages = $this->getExtensionArchiveLanguages($zip, $pathBase); + $fileData = $zip->getFromName($pathBase.$this->yellow->system->get("updateExtensionFile")); + $settings = $this->yellow->toolbox->getTextSettings($fileData, ""); + list($extension, $version, $newModified, $oldModified) = $this->getExtensionInformation($settings); + if (!empty($extension) && !empty($version)) { + $statusCode = $this->updateExtensionSettings($extension, $settings, $action); + if ($statusCode==200) { + foreach ($this->getExtensionFileNames($settings) as $fileName) { + list($entry, $flags) = $this->yellow->toolbox->getTextList($settings[$fileName], ",", 2); + if (strposu($entry, ".")===false) { // TODO: remove later, converts old extension settings + list($dummy, $entry, $flags) = $this->yellow->toolbox->getTextList($settings[$fileName], ",", 3); + } + if (!$this->yellow->lookup->isContentFile($fileName)) { + $fileData = $zip->getFromName($pathBase.$entry); + $lastModified = $this->yellow->toolbox->getFileModified($fileName); + $statusCode = $this->updateExtensionFile($fileName, $fileData, + $newModified, $oldModified, $lastModified, $flags, $extension); + } else { + foreach ($this->getExtensionContentRootPages() as $page) { + list($fileNameSource, $fileNameDestination) = $this->getExtensionContentFileNames( + $fileName, $pathBase, $entry, $flags, $languages, $page); + $fileData = $zip->getFromName($fileNameSource); + $lastModified = $this->yellow->toolbox->getFileModified($fileNameDestination); + $statusCode = $this->updateExtensionFile($fileNameDestination, $fileData, + $newModified, $oldModified, $lastModified, $flags, $extension); + } + } + if ($statusCode!=200) break; + } + $statusCode = max($statusCode, $this->processUpdateNotification($extension, $action)); + } + $this->yellow->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'"); + ++$this->updates; + } + $zip->close(); + } else { + $statusCode = 500; + $this->yellow->page->error(500, "Can't open file '$path'!"); + } + return $statusCode; + } + + // Update extension from file + public function updateExtensionFile($fileName, $fileData, $newModified, $oldModified, $lastModified, $flags, $extension) { + $statusCode = 200; + $fileName = $this->yellow->toolbox->normaliseTokens($fileName); + if ($this->yellow->lookup->isValidFile($fileName)) { + $create = $update = $delete = false; + if (preg_match("/create/i", $flags) && !is_file($fileName) && !empty($fileData)) $create = true; + if (preg_match("/update/i", $flags) && is_file($fileName) && !empty($fileData)) $update = true; + if (preg_match("/delete/i", $flags) && is_file($fileName)) $delete = true; + if (preg_match("/optional/i", $flags) && $this->yellow->extension->isExisting($extension)) $create = $update = $delete = false; + if (preg_match("/careful/i", $flags) && is_file($fileName) && $lastModified!=$oldModified) $update = false; + if ($create) { + if (!$this->yellow->toolbox->createFile($fileName, $fileData, true) || + !$this->yellow->toolbox->modifyFile($fileName, $newModified)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + } + if ($update) { + if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory")) || + !$this->yellow->toolbox->createFile($fileName, $fileData) || + !$this->yellow->toolbox->modifyFile($fileName, $newModified)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + } + if ($delete) { + if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory"))) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!"); + } + } + if (defined("DEBUG") && DEBUG>=2) { + $debug = "action:".($create ? "create" : "").($update ? "update" : "").($delete ? "delete" : ""); + if (!$create && !$update && !$delete) $debug = "action:none"; + echo "YellowUpdate::updateExtensionFile file:$fileName $debug<br/>\n"; + } + } + return $statusCode; + } + + // Remove extensions + public function removeExtensions($settings) { + $statusCode = 200; + if (function_exists("opcache_reset")) opcache_reset(); + foreach ($settings as $extension=>$block) { + $statusCode = max($statusCode, $this->removeExtensionArchive($extension, $block, "uninstall")); + } + return $statusCode; + } + + // Remove extension archive + public function removeExtensionArchive($extension, $settings, $action) { + $statusCode = 200; + $fileNames = $this->getExtensionFileNames($settings, true); + if (count($fileNames)) { + $statusCode = max($statusCode, $this->processUpdateNotification($extension, $action)); + foreach ($fileNames as $fileName) { + $statusCode = max($statusCode, $this->removeExtensionFile($fileName)); + } + if ($statusCode==200) $statusCode = $this->updateExtensionSettings($extension, $settings, $action); + $version = $settings->get("version"); + $this->yellow->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'"); + ++$this->updates; + } else { + $statusCode = 500; + $this->yellow->page->error(500, "Please delete extension '$extension' manually!"); + } + return $statusCode; + } + + // Remove extension file + public function removeExtensionFile($fileName) { + $statusCode = 200; + $fileName = $this->yellow->toolbox->normaliseTokens($fileName); + if ($this->yellow->lookup->isValidFile($fileName) && is_file($fileName)) { + if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory"))) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!"); + } + if (defined("DEBUG") && DEBUG>=2) { + echo "YellowUpdate::removeExtensionFile file:$fileName action:delete<br/>\n"; + } + } + return $statusCode; + } + + // Return extensions from text, space separated + public function getExtensionsFromText($text) { + return array_unique(array_filter($this->yellow->toolbox->getTextArguments($text), "strlen")); + } + + // Return extension install information + public function getExtensionInstallInformation($extensions) { + $settings = array(); + list($statusCodeCurrent, $settingsCurrent) = $this->getExtensionSettings(false); + list($statusCodeLatest, $settingsLatest) = $this->getExtensionSettings(true); + $statusCode = max($statusCodeCurrent, $statusCodeLatest); + foreach ($extensions as $extension) { + $found = false; + foreach ($settingsLatest as $key=>$value) { + if (strtoloweru($key)==strtoloweru($extension)) { + if (!$settingsCurrent->isExisting($key)) $settings[$key] = $settingsLatest[$key]; + $found = true; + break; + } + } + if (!$found) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); + } + } + return array($statusCode, $settings); + } + + // Return extension uninstall information + public function getExtensionUninstallInformation($extensions, $extensionsProtected) { + $settings = array(); + list($statusCode, $settingsCurrent) = $this->getExtensionSettings(false); + foreach ($extensions as $extension) { + $found = false; + foreach ($settingsCurrent as $key=>$value) { + if (strtoloweru($key)==strtoloweru($extension)) { + $settings[$key] = $settingsCurrent[$key]; + $found = true; + break; + } + } + if (!$found) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); + } + } + $protected = preg_split("/\s*,\s*/", $extensionsProtected); + foreach ($settings as $key=>$value) { + if (in_array($key, $protected)) unset($settings[$key]); + } + return array($statusCode, $settings); + } + + // Return extension update information + public function getExtensionUpdateInformation($extensions) { + $settings = array(); + list($statusCodeCurrent, $settingsCurrent) = $this->getExtensionSettings(false); + list($statusCodeLatest, $settingsLatest) = $this->getExtensionSettings(true); + $statusCode = max($statusCodeCurrent, $statusCodeLatest); + if (empty($extensions)) { + foreach ($settingsCurrent as $key=>$value) { + if ($settingsLatest->isExisting($key)) { + $versionCurrent = $settingsCurrent[$key]->get("version"); + $versionLatest = $settingsLatest[$key]->get("version"); + if (strnatcasecmp($versionCurrent, $versionLatest)<0) { + $settings[$key] = $settingsLatest[$key]; + } + } + } + } else { + $force = false; + foreach ($extensions as $extension) { + $found = false; + if ($extension=="force") { $force = true; continue; } + foreach ($settingsCurrent as $key=>$value) { + if (strtoloweru($key)==strtoloweru($extension) && $settingsLatest->isExisting($key)) { + $settings[$key] = $settingsLatest[$key]; + $found = true; + break; + } + } + if (!$found) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); + } + } + if (!$force) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Please use 'force' to update an extension!"); + } + } + if ($statusCode==200) { + foreach ($settings as $key=>$value) { + echo ucfirst($key)." ".$value->get("version")."\n"; + } + } + return array($statusCode, $settings); + } + + // Return extension settings + public function getExtensionSettings($latest) { + $statusCode = 200; + $settings = array(); + if (!$latest) { + $fileNameCurrent = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateCurrentFile"); + if (!is_file($fileNameCurrent)) $statusCode = $this->createExtensionSettings(); + $fileData = $this->yellow->toolbox->readFile($fileNameCurrent); + $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension"); + foreach ($settings as $key=>$value) { + if (!$this->yellow->extension->isExisting($key)) unset($settings[$key]); + } + foreach ($this->yellow->extension->data as $key=>$value) { + if (!$settings->isExisting($key)) $settings[$key] = new YellowArray(); + $settings[$key]["extension"] = ucfirst($key); + $settings[$key]["version"] = $value["version"]; + } + } else { + $fileNameLatest = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateLatestFile"); + $expire = $this->yellow->toolbox->getFileModified($fileNameLatest) + 60*10; + if ($expire<=time()) { + $url = $this->yellow->system->get("updateExtensionUrl")."/raw/master/".$this->yellow->system->get("updateLatestFile"); + list($statusCode, $fileData) = $this->getExtensionFile($url); + if ($statusCode==200 && !$this->yellow->toolbox->createFile($fileNameLatest, $fileData)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileNameLatest'!"); + } + } + $fileData = $this->yellow->toolbox->readFile($fileNameLatest); + $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension"); + } + $settings->uksort("strnatcasecmp"); + return array($statusCode, $settings); + } + + // Return extension archive languages + public function getExtensionArchiveLanguages($zip, $pathBase) { + $languages = array(); + for ($index=0; $index<$zip->numFiles; ++$index) { + $entry = substru($zip->getNameIndex($index), strlenu($pathBase)); + if (preg_match("#^(.*)\/.*?$#", $entry, $matches)) { + array_push($languages, $matches[1]); + } + } + return array_unique($languages); + } + + // Return extension information + public function getExtensionInformation($settings) { + $extension = lcfirst($settings->get("extension")); + $version = $settings->get("version"); + $newModified = strtotime($settings->get("published")); + $oldModified = 0; + foreach ($settings as $key=>$value) { + if (strposu($key, "/") && is_file($key)) { + $oldModified = filemtime($key); + break; + } + } + return array($extension, $version, $newModified, $oldModified); + } + + // Return extension file names + public function getExtensionFileNames($settings, $reverse = false) { + $fileNames = array(); + foreach ($settings as $key=>$value) { + if (strposu($key, "/")) array_push($fileNames, $key); + } + if ($reverse) $fileNames = array_reverse($fileNames); + return $fileNames; + } + + // Return extension root pages for content files + public function getExtensionContentRootPages() { + $rootPages = array(); + foreach ($this->yellow->content->scanLocation("") as $page) { + if ($page->isAvailable() && $page->isVisible()) array_push($rootPages, $page); + } + return $rootPages; + } + + // Return extension files names for content files + public function getExtensionContentFileNames($fileName, $pathBase, $entry, $flags, $languages, $page) { + if (preg_match("/multi-language/i", $flags)) { + $languageFound = ""; + $languagesWanted = array($page->get("language"), "en"); + foreach ($languagesWanted as $language) { + if (in_array($language, $languages)) { + $languageFound = $language; + break; + } + } + $pathLanguage = $languageFound ? "$languageFound/" : ""; + $fileNameSource = $pathBase.$pathLanguage.$entry; + } else { + $fileNameSource = $pathBase.$entry; + } + if ($this->yellow->system->get("coreMultiLanguageMode")) { + $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); + $fileNameDestination = $page->fileName.substru($fileName, $contentDirectoryLength); + } else { + $fileNameDestination = $fileName; + } + return array($fileNameSource, $fileNameDestination); + } + + // Return extension file + public function getExtensionFile($url) { + $urlRequest = $url; + if (preg_match("#^https://github.com/(.+)/raw/(.+)$#", $url, $matches)) $urlRequest = "https://raw.githubusercontent.com/".$matches[1]."/".$matches[2]; + $curlHandle = curl_init(); + curl_setopt($curlHandle, CURLOPT_URL, $urlRequest); + curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; YellowUpdate/".YellowUpdate::VERSION."; SoftwareUpdater)"); + curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30); + $rawData = curl_exec($curlHandle); + $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); + $fileData = ""; + curl_close($curlHandle); + if ($statusCode==200) { + $fileData = $rawData; + } elseif ($statusCode==0) { + $statusCode = 500; + list($scheme, $address) = $this->yellow->lookup->getUrlInformation($url); + $this->yellow->page->error($statusCode, "Can't connect to server '$scheme://$address'!"); + } else { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't download file '$url'!"); + } + if (defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::getExtensionFile status:$statusCode url:$url<br/>\n"; + return array($statusCode, $fileData); + } + + // Check if extension pending + public function isExtensionPending() { + $path = $this->yellow->system->get("coreExtensionDirectory"); + return count($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", false, false))>0; + } +} diff --git a/system/extensions/wiki/extension.ini b/system/extensions/wiki/extension.ini new file mode 100644 index 0000000..f981621 --- /dev/null +++ b/system/extensions/wiki/extension.ini @@ -0,0 +1,16 @@ +# Datenstrom Yellow extension settings + +Extension: Wiki +Version: 0.8.11 +Description: Wiki for your website. +HelpUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/wiki +DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/wiki.zip +Published: 2020-08-16 13:29:15 +Developer: Datenstrom +Tag: feature +system/extensions/wiki.php: wiki.php,create,update +system/layouts/wiki.html: wiki.html,create,update,careful +system/layouts/wikipages.html: wikipages.html,create,update,careful +content/shared/page-new-wiki.md: page-new-wiki.md,create,optional +content/2-wiki/page.md: page.md,create,optional +content/2-wiki/wiki-example.md: wiki-example.md,create,optional diff --git a/system/extensions/wiki/page-new-wiki.md b/system/extensions/wiki/page-new-wiki.md new file mode 100644 index 0000000..b26ac69 --- /dev/null +++ b/system/extensions/wiki/page-new-wiki.md @@ -0,0 +1,6 @@ +--- +Title: Wiki page +Layout: wiki +Tag: Example +--- +This is a new wiki page. \ No newline at end of file diff --git a/system/extensions/wiki/page.md b/system/extensions/wiki/page.md new file mode 100644 index 0000000..d2250ca --- /dev/null +++ b/system/extensions/wiki/page.md @@ -0,0 +1,9 @@ +--- +Title: Wiki +Layout: wikipages +LayoutNew: wiki +Tag: Example +--- +Datenstrom Yellow is for people who make small websites. Create small web pages, blogs and wikis. The focus is on people and that it's useful for you. No database, no admin panel, nothing that gets in your way. You make your website, we take care of the rest. + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna pizza. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. diff --git a/system/extensions/wiki/wiki-example.md b/system/extensions/wiki/wiki-example.md new file mode 100644 index 0000000..6f11757 --- /dev/null +++ b/system/extensions/wiki/wiki-example.md @@ -0,0 +1,8 @@ +--- +Title: Wiki example +Layout: wiki +Tag: Example +--- +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna pizza. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna pizza. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. \ No newline at end of file diff --git a/system/extensions/wiki/wiki.html b/system/extensions/wiki/wiki.html new file mode 100644 index 0000000..0f64c47 --- /dev/null +++ b/system/extensions/wiki/wiki.html @@ -0,0 +1,20 @@ +<?php $this->yellow->layout("header") ?> +<div class="content"> +<div class="main" role="main"> +<?php $this->yellow->page->set("entryClass", "entry") ?> +<?php if ($this->yellow->page->isExisting("tag")): ?> +<?php foreach (preg_split("/\s*,\s*/", $this->yellow->page->get("tag")) as $tag) { $this->yellow->page->set("entryClass", $this->yellow->page->get("entryClass")." tag-".$this->yellow->toolbox->normaliseArguments($tag, false)); } ?> +<?php endif ?> +<div class="<?php echo $this->yellow->page->getHtml("entryClass") ?>"> +<div class="entry-title"><h1><?php echo $this->yellow->page->getHtml("titleContent") ?></h1></div> +<div class="entry-meta"><p><?php echo $this->yellow->language->getTextHtml("wikiModified") ?> <?php echo $this->yellow->page->getDateHtml("modified") ?></p></div> +<div class="entry-content"><?php echo $this->yellow->page->getContent() ?></div> +<?php if ($this->yellow->page->isExisting("tag")): ?> +<div class="entry-tags"> +<p><?php echo $this->yellow->language->getTextHtml("wikiTag") ?> <?php $tagCounter = 0; foreach (preg_split("/\s*,\s*/", $this->yellow->page->get("tag")) as $tag) { if (++$tagCounter>1) echo ", "; echo "<a href=\"".$this->yellow->page->getPage("wiki")->getLocation(true).$this->yellow->toolbox->normaliseArguments("tag:$tag")."\">".htmlspecialchars($tag)."</a>"; } ?></p> +</div> +<?php endif ?> +</div> +</div> +</div> +<?php $this->yellow->layout("footer") ?> diff --git a/system/extensions/wiki/wiki.php b/system/extensions/wiki/wiki.php new file mode 100644 index 0000000..bf1c4e7 --- /dev/null +++ b/system/extensions/wiki/wiki.php @@ -0,0 +1,275 @@ +<?php +// Wiki extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/wiki + +class YellowWiki { + const VERSION = "0.8.11"; + public $yellow; // access to API + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + $this->yellow->system->setDefault("wikiLocation", ""); + $this->yellow->system->setDefault("wikiNewLocation", "@title"); + $this->yellow->system->setDefault("wikiPagesMax", "5"); + $this->yellow->system->setDefault("wikiPaginationLimit", "30"); + } + + // Handle page meta data + public function onParseMeta($page) { + if ($page===$this->yellow->page) { + if ($page->get("layout")=="wikipages" && !$this->yellow->toolbox->isLocationArguments()) { + $page->set("layout", $page->isExisting("layoutShow") ? $page->get("layoutShow") : "wiki"); + } + } + } + + // Handle page content of shortcut + public function onParseContentShortcut($page, $name, $text, $type) { + $output = null; + if (substru($name, 0, 4)=="wiki" && ($type=="block" || $type=="inline")) { + switch($name) { + case "wikiauthors": $output = $this->getShorcutWikiauthors($page, $name, $text); break; + case "wikipages": $output = $this->getShorcutWikipages($page, $name, $text); break; + case "wikichanges": $output = $this->getShorcutWikichanges($page, $name, $text); break; + case "wikirelated": $output = $this->getShorcutWikirelated($page, $name, $text); break; + case "wikitags": $output = $this->getShorcutWikitags($page, $name, $text); break; + } + } + return $output; + } + + // Return wikiauthors shortcut + public function getShorcutWikiauthors($page, $name, $text) { + $output = null; + list($location, $pagesMax) = $this->yellow->toolbox->getTextArguments($text); + if (empty($location)) $location = $this->yellow->system->get("wikiLocation"); + if (strempty($pagesMax)) $pagesMax = $this->yellow->system->get("wikiPagesMax"); + $wiki = $this->yellow->content->find($location); + $pages = $this->getWikiPages($location); + $page->setLastModified($pages->getModified()); + $authors = $this->getMeta($pages, "author"); + if (count($authors)) { + $authors = $this->yellow->lookup->normaliseUpperLower($authors); + if ($pagesMax!=0 && count($authors)>$pagesMax) { + uasort($authors, "strnatcasecmp"); + $authors = array_slice($authors, -$pagesMax); + } + uksort($authors, "strnatcasecmp"); + $output = "<div class=\"".htmlspecialchars($name)."\">\n"; + $output .= "<ul>\n"; + foreach ($authors as $key=>$value) { + $output .= "<li><a href=\"".$wiki->getLocation(true).$this->yellow->toolbox->normaliseArguments("author:$key")."\">"; + $output .= htmlspecialchars($key)."</a></li>\n"; + } + $output .= "</ul>\n"; + $output .= "</div>\n"; + } else { + $page->error(500, "Wikiauthors '$location' does not exist!"); + } + return $output; + } + + // Return wikiauthors shortcut + public function getShorcutWikipages($page, $name, $text) { + $output = null; + list($location, $pagesMax, $author, $tag) = $this->yellow->toolbox->getTextArguments($text); + if (empty($location)) $location = $this->yellow->system->get("wikiLocation"); + if (strempty($pagesMax)) $pagesMax = $this->yellow->system->get("wikiPagesMax"); + $wiki = $this->yellow->content->find($location); + $pages = $this->getWikiPages($location); + if (!empty($author)) $pages->filter("author", $author); + if (!empty($tag)) $pages->filter("tag", $tag); + $pages->sort("title"); + $page->setLastModified($pages->getModified()); + if (count($pages)) { + if ($pagesMax!=0) $pages->limit($pagesMax); + $output = "<div class=\"".htmlspecialchars($name)."\">\n"; + $output .= "<ul>\n"; + foreach ($pages as $pageWiki) { + $output .= "<li><a".($pageWiki->isExisting("tag") ? " class=\"".$this->getClass($pageWiki)."\"" : ""); + $output .= " href=\"".$pageWiki->getLocation(true)."\">".$pageWiki->getHtml("title")."</a></li>\n"; + } + $output .= "</ul>\n"; + $output .= "</div>\n"; + } else { + $page->error(500, "Wikipages '$location' does not exist!"); + } + return $output; + } + + // Return wikiauthors shortcut + public function getShorcutWikichanges($page, $name, $text) { + $output = null; + list($location, $pagesMax, $author, $tag) = $this->yellow->toolbox->getTextArguments($text); + if (empty($location)) $location = $this->yellow->system->get("wikiLocation"); + if (strempty($pagesMax)) $pagesMax = $this->yellow->system->get("wikiPagesMax"); + $wiki = $this->yellow->content->find($location); + $pages = $this->getWikiPages($location); + if (!empty($author)) $pages->filter("author", $author); + if (!empty($tag)) $pages->filter("tag", $tag); + $pages->sort("modified", false); + $page->setLastModified($pages->getModified()); + if (count($pages)) { + if ($pagesMax!=0) $pages->limit($pagesMax); + $output = "<div class=\"".htmlspecialchars($name)."\">\n"; + $output .= "<ul>\n"; + foreach ($pages as $pageWiki) { + $output .= "<li><a".($pageWiki->isExisting("tag") ? " class=\"".$this->getClass($pageWiki)."\"" : ""); + $output .= " href=\"".$pageWiki->getLocation(true)."\">".$pageWiki->getHtml("title")."</a></li>\n"; + } + $output .= "</ul>\n"; + $output .= "</div>\n"; + } else { + $page->error(500, "Wikichanges '$location' does not exist!"); + } + return $output; + } + + // Return wikiauthors shortcut + public function getShorcutWikirelated($page, $name, $text) { + $output = null; + list($location, $pagesMax) = $this->yellow->toolbox->getTextArguments($text); + if (empty($location)) $location = $this->yellow->system->get("wikiLocation"); + if (strempty($pagesMax)) $pagesMax = $this->yellow->system->get("wikiPagesMax"); + $wiki = $this->yellow->content->find($location); + $pages = $this->getWikiPages($location); + $pages->similar($page->getPage("main")); + $page->setLastModified($pages->getModified()); + if (count($pages)) { + if ($pagesMax!=0) $pages->limit($pagesMax); + $output = "<div class=\"".htmlspecialchars($name)."\">\n"; + $output .= "<ul>\n"; + foreach ($pages as $pageWiki) { + $output .= "<li><a".($pageWiki->isExisting("tag") ? " class=\"".$this->getClass($pageWiki)."\"" : ""); + $output .= " href=\"".$pageWiki->getLocation(true)."\">".$pageWiki->getHtml("title")."</a></li>\n"; + } + $output .= "</ul>\n"; + $output .= "</div>\n"; + } else { + $page->error(500, "Wikirelated '$location' does not exist!"); + } + return $output; + } + + // Return wikiauthors shortcut + public function getShorcutWikitags($page, $name, $text) { + $output = null; + list($location, $pagesMax) = $this->yellow->toolbox->getTextArguments($text); + if (empty($location)) $location = $this->yellow->system->get("wikiLocation"); + if (strempty($pagesMax)) $pagesMax = $this->yellow->system->get("wikiPagesMax"); + $wiki = $this->yellow->content->find($location); + $pages = $this->getWikiPages($location); + $page->setLastModified($pages->getModified()); + $tags = $this->getMeta($pages, "tag"); + if (count($tags)) { + $tags = $this->yellow->lookup->normaliseUpperLower($tags); + if ($pagesMax!=0 && count($tags)>$pagesMax) { + uasort($tags, "strnatcasecmp"); + $tags = array_slice($tags, -$pagesMax); + } + uksort($tags, "strnatcasecmp"); + $output = "<div class=\"".htmlspecialchars($name)."\">\n"; + $output .= "<ul>\n"; + foreach ($tags as $key=>$value) { + $output .= "<li><a href=\"".$wiki->getLocation(true).$this->yellow->toolbox->normaliseArguments("tag:$key")."\">"; + $output .= htmlspecialchars($key)."</a></li>\n"; + } + $output .= "</ul>\n"; + $output .= "</div>\n"; + } else { + $page->error(500, "Wikitags '$location' does not exist!"); + } + return $output; + } + + // Handle page layout + public function onParsePageLayout($page, $name) { + if ($name=="wikipages") { + $chronologicalOrder = false; + $pages = $this->getWikiPages($this->yellow->page->location); + $pagesFilter = array(); + if ($page->getRequest("special")=="pages") { + array_push($pagesFilter, $this->yellow->language->getText("wikiSpecialPages")); + } + if ($page->getRequest("special")=="changes") { + $chronologicalOrder = true; + array_push($pagesFilter, $this->yellow->language->getText("wikiSpecialChanges")); + } + if ($page->isRequest("tag")) { + $pages->filter("tag", $page->getRequest("tag")); + array_push($pagesFilter, $pages->getFilter()); + } + if ($page->isRequest("author")) { + $pages->filter("author", $page->getRequest("author"), false); + array_push($pagesFilter, $pages->getFilter()); + } + if ($page->isRequest("modified")) { + $pages->filter("modified", $page->getRequest("modified"), false); + array_push($pagesFilter, $this->yellow->language->normaliseDate($pages->getFilter())); + } + $pages->sort($chronologicalOrder ? "modified" : "title", $chronologicalOrder); + $pages->pagination($this->yellow->system->get("wikiPaginationLimit")); + if (!$pages->getPaginationNumber()) $this->yellow->page->error(404); + if (!empty($pagesFilter)) { + $text = implode(" ", $pagesFilter); + $this->yellow->page->set("titleHeader", $text." - ".$this->yellow->page->get("sitename")); + $this->yellow->page->set("titleContent", $this->yellow->page->get("title").": ".$text); + $this->yellow->page->set("title", $this->yellow->page->get("title").": ".$text); + $this->yellow->page->set("wikipagesChronologicalOrder", $chronologicalOrder); + } + $this->yellow->page->setPages("wiki", $pages); + $this->yellow->page->setLastModified($pages->getModified()); + $this->yellow->page->setHeader("Cache-Control", "max-age=60"); + } + if ($name=="wiki") { + $location = $this->yellow->system->get("wikiLocation"); + if (empty($location)) $location = $this->yellow->lookup->getDirectoryLocation($this->yellow->page->location); + $wiki = $this->yellow->content->find($location); + $this->yellow->page->setPage("wiki", $wiki); + } + } + + // Handle content file editing + public function onEditContentFile($page, $action, $email) { + if ($page->get("layout")=="wiki") $page->set("pageNewLocation", $this->yellow->system->get("wikiNewLocation")); + } + + // Return wiki pages + public function getWikiPages($location) { + $pages = $this->yellow->content->clean(); + $wiki = $this->yellow->content->find($location); + if ($wiki) { + if ($location==$this->yellow->system->get("wikiLocation")) { + $pages = $this->yellow->content->index(!$wiki->isVisible()); + } else { + $pages = $wiki->getChildren(!$wiki->isVisible()); + } + $wiki->set("layout", $wiki->isExisting("layoutShow") ? $wiki->get("layoutShow") : "wiki"); + $pages->append($wiki)->filter("layout", "wiki"); + } + return $pages; + } + + // Return class for page + public function getClass($page) { + if ($page->isExisting("tag")) { + foreach (preg_split("/\s*,\s*/", $page->get("tag")) as $tag) { + $class .= " tag-".$this->yellow->toolbox->normaliseArguments($tag, false); + } + } + return trim($class); + } + + // Return meta data from page collection + public function getMeta($pages, $key) { + $data = array(); + foreach ($pages as $page) { + if ($page->isExisting($key)) { + foreach (preg_split("/\s*,\s*/", $page->get($key)) as $entry) { + ++$data[$entry]; + } + } + } + return $data; + } +} diff --git a/system/extensions/wiki/wikipages.html b/system/extensions/wiki/wikipages.html new file mode 100644 index 0000000..c769b04 --- /dev/null +++ b/system/extensions/wiki/wikipages.html @@ -0,0 +1,20 @@ +<?php $this->yellow->layout("header") ?> +<div class="content"> +<div class="main" role="main"> +<h1><?php echo $this->yellow->page->getHtml("titleContent") ?></h1> +<ul> +<?php $section = $sectionNew = "" ?> +<?php foreach ($this->yellow->page->getPages("wiki") as $page): ?> +<?php if ($this->yellow->page->get("wikipagesChronologicalOrder")): ?> +<?php $sectionNew = htmlspecialchars($page->getDate("modified")) ?> +<?php else: ?> +<?php $sectionNew = htmlspecialchars(strtoupperu(substru($page->get("title"), 0, 1))) ?> +<?php endif ?> +<?php if ($section!=$sectionNew) { $section = $sectionNew; echo "</ul><h2>$section</h2><ul>\n"; } ?> +<li><a href="<?php echo $page->getLocation(true) ?>"><?php echo $page->getHtml("title") ?></a></li> +<?php endforeach ?> +</ul> +<?php $this->yellow->layout("pagination", $this->yellow->page->getPages("wiki")) ?> +</div> +</div> +<?php $this->yellow->layout("footer") ?> diff --git a/system/extensions/yellow.log b/system/extensions/yellow.log new file mode 100644 index 0000000..dcf43e8 --- /dev/null +++ b/system/extensions/yellow.log @@ -0,0 +1,5 @@ +2020-08-21 08:49:53 info Datenstrom Yellow 0.8.15, PHP 7.3.8, Apache 2.4.34, Mac +2020-08-21 08:49:53 info Install extension 'English 0.8.24' +2020-08-21 08:49:53 info Install extension 'French 0.8.24' +2020-08-21 08:49:53 info Install extension 'German 0.8.24' +2020-08-21 08:50:35 info Add user 'Alphonse' diff --git a/system/layouts/default.html b/system/layouts/default.html new file mode 100644 index 0000000..9c1fb60 --- /dev/null +++ b/system/layouts/default.html @@ -0,0 +1,8 @@ +<?php $this->yellow->layout("header") ?> +<div class="content"> +<div class="main" role="main"> +<h1><?php echo $this->yellow->page->getHtml("titleContent") ?></h1> +<?php echo $this->yellow->page->getContent() ?> +</div> +</div> +<?php $this->yellow->layout("footer") ?> diff --git a/system/layouts/error.html b/system/layouts/error.html new file mode 100644 index 0000000..9c1fb60 --- /dev/null +++ b/system/layouts/error.html @@ -0,0 +1,8 @@ +<?php $this->yellow->layout("header") ?> +<div class="content"> +<div class="main" role="main"> +<h1><?php echo $this->yellow->page->getHtml("titleContent") ?></h1> +<?php echo $this->yellow->page->getContent() ?> +</div> +</div> +<?php $this->yellow->layout("footer") ?> diff --git a/system/layouts/footer.html b/system/layouts/footer.html new file mode 100644 index 0000000..bd6fdbb --- /dev/null +++ b/system/layouts/footer.html @@ -0,0 +1,10 @@ +<div class="footer" role="contentinfo"> +<div class="siteinfo"> +<?php echo $this->yellow->page->getPage("footer")->getContent() ?> +</div> +<div class="siteinfo-banner"></div> +</div> +</div> +<?php echo $this->yellow->page->getExtra("footer") ?> +</body> +</html> diff --git a/system/layouts/header.html b/system/layouts/header.html new file mode 100644 index 0000000..d078bff --- /dev/null +++ b/system/layouts/header.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html lang="<?php echo $this->yellow->page->getHtml("language") ?>"> +<head> +<title><?php echo $this->yellow->page->getHtml("titleHeader") ?> + +" /> +" /> + + +yellow->page->getExtra("header") ?> + + +
        yellow->page->getHtml("layout") ?>"> + diff --git a/system/layouts/navigation.html b/system/layouts/navigation.html new file mode 100644 index 0000000..96c70b5 --- /dev/null +++ b/system/layouts/navigation.html @@ -0,0 +1,10 @@ +yellow->content->top() ?> +yellow->page->setLastModified($pages->getModified()) ?> + + diff --git a/system/layouts/pagination.html b/system/layouts/pagination.html new file mode 100644 index 0000000..2bb8d67 --- /dev/null +++ b/system/layouts/pagination.html @@ -0,0 +1,11 @@ +yellow->getLayoutArguments() ?> +isPagination()): ?> + + diff --git a/system/settings/language.ini b/system/settings/language.ini new file mode 100644 index 0000000..177002a --- /dev/null +++ b/system/settings/language.ini @@ -0,0 +1,9 @@ +# Datenstrom Yellow language settings + +Language: en +CoreDateFormatShort: F Y +CoreDateFormatMedium: Y-m-d +CoreDateFormatLong: Y-m-d H:i +EditMailFooter: @sitename +ImageDefaultAlt: Image without description +media/images/photo.jpg: This is an example image diff --git a/system/settings/system.ini b/system/settings/system.ini new file mode 100644 index 0000000..c2dff4d --- /dev/null +++ b/system/settings/system.ini @@ -0,0 +1,70 @@ +# Datenstrom Yellow system settings + +Sitename: Datenstrom Yellow +Author: Alphonse +Email: alphonse@brown.com +Language: en +Layout: default +Theme: stockholm +Parser: markdown +Status: public +CoreStaticUrl: http://localhost/sandbox/www/yellow/ +CoreServerUrl: auto +CoreServerTimezone: Europe/Zurich +CoreMultiLanguageMode: 0 +CoreMediaLocation: /media/ +CoreDownloadLocation: /media/downloads/ +CoreImageLocation: /media/images/ +CoreExtensionLocation: /media/extensions/ +CoreThemeLocation: /media/themes/ +CoreMediaDirectory: media/ +CoreDownloadDirectory: media/downloads/ +CoreImageDirectory: media/images/ +CoreSystemDirectory: system/ +CoreExtensionDirectory: system/extensions/ +CoreSettingDirectory: system/settings/ +CoreLayoutDirectory: system/layouts/ +CoreThemeDirectory: system/themes/ +CoreTrashDirectory: system/trash/ +CoreCacheDirectory: cache/ +CoreContentDirectory: content/ +CoreContentRootDirectory: default/ +CoreContentHomeDirectory: home/ +CoreContentSharedDirectory: shared/ +CoreContentDefaultFile: page.md +CoreContentErrorFile: page-error-(.*).md +CoreContentExtension: .md +CoreDownloadExtension: .download +CoreUserFile: user.ini +CoreLanguageFile: language.ini +CoreLogFile: yellow.log +UpdateExtensionUrl: https://github.com/datenstrom/yellow-extensions +UpdateExtensionFile: extension.ini +UpdateCurrentFile: update-current.ini +UpdateLatestFile: update-latest.ini +UpdateNotification: none +CommandStaticBuildDirectory: public/ +CommandStaticDefaultFile: index.html +CommandStaticErrorFile: 404.html +EditLocation: /edit/ +EditUploadNewLocation: /media/@group/@filename +EditUploadExtensions: .gif, .jpg, .pdf, .png, .svg, .zip +EditKeyboardShortcuts: ctrl+b bold, ctrl+i italic, ctrl+k strikethrough, ctrl+e code, ctrl+s save, ctrl+alt+p preview +EditToolbarButtons: auto +EditEndOfLine: auto +EditNewFile: page-new-(.*).md +EditUserPasswordMinLength: 8 +EditUserHashAlgorithm: bcrypt +EditUserHashCost: 10 +EditUserHome: / +EditUserAccess: create, edit, delete, upload +EditLoginRestriction: 0 +EditLoginSessionTimeout: 2592000 +EditBruteForceProtection: 25 +ImageUploadWidthMax: 1280 +ImageUploadHeightMax: 1280 +ImageUploadJpgQuality: 80 +ImageThumbnailLocation: /media/thumbnails/ +ImageThumbnailDirectory: media/thumbnails/ +ImageThumbnailJpgQuality: 80 +MetaDefaultImage: favicon diff --git a/system/settings/user.ini b/system/settings/user.ini new file mode 100644 index 0000000..d95b616 --- /dev/null +++ b/system/settings/user.ini @@ -0,0 +1,13 @@ +# Datenstrom Yellow user settings + +Email: alphonse@brown.com +Name: Alphonse +Language: en +Home: / +Access: create, edit, delete, upload, system, update +Hash: $2y$10$23/5JKZ8aWZQiImOfk9huuxR.VqVPfyWY8qHu1A9Ps3uelEGb5Ss. +Stamp: ed6881f247fd2846df04 +Pending: none +Failed: 0 +Modified: 2020-08-21 10:51:08 +Status: active diff --git a/system/themes/stockholm-opensans-bold.woff b/system/themes/stockholm-opensans-bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..ca2f1c277d2051aad3e7af99a3b6f1761469f8f7 GIT binary patch literal 14192 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 J*BSue{{xGvL4g1O literal 0 HcmV?d00001 diff --git a/system/themes/stockholm-opensans-license.txt b/system/themes/stockholm-opensans-license.txt new file mode 100644 index 0000000..989e2c5 --- /dev/null +++ b/system/themes/stockholm-opensans-license.txt @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/system/themes/stockholm-opensans-light.woff b/system/themes/stockholm-opensans-light.woff new file mode 100644 index 0000000000000000000000000000000000000000..32da261ff36f23ee1e26f76d94a1071d7b32dfd9 GIT binary patch literal 14140 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 Vr-`e1+#_?EcHIe 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 literal 0 HcmV?d00001 diff --git a/system/themes/stockholm.css b/system/themes/stockholm.css new file mode 100644 index 0000000..14a4885 --- /dev/null +++ b/system/themes/stockholm.css @@ -0,0 +1,362 @@ +/* Stockholm extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/stockholm */ + +html, body, div, form, pre, span, tr, th, td, img { + margin: 0; + padding: 0; + border: 0; + vertical-align: baseline; +} +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 300; + src: url(stockholm-opensans-light.woff) format("woff"); +} +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 400; + src: url(stockholm-opensans-regular.woff) format("woff"); +} +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 700; + src: url(stockholm-opensans-bold.woff) format("woff"); +} +body { + margin: 1em; + background-color: #fff; + color: #666; + font-family: "Open Sans", Helvetica, sans-serif; + font-size: 1em; + font-weight: 300; + line-height: 1.5; +} +h1, +h2, +h3, +h4, +h5, +h6 { + color: #111; + font-weight: 400; +} +h1 { + font-size: 2em; +} +hr { + height: 1px; + background: #ddd; + border: 0; +} +strong { + font-weight: bold; +} +code { + font-size: 1.1em; +} +a { + color: #07d; + text-decoration: none; +} +a:hover { + color: #07d; + text-decoration: underline; +} + +/* Content */ + +.content h1 { + margin: 1em 0; +} +.content h1 a { + color: #111; +} +.content h1 a:hover { + color: #111; + text-decoration: none; +} +.content img { + max-width: 100%; + height: auto; +} +.content form { + margin: 1em 0; +} +.content table { + border-spacing: 0; + border-collapse: collapse; +} +.content th { + text-align: left; + padding: 0.3em; +} +.content td { + text-align: left; + padding: 0.3em; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; +} +.content code, +.content pre { + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 90%; +} +.content code { + padding: 0.15em 0.4em; + margin: 0; + background-color: #f7f7f7; + border-radius: 3px; +} +.content pre > code { + padding: 0; + margin: 0; + white-space: pre; + background: transparent; + border: 0; + font-size: inherit; +} +.content pre { + padding: 1em; + overflow: auto; + line-height: 1.45; + background-color: #f7f7f7; + border-radius: 3px; +} +.content blockquote { + margin-left: 0; + padding-left: 1em; + border-left: 1px solid #ddd; +} +.content .notice1 { + margin: 1em 0; + padding: 10px 1em; + background-color: #fffbf0; + border-left: 10px solid #fb0; +} +.content .notice2 { + margin: 1em 0; + padding: 10px 1em; + background-color: #fdf0f0; + border-left: 10px solid #d00; +} +.content .notice3, +.content .notice4, +.content .notice5, +.content .notice6 { + margin: 1em 0; + padding: 10px 1em; + background-color: #f0f8fe; + border-left: 10px solid #08e; +} +.content .flexible { + position: relative; + padding-top: 0; + padding-bottom: 56.25%; +} +.content .flexible iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.content .task-list-item { + list-style-type: none; +} +.content .task-list-item input { + margin: 0 0.2em 0.25em -1.75em; + vertical-align: middle; +} +.content .toc { + margin: 0; + padding: 0; + list-style: none; +} +.content .wikipages ul, +.content .wikitags ul, +.content .wikilinks ul { + padding: 0; + list-style: none; + column-width: 19em; +} +.content .entry-links .previous { + margin-right: 1em; +} +.content .pagination .previous { + margin-right: 1em; +} +.content .pagination { + margin: 1em 0; +} +.content .left { + float: left; + margin: 0 1em 0 0; +} +.content .center { + display: block; + margin: 0 auto; +} +.content .right { + float: right; + margin: 0 0 0 1em; +} +.content .rounded { + border-radius: 4px; +} + +/* Header */ + +.header { + margin: 2em 0; +} +.header .sitename { + display: block; + float: left; +} +.header .sitename h1 { + margin: 0; + font-size: 1em; + font-weight: 300; +} +.header .sitename h1 a { + color: #666; + border-bottom: solid 3px #fff; + text-decoration: none; + padding: 0.5em 0; +} +.header .sitename h1 a:hover { + color: #07d; + border-bottom: solid 3px #29f; +} +.header .sitename p { + margin-top: 0; + color: #666; +} + +/* Navigation */ + +.navigation { + display: block; + float: right; +} +.navigation a { + color: #666; + border-bottom: solid 3px #fff; + text-decoration: none; + padding: 0.5em 0; + margin: 0 0.5em; +} +.navigation a:hover { + color: #07d; + border-bottom: solid 3px #29f; +} +.navigation ul { + margin: 0 -0.5em; + padding: 0; + list-style: none; +} +.navigation li { + display: inline; +} +.navigation li a.active { + border-bottom: solid 3px #29f; +} +.navigation-banner { + clear: both; +} + +/* Footer */ + +.footer { + margin: 2em 0; +} +.footer .siteinfo a { + color: #07d; +} +.footer .siteinfo a:hover { + color: #07d; + text-decoration: underline; +} + +/* Forms and buttons */ + +.form-control { + margin: 0; + padding: 2px 4px; + display: inline-block; + min-width: 7em; + background-color: #fff; + color: #666; + background-image: linear-gradient(to bottom, #fff, #fff); + border: 1px solid #bbb; + border-radius: 4px; + font-size: 0.9em; + font-family: inherit; + font-weight: normal; + line-height: normal; +} +.btn { + margin: 0; + padding: 4px 22px; + display: inline-block; + min-width: 7em; + background-color: #eaeaea; + color: #333333; + background-image: linear-gradient(to bottom, #f8f8f8, #e1e1e1); + border: 1px solid #bbb; + border-color: #c1c1c1 #c1c1c1 #aaaaaa; + border-radius: 4px; + outline-offset: -2px; + font-size: 0.9em; + font-family: inherit; + font-weight: normal; + line-height: 1; + text-align: center; + text-decoration: none; + box-sizing: border-box; +} +.btn:hover, +.btn:focus, +.btn:active { + color: #333333; + background-image: none; + text-decoration: none; +} +.btn:active { + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Responsive and print */ + +.page { + margin: 0 auto; + max-width: 1000px; +} + +@media screen and (min-width: 62em) { + body { + width: 60em; + margin: 1em auto; + } + .page { + margin: 0; + max-width: none; + } +} +@media screen and (max-width: 32em) { + body { + margin: 0.5em; + font-size: 0.9em; + } + .content h1, + .content h2 { + font-size: 1.5em; + } +} +@media print { + .page { + border: none !important; + } +} diff --git a/system/themes/stockholm.png b/system/themes/stockholm.png new file mode 100644 index 0000000000000000000000000000000000000000..7d0ecef87da32e4a6eb7153aa4c288b54214cec5 GIT binary patch literal 2033 zcmVPx+t4TybRCwCmTT5&kMHo(Lc}Tn^5E6n5;wf=JAf6IZ5K>XR_HNSFLLh`F5(qBb zflJy09N@$QLWe|BxpuGijK$FC6S z)6UG!WRPyHf@Ehj3ddbMEX){0kRJ9%q?ty`UT?7A@ zKY)QZ0rZKpaEM~zr2!<-BD>XCWy6j$qOVPr`~MlwJ(S) zTs#2Ax>iJ#=!v2L{1ylFic9Df@6^PL?}lUX-8*pB(cJ~Z$=#9YIk<3u&o-t_1PCZ2 z4ESY)djxvOq?wOB=?V+j0b#-2efI5Z1>%pfb!{BZ?*}615HEYki?9e(-x2D_)=BMN zQHVY!Uz>3_6~7f{@$(YJIFd06HnAqe-N<8D6zs6T{NF}l`2A|%$NDTRxVsCpjt0Mq zIA3*lB*SEh`-DaLG={|{Uo+cMF`x@! z0iy%gxc2mu+UrRCEc_nld-^uqJxo}bd~LbF#V#0y5zRF*C`LzC_u1JeYO8x8_6!mo zBO}NyaYhVhPlqmn#xmjTK7e2M(YE;m%c&1l>?2#-wm$H~h|}30g`rzmUj3}%ijG@a zT_o-Wj<1%T&@16?FJH*zMmxw4Gj%zi1>1??elDJ#E>7Kw4 zVPPL;M0el!xLG;Opu0&NW_ksCw?G^DK?-dq+iN>k$gn_Hx{2J~I#pHH>an}ag6~3P zGuciYh)XE%3PziQeF4gq0su|uQ2 zGrs`>-#03gZO7Iec*}APu;y+fAwZLkWDHrWAJn(JM+gpqwf-`=C1ic_*{}j+8+6oc z(5z2B>uHyBfzQy*|I)BumG$5WpLZDVxxTmthhUNrB~_)?iU0L&=o^s z0r`No%Q2@-%+ogJkL5}mDFI?0qZ)MTdgrOn2UHyh2Gr%dk6;0MY+%Q*)D8ibBnD}O zp!Z!uumDT_VQ5QO^rRQ--*-*`zrh!M@;yed0E_))@V2nfEddsq56a8>1i=C<^q0YH z!+f^|m~R!)24zvndVcL{?Slnq+Hq9UfN1A2*IFA%0oq{PoX5zZvMwQ5fVtin76!Ux zS@#9VND|06bw$iI&g(u;J>%|2ri`A!0{9u1sW+xsnI58yg{Y@D5Fw~sd>{PZVjw<5}WCcG0mZb zsHZe`BN;z17bkxGiR(l925~LJvppB@I_Llj*YX*0HmR5>P#seUS7b2GMpRQ(j zgA$twJ8=Ji*)w0#r}eJtpO?JYnF;W;P?GP@QqNRG8*zc zb~XlUc+~`d?dhqAiDjjb#&r@UeG21zUWIZ7e~qT1$BWa^XI)q2kW*`yB!#Q#`c(8S z9IOEO5wK{xOerSj%hHBUNkF!4m3YJ~&BlOcolPlgEEU`9x;opAtM%eF*$c<_-iQNz ziYi})m0@lS=8IC~+J;D(h!@e#Av;17aT#rRx8k+@l4rZWIifGQ#Pwk-HFOUebCesg zpjJe%>Wob~5YtJY_d#&#G9+%`?87Rb@hcdPA3;jJD|DQ1>+?6^#&|KQJhzopGO z2XsZ688o(3Gd|)Wk*h&!!^}wtN$G+d_CgUD--TUO}Tb z$R*7bH{L{)Igv!fO{Pk!Njgy#qQr^MsX!uQ`h<~;J?y?2-hnIft$Z5y*MxGrF{ONn zM*hRsheV{zNQi_$N)pT_Zk8y-7B$#PD4DIvp_f~}1l{?s#cJ!r)f37i=(68g)3)*Y zWW?g|5}kCA5S}Dj0^=NKpUxkc{!^b+K4>I*e|i&RAPexxvVpH6GXJSOMh{BYr z#OuM|lV~O_dXtf_E{_f0*0VTo*ZQDOL=GaEb97LlnZ>k;D09*i92TJ}R3h@Nkx(A( zt1ra1{psx3;0@@?hln&RXhgb3$U9 z8=7e%O|!l=F|^@Y;5hLe_|0(O`XK(kn1~Ld5aar|a(MNa@SRVW7r6fb@RX#K2b%_6 P00000NkvXXu0mjfUmDtb literal 0 HcmV?d00001 diff --git a/yellow.php b/yellow.php new file mode 100644 index 0000000..be7b8e5 --- /dev/null +++ b/yellow.php @@ -0,0 +1,14 @@ +load(); + $yellow->request(); +} else { + $yellow = new YellowCore(); + $yellow->load(); + exit($yellow->command()); +}