commit d15f38582f400caf6dab16480676c9a1b0f4f7e2
parent c51f8cbd5a5ab2905bfff2202146e700372f259b
Author: Ashymad <szymon.mikulicz@posteo.net>
Date: Mon, 2 Mar 2026 22:31:33 +0100
Merge remote-tracking branch 'upstream'
Diffstat:
42 files changed, 15868 insertions(+), 14757 deletions(-)
diff --git a/.gitattributes b/.gitattributes
@@ -1,5 +1,4 @@
.github export-ignore
.gitattributes export-ignore
-LICENSE.md export-ignore
-README*.md export-ignore
-SCREENSHOT*.png export-ignore
+readme.md export-ignore
+screenshot.png export-ignore
diff --git a/.github/workflows/system-tests.yml b/.github/workflows/system-tests.yml
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
@@ -21,7 +21,7 @@ jobs:
run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
- name: Set up test environment
run: |
- php yellow.php skip installation maximal
+ php yellow.php skip installation large
echo "Generate:exclude" > content/contact/page.md
echo "Generate:exclude" > content/search/page.md
- name: Run tests
@@ -30,11 +30,11 @@ jobs:
name: PHP ${{ matrix.php }}
strategy:
matrix:
- php: [8.3, 8.2, 8.1, 8.0, 7.4, 7.3, 7.2, 7.1, 7.0]
+ php: [8.5, 8.4, 8.3, 8.2, 8.1, 8.0, 7.4, 7.3, 7.2, 7.1, 7.0]
runs-on: ubuntu-latest
steps:
- name: Check out code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
@@ -46,6 +46,6 @@ jobs:
- name: Set up problem matcher
run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
- name: Set up test environment
- run: php yellow.php skip installation minimal
+ run: php yellow.php skip installation
- name: Run tests
run: php yellow.php generate tests
diff --git a/content/shared/page-error-404.md b/content/shared/page-error-404.md
@@ -2,4 +2,4 @@
Title: File not found
Layout: error
---
-The requested file was not found. Oh no...
+The requested file was not found. Oh no…
diff --git a/license.md b/license.md
@@ -0,0 +1,294 @@
+# 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 this license to this software
+
+At the start of each source file you see a link to a website, that contains
+this license and contact information. For example:
+
+ Datenstrom Yellow, https://github.com/datenstrom/yellow
+
+If your software is based on modified source code, you have to keep the
+original link in each source file. For example:
+
+ Gnomovision Orange, https://github.com/gnomovision/orange
+ Based on Datenstrom Yellow, https://github.com/datenstrom/yellow
+
+All source files are copyrighted by the respective author(s)
+and licensed under GPL version 2, unless stated otherwise.
diff --git a/media/downloads/yellow-deutsch.pdf b/media/downloads/yellow-deutsch.pdf
Binary files differ.
diff --git a/media/downloads/yellow.pdf b/media/downloads/yellow-english.pdf
Binary files differ.
diff --git a/media/downloads/yellow-svenska.pdf b/media/downloads/yellow-svenska.pdf
Binary files differ.
diff --git a/media/images/photo.jpg b/media/images/photo.jpg
Binary files differ.
diff --git a/media/thumbnails/photo-100x40.jpg b/media/thumbnails/photo-100x40.jpg
Binary files differ.
diff --git a/readme.md b/readme.md
@@ -0,0 +1,21 @@
+# Datenstrom Yellow 0.9
+
+Datenstrom Yellow is for people who make small websites. [Try the demo](https://datenstrom.se/yellow/demo/).
+
+<p align="left"><img src="screenshot.png" alt="Screenshot" /></p>
+
+## How to get started
+
+Download one file, unzip it and copy everything to your web server. Your website is immediately available. The most important things are included. You can add more later. There are extensions with additional features, languages and themes that you can install. Behind the minimalist design lies powerful technology. [Get started](https://datenstrom.se/yellow/help/how-to-get-started).
+
+## How to get extensions
+
+Extensions give you additional features, languages and themes. The core has the core functionality of your website. Everything else is up to you. There is an image gallery, contact form for emails, search feature, comment feature and much more. [Get extensions for your website](https://datenstrom.se/yellow/extensions/).
+
+## How to get help
+
+The help shows you how to make small websites, wikis and blogs. Here you will find instructions on how to customise your website in English, German and Swedish. For developers there are descriptions of files, folders and what can be done with the API. [Get help for your website](https://datenstrom.se/yellow/help/).
+
+## Acknowledgements
+
+Made in Europe. Thank you to all developers, designers and translators.
diff --git a/screenshot.png b/screenshot.png
Binary files differ.
diff --git a/system/extensions/core.php b/system/extensions/core.php
@@ -1,3896 +0,0 @@
-<?php
-// Core extension, https://github.com/annaesvensson/yellow-core
-
-class YellowCore {
- const VERSION = "0.8.125";
- const RELEASE = "0.8.23";
- public $content; // content files
- public $media; // media files
- public $system; // system settings
- public $language; // language settings
- public $user; // user settings
- public $extension; // extensions
- public $lookup; // lookup and normalisation methods
- public $toolbox; // toolbox with helper methods
- public $page; // current page
-
- public function __construct() {
- $this->content = new YellowContent($this);
- $this->media = new YellowMedia($this);
- $this->system = new YellowSystem($this);
- $this->language = new YellowLanguage($this);
- $this->user = new YellowUser($this);
- $this->extension = new YellowExtension($this);
- $this->lookup = new YellowLookup($this);
- $this->toolbox = new YellowToolbox($this);
- $this->page = new YellowPage($this);
- $this->checkRequirements();
- $this->system->setDefault("sitename", "Localhost");
- $this->system->setDefault("author", "Datenstrom");
- $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("coreServerUrl", "auto");
- $this->system->setDefault("coreTimezone", "UTC");
- $this->system->setDefault("coreContentExtension", ".md");
- $this->system->setDefault("coreContentDefaultFile", "page.md");
- $this->system->setDefault("coreContentErrorFile", "page-error-(.*).md");
- $this->system->setDefault("coreLanguageFile", "yellow-language.ini");
- $this->system->setDefault("coreUserFile", "yellow-user.ini");
- $this->system->setDefault("coreWebsiteFile", "yellow-website.log");
- $this->system->setDefault("coreMediaLocation", "/media/");
- $this->system->setDefault("coreDownloadLocation", "/media/downloads/");
- $this->system->setDefault("coreImageLocation", "/media/images/");
- $this->system->setDefault("coreThumbnailLocation", "/media/thumbnails/");
- $this->system->setDefault("coreExtensionLocation", "/media/extensions/");
- $this->system->setDefault("coreThemeLocation", "/media/themes/");
- $this->system->setDefault("coreMultiLanguageMode", "0");
- $this->system->setDefault("coreDebugMode", "0");
- }
-
- public function __destruct() {
- $this->shutdown();
- }
-
- // Check requirements
- public function checkRequirements() {
- if (!version_compare(PHP_VERSION, "7.0", ">=")) $this->exitFatalError("Datenstrom Yellow requires PHP 7.0 or higher!");
- if (!extension_loaded("curl")) $this->exitFatalError("Datenstrom Yellow requires PHP curl extension!");
- if (!extension_loaded("gd")) $this->exitFatalError("Datenstrom Yellow requires PHP gd extension!");
- if (!extension_loaded("mbstring")) $this->exitFatalError("Datenstrom Yellow requires PHP mbstring extension!");
- if (!extension_loaded("zip")) $this->exitFatalError("Datenstrom Yellow requires PHP zip extension!");
- mb_internal_encoding("UTF-8");
- }
-
- // Handle initialisation
- public function load() {
- $this->system->load("system/extensions/yellow-system.ini");
- $this->system->set("coreSystemFile", "yellow-system.ini");
- $this->system->set("coreContentDirectory", "content/");
- $this->system->set("coreMediaDirectory", $this->lookup->findMediaDirectory("coreMediaLocation"));
- $this->system->set("coreSystemDirectory", "system/");
- $this->system->set("coreCacheDirectory", "system/cache/");
- $this->system->set("coreExtensionDirectory", "system/extensions/");
- $this->system->set("coreLayoutDirectory", "system/layouts/");
- $this->system->set("coreThemeDirectory", "system/themes/");
- $this->system->set("coreTrashDirectory", "system/trash/");
- list($pathInstall, $pathRoot, $pathHome) = $this->lookup->findFileSystemInformation();
- $this->system->set("coreServerInstallDirectory", $pathInstall);
- $this->system->set("coreServerRootDirectory", $pathRoot);
- $this->system->set("coreServerHomeDirectory", $pathHome);
- register_shutdown_function(array($this, "processFatalError"));
- if ($this->system->get("coreDebugMode")>=1) {
- ini_set("display_errors", 1);
- error_reporting(E_ALL);
- }
- date_default_timezone_set($this->system->get("coreTimezone"));
- $this->extension->load($this->system->get("coreExtensionDirectory"));
- $this->language->load($this->system->get("coreExtensionDirectory").$this->system->get("coreLanguageFile"));
- $this->user->load($this->system->get("coreExtensionDirectory").$this->system->get("coreUserFile"));
- $this->startup();
- }
-
- // Handle request from web browser
- public function request() {
- $statusCode = 0;
- $this->toolbox->timerStart($time);
- ob_start();
- list($scheme, $address, $base, $location, $fileName) = $this->lookup->getRequestInformation();
- $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName, true);
- 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->isError()) $statusCode = $this->processRequestError();
- ob_end_flush();
- $this->toolbox->timerStop($time);
- if ($this->system->get("coreDebugMode")>=1 && ($this->lookup->isContentFile($fileName) || $this->page->isError())) {
- 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)) {
- $statusCode = $this->sendPage($scheme, $address, $base, $location, $fileName, $cacheable, true);
- } elseif (!is_string_empty($fileName)) {
- $statusCode = $this->sendFile(200, $fileName, $cacheable);
- }
- if (!is_readable($fileName)) $this->page->error(404);
- }
- if ($this->system->get("coreDebugMode")>=1 && ($this->lookup->isContentFile($fileName) || $this->page->isError())) {
- echo "YellowCore::processRequest file:$fileName<br/>\n";
- }
- return $statusCode;
- }
-
- // Process request with error
- public function processRequestError() {
- ob_clean();
- $statusCode = $this->sendPage($this->page->scheme, $this->page->address, $this->page->base,
- $this->page->location, $this->page->fileName, false, false);
- if ($this->system->get("coreDebugMode")>=1) echo "YellowCore::processRequestError file:".$this->page->fileName."<br/>\n";
- return $statusCode;
- }
-
- // Process fatal runtime error
- public function processFatalError() {
- $error = error_get_last();
- if (!is_null($error) && isset($error["type"]) && ($error["type"]==E_ERROR || $error["type"]==E_PARSE)) {
- $fileNameAbsolute = isset($error["file"]) ? $error["file"] : "";
- $fileName = substru($fileNameAbsolute, strlenu($this->system->get("coreServerInstallDirectory")));
- $this->toolbox->log("error", "Can't parse file '$fileName'!");
- $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted(500));
- $troubleshooting = PHP_SAPI!="cli" ?
- "<a href=\"".$this->toolbox->getTroubleshootingUrl()."\">See troubleshooting</a>." : "See ".$this->toolbox->getTroubleshootingUrl();
- echo "<br/>\nDatenstrom Yellow stopped with fatal error. Activate the debug mode for more information. $troubleshooting\n";
- }
- }
-
- // Show error message and terminate immediately
- public function exitFatalError($errorMessage = "") {
- $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted(500));
- $troubleshooting = PHP_SAPI!="cli" ?
- "<a href=\"".$this->toolbox->getTroubleshootingUrl()."\">See troubleshooting</a>." : "See ".$this->toolbox->getTroubleshootingUrl();
- echo "$errorMessage $troubleshooting\n";
- exit(1);
- }
-
- // Send page response
- public function sendPage($scheme, $address, $base, $location, $fileName, $cacheable, $showSource) {
- $rawData = $showSource ? $this->toolbox->readFile($fileName) : $this->page->getRawDataError();
- $statusCode = max($this->page->statusCode, 200);
- $errorMessage = $this->page->errorMessage;
- $this->page = new YellowPage($this);
- $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName, $cacheable);
- $this->page->parseMeta($rawData, $statusCode, $errorMessage);
- $this->language->set($this->page->get("language"));
- $this->page->parseContent();
- $this->page->parsePage();
- $statusCode = $this->sendData($this->page->statusCode, $this->page->headerData, $this->page->outputData);
- if ($this->system->get("coreDebugMode")>=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 data response
- public function sendData($statusCode, $headerData, $outputData) {
- $lastModifiedFormatted = isset($headerData["Last-Modified"]) ? $headerData["Last-Modified"] : "";
- if ($statusCode==200 && !isset($headerData["Cache-Control"]) && $this->toolbox->isNotModified($lastModifiedFormatted)) {
- $statusCode = 304;
- $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode));
- } else {
- $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode));
- foreach ($headerData as $key=>$value) {
- $this->toolbox->sendHttpHeader("$key: $value");
- }
- if (!is_null($outputData)) echo $outputData;
- }
- 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;
- $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode));
- } else {
- $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode));
- if (!$cacheable) $this->toolbox->sendHttpHeader("Cache-Control: no-cache, no-store");
- $this->toolbox->sendHttpHeader("Content-Type: ".$this->toolbox->getMimeContentType($fileName));
- $this->toolbox->sendHttpHeader("Last-Modified: ".$lastModifiedFormatted);
- echo $this->toolbox->readFile($fileName);
- }
- return $statusCode;
- }
-
- // Send status response
- public function sendStatus($statusCode, $location = "") {
- if (!is_string_empty($location)) $this->page->status($statusCode, $location);
- $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode));
- foreach ($this->page->headerData as $key=>$value) {
- $this->toolbox->sendHttpHeader("$key: $value");
- }
- return $statusCode;
- }
-
- // Handle command from command line
- public function command($line = "") {
- $statusCode = 0;
- $this->toolbox->timerStart($time);
- list($command, $text) = $this->lookup->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 && is_string_empty($command)) {
- $lines = array();
- foreach ($this->extension->data as $key=>$value) {
- if (method_exists($value["object"], "onCommandHelp")) {
- $this->lookup->commandHandler = $key;
- $output = $value["object"]->onCommandHelp();
- $lines = array_merge($lines, is_array($output) ? $output : array($output));
- }
- }
- usort($lines, "strnatcasecmp");
- $this->showCommandHelp($lines);
- $statusCode = 200;
- }
- if ($statusCode==0) {
- $this->lookup->commandHandler = "core";
- $statusCode = 400;
- echo "Yellow $command: Command not found\n";
- }
- $this->toolbox->timerStop($time);
- if ($this->system->get("coreDebugMode")>=1) {
- echo "YellowCore::command status:$statusCode time:$time ms<br/>\n";
- }
- return $statusCode<400 ? 0 : 1;
- }
-
- // Show command help
- public function showCommandHelp($lines) {
- echo "Datenstrom Yellow is for people who make small websites. https://datenstrom.se/yellow/\n";
- $lineCounter = 0;
- foreach ($lines as $line) {
- echo(++$lineCounter>1 ? " " : "Syntax: ")."php yellow.php $line\n";
- }
- }
-
- // Handle startup
- public function startup() {
- if (isset($this->extension->data)) {
- foreach ($this->extension->data as $key=>$value) {
- if (method_exists($value["object"], "onStartup")) $value["object"]->onStartup();
- }
- }
- }
-
- // Handle shutdown
- public function shutdown() {
- if (isset($this->extension->data)) {
- foreach ($this->extension->data as $key=>$value) {
- if (method_exists($value["object"], "onShutdown")) $value["object"]->onShutdown();
- }
- }
- }
-
- // 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);
- }
-}
-
-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])) {
- $this->pages[$location] = array();
- $scheme = $this->yellow->page->scheme;
- $address = $this->yellow->page->address;
- $base = $this->yellow->page->base;
- if (is_string_empty($location)) {
- $rootLocations = $this->yellow->lookup->findContentRootLocations();
- foreach ($rootLocations as $rootLocation=>$rootFileName) {
- $page = new YellowPage($this->yellow);
- $page->setRequestInformation($scheme, $address, $base, $rootLocation, $rootFileName, false);
- $page->parseMeta("");
- array_push($this->pages[$location], $page);
- }
- } else {
- if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowContent::scanLocation location:$location<br/>\n";
- $fileNames = $this->yellow->lookup->findChildrenFromContentLocation($location);
- foreach ($fileNames as $fileName) {
- $page = new YellowPage($this->yellow);
- $page->setRequestInformation($scheme, $address, $base,
- $this->yellow->lookup->findContentLocationFromFile($fileName), $fileName, false);
- $page->parseMeta($this->yellow->toolbox->readFile($fileName, 4096));
- 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->getContentHtml(), $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, false);
- $page->parseMeta("---\nTitle: $match[3]\n---\n");
- $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();
- if ($this->yellow->system->get("coreMultiLanguageMode")) {
- 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);
- $sharedLocation = $this->getHomeLocation($location)."shared/";
- return $pages->merge($this->scanLocation($sharedLocation));
- }
-
- // 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) {
- $parentLocation = "";
- $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 (is_string_empty($parentLocation)) $parentLocation = "root$token/";
- return $parentLocation;
- }
-
- // Return top-level location
- public function getParentTopLocation($location) {
- $parentTopLocation = "";
- $token = rtrim(substru($this->getRootLocation($location), 4), "/");
- if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1];
- if (is_string_empty($parentTopLocation)) $parentTopLocation = "$token/";
- return $parentTopLocation;
- }
-}
-
-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])) {
- $this->files[$location] = array();
- $scheme = $this->yellow->page->scheme;
- $address = $this->yellow->page->address;
- $base = $this->yellow->system->get("coreServerBase");
- if (is_string_empty($location)) {
- $fileNames = array($this->yellow->system->get("coreMediaDirectory"));
- } else {
- if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowMedia::scanLocation location:$location<br/>\n";
- $fileNames = $this->yellow->lookup->findChildrenFromMediaLocation($location);
- }
- foreach ($fileNames as $fileName) {
- $file = new YellowPage($this->yellow);
- $file->setRequestInformation($scheme, $address, $base,
- $this->yellow->lookup->findMediaLocationFromFile($fileName), $fileName, false);
- $file->parseMeta(null);
- 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 (is_string_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 (is_string_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) {
- $this->modified = $this->yellow->toolbox->getFileModified($fileName);
- $fileData = $this->yellow->toolbox->readFile($fileName);
- $this->settings = $this->yellow->toolbox->getTextSettings($fileData, "");
- if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowSystem::load file:$fileName<br/>\n";
- if ($this->yellow->system->get("coreDebugMode")>=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 (!is_string_empty($key) && !is_string_empty($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 default system settings
- public function setDefaults($lines) {
- foreach ($lines as $line) {
- if (preg_match("/^\#/", $line)) continue;
- if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
- if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
- $this->settingsDefaults[$matches[1]] = $matches[2];
- }
- }
- }
- }
-
- // 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 different value for system setting
- public function getDifferent($key) {
- $array = array_diff($this->getAvailable($key), array($this->get($key)));
- return reset($array);
- }
-
- // Return available values for system setting
- public function getAvailable($key) {
- $values = array();
- $valueDefault = isset($this->settingsDefaults[$key]) ? $this->settingsDefaults[$key] : "";
- 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 !is_array_empty($values) ? $values : array($valueDefault);
- }
- public function getValues($key) { return $this->getAvailable($key); } //TODO: remove later, for backwards compatibility
-
- // Return system settings
- public function getSettings($filterStart = "", $filterEnd = "") {
- $settings = array();
- if (is_string_empty($filterStart) && is_string_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 (!is_string_empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value;
- if (!is_string_empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $settings[$key] = $value;
- }
- }
- return $settings;
- }
-
- // 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 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) {
- $this->modified = $this->yellow->toolbox->getFileModified($fileName);
- $fileData = $this->yellow->toolbox->readFile($fileName);
- $settings = $this->yellow->toolbox->getTextSettings($fileData, "language");
- foreach ($settings as $language=>$block) {
- if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray();
- foreach ($block as $key=>$value) {
- $this->settings[$language][$key] = $value;
- }
- }
- if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowLanguage::load file:$fileName<br/>\n";
- foreach ($this->settings->getArrayCopy() as $key=>$value) {
- if (!isset($this->settings[$key]["languageDescription"])) {
- unset($this->settings[$key]);
- }
- }
- $callback = function ($a, $b) {
- return strnatcmp($a["languageDescription"], $b["languageDescription"]);
- };
- $this->settings->uasort($callback);
- }
-
- // Set current language
- public function set($language) {
- $this->language = $language;
- }
-
- // Set default language setting
- public function setDefault($key, $value, $language) {
- if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray();
- $this->settings[$language][$key] = $value;
- $this->settingsDefaults[$key] = true;
- }
-
- // Set default language settings
- public function setDefaults($lines) {
- $language = "";
- foreach ($lines as $line) {
- if (preg_match("/^\#/", $line)) continue;
- if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
- if (lcfirst($matches[1])=="language" && !is_string_empty($matches[2])) {
- $language = $matches[2];
- if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray();
- }
- if (!is_string_empty($language) && !is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
- $this->settings[$language][$matches[1]] = $matches[2];
- $this->settingsDefaults[$matches[1]] = 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 (is_string_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 text as language specific date, convert to one of the standard formats
- public function getDateStandard($text, $language = "") {
- if (preg_match("/^\d+$/", $text)) {
- $output = $text;
- } elseif (preg_match("/^\d+\-\d+$/", $text)) {
- $format = $this->getText("coreDateFormatShort", $language);
- $output = $this->getDateFormatted(strtotime($text), $format, $language);
- } elseif (preg_match("/^\d+\-\d+\-\d+$/", $text)) {
- $format = $this->getText("coreDateFormatMedium", $language);
- $output = $this->getDateFormatted(strtotime($text), $format, $language);
- } else {
- $format = $this->getText("coreDateFormatLong", $language);
- $output = $this->getDateFormatted(strtotime($text), $format, $language);
- }
- return $output;
- }
-
- // Return Unix time as 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 Unix time as date
- public function getDateFormatted($timestamp, $format, $language = "") {
- $dateMonthsNominative = preg_split("/\s*,\s*/", $this->getText("coreDateMonthsNominative", $language));
- $dateMonthsGenitive = preg_split("/\s*,\s*/", $this->getText("coreDateMonthsGenitive", $language));
- $dateWeekdays = preg_split("/\s*,\s*/", $this->getText("coreDateWeekdays", $language));
- $monthNominative = $dateMonthsNominative[date("n", $timestamp) - 1];
- $monthGenitive = $dateMonthsGenitive[date("n", $timestamp) - 1];
- $weekday = $dateWeekdays[date("N", $timestamp) - 1];
- $timeZone = $this->yellow->system->get("coreTimezone");
- $timeZoneHelper = new DateTime("now", new DateTimeZone($timeZone));
- $timeZoneOffset = $timeZoneHelper->getOffset();
- $timeZoneAbbreviation = "GMT".($timeZoneOffset<0 ? "-" : "+").abs(intval($timeZoneOffset/3600));
- $format = preg_replace("/(?<!\\\)F/", addcslashes($monthNominative, "A..Za..z"), $format);
- $format = preg_replace("/(?<!\\\)V/", addcslashes($monthGenitive, "A..Za..z"), $format);
- $format = preg_replace("/(?<!\\\)M/", addcslashes(substru($monthNominative, 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 language settings
- public function getSettings($filterStart = "", $filterEnd = "", $language = "") {
- $settings = array();
- if (is_string_empty($language)) $language = $this->language;
- if (isset($this->settings[$language])) {
- if (is_string_empty($filterStart) && is_string_empty($filterEnd)) {
- $settings = $this->settings[$language]->getArrayCopy();
- } else {
- foreach ($this->settings[$language] as $key=>$value) {
- if (!is_string_empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value;
- if (!is_string_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;
- }
-
- // Check if language setting exists
- public function isText($key, $language = "") {
- if (is_string_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 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) {
- $this->modified = $this->yellow->toolbox->getFileModified($fileName);
- $fileData = $this->yellow->toolbox->readFile($fileName);
- $this->settings = $this->yellow->toolbox->getTextSettings($fileData, "email");
- if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUser::load file:$fileName<br/>\n";
- }
-
- // 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 (!is_string_empty($key) && !is_string_empty($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 (is_string_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 (is_string_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 (is_string_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 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) {
- $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));
- if ($this->yellow->system->get("coreDebugMode")>=3) echo "YellowExtension::load file:$entry<br/>\n";
- }
- $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);
- }
- }
-
- // 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;
- }
-
- // Return file system information
- public function findFileSystemInformation() {
- $pathInstall = substru(__DIR__, 0, 1-strlenu($this->yellow->system->get("coreExtensionDirectory")));
- $pathBase = $this->yellow->system->get("coreContentDirectory");
- $pathRoot = $this->yellow->system->get("coreMultiLanguageMode") ? "default/" : "";
- $pathHome = "home/";
- if (!is_string_empty($pathRoot)) {
- $firstRoot = "";
- $token = $root = rtrim($pathRoot, "/");
- foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) {
- if (is_string_empty($firstRoot)) $firstRoot = $token = $entry;
- if ($this->normaliseToken($entry)==$root) {
- $token = $entry;
- break;
- }
- }
- $pathRoot = $this->normaliseToken($token)."/";
- $pathBase .= "$firstRoot/";
- }
- if (!is_string_empty($pathHome)) {
- $firstHome = "";
- $token = $home = rtrim($pathHome, "/");
- foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) {
- if (is_string_empty($firstHome)) $firstHome = $token = $entry;
- if ($this->normaliseToken($entry)==$home) {
- $token = $entry;
- break;
- }
- }
- $pathHome = $this->normaliseToken($token)."/";
- }
- return array($pathInstall, $pathRoot, $pathHome);
- }
-
- // Return content language
- public function findContentLanguage($fileName, $languageDefault) {
- $language = $languageDefault;
- $pathBase = $this->yellow->system->get("coreContentDirectory");
- $pathRoot = $this->yellow->system->get("coreServerRootDirectory");
- if (!is_string_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 content root locations
- public function findContentRootLocations() {
- $rootLocations = array();
- $pathBase = $this->yellow->system->get("coreContentDirectory");
- $pathRoot = $this->yellow->system->get("coreServerRootDirectory");
- if (!is_string_empty($pathRoot)) {
- foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) {
- $token = $this->normaliseToken($entry)."/";
- if ($token==$pathRoot) $token = "";
- $rootLocations["root/$token"] = "$pathBase$entry/";
- }
- } else {
- $rootLocations["root/"] = "$pathBase";
- }
- if ($this->yellow->system->get("coreDebugMode")>=3) {
- foreach ($rootLocations as $key=>$key) {
- echo "YellowLookup::findContentRootLocations $key -> $value<br/>\n";
- }
- }
- return $rootLocations;
- }
-
- // Return content location from file path
- public function findContentLocationFromFile($fileName) {
- $invalid = false;
- $location = "/";
- $pathBase = $this->yellow->system->get("coreContentDirectory");
- $pathRoot = $this->yellow->system->get("coreServerRootDirectory");
- $pathHome = $this->yellow->system->get("coreServerHomeDirectory");
- $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 (!is_string_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);
- if ($token!=$fileDefault) {
- $location .= $this->normaliseToken($tokens[$i], $fileExtension, true);
- }
- $extension = ($pos = strrposu($fileName, ".")) ? substru($fileName, $pos) : "";
- if ($extension!=$fileExtension) $invalid = true;
- } else {
- $invalid = true;
- }
- if ($this->yellow->system->get("coreDebugMode")>=2) {
- $debug = ($invalid ? "INVALID" : $location)." <- $pathBase$fileName";
- echo "YellowLookup::findContentLocationFromFile $debug<br/>\n";
- }
- return $invalid ? "" : $location;
- }
-
- // Return file path from content location
- public function findFileFromContentLocation($location, $directory = false) {
- $found = $invalid = false;
- $path = $this->yellow->system->get("coreContentDirectory");
- $pathRoot = $this->yellow->system->get("coreServerRootDirectory");
- $pathHome = $this->yellow->system->get("coreServerHomeDirectory");
- $fileDefault = $this->yellow->system->get("coreContentDefaultFile");
- $fileExtension = $this->yellow->system->get("coreContentExtension");
- $tokens = explode("/", $location);
- if ($this->isRootLocation($location)) {
- if (!is_string_empty($pathRoot)) {
- $token = (count($tokens)>2) ? $tokens[1] : rtrim($pathRoot, "/");
- $path .= $this->findFileDirectory($path, $token, "", true, true, $found, $invalid);
- }
- } else {
- if (!is_string_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 (!is_string_empty($tokens[$i])) {
- $token = $tokens[$i].$fileExtension;
- if ($token==$fileDefault) $invalid = true;
- $path .= $this->findFileDirectory($path, $token, $fileExtension, false, true, $found, $invalid);
- } else {
- $path .= $this->findFileDefault($path, $fileDefault, $fileExtension, false);
- }
- if ($this->yellow->system->get("coreDebugMode")>=2) {
- $debug = "$location -> ".($invalid ? "INVALID" : $path);
- echo "YellowLookup::findFileFromContentLocation $debug<br/>\n";
- }
- }
- }
- return $invalid ? "" : $path;
- }
-
- // Return children from content location
- public function findChildrenFromContentLocation($location) {
- $fileNames = array();
- if (!$this->isFileLocation($location)) {
- $path = $this->findFileFromContentLocation($location, true);
- $fileDefault = $this->yellow->system->get("coreContentDefaultFile");
- $fileExtension = $this->yellow->system->get("coreContentExtension");
- 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)) {
- $regex = "/^.*\\".$fileExtension."$/";
- foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) {
- if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) continue;
- array_push($fileNames, $path.$entry);
- }
- }
- }
- return $fileNames;
- }
-
- // Return media location from file path
- public function findMediaLocationFromFile($fileName) {
- $location = "";
- $extensionDirectoryLength = strlenu($this->yellow->system->get("coreExtensionDirectory"));
- $themeDirectoryLength = strlenu($this->yellow->system->get("coreThemeDirectory"));
- $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory"));
- if (substru($fileName, 0, $extensionDirectoryLength)==$this->yellow->system->get("coreExtensionDirectory")) {
- if ($this->isSafeFile($fileName)) {
- $location = $this->yellow->system->get("coreExtensionLocation").substru($fileName, $extensionDirectoryLength);
- }
- } elseif (substru($fileName, 0, $themeDirectoryLength)==$this->yellow->system->get("coreThemeDirectory")) {
- if ($this->isSafeFile($fileName)) {
- $location = $this->yellow->system->get("coreThemeLocation").substru($fileName, $themeDirectoryLength);
- }
- } elseif (substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory")) {
- $location = "/".$fileName;
- }
- return $location;
- }
-
- // Return file path from media location
- public function findFileFromMediaLocation($location) {
- $fileName = "";
- $extensionLocationLength = strlenu($this->yellow->system->get("coreExtensionLocation"));
- $themeLocationLength = strlenu($this->yellow->system->get("coreThemeLocation"));
- $mediaLocationLength = strlenu($this->yellow->system->get("coreMediaLocation"));
- if (substru($location, 0, $extensionLocationLength)==$this->yellow->system->get("coreExtensionLocation")) {
- if ($this->isSafeFile($location)) {
- $fileName = $this->yellow->system->get("coreExtensionDirectory").substru($location, $extensionLocationLength);
- }
- } elseif (substru($location, 0, $themeLocationLength)==$this->yellow->system->get("coreThemeLocation")) {
- if ($this->isSafeFile($location)) {
- $fileName = $this->yellow->system->get("coreThemeDirectory").substru($location, $themeLocationLength);
- }
- } elseif (substru($location, 0, $mediaLocationLength)==$this->yellow->system->get("coreMediaLocation")) {
- $fileName = substru($location, 1);
- }
- return $fileName;
- }
-
- // Return children from media location
- public function findChildrenFromMediaLocation($location) {
- $fileNames = array();
- if (!$this->isFileLocation($location)) {
- $path = $this->findFileFromMediaLocation($location);
- 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);
- }
- }
- return $fileNames;
- }
-
- // Return media directory from a system setting
- public function findMediaDirectory($key) {
- return substru($key, -8, 8)=="Location" ? $this->findFileFromMediaLocation($this->yellow->system->get($key)) : "";
- }
-
- // 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)) {
- $regex = "/^[\d\-\_\.]*($fileDefault)$/";
- foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) {
- if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) {
- $token = $entry;
- break;
- }
- }
- }
- return $includePath ? "$path/$token" : $token;
- }
-
- // Normalise file/directory token
- public function normaliseToken($text, $fileExtension = "", $removeExtension = false) {
- if (!is_string_empty($fileExtension)) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text;
- if (preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !is_string_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) && !is_string_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 (!is_string_empty($prefix) && !preg_match("/[\-\_\.]$/", $prefix)) $prefix .= "-";
- return $prefix;
- }
-
- // 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", "picture", "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", "allow", "allowfullscreen", "alt", "autocomplete", "autoplay", "background", "bgcolor", "border", "cellpadding", "cellspacing", "charset", "checked", "cite", "class", "clear", "color", "cols", "colspan", "content", "contenteditable", "controls", "coords", "crossorigin", "datetime", "default", "dir", "disabled", "download", "enctype", "face", "for", "frameborder", "headers", "height", "hidden", "high", "href", "hreflang", "id", "integrity", "ismap", "label", "lang", "list", "loading", "loop", "low", "max", "maxlength", "media", "method", "min", "multiple", "muted", "name", "noshade", "novalidate", "nowrap", "open", "optimum", "pattern", "placeholder", "poster", "prefix", "preload", "property", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "sandbox", "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");
- $attributesAllowEmptyString = array("alt", "download", "sandbox", "value");
- $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, $attributesAllowEmptyString);
- 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) && !$this->isSafeUrl($href)) {
- $elementAttributes["href"] = "error-xss-filter";
- }
- $href = isset($elementAttributes["xlink:href"]) ? $elementAttributes["xlink:href"] : "";
- if (preg_match("/^\w+:/", $href) && !$this->isSafeUrl($href)) {
- $elementAttributes["xlink:href"] = "error-xss-filter";
- }
- }
- $output .= "<$elementStart$elementName";
- foreach ($elementAttributes as $key=>$value) $output .= " $key=\"$value\"";
- if (!is_string_empty($elementEnd)) $output .= " ";
- $output .= "$elementEnd>";
- }
- if (!$elementFound) break;
- $offsetBytes = $matches[0][1] + strlenb($matches[0][0]);
- }
- return $output;
- }
-
- // Normalise fields in MIME headers
- public function normaliseHeaders($input, $type = "mime", $filterStrict = true) {
- $output = "";
- if ($type=="mime") {
- $keysMixedEncoding = array("To", "From", "Reply-To", "Cc", "Bcc");
- foreach ($input as $key=>$value) {
- $key = ucwords(preg_replace("/[^a-zA-Z\-]/u", "-", $key), "-");
- if (in_array($key, $keysMixedEncoding)) {
- $text = "$key: ";
- foreach (preg_split("/\s*,\s*/", $value) as $email) {
- if (!preg_match("/^(.*?)(\s*)<(.*?)>$/", $email, $matches)) {
- $matches[1] = $matches[2] = "";
- $matches[3] = $email;
- }
- if ($filterStrict && !preg_match("/[\w\+\-\.\@]+/", $matches[3])) {
- $matches[3] = "error-mail-filter";
- }
- if (substru($text, -2, 2)!=": ") $text .= ",\r\n ";
- $text = $this->getMimeHeader($text, $matches[1]);
- $text = $this->getMimeHeader($text, "$matches[2]<$matches[3]>", false);
- }
- $text .= "\r\n";
- } else {
- $text = $this->getMimeHeader("$key: ", $value)."\r\n";
- }
- $output .= $text;
- }
- }
- return $output;
- }
-
- // Normalise relative path tokens
- public function normalisePath($text) {
- $textFiltered = "";
- $textLength = strlenb($text);
- for ($pos=0; $pos<$textLength; ++$pos) {
- if ($text[$pos]=="." && ($pos==0 || $text[$pos-1]=="/")) {
- while ($text[$pos]==".") ++$pos;
- if ($text[$pos]=="/") ++$pos;
- --$pos;
- continue;
- }
- $textFiltered .= $text[$pos];
- }
- return $textFiltered;
- }
-
- // 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;
- }
-
- // 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;
- }
- } else {
- $location = $pageBase.$pageLocation.$location;
- }
- $location = str_replace("/./", "/", $location);
- $location = str_replace(":", $this->yellow->toolbox->getLocationArgumentsSeparator(), $location);
- } else {
- if ($filterStrict && !$this->isSafeUrl($location)) $location = "error-xss-filter";
- }
- return $location;
- }
-
- // 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->yellow->toolbox->getLocationArgumentsSeparator(), $text);
- return str_replace(array("%2F","%3A","%3D"), array("/",":","="), rawurlencode($text));
- }
-
- // 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 && !$this->isSafeUrl($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 request information
- public function getRequestInformation($scheme = "", $address = "", $base = "") {
- if (is_string_empty($scheme) && is_string_empty($address) && is_string_empty($base)) {
- $url = $this->yellow->system->get("coreServerUrl");
- if ($url=="auto" || $this->isCommandLine()) $url = $this->yellow->toolbox->detectServerUrl();
- list($scheme, $address, $base) = $this->getUrlInformation($url);
- $this->yellow->system->set("coreServerScheme", $scheme);
- $this->yellow->system->set("coreServerAddress", $address);
- $this->yellow->system->set("coreServerBase", $base);
- if ($this->yellow->system->get("coreDebugMode")>=3) {
- echo "YellowLookup::getRequestInformation $scheme://$address$base<br/>\n";
- }
- }
- $location = substru($this->yellow->toolbox->detectServerLocation(), strlenu($base));
- $fileName = "";
- if (is_string_empty($fileName)) $fileName = $this->findFileFromMediaLocation($location);
- if (is_string_empty($fileName)) $fileName = $this->findFileFromContentLocation($location);
- return array($scheme, $address, $base, $location, $fileName);
- }
-
- // Return command information
- public function getCommandInformation($line = "") {
- if (is_string_empty($line)) {
- $line = $this->yellow->toolbox->getTextString(array_slice($this->yellow->toolbox->getServer("argv"), 1));
- if ($this->yellow->system->get("coreDebugMode")>=3) {
- echo "YellowLookup::getCommandInformation $line<br/>\n";
- }
- }
- return $this->yellow->toolbox->getTextList($line, " ", 2);
- }
-
- // Return request handler
- public function getRequestHandler() {
- return $this->requestHandler;
- }
-
- // Return command handler
- public function getCommandHandler() {
- return $this->commandHandler;
- }
-
- // Return attributes from text
- public function getTextAttributes($text, $attributesAllowEmptyString) {
- $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 (!is_string_empty($key) && (!is_string_empty($value) || in_array(strtolower($key), $attributesAllowEmptyString))) {
- $attributes[$key] = $value;
- }
- }
- return $attributes;
- }
-
- // Return MIME header field, encode and fold if necessary
- public function getMimeHeader($text, $field, $allowEncode = true) {
- if ($allowEncode) {
- $encode = preg_match("/[\x7F-\xFF]/", $field);
- $fieldPos = 0;
- while (true) {
- $textPos = strlenb($text)-(($pos = strrposb($text, "\n")) ? $pos+1 : 0);
- $bytesAvailable = max(0, 78-$textPos);
- $fragment = substrb($field, $fieldPos);
- if ($encode && !is_string_empty($fragment)) $fragment = "=?UTF-8?B?".base64_encode($fragment)."?=";
- if ($bytesAvailable<strlenb($fragment)) {
- $bytesHandled = $bytesAvailable;
- if (!$encode) {
- for ($pos=$bytesHandled;$pos>0;--$pos) {
- if ($field[$fieldPos+$pos]==" ") {
- $fragment = substrb($field, $fieldPos, $pos);
- $bytesHandled = $pos+1;
- break;
- }
- }
- if ($pos==0) $encode = true;
- }
- if ($encode) {
- while (true) {
- $fragment = substrb($field, $fieldPos, $bytesHandled);
- if (!is_string_empty($fragment)) $fragment = "=?UTF-8?B?".base64_encode($fragment)."?=";
- if ($bytesAvailable>=strlenb($fragment) || $bytesHandled==0) break;
- --$bytesHandled;
- }
- }
- $text .= $fragment."\r\n ";
- $fieldPos += $bytesHandled;
- } else {
- $text .= $fragment;
- break;
- }
- }
- } else {
- $textPos = strlenb($text)-(($pos = strrposb($text, "\n")) ? $pos+1 : 0);
- $bytesAvailable = max(0, 78-$textPos);
- if ($bytesAvailable<strlenb($field)) {
- $text .= "\r\n ".ltrim($field);
- } else {
- $text .= $field;
- }
- }
- return $text;
- }
-
- // 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->findFileFromContentLocation("$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 (!is_array_empty($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false))) $nested = true;
- }
- return $nested;
- }
-
- // Check if location is within shared directory
- public function isSharedLocation($location) {
- $sharedLocation = $this->yellow->content->getHomeLocation($location)."shared/";
- return substru($location, 0, strlenu($sharedLocation))==$sharedLocation;
- }
-
- // 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 URL is a well-known URL scheme
- public function isSafeUrl($url) {
- return preg_match("/^(http|https|ftp|mailto|tel):/", $url);
- }
-
- // Check if file is a well-known file type
- public function isSafeFile($fileName) {
- return preg_match("/\.(css|gif|ico|js|jpg|map|png|scss|svg|woff|woff2)$/", $fileName);
- }
-
- // 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 strposu($fileName, "/")===false ||
- 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");
- }
-
- // Check if running at command line
- public function isCommandLine() {
- return isset($this->commandHandler);
- }
-}
-
-class YellowToolbox {
- public $yellow; // access to API
-
- public function __construct($yellow) {
- $this->yellow = $yellow;
- }
-
- // 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 (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
- if (!is_string_empty($locationArguments)) $locationArguments .= "/";
- $locationArguments .= "$matches[1]:$matches[2]";
- }
- }
- }
- if (!$found && !is_string_empty($key) && !is_string_empty($value)) {
- if (!is_string_empty($locationArguments)) $locationArguments .= "/";
- $locationArguments .= "$key:$value";
- }
- if (!is_string_empty($locationArguments)) {
- $locationArguments = $this->yellow->lookup->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 (!is_string_empty($key) && !is_string_empty($value)) {
- if (!is_string_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 (!is_string_empty($locationArguments)) {
- $locationArguments = $this->yellow->lookup->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 420: $text = "Not public"; break;
- case 430: $text = "Login failed"; break;
- case 434: $text = "Can create"; break;
- case 435: $text = "Can restore"; break;
- case 450: $text = "Update error"; 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",
- "scss" => "text/x-scss",
- "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 (is_string_empty($fileType)) {
- $contentType = $contentTypes["html"];
- } elseif (array_key_exists($fileType, $contentTypes)) {
- $contentType = $contentTypes[$fileType];
- }
- return $contentType;
- }
-
- // Send HTTP header
- public function sendHttpHeader($text) {
- if (!headers_sent()) header($text);
- }
-
- // 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->yellow->lookup->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);
- if (flock($fileHandle, LOCK_SH)) {
- $fileSize = $sizeMax ? $sizeMax : filesize($fileName);
- if ($fileSize) $fileData = fread($fileHandle, $fileSize);
- flock($fileHandle, LOCK_UN);
- }
- fclose($fileHandle);
- }
- return $fileData;
- }
-
- // Create file
- public function createFile($fileName, $fileData, $mkdir = false) {
- $ok = false;
- if ($mkdir) {
- $path = dirname($fileName);
- if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true);
- }
- $fileHandle = @fopen($fileName, "cb");
- 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 (!is_string_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 (!is_string_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 (!is_string_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 (is_string_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"));
- $fileNameDestination .= ".".pathinfo($fileName, PATHINFO_EXTENSION);
- $ok = @rename($fileName, $fileNameDestination);
- }
- return $ok;
- }
-
- // Delete directory
- public function deleteDirectory($path, $pathTrash = "") {
- clearstatcache();
- if (is_string_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"));
- $ok = @rename($path, $pathDestination);
- }
- return $ok;
- }
-
- // Set file/directory modification date, Unix time
- public function modifyFile($fileName, $modified) {
- clearstatcache(true, $fileName);
- return @touch($fileName, $modified);
- }
-
- // Return file/directory modification date, Unix time
- public function getFileModified($fileName) {
- return (is_file($fileName) || is_dir($fileName)) ? filemtime($fileName) : 0;
- }
-
- // Return file/directory deletion date, Unix time
- public function getFileDeleted($fileName) {
- $deleted = 0;
- $text = basename($fileName);
- $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text;
- if (preg_match("#^(.+)-(\d\d\d\d-\d\d-\d\d)-(\d\d)-(\d\d)-(\d\d)$#", $text, $matches)) {
- $deleted = strtotime("$matches[2] $matches[3]:$matches[4]:$matches[5]");
- }
- return $deleted;
- }
-
- // 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 number of bytes
- public function getNumberBytes($text) {
- $bytes = intval($text);
- switch (strtoupperu(substru($text, -1))) {
- case "G": $bytes *= 1024*1024*1024; break;
- case "M": $bytes *= 1024*1024; break;
- case "K": $bytes *= 1024; break;
- }
- return $bytes;
- }
-
- // Return lines from text, including newline
- public function getTextLines($text) {
- $lines = preg_split("/\n/", $text);
- foreach ($lines as &$line) {
- $line = $line."\n";
- }
- if (is_string_empty($text) || substru($text, -1, 1)=="\n") array_pop($lines);
- return $lines;
- }
-
- // Return settings from text
- function getTextSettings($text, $blockStart) {
- $settings = new YellowArray();
- if (is_string_empty($blockStart)) {
- foreach ($this->getTextLines($text) as $line) {
- if (preg_match("/^\#/", $line)) continue;
- if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
- if (!is_string_empty($matches[1]) && !is_string_empty($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 && !is_string_empty($matches[2])) {
- $blockKey = $matches[2];
- $settings[$blockKey] = new YellowArray();
- }
- if (!is_string_empty($blockKey) && !is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
- $settings[$blockKey][$matches[1]] = $matches[2];
- }
- }
- }
- }
- return $settings;
- }
-
- // Set settings in text
- function setTextSettings($text, $blockStart, $blockKey, $settings) {
- $textNew = "";
- if (is_string_empty($blockStart)) {
- foreach ($this->getTextLines($text) as $line) {
- if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
- if (!is_string_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 && !is_string_empty($matches[2])) {
- $scan = lcfirst($matches[2])==lcfirst($blockKey);
- }
- }
- if (!$scan && is_string_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 (!is_string_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 (!is_string_empty($textMiddle)) {
- $textMiddle = $textSettings;
- if (!is_string_empty($textEnd)) $textMiddle .= "\n";
- } else {
- if (!is_string_empty($textStart)) $textEnd .= "\n";
- $textEnd .= $textSettings;
- }
- $textNew = $textStart.$textMiddle.$textEnd;
- }
- return $textNew;
- }
-
- // Remove settings from text
- function unsetTextSettings($text, $blockStart, $blockKey) {
- $textNew = "";
- if (!is_string_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 && !is_string_empty($matches[2])) {
- $scan = lcfirst($matches[2])==lcfirst($blockKey);
- }
- }
- if (!$scan && is_string_empty($textMiddle)) {
- $textStart .= $line;
- } elseif ($scan) {
- $textMiddle .= $line;
- } else {
- $textEnd .= $line;
- }
- }
- $textNew = rtrim($textStart.$textEnd)."\n";
- }
- return $textNew;
- }
-
- // Return array of specific size from text
- public function getTextList($text, $separator, $size) {
- $tokens = explode($separator, $text, $size);
- return array_pad($tokens, $size, "");
- }
-
- // Return array of variable size 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 (is_null($value) || $value==$optional) $tokens[$key] = "";
- }
- return array_pad($tokens, $sizeMin, "");
- }
-
- // Return text from array, space separated
- public function getTextString($tokens, $optional = "-") {
- $text = "";
- foreach ($tokens as $token) {
- if (preg_match("/\s/", $token)) $token = "\"$token\"";
- if (is_string_empty($token)) $token = $optional;
- if (!is_string_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) {
- $hiddenLevel = 0;
- $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] : "";
- $elementAttributes = isset($matches[3][0]) ? $matches[3][0] : "";
- $elementEnd = isset($matches[4][0]) ? $matches[4][0] : "";
- if (!is_string_empty($elementBefore) && !$hiddenLevel) {
- $rawText = preg_replace("/\s+/s", " ", html_entity_decode($elementBefore, ENT_QUOTES, "UTF-8"));
- if (is_string_empty($elementStart) && in_array(strtolower($elementName), $elementsBlock)) $rawText = rtrim($rawText)." ";
- if (substru($rawText, 0, 1)==" " && (is_string_empty($output) || substru($output, -1)==" ")) $rawText = ltrim($rawText);
- $output .= $this->getTextTruncated($rawText, $lengthMax);
- $lengthMax -= strlenu($rawText);
- }
- if (!is_string_empty($elementRawData) && $elementRawData==$endMarker) {
- $output .= $endMarkerText;
- $lengthMax = 0;
- }
- if ($lengthMax<=0 || !$elementFound) break;
- if ($hiddenLevel>0 ||
- preg_match("/aria-hidden=\"true\"/i", $elementAttributes) ||
- preg_match("/role=\"doc-noteref\"/i", $elementAttributes)) {
- if (!is_string_empty($elementName) && is_string_empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) {
- if (is_string_empty($elementStart)) {
- ++$hiddenLevel;
- } else {
- --$hiddenLevel;
- }
- }
- }
- $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 (!is_string_empty($elementBefore)) {
- $output .= $this->getTextTruncated($elementBefore, $lengthMax);
- $lengthMax -= strlenu($elementBefore);
- }
- if (!is_string_empty($elementRawData) && $elementRawData==$endMarker) {
- $output .= $endMarkerText;
- $lengthMax = 0;
- }
- if ($lengthMax<=0 || !$elementFound) break;
- if (!is_string_empty($elementName) && is_string_empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) {
- if (is_string_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 (is_string_empty($dataBuffer) && function_exists("random_bytes")) {
- $dataBuffer = @random_bytes($dataBufferSize);
- }
- if (is_string_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 (is_string_empty($salt) || strlenb($hash)!=60) $hash = "";
- break;
- case "sha256": $prefix = "$5y$";
- $salt = $this->createSalt(32);
- $hash = "$prefix$salt".hash("sha256", $salt.$text);
- if (is_string_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 && !is_string_empty($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;
- }
-
- // Remove meta data in raw data
- public function unsetMetaData($rawData, $key) {
- if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) {
- $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) continue;
- }
- $rawDataMiddle .= $line;
- }
- $rawDataNew = $parts[1]."---\n".$rawDataMiddle."---\n".$parts[3];
- } else {
- $rawDataNew = $rawData;
- }
- return $rawDataNew;
- }
-
- // Return troubleshooting URL
- public function getTroubleshootingUrl() {
- return "https://datenstrom.se/yellow/help/troubleshooting";
- }
-
- // 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->yellow->lookup->normalisePath($location);
- if (substru($location, 0, 1)!="/") $location = "/".$location;
- $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 (!is_string_empty($matches[1]) && !is_string_empty($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 sitename
- public function detectServerSitename() {
- $sitename = "Localhost";
- if (preg_match("#^(www\.)?([\w\-]+)#", $this->getServer("SERVER_NAME"), $matches)) {
- $sitename = ucfirst($matches[2]);
- }
- return $sitename;
- }
-
- // Detect server timezone
- public function detectServerTimezone() {
- $timezone = ini_get("date.timezone");
- if (is_string_empty($timezone)) {
- if (PHP_OS=="Darwin") {
- if (preg_match("#zoneinfo/(.*)#", @readlink("/etc/localtime"), $matches)) $timezone = $matches[1];
- } else {
- if (preg_match("/^(\S+)\/(\S+)/", $this->readFile("/etc/timezone"), $matches)) $timezone = $matches[1];
- }
- }
- if (!in_array($timezone, timezone_identifiers_list())) $timezone = "UTC";
- return $timezone;
- }
-
- // Detect server name, version and operating system
- public function detectServerInformation() {
- $name = "Unknown";
- $version = "x.x.x";
- $os = PHP_OS;
- if (preg_match("/^(\S+)\/(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) {
- $name = $matches[1];
- $version = $matches[2];
- } elseif (preg_match("/^(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) {
- $name = $matches[1];
- }
- if (PHP_SAPI=="cli" || PHP_SAPI=="cli-server") {
- $name = "Built-in";
- $version = PHP_VERSION;
- }
- if (PHP_OS=="Darwin") {
- $os = "Mac";
- } elseif (strtoupperu(substru(PHP_OS, 0, 3))=="WIN") {
- $os = "Windows";
- }
- return array($name, $version, $os);
- }
-
- // Detect browser language
- public function detectBrowserLanguage($languages, $languageDefault) {
- $languageFound = $languageDefault;
- foreach (preg_split("/\s*,\s*/", $this->getServer("HTTP_ACCEPT_LANGUAGE")) as $text) {
- list($language, $dummy) = $this->getTextList($text, ";", 2);
- if (!is_string_empty($language) && in_array($language, $languages)) {
- $languageFound = $language;
- break;
- }
- }
- return $languageFound;
- }
-
- // Detect terminal width and height
- public function detectTerminalInformation() {
- $width = $height = 0;
- if (strtoupperu(substru(PHP_OS, 0, 3))=="WIN") {
- exec("powershell \$Host.UI.RawUI.WindowSize.Width", $outputLines, $returnStatus);
- if ($returnStatus==0 && !is_array_empty($outputLines)) {
- $width = intval(end($outputLines));
- }
- exec("powershell \$Host.UI.RawUI.WindowSize.Height", $outputLines, $returnStatus);
- if ($returnStatus==0 && !is_array_empty($outputLines)) {
- $height = intval(end($outputLines));
- }
- } else {
- exec("stty size", $outputLines, $returnStatus);
- if ($returnStatus==0 && preg_match("/^(\d+)\s+(\d+)/", implode("\n", $outputLines), $matches)) {
- $width = intval($matches[2]);
- $height = intval($matches[1]);
- }
- }
- return array($width, $height);
- }
-
- // Detect image width, height, orientation and type for GIF/JPG/PNG/SVG
- public function detectImageInformation($fileName, $fileType = "") {
- $width = $height = $orientation = 0;
- $type = "";
- $fileHandle = @fopen($fileName, "rb");
- if ($fileHandle) {
- if (is_string_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, 2);
- if (!feof($fileHandle) && $dataSignature=="\xff\xd8") {
- for ($pos=2; $pos+8<$dataBufferSize; $pos+=$length) {
- if ($dataBuffer[$pos]!="\xff") break;
- $dataMarker = $dataBuffer[$pos+1];
- if ($dataMarker=="\xe1") {
- $orientation = $this->getImageOrientationFromBuffer($dataBuffer, $pos+4, $dataBufferSize);
- }
- if (($dataMarker>="\xc0" && $dataMarker<="\xc3") ||
- ($dataMarker>="\xc5" && $dataMarker<="\xc7") ||
- ($dataMarker>="\xc9" && $dataMarker<="\xcb") ||
- ($dataMarker>="\xcd" && $dataMarker<="\xcf")) {
- $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, $orientation, $type);
- }
-
- // Return image orientation from Exif
- public function getImageOrientationFromBuffer($dataBuffer, $pos, $size) {
- $orientation = 0;
- $dataSignature = substrb($dataBuffer, $pos, 6);
- if ($dataSignature=="\x45\x78\x69\x66\x00\x00" && $pos+14<=$size) {
- $startPos = $pos+6;
- $bigEndian = $dataBuffer[$startPos]=="M";
- $ifdOffset = $this->getLongFromBuffer($dataBuffer, $startPos+4, $bigEndian);
- $ifdStartPos = $startPos+$ifdOffset;
- $ifdCount = $ifdStartPos+2<=$size ? $this->getShortFromBuffer($dataBuffer, $ifdStartPos, $bigEndian) : 0;
- $pos = $ifdStartPos+2;
- while ($ifdCount && $pos+12<=$size) {
- $ifdTag = $this->getShortFromBuffer($dataBuffer, $pos, $bigEndian);
- $ifdFormat = $this->getShortFromBuffer($dataBuffer, $pos+2, $bigEndian);
- if ($ifdTag==0x8769 && $ifdFormat==4) {
- $ifdOffset = $this->getLongFromBuffer($dataBuffer, $pos+8, $bigEndian);
- $ifdStartPos = $startPos+$ifdOffset;
- $ifdCount = $ifdStartPos+2<=$size ? $this->getShortFromBuffer($dataBuffer, $ifdStartPos, $bigEndian) : 0;
- $pos = $ifdStartPos+2;
- continue;
- }
- if ($ifdTag==0x0112 && $ifdFormat==3) {
- $orientation = $this->getShortFromBuffer($dataBuffer, $pos+8, $bigEndian);
- break;
- }
- --$ifdCount;
- $pos += 12;
- }
- }
- return $orientation;
- }
-
- // Return unsigned short value from buffer
- public function getShortFromBuffer($dataBuffer, $pos, $bigEndian) {
- if ($bigEndian) {
- $value = (ord($dataBuffer[$pos])<<8) + ord($dataBuffer[$pos+1]);
- } else {
- $value = (ord($dataBuffer[$pos+1])<<8) + ord($dataBuffer[$pos]);
- }
- return $value;
- }
-
- // Return unsigned long value from buffer
- public function getLongFromBuffer($dataBuffer, $pos, $bigEndian) {
- if ($bigEndian) {
- $value = (ord($dataBuffer[$pos])<<24) + (ord($dataBuffer[$pos+1])<<16) +
- (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]);
- } else {
- $value = (ord($dataBuffer[$pos+3])<<24) + (ord($dataBuffer[$pos+2])<<16) +
- (ord($dataBuffer[$pos+1])<<8) + ord($dataBuffer[$pos]);
- }
- return $value;
- }
-
- // Send email message
- public function mail($action, $headers, $message) {
- $statusCode = 0;
- foreach ($this->yellow->extension->data as $key=>$value) {
- if (method_exists($value["object"], "onMail")) {
- $statusCode = $value["object"]->onMail($action, $headers, $message);
- if ($statusCode!=0) break;
- }
- }
- if ($statusCode==0) {
- $text = $this->yellow->lookup->normaliseHeaders($headers, "mime");
- $to = $subject = $remaining = $key = "";
- foreach (preg_split("/\r\n/", $text) as $line) {
- if (preg_match("/^(.*?):\s*(.*?)$/", $line, $matches) && !is_string_empty($matches[1])) {
- $key = $matches[1];
- $fragment = $matches[2];
- } else {
- $fragment = $line;
- }
- if ($key=="To") { $to .= $fragment; continue; }
- if ($key=="Subject") { $subject .= $fragment; continue; }
- $remaining .= $line."\r\n";
- }
- $statusCode = mail($to, $subject, $message, $remaining) ? 200 : 500;
- }
- return $statusCode==200;
- }
-
- // Write information to log file
- public function log($action, $message) {
- $statusCode = 0;
- foreach ($this->yellow->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->appendFile($this->yellow->system->get("coreServerInstallDirectory").
- $this->yellow->system->get("coreExtensionDirectory").
- $this->yellow->system->get("coreWebsiteFile"), $line);
- }
- }
-
- // 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 (is_string_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 normaliseArguments($text, $appendSlash = true, $filterStrict = true) { return $this->yellow->lookup->normaliseArguments($text, $appendSlash, $filterStrict); }
- public function normalisePath($text) { return $this->yellow->lookup->normalisePath($text); }
-}
-
-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 $statusCode; // status code
- public $errorMessage; // error message
- public $lastModified; // last modification date
- 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 function __construct($yellow) {
- $this->yellow = $yellow;
- $this->scheme = "";
- $this->address = "";
- $this->base = "";
- $this->location = "";
- $this->fileName = "";
- $this->metaData = new YellowArray();
- $this->pageCollections = array();
- $this->sharedPages = array();
- $this->headerData = array();
- }
-
- // Set request information
- public function setRequestInformation($scheme, $address, $base, $location, $fileName, $cacheable) {
- $this->scheme = $scheme;
- $this->address = $address;
- $this->base = $base;
- $this->location = $location;
- $this->fileName = $fileName;
- $this->cacheable = $cacheable;
- }
-
- // Parse page meta
- public function parseMeta($rawData, $statusCode = 0, $errorMessage = "") {
- $this->rawData = $rawData;
- $this->parser = null;
- $this->parserData = "";
- $this->statusCode = $statusCode;
- $this->errorMessage = $errorMessage;
- $this->lastModified = 0;
- $this->available = true;
- $this->visible = true;
- $this->active = $this->yellow->lookup->isActiveLocation($this->location, $this->yellow->page->location);
- $this->parseMetaData();
- }
-
- // Parse page meta update
- public function parseMetaUpdate() {
- if ($this->statusCode==0) {
- $this->rawData = $this->yellow->toolbox->readFile($this->fileName);
- $this->statusCode = 200;
- $this->parseMetaData();
- }
- }
-
- // Parse page meta data
- public function parseMetaData() {
- $this->metaData = new YellowArray();
- $this->metaDataOffsetBytes = 0;
- if (!is_null($this->rawData)) {
- $this->set("title", $this->yellow->toolbox->createTextTitle($this->location));
- $this->set("language", $this->yellow->lookup->findContentLanguage($this->fileName, $this->yellow->system->get("language")));
- $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName)));
- $this->parseMetaDataRaw(array("sitename", "author", "layout", "theme", "parser", "status"));
- $this->parseMetaDataShared();
- $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;
- } else {
- $this->set("size", filesize($this->fileName));
- $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)));
- }
- foreach ($this->yellow->extension->data as $key=>$value) {
- if (method_exists($value["object"], "onParseMetaData")) $value["object"]->onParseMetaData($this);
- }
- }
-
- // Parse page meta data from raw data
- public function parseMetaDataRaw($defaultKeys) {
- foreach ($defaultKeys as $key) {
- $value = $this->yellow->system->get($key);
- if (!is_string_empty($key) && !is_string_empty($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 (!is_string_empty($matches[1]) && !is_string_empty($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 meta data for shared pages
- public function parseMetaDataShared() {
- $this->sharedPages["main"] = $this;
- if (!$this->yellow->lookup->isSharedLocation($this->location) && $this->statusCode!=0) {
- foreach ($this->yellow->content->getShared($this->location) as $page) {
- $this->sharedPages[basename($page->location)] = $page;
- $page->sharedPages["main"] = $this;
- }
- }
- if ($this->yellow->lookup->isSharedLocation($this->location)) {
- $this->set("status", "shared");
- }
- }
-
- // Parse page content on demand
- public function parseContent() {
- 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->getContentRaw();
- $this->parserData = $this->parser->onParseContentRaw($this, $this->parserData);
- 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->getContentRaw();
- $this->parserData = preg_replace("/\[yellow error\]/i", $this->errorMessage, $this->parserData);
- }
- if (!$this->isExisting("description")) {
- $description = $this->yellow->toolbox->createTextDescription($this->parserData, 150);
- $this->set("description", !is_string_empty($description) ? $description : $this->get("title"));
- }
- if ($this->yellow->system->get("coreDebugMode")>=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" && $text=="error") {
- $output = $this->errorMessage;
- }
- }
- if ($this->yellow->system->get("coreDebugMode")>=3 && !is_string_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 (!$this->yellow->language->isExisting($this->get("language"))) {
- $this->error(500, "Language '".$this->get("language")."' does not exist!");
- }
- if (!is_object($this->parser)) {
- $this->error(500, "Parser '".$this->get("parser")."' 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->lookup->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->status(301, $location);
- }
- if ($this->yellow->lookup->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) {
- $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 ($this->yellow->system->get("coreDebugMode")>=2) {
- echo "YellowPage::includeLayout file:$fileNameLayoutTheme<br/>\n";
- }
- $this->setLastModified(filemtime($fileNameLayoutTheme));
- require($fileNameLayoutTheme);
- } elseif (is_file($fileNameLayoutNormal)) {
- if ($this->yellow->system->get("coreDebugMode")>=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 (!is_string_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 (!is_string_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 data, raw format
- public function getContentRaw() {
- $this->parseMetaUpdate();
- return substrb($this->rawData, $this->metaDataOffsetBytes);
- }
-
- // Return page content data, HTML encoded or raw format
- public function getContentHtml() {
- $this->parseContent();
- return $this->parserData;
- }
-
- // Return page extra data, HTML encoded
- public function getExtraHtml($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;
- }
-
- // 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] : "";
- }
-
- // 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 raw data for error page
- public function getRawDataError() {
- $statusCode = $this->statusCode;
- $sharedLocation = $this->yellow->content->getHomeLocation($this->location)."shared/";
- $fileNameError = $this->yellow->lookup->findFileFromContentLocation($sharedLocation, true).$this->yellow->system->get("coreContentErrorFile");
- $fileNameError = str_replace("(.*)", $statusCode, $fileNameError);
- $languageError = $this->yellow->lookup->findContentLanguage($this->fileName, $this->yellow->system->get("language"));
- if (is_file($fileNameError)) {
- $rawData = $this->yellow->toolbox->readFile($fileNameError);
- } elseif ($this->yellow->language->isText("coreError{$statusCode}Title", $languageError)) {
- $rawData = "---\nTitle: ".$this->yellow->language->getText("coreError{$statusCode}Title", $languageError)."\n";
- $rawData .= "Layout: error\n---\n".$this->yellow->language->getText("coreError{$statusCode}Text", $languageError);
- } else {
- $rawData = "---\nTitle:".$this->yellow->toolbox->getHttpStatusFormatted($statusCode, true)."\n";
- $rawData .= "Layout:error\n---\n".$this->errorMessage;
- }
- return $rawData;
- }
-
- // 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 (!is_string_empty($this->errorMessage)) $statusCode .= ": ".$this->errorMessage;
- }
- return $statusCode;
- }
-
- // Respond with status code, no page content
- public function status($statusCode, $location = "") {
- if ($statusCode>0 && !$this->isExisting("pageClean")) {
- $this->statusCode = $statusCode;
- $this->lastModified = 0;
- $this->headerData = array();
- if (!is_string_empty($location)) {
- $this->setHeader("Location", $location);
- $this->setHeader("Cache-Control", "no-cache, no-store");
- }
- $this->set("pageClean", (string)$statusCode);
- }
- }
-
- // Respond with error page
- public function error($statusCode, $errorMessage = "") {
- if ($statusCode>=400 && is_string_empty($this->errorMessage)) {
- $this->statusCode = $statusCode;
- $this->errorMessage = is_string_empty($errorMessage) ? "Page error!" : $errorMessage;
- }
- }
-
- // 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]);
- }
-
- // TODO: remove later, for backwards compatibility
- public function getContent($rawFormat = false) { return $rawFormat ? $this->getContentRaw() : $this->getContentHtml(); }
- public function getExtra($name) { return $this->getExtraHtml($name); }
-}
-
-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;
- }
-
- // Append page to end of page collection
- #[\ReturnTypeWillChange]
- public function append($page) {
- parent::append($page);
- }
-
- // Prepend page to start of page collection
- #[\ReturnTypeWillChange]
- public function prepend($page) {
- $array = $this->getArrayCopy();
- array_unshift($array, $page);
- $this->exchangeArray($array);
- }
-
- // Remove page from page collection
- public function remove($page): YellowPageCollection {
- $array = array();
- $location = $page->location;
- foreach ($this->getArrayCopy() as $page) {
- if ($page->location!=$location) array_push($array, $page);
- }
- $this->exchangeArray($array);
- return $this;
- }
-
- // Filter page collection by page setting
- public function filter($key, $value, $exactMatch = true): YellowPageCollection {
- $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 (is_string_empty($this->filterValue)) $this->filterValue = substru($pageValue, 0, $pageValueLength);
- array_push($array, $page);
- break;
- }
- }
- }
- }
- $this->exchangeArray($array);
- return $this;
- }
-
- // Filter page collection by location or file
- public function match($regex = "/.*/", $filterByLocation = true): YellowPageCollection {
- $array = array();
- $this->filterValue = $regex;
- foreach ($this->getArrayCopy() as $page) {
- if (preg_match($regex, $filterByLocation ? $page->location : $page->fileName)) array_push($array, $page);
- }
- $this->exchangeArray($array);
- return $this;
- }
-
- // Sort page collection by settings similarity
- public function similar($page): YellowPageCollection {
- $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 (!is_array_empty($tokens)) {
- $array = array();
- foreach ($this->getArrayCopy() as $page) {
- $sortScore = 0;
- foreach ($tokens as $token) {
- if (stristr($page->get("title"), $token)) $sortScore += 50;
- if (stristr($page->get("tag"), $token)) $sortScore += 5;
- if (stristr($page->get("author"), $token)) $sortScore += 2;
- }
- if ($page->location!=$location) {
- $page->set("sortScore", $sortScore);
- array_push($array, $page);
- }
- }
- $this->exchangeArray($array);
- $this->sort("modified", false)->sort("sortScore", false);
- }
- return $this;
- }
-
- // Sort page collection by page setting
- public function sort($key, $ascendingOrder = true): YellowPageCollection {
- $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;
- }
-
- // Group page collection by page setting, return array with multiple collections
- public function group($key, $ascendingOrder = true, $format = ""): array {
- $array = array();
- $groupByInitial = $format=="initial";
- $groupByDate = !is_string_empty($format) && $format!="count" && $format!="initial";
- foreach ($this->getIterator() as $page) {
- if ($page->isExisting($key)) {
- foreach (preg_split("/\s*,\s*/", $page->get($key)) as $group) {
- if ($groupByInitial) {
- $group = strtoupperu(substru($group, 0, 1));
- } elseif ($groupByDate) {
- $group = $this->yellow->language->getDateFormatted(strtotime($group), $format);
- }
- if (!is_string_empty($group)) {
- if (!isset($array[$group])) {
- $groupSearch = strtoloweru($group);
- foreach (array_keys($array) as $groupFound) {
- if (strtoloweru($groupFound)==$groupSearch) {
- $group = $groupFound;
- break;
- }
- }
- if (!isset($array[$group])) $array[$group] = new YellowPageCollection($this->yellow);
- }
- $array[$group]->append($page);
- }
- }
- }
- }
- $callbackString = function ($a, $b) use ($ascendingOrder) {
- return $ascendingOrder ? strnatcasecmp($a, $b) : strnatcasecmp($b, $a);
- };
- $callbackCollection = function ($a, $b) use ($ascendingOrder) {
- return $ascendingOrder ? count($a)-count($b) : count($b)-count($a);
- };
- if ($format!="count") {
- uksort($array, $callbackString);
- } else {
- uasort($array, $callbackCollection);
- }
- return $array;
- }
-
- // Calculate union, merge page collection
- public function merge($input): YellowPageCollection {
- $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): YellowPageCollection {
- $callback = function ($a, $b) {
- return strcmp($a->location, $b->location);
- };
- $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): YellowPageCollection {
- $callback = function ($a, $b) {
- return strcmp($a->location, $b->location);
- };
- $this->exchangeArray(array_udiff($this->getArrayCopy(), (array)$input, $callback));
- return $this;
- }
-
- // Limit the number of pages in page collection
- public function limit($pagesMax): YellowPageCollection {
- $this->exchangeArray(array_slice($this->getArrayCopy(), 0, $pagesMax));
- return $this;
- }
-
- // Reverse page collection
- public function reverse(): YellowPageCollection {
- $this->exchangeArray(array_reverse($this->getArrayCopy()));
- return $this;
- }
-
- // Randomize page collection
- public function shuffle(): YellowPageCollection {
- $array = $this->getArrayCopy();
- shuffle($array);
- $this->exchangeArray($array);
- return $this;
- }
-
- // Paginate page collection
- public function paginate($limit): YellowPageCollection {
- if (!$this->isPagination() && $limit!=0) {
- $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<0 || $this->paginationNumber>$this->paginationCount) $this->paginationNumber = 0;
- if ($this->paginationNumber) {
- $this->exchangeArray(array_slice($this->getArrayCopy(), ($this->paginationNumber - 1) * $limit, $limit));
- } else {
- $this->yellow->page->error(404);
- }
- }
- 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;
- }
-
- // Check if page collection is empty
- public function isEmpty() {
- return empty($this->getArrayCopy());
- }
-}
-
-class YellowArray extends ArrayObject {
- public function __construct($array = []) {
- 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
- #[\ReturnTypeWillChange]
- public function offsetGet($key) {
- if (is_string($key)) $key = lcfirst($key);
- return parent::offsetGet($key);
- }
-
- // Set array element
- #[\ReturnTypeWillChange]
- public function offsetSet($key, $value) {
- if (is_string($key)) $key = lcfirst($key);
- parent::offsetSet($key, $value);
- }
-
- // Remove array element
- #[\ReturnTypeWillChange]
- public function offsetUnset($key) {
- if (is_string($key)) $key = lcfirst($key);
- parent::offsetUnset($key);
- }
-
- // Check if array element exists
- #[\ReturnTypeWillChange]
- public function offsetExists($key) {
- if (is_string($key)) $key = lcfirst($key);
- return parent::offsetExists($key);
- }
-
- // Check if array is empty
- public function isEmpty() {
- return empty($this->getArrayCopy());
- }
-}
-
-// 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 is_string_empty($string) {
- return is_null($string) || $string==="";
-}
-function strempty($string) { return is_null($string) || $string===""; } //TODO: remove later, for backwards compatibility
-
-// Check if array is empty
-function is_array_empty($array) {
- return is_null($array) || (is_array($array) ? empty($array) : empty($array->getArrayCopy()));
-}
diff --git a/system/extensions/edit.css b/system/extensions/edit.css
@@ -1,602 +0,0 @@
-/* Edit extension, https://github.com/annaesvensson/yellow-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 .yellow-toolbar-disabled,
-.yellow-toolbar .yellow-toolbar-disabled:hover {
- background-color: inherit;
- border-color: #c1c1c1 #c1c1c1 #aaa;
- color: #aaa;
-}
-.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 #aaa;
- 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-emoji {
- padding: 10px;
- width: 14em;
-}
-#yellow-popup-emoji a {
- padding: 0.2em;
-}
-#yellow-popup-emoji .yellow-dropdown li {
- display: inline-block;
-}
-#yellow-popup-icon {
- padding: 10px;
- width: 13em;
-}
-#yellow-popup-icon a {
- padding: 0.18em 0.3em;
- min-width: 1em;
- text-align: center;
-}
-#yellow-popup-icon .yellow-dropdown li {
- display: inline-block;
-}
-
-/* Icons */
-
-.yellow-icon {
- display: inline-block;
- width: 1em;
- height: 1em;
- background-color: currentcolor;
- background-size: 1em 1em;
- background-repeat: no-repeat;
- background-position: center center;
-}
-.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 {
- -webkit-mask: url("edit-stack.svg#preview");
- mask: url("edit-stack.svg#preview");
-}
-.yellow-icon-format,
-.yellow-icon-paragraph {
- -webkit-mask: url("edit-stack.svg#format");
- mask: url("edit-stack.svg#format");
-}
-.yellow-icon-heading {
- -webkit-mask: url("edit-stack.svg#heading");
- mask: url("edit-stack.svg#heading");
-}
-.yellow-icon-h1 {
- -webkit-mask: url("edit-stack.svg#h1");
- mask: url("edit-stack.svg#h1");
-}
-.yellow-icon-h2 {
- -webkit-mask: url("edit-stack.svg#h2");
- mask: url("edit-stack.svg#h2");
-}
-.yellow-icon-h3 {
- -webkit-mask: url("edit-stack.svg#h3");
- mask: url("edit-stack.svg#h3");
-}
-.yellow-icon-bold {
- -webkit-mask: url("edit-stack.svg#bold");
- mask: url("edit-stack.svg#bold");
-}
-.yellow-icon-italic {
- -webkit-mask: url("edit-stack.svg#italic");
- mask: url("edit-stack.svg#italic");
-}
-.yellow-icon-strikethrough {
- -webkit-mask: url("edit-stack.svg#strikethrough");
- mask: url("edit-stack.svg#strikethrough");
-}
-.yellow-icon-quote {
- -webkit-mask: url("edit-stack.svg#quote");
- mask: url("edit-stack.svg#quote");
-}
-.yellow-icon-code,
-.yellow-icon-pre {
- -webkit-mask: url("edit-stack.svg#code");
- mask: url("edit-stack.svg#code");
-}
-.yellow-icon-link {
- -webkit-mask: url("edit-stack.svg#link");
- mask: url("edit-stack.svg#link");
-}
-.yellow-icon-file {
- -webkit-mask: url("edit-stack.svg#file");
- mask: url("edit-stack.svg#file");
-}
-.yellow-icon-list,
-.yellow-icon-ul {
- -webkit-mask: url("edit-stack.svg#list");
- mask: url("edit-stack.svg#list");
-}
-.yellow-icon-ol {
- -webkit-mask: url("edit-stack.svg#ol");
- mask: url("edit-stack.svg#ol");
-}
-.yellow-icon-tl {
- -webkit-mask: url("edit-stack.svg#tl");
- mask: url("edit-stack.svg#tl");
-}
-.yellow-icon-hr {
- -webkit-mask: url("edit-stack.svg#hr");
- mask: url("edit-stack.svg#hr");
-}
-.yellow-icon-table {
- -webkit-mask: url("edit-stack.svg#table");
- mask: url("edit-stack.svg#table");
-}
-.yellow-icon-emoji {
- -webkit-mask: url("edit-stack.svg#emoji");
- mask: url("edit-stack.svg#emoji");
-}
-.yellow-icon-icon {
- -webkit-mask: url("edit-stack.svg#icon");
- mask: url("edit-stack.svg#icon");
-}
-.yellow-icon-status {
- -webkit-mask: url("edit-stack.svg#status");
- mask: url("edit-stack.svg#status");
-}
-.yellow-icon-undo {
- -webkit-mask: url("edit-stack.svg#undo");
- mask: url("edit-stack.svg#undo");
-}
-.yellow-icon-redo {
- -webkit-mask: url("edit-stack.svg#redo");
- mask: url("edit-stack.svg#redo");
-}
-.yellow-icon-spinner {
- -webkit-mask: url("edit-stack.svg#spinner");
- mask: url("edit-stack.svg#spinner");
-}
-.yellow-icon-select {
- -webkit-mask: url("edit-stack.svg#select");
- mask: url("edit-stack.svg#select");
-}
-.yellow-icon-search {
- -webkit-mask: url("edit-stack.svg#search");
- mask: url("edit-stack.svg#search");
-}
-.yellow-icon-close {
- -webkit-mask: url("edit-stack.svg#close");
- mask: url("edit-stack.svg#close");
-}
-.yellow-icon-help {
- -webkit-mask: url("edit-stack.svg#help");
- mask: url("edit-stack.svg#help");
-}
-.yellow-icon-logo {
- -webkit-mask: url("edit-stack.svg#logo");
- mask: url("edit-stack.svg#logo");
-}
diff --git a/system/extensions/edit.js b/system/extensions/edit.js
@@ -1,1524 +0,0 @@
-// Edit extension, https://github.com/annaesvensson/yellow-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.coreDebugMode) 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\" aria-label=\""+this.getText("CancelButton")+"\"></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\" aria-label=\""+this.getText("CancelButton")+"\"></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\" aria-label=\""+this.getText("CancelButton")+"\"></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\" aria-label=\""+this.getText("CancelButton")+"\"></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\" aria-label=\""+this.getText("CancelButton")+"\"></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\" aria-label=\""+this.getText("CancelButton")+"\"></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-configure":
- elementDiv.innerHTML =
- "<form method=\"post\">"+
- "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\" aria-label=\""+this.getText("CancelButton")+"\"></i></a>"+
- "<div class=\"yellow-title\"><h1 id=\"yellow-pane-configure-title\">"+this.getText("ConfigureTitle")+"</h1></div>"+
- "<div class=\"yellow-status\"><p id=\"yellow-pane-configure-status\" class=\""+paneStatus+"\">"+this.getText("ConfigureStatus", "", paneStatus)+"</p></div>"+
- "<div class=\"yellow-settings\">"+
- "<div id=\"yellow-pane-configure-settings-actions\" class=\"yellow-settings-left\"><p>"+this.getRawDataSettingsActions(paneAction)+"</p></div>"+
- "<div id=\"yellow-pane-configure-settings-separator\" class=\"yellow-settings-left yellow-settings-separator\"> </div>"+
- "<div id=\"yellow-pane-configure-settings-fields\" class=\"yellow-settings-right yellow-fields\">"+
- "<input type=\"hidden\" name=\"action\" value=\"configure\" />"+
- "<input type=\"hidden\" name=\"csrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("csrftoken"))+"\" />"+
- "<p><label for=\"yellow-pane-configure-sitename\">"+this.getText("ConfigureSitename")+"</label><br /><input class=\"yellow-form-control\" name=\"sitename\" id=\"yellow-pane-configure-sitename\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("sitename"))+"\" /></p>"+
- "<p><label for=\"yellow-pane-configure-author\">"+this.getText("ConfigureAuthor")+"</label><br /><input class=\"yellow-form-control\" name=\"author\" id=\"yellow-pane-configure-author\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("author"))+"\" /></p>"+
- "<p><label for=\"yellow-pane-configure-email\">"+this.getText("ConfigureEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-configure-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+
- "<p>"+this.getText("ConfigureInformation")+"</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\" aria-label=\""+this.getText("CancelButton")+"\"></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\" aria-label=\""+this.getText("CancelButton")+"\"></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\" aria-label=\""+this.getText("CancelButton")+"\"></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 = "";
- if (document.getElementById("yellow-pane-account-"+yellow.user.language)) {
- document.getElementById("yellow-pane-account-"+yellow.user.language).checked = true;
- }
- }
- break;
- case "yellow-pane-configure":
- if (paneStatus=="none") {
- document.getElementById("yellow-pane-configure-status").innerHTML = this.getText("ConfigureStatusNone");
- document.getElementById("yellow-pane-configure-sitename").value = yellow.system.sitename;
- document.getElementById("yellow-pane-configure-author").value = yellow.system.author;
- document.getElementById("yellow-pane-configure-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")) {
- document.getElementById(paneId+"-text").readOnly = true;
- var elements = document.getElementsByClassName("yellow-toolbar-btn-icon");
- for (var i=0, l=elements.length; i<l; i++) {
- yellow.toolbox.addClass(elements[i], "yellow-toolbar-disabled");
- }
- yellow.toolbox.setVisible(document.getElementById(paneId+"-submit"), false);
- }
- }
- 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-configure":
- 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.coreDebugMode) 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.coreDebugMode) 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.coreDebugMode) 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 "configure": 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 "toolbar": this.processToolbar(status, arguments); break;
- case "settings": this.processSettings(arguments); break;
- case "submit": this.processSubmit(arguments); break;
- case "restore": this.processSubmit("action:"+action); break;
- case "help": this.processHelp(); break;
- case "close": this.processClose(); break;
- }
- }
- },
-
- // Process toolbar
- processToolbar: function(status, arguments) {
- if (yellow.system.coreDebugMode) 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 (this.isExpandable(status)) {
- this.showPopup("yellow-popup-"+status, status);
- } else {
- this.hidePopup(this.popupId);
- }
- }
- if (!elementText.readOnly) {
- if (status=="preview") this.showPreview(elementText, elementPreview);
- if (status=="save" && this.paneAction!="delete") this.processSubmit("action:"+this.paneAction);
- if (status=="help") window.open(this.getText("YellowHelpUrl"), "_blank");
- }
- },
-
- // 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.coreDebugMode) 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");
- },
-
- // Process close
- processClose: function() {
- this.hidePane(this.paneId);
- if (yellow.page.action=="login") {
- var url = yellow.system.coreServerScheme+"://"+
- yellow.system.coreServerAddress+
- yellow.system.coreServerBase+
- yellow.page.location;
- window.open(url, "_self");
- }
- },
-
- // Create popup
- createPopup: function(popupId) {
- if (yellow.system.coreDebugMode) 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-emoji":
- var rawDataEmojis = "";
- if (yellow.system.emojiToolbarButtons && yellow.system.emojiToolbarButtons!="none") {
- var tokens = yellow.system.emojiToolbarButtons.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=\"emoji emoji-"+yellow.toolbox.encodeHtml(className)+"\"></i></a></li>";
- }
- }
- elementDiv.innerHTML = "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+rawDataEmojis+"</ul>";
- break;
- case "yellow-popup-icon":
- var rawDataIcons = "";
- if (yellow.system.iconToolbarButtons && yellow.system.iconToolbarButtons!="none") {
- var tokens = yellow.system.iconToolbarButtons.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=\"icon "+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.coreDebugMode) 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.coreDebugMode) 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("DOMContentLoaded"));
- } 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").indexOf("#data-action-")!=-1) {
- var position = elements[i].getAttribute("href").indexOf("#data-action-");
- var action = elements[i].getAttribute("href").substring(position+13);
- var href = elements[i].getAttribute("href").substring(0, position);
- if (href=="" || href==yellow.page.base+yellow.page.location) elements[i].setAttribute("data-action", action);
- }
- 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 || yellow.page.statusCode==435) 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=="emoji" || name=="icon");
- },
-
- // 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.coreDebugMode) 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.coreDebugMode) 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
@@ -1,2018 +0,0 @@
-<?php
-// Edit extension, https://github.com/annaesvensson/yellow-edit
-
-class YellowEdit {
- const VERSION = "0.8.77";
- public $yellow; // access to API
- public $response; // web response
- public $merge; // text merge
- public $editable; // page can be edited? (boolean)
-
- // Handle initialisation
- public function onLoad($yellow) {
- $this->yellow = $yellow;
- $this->response = new YellowEditResponse($yellow);
- $this->merge = new YellowEditMerge($yellow);
- $this->yellow->system->setDefault("editSiteEmail", "noreply");
- $this->yellow->system->setDefault("editLocation", "/edit/");
- $this->yellow->system->setDefault("editUploadNewLocation", "/media/@group/@filename");
- $this->yellow->system->setDefault("editUploadExtensions", ".gif, .jpg, .mp3, .ogg, .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("editUserAccess", "create, edit, delete, restore, upload");
- $this->yellow->system->setDefault("editUserHome", "/");
- $this->yellow->system->setDefault("editLoginRestriction", "0");
- $this->yellow->system->setDefault("editLoginSessionTimeout", "2592000");
- $this->yellow->system->setDefault("editBruteForceProtection", "25");
- }
-
- // Handle update
- public function onUpdate($action) {
- if ($action=="clean" || $action=="daily") {
- $cleanup = false;
- $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$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" && !is_string_empty($matches[2])) {
- $status = $this->yellow->user->getUser("status", $matches[2]);
- $reserved = strtotime($this->yellow->user->getUser("modified", $matches[2])) + 60*60*24;
- $cleanup = $status!="active" && $status!="inactive" && $reserved<=time();
- }
- }
- if (!$cleanup) $fileDataNew .= $line;
- }
- $fileDataNew = rtrim($fileDataNew)."\n";
- if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileNameUser, $fileDataNew)) {
- $this->yellow->toolbox->log("error", "Can't write file '$fileNameUser'!");
- }
- }
- }
-
- // Handle request
- public function onRequest($scheme, $address, $base, $location, $fileName) {
- $statusCode = 0;
- if ($this->isEditLocation($location)) {
- $this->editable = true;
- $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->lookup->getRequestInformation($scheme, $address, $base);
- $this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName, false);
- $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName);
- }
- return $statusCode;
- }
-
- // 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]";
- }
-
- // Handle page meta data
- public function onParseMetaData($page) {
- $page->set("editPageUrl", $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"), "/").$page->location));
- }
-
- // Handle page content of shortcut
- public function onParseContentShortcut($page, $name, $text, $type) {
- $output = null;
- if ($name=="edit" && $type=="inline") {
- list($target, $description) = $this->yellow->toolbox->getTextList($text, " ", 2);
- if (is_string_empty($target) || $target=="-") $target = "main";
- if (is_string_empty($description)) $description = ucfirst($name);
- $pageTarget = $target=="main" ? $page->getPage("main") : $page->getPage("main")->getPage($target);
- $output = "<a href=\"".$pageTarget->get("editPageUrl")."\">".htmlspecialchars($description)."</a>";
- }
- return $output;
- }
-
- // Handle page layout
- public function onParsePageLayout($page, $name) {
- if ($this->editable) {
- $this->response->processPageData($page);
- }
- }
-
- // Handle page extra data
- public function onParsePageExtra($page, $name) {
- $output = null;
- if ($this->editable && $name=="header") {
- $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;
- }
-
- // 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) {
- $data[$key] = "$value[email] - User account by $value[name].";
- }
- uksort($data, "strnatcasecmp");
- foreach ($data as $line) echo "$line\n";
- if (is_array_empty($data)) echo "Yellow $command: No user accounts\n";
- return 200;
- }
-
- // Add user account
- public function userAdd($command, $text) {
- $status = "ok";
- list($option, $email, $password) = $this->yellow->toolbox->getTextArguments($text);
- if (is_string_empty($email) || is_string_empty($password)) $status = $this->response->status = "incomplete";
- 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") {
- $name = $this->yellow->system->get("sitename");
- $userLanguage = $this->yellow->system->get("language");
- $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile");
- $settings = array(
- "name" => $name,
- "description" => $this->yellow->language->getText("editUserDescription", $userLanguage),
- "language" => $userLanguage,
- "access" => $this->yellow->system->get("editUserAccess"),
- "home" => $this->yellow->system->get("editUserHome"),
- "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->toolbox->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) = $this->yellow->toolbox->getTextArguments($text);
- if (is_string_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("coreExtensionDirectory").$this->yellow->system->get("coreUserFile");
- $settings = array(
- "hash" => is_string_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);
- if (is_string_empty($email)) $status = $this->response->status = "invalid";
- 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") {
- $name = $this->yellow->user->getUser("name", $email);
- $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$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->toolbox->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 "configure": $statusCode = $this->processRequestConfigure($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 "restore": $statusCode = $this->processRequestRestore($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->getRedirectLocation($location);
- $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
- $statusCode = $this->yellow->sendStatus(301, $location);
- } else {
- $statusCode = 404;
- if ($this->response->isUserAccess("create", $location)) $statusCode = 434;
- if ($this->response->isUserAccess("restore", $location) && $this->response->isDeletedLocation($location)) {
- $statusCode = 435;
- }
- $this->yellow->page->error($statusCode);
- $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("coreExtensionDirectory").$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 (is_string_empty($name) || is_string_empty($email) || is_string_empty($password) || is_string_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") {
- $userLanguage = $this->yellow->lookup->findContentLanguage($fileName, $this->yellow->system->get("language"));
- $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile");
- $settings = array(
- "name" => $name,
- "description" => $this->yellow->language->getText("editUserDescription", $userLanguage),
- "language" => $userLanguage,
- "access" => $this->yellow->system->get("editUserAccess"),
- "home" => $this->yellow->system->get("editUserHome"),
- "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("coreExtensionDirectory").$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("coreExtensionDirectory").$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->toolbox->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 (is_string_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("coreExtensionDirectory").$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("coreExtensionDirectory").$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("coreExtensionDirectory").$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) || is_string_empty($hash)) $this->response->status = "done";
- }
- if ($this->response->status=="ok") {
- $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$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("coreExtensionDirectory").$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 (is_string_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("coreExtensionDirectory").$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->toolbox->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("coreExtensionDirectory").$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 || !is_string_empty($password)) {
- if (is_string_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("coreExtensionDirectory").$this->yellow->system->get("coreUserFile");
- $settings = array(
- "name" => $name,
- "description" => $this->yellow->user->getUser("description", $emailSource),
- "language" => $language,
- "access" => $this->yellow->user->getUser("access", $emailSource),
- "home" => $this->yellow->user->getUser("home", $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("coreExtensionDirectory").$this->yellow->system->get("coreUserFile");
- $settings = array(
- "name" => $name,
- "language" => $language,
- "pending" => $email.":".(is_string_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("coreExtensionDirectory").$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 settings
- public function processRequestConfigure($scheme, $address, $base, $location, $fileName) {
- $statusCode = 0;
- if ($this->response->isUserAccess("configure")) {
- $this->response->action = "configure";
- $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 (is_string_empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) $this->response->status = "invalid";
- }
- if ($this->response->status=="ok") {
- $fileNameSystem = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
- $settings = array("sitename" => $sitename, "author" => $author, "email" => $email);
- $file = $this->response->getFileSystem($scheme, $address, $base, $location, $fileNameSystem, $settings);
- $this->response->status = (!$file->isError() && $this->yellow->system->save($fileNameSystem, $settings)) ? "done" : "error";
- if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameSystem'!");
- }
- 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 = is_string_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 all")==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) && !is_string_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->errorMessage);
- $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) && !is_string_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)) {
- $ok = $this->yellow->toolbox->renameFile($fileName, $page->fileName, true) &&
- $this->yellow->toolbox->createFile($page->fileName, $page->rawData);
- } else {
- $ok = $this->yellow->toolbox->renameDirectory(dirname($fileName), dirname($page->fileName), true) &&
- $this->yellow->toolbox->createFile($page->fileName, $page->rawData);
- }
- if ($ok) {
- $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->errorMessage);
- $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()) {
- if ($this->yellow->lookup->isFileLocation($location)) {
- $ok = $this->response->deleteFileLocation($location, $fileName);
- } else {
- $ok = $this->response->deleteDirectoryLocation($location, $fileName);
- }
- if ($ok) {
- $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->errorMessage);
- $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
- }
- }
- return $statusCode;
- }
-
- // Process request to restore deleted page
- public function processRequestRestore($scheme, $address, $base, $location, $fileName) {
- $statusCode = 0;
- if ($this->response->isUserAccess("restore", $location) && !is_file($fileName)) {
- $page = $this->response->getPageRestore($scheme, $address, $base, $location, $fileName);
- if (!$page->isError()) {
- if ($this->yellow->lookup->isFileLocation($location)) {
- $ok = $this->response->restoreFileLocation($location);
- } else {
- $ok = $this->response->restoreDirectoryLocation($location);
- }
- if ($ok) {
- $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
- $statusCode = $this->yellow->sendStatus(303, $location);
- } else {
- $this->yellow->page->error(500, "Can't restore file '$fileName'!");
- $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
- }
- } else {
- $this->yellow->page->error(500, $page->errorMessage);
- $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"));
- $page->headerData = array(
- "Cache-Control"=>"no-cache, no-store",
- "Content-Type"=>$this->yellow->toolbox->getMimeContentType("a.html"),
- "Last-Modified"=>$this->yellow->toolbox->getHttpDateFormatted(time()));
- $statusCode = $this->yellow->sendData($page->statusCode, $page->headerData, $page->outputData);
- if ($this->yellow->system->get("coreDebugMode")>=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'!";
- }
- $headerData = array(
- "Cache-Control"=>"no-cache, no-store",
- "Content-Type"=>$this->yellow->toolbox->getMimeContentType("a.json"),
- "Last-Modified"=>$this->yellow->toolbox->getHttpDateFormatted(time()));
- return $this->yellow->sendData(isset($data["error"]) ? 500 : 200, $headerData, json_encode($data));
- }
-
- // 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 (is_string_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 (!is_string_empty($authToken) && !is_string_empty($csrfToken)) {
- $csrfTokenReceived = isset($_POST["csrftoken"]) ? $_POST["csrftoken"] : "";
- $csrfTokenIrrelevant = is_string_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 (is_string_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 (!is_string_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("coreExtensionDirectory").$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 (!is_string_empty($password) && strlenu($password)<$this->yellow->system->get("editUserPasswordMinLength")) $status = "short";
- if (!is_string_empty($password) && $password==$email) $status = "weak";
- if (!is_string_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 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;
- }
-
- // 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 edit location
- public function isEditLocation($location) {
- $locationLength = strlenu($this->yellow->system->get("editLocation"));
- return substru($location, 0, $locationLength)==$this->yellow->system->get("editLocation");
- }
-}
-
-class YellowEditResponse {
- public $yellow; // access to API
- public $extension; // access to extension
- 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");
- $this->userEmail = "";
- }
-
- // Process page data
- public function processPageData($page) {
- if ($this->isUser()) {
- if (is_string_empty($this->rawDataSource)) $this->rawDataSource = $page->rawData;
- if (is_string_empty($this->rawDataEdit)) $this->rawDataEdit = $page->rawData;
- if (is_string_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 || $page->statusCode==435) {
- $this->rawDataEdit = $this->getRawDataNew($page, true);
- $this->rawDataReadonly = false;
- }
- }
- if (is_string_empty($this->language)) $this->language = $page->get("language");
- if (is_string_empty($this->action)) $this->action = $this->isUser() ? "none" : "login";
- if (is_string_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->lookup->normaliseLines($rawData, $endOfLine);
- $page = new YellowPage($this->yellow);
- $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false);
- $page->parseMeta($rawData);
- $this->editContentFile($page, "create", $this->userEmail);
- if ($this->yellow->content->find($page->location)) {
- $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("editNewLocation"));
- $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published"));
- while ($this->yellow->content->find($page->location) || is_string_empty($page->fileName)) {
- $page->rawData = $this->yellow->toolbox->setMetaData($page->rawData, "title", $this->getTitleNext($page->rawData));
- $page->rawData = $this->yellow->lookup->normaliseLines($page->rawData, $endOfLine);
- $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("editNewLocation"));
- $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published"));
- if (++$pageCounter>999) break;
- }
- if ($this->yellow->content->find($page->location) || is_string_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->lookup->normaliseLines($rawDataSource, $endOfLine);
- $rawDataEdit = $this->yellow->lookup->normaliseLines($rawDataEdit, $endOfLine);
- $rawDataFile = $this->yellow->lookup->normaliseLines($rawDataFile, $endOfLine);
- $rawData = $this->extension->merge->merge($rawDataSource, $rawDataEdit, $rawDataFile);
- $page = new YellowPage($this->yellow);
- $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false);
- $page->parseMeta($rawData);
- $pageSource = new YellowPage($this->yellow);
- $pageSource->setRequestInformation($scheme, $address, $base, $location, $fileName, false);
- $pageSource->parseMeta($rawDataSource);
- $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("editNewLocation"), true);
- $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published"));
- if ($page->location!=$pageSource->location && ($this->yellow->content->find($page->location) || is_string_empty($page->fileName))) {
- $page->error(500, "Page '".$page->get("title")."' is not possible!");
- }
- }
- if (is_string_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->lookup->normaliseLines($rawData, $endOfLine);
- $page = new YellowPage($this->yellow);
- $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false);
- $page->parseMeta($rawData);
- $this->editContentFile($page, "delete", $this->userEmail);
- if (!$this->isUserAccess("delete", $page->location)) {
- $page->error(500, "Page '".$page->get("title")."' is restricted!");
- }
- return $page;
- }
-
- // Return restored page
- public function getPageRestore($scheme, $address, $base, $location, $fileName) {
- $page = new YellowPage($this->yellow);
- $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false);
- $page->parseMeta("");
- $this->editContentFile($page, "restore", $this->userEmail);
- if (!$this->isUserAccess("restore", $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->lookup->normaliseLines($rawData, $endOfLine);
- $page = new YellowPage($this->yellow);
- $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false);
- $page->parseMeta($rawData, 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->getContentHtml();
- $output .= "</div></div></div>";
- $page->statusCode = 200;
- $page->outputData = $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, false);
- $file->parseMeta(null);
- $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->lookup->normaliseData($fileData, $file->get("type"));
- if (is_string_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, $fileNameSystem, $settings) {
- $file = new YellowPage($this->yellow);
- $file->setRequestInformation($scheme, $address, $base, "/".$fileNameSystem, $fileNameSystem, false);
- $file->parseMeta(null);
- foreach ($settings as $key=>$value) $file->set($key, $value);
- $this->editSystemFile($file, "configure", $this->userEmail);
- return $file;
- }
-
- // Return page data including status information
- public function getPageData($page) {
- $data = array();
- $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->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;
- }
- 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["coreDebugMode"] = $this->yellow->system->get("coreDebugMode");
- $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->getAvailable("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["emojiToolbarButtons"] = $this->yellow->system->get("emojiToolbarButtons");
- $data["iconToolbarButtons"] = $this->yellow->system->get("iconToolbarButtons");
- if ($this->isUserAccess("configure")) {
- $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());
- }
- 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["description"] = $this->yellow->user->getUser("description", $this->userEmail);
- $data["language"] = $this->yellow->user->getUser("language", $this->userEmail);
- $data["status"] = $this->yellow->user->getUser("status", $this->userEmail);
- $data["access"] = $this->yellow->user->getUser("access", $this->userEmail);
- $data["home"] = $this->yellow->user->getUser("home", $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("configure")) $settingsActions .= ", configure";
- if ($this->isUserAccess("update")) $settingsActions .= ", update";
- return $settingsActions=="account" ? "none" : $settingsActions;
- }
-
- // Return toolbar buttons
- public function getToolbarButtons() {
- $toolbarButtons = $this->yellow->system->get("editToolbarButtons");
- if ($toolbarButtons=="auto") {
- $toolbarButtons = "format, bold, italic, strikethrough, code, separator, list, link, file";
- if ($this->yellow->extension->isExisting("emoji")) $toolbarButtons .= ", emoji";
- if ($this->yellow->extension->isExisting("icon")) $toolbarButtons .= ", icon";
- $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 = is_string_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 (!is_string_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)."shared/";
- $fileName = $this->yellow->lookup->findFileFromContentLocation($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)."shared/";
- $fileName = $this->yellow->lookup->findFileFromContentLocation($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, $editNewLocation, $pageMatchLocation = false) {
- $location = is_string_empty($editNewLocation) ? "@title" : $editNewLocation;
- $location = preg_replace("/@title/i", $this->getPageNewTitle($rawData), $location);
- $location = preg_replace("/@timestamp/i", $this->getPageNewData($rawData, "published", "U"), $location);
- $location = preg_replace("/@date/i", $this->getPageNewData($rawData, "published", "Y-m-d"), $location);
- $location = preg_replace("/@year/i", $this->getPageNewData($rawData, "published", "Y"), $location);
- $location = preg_replace("/@month/i", $this->getPageNewData($rawData, "published", "m"), $location);
- $location = preg_replace("/@day/i", $this->getPageNewData($rawData, "published", "d"), $location);
- $location = preg_replace("/@tag/i", $this->getPageNewData($rawData, "tag"), $location);
- $location = preg_replace("/@author/i", $this->getPageNewData($rawData, "author"), $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 (preg_match("/\d/", $location)) {
- $locationNew = "";
- $tokens = explode("/", $location);
- for ($i=1; $i<count($tokens); ++$i) {
- $locationNew .= "/".$this->yellow->lookup->normaliseToken($tokens[$i]);
- }
- $location = $locationNew;
- }
- 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 = is_string_empty($titleSlug) ? $title : $titleSlug;
- $value = $this->yellow->lookup->normaliseName($value, false, false, true);
- return trim(preg_replace("/-+/", "-", $value), "-");
- }
-
- // Return data for new/modified page
- public function getPageNewData($rawData, $key, $dateFormat = "") {
- $value = $this->yellow->toolbox->getMetaData($rawData, $key);
- if (preg_match("/^(.*?)\,(.*)$/", $value, $matches)) $value = $matches[1];
- if (!is_string_empty($dateFormat)) $value = date($dateFormat, strtotime($value));
- if (is_string_empty($value)) $value = "none";
- $value = $this->yellow->lookup->normaliseName($value, false, false, true);
- return trim(preg_replace("/-+/", "-", $value), "-");
- }
-
- // Return file name for new/modified page
- public function getPageNewFile($location, $pageFileName = "", $pagePrefix = "") {
- $fileName = $this->yellow->lookup->findFileFromContentLocation($location);
- if (!is_string_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 = is_string_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 (is_string_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 = is_string_empty($fileNewLocation) ? $this->yellow->system->get("editUploadNewLocation") : $fileNewLocation;
- $location = preg_replace("/@timestamp/i", time(), $location);
- $location = preg_replace("/@date/i", date("Y-m-d"), $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";
- $fileType = $this->yellow->toolbox->getFileType($fileNameShort);
- $locationMedia = $this->yellow->system->get("coreMediaLocation");
- $locationGroup = $this->yellow->system->get("coreDownloadLocation");
- if (preg_match("/(gif|jpg|png|svg)$/", $fileType)) {
- $locationGroup = $this->yellow->system->get("coreImageLocation");
- }
- if (preg_match("#^$locationMedia(.+?)\/#", $locationGroup, $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 = is_string_empty($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 = is_string_empty($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);
- $locationArguments = "/action:$action/email:$email/expire:$expire/language:$userLanguage/actiontoken:$actionToken/";
- $url = "$scheme://$address$base".$this->yellow->lookup->normaliseArguments($locationArguments, false, false);
- }
- $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");
- $siteEmail = $this->yellow->system->get("editSiteEmail");
- $subject = $this->yellow->language->getText("{$prefix}Subject", $userLanguage);
- $footer = $this->yellow->language->getText("editMailFooter", $userLanguage);
- $footer = str_replace("\\n", "\r\n", $footer);
- $footer = preg_replace("/@sitename/i", $sitename, $footer);
- $mailHeaders = array(
- "To" => "$userName <$userEmail>",
- "From" => "$sitename <$siteEmail>",
- "Subject" => $subject,
- "Date" => date(DATE_RFC2822),
- "Mime-Version" => "1.0",
- "Content-Type" => "text/plain; charset=utf-8",
- "X-Request-Url" => "$scheme://$address$base");
- $mailMessage = "$message\r\n\r\n$url\r\n-- \r\n$footer";
- return $this->yellow->toolbox->mail($action, $mailHeaders, $mailMessage);
- }
-
- // 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/");
- setcookie("csrftoken", "", 1, "$base/");
- }
-
- // 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 (is_string_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 (is_string_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 (is_string_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 (is_string_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);
- }
- }
- }
-
- // Delete file
- public function deleteFileLocation($location, $fileName) {
- $rawData = $this->yellow->toolbox->readFile($fileName);
- $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalLocation", $location);
- $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalFileName", $fileName);
- return $this->yellow->toolbox->createFile($fileName, $rawData) &&
- $this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory"));
- }
-
- // Delete directory
- public function deleteDirectoryLocation($location, $fileName) {
- $rawData = $this->yellow->toolbox->readFile($fileName);
- $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalLocation", $location);
- $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalFileName", $fileName);
- return $this->yellow->toolbox->createFile($fileName, $rawData) &&
- $this->yellow->toolbox->deleteDirectory(dirname($fileName), $this->yellow->system->get("coreTrashDirectory"));
- }
-
- // Restore deleted file from trash
- public function restoreFileLocation($location) {
- $fileNameDeleted = $fileNameRestored = "";
- $deleted = 0;
- $pathTrash = $this->yellow->system->get("coreTrashDirectory");
- $regex = "/^.*\\".$this->yellow->system->get("coreContentExtension")."$/";
- foreach ($this->yellow->toolbox->getDirectoryEntries($pathTrash, $regex, false, false) as $entry) {
- $rawDataOriginal = $this->yellow->toolbox->readFile($entry);
- $locationOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalLocation");
- $fileNameOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalFileName");
- $deletedOriginal = $this->yellow->toolbox->getFileDeleted($entry);
- if ($location==$locationOriginal && $deleted<=$deletedOriginal) {
- $fileNameDeleted = $entry;
- $fileNameRestored = $fileNameOriginal;
- $rawDataRestored = $rawDataOriginal;
- $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalLocation");
- $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalFileName");
- $deleted = $deletedOriginal;
- }
- }
- return !is_string_empty($fileNameDeleted) && $this->yellow->lookup->isContentFile($fileNameRestored) &&
- $this->yellow->toolbox->renameFile($fileNameDeleted, $fileNameRestored, true) &&
- $this->yellow->toolbox->createFile($fileNameRestored, $rawDataRestored);
- }
-
- // Restore deleted directory from trash
- public function restoreDirectoryLocation($location) {
- $pathDeleted = $fileNameRestored = "";
- $deleted = 0;
- $pathTrash = $this->yellow->system->get("coreTrashDirectory");
- foreach ($this->yellow->toolbox->getDirectoryEntries($pathTrash, "/.*/", false, true) as $entry) {
- $fileName = $entry."/".$this->yellow->system->get("coreContentDefaultFile");
- if (!is_file($fileName)) continue;
- $rawDataOriginal = $this->yellow->toolbox->readFile($fileName);
- $locationOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalLocation");
- $fileNameOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalFileName");
- $deletedOriginal = $this->yellow->toolbox->getFileDeleted($entry);
- if ($location==$locationOriginal && $deleted<=$deletedOriginal) {
- $pathDeleted = $entry;
- $fileNameRestored = $fileNameOriginal;
- $rawDataRestored = $rawDataOriginal;
- $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalLocation");
- $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalFileName");
- $deleted = $deletedOriginal;
- }
- }
- return !is_string_empty($pathDeleted) && $this->yellow->lookup->isContentFile($fileNameRestored) &&
- $this->yellow->toolbox->renameDirectory($pathDeleted, dirname($fileNameRestored), true) &&
- $this->yellow->toolbox->createFile($fileNameRestored, $rawDataRestored);
- }
-
- // Check if location has been deleted
- public function isDeletedLocation($location) {
- $found = false;
- $pathTrash = $this->yellow->system->get("coreTrashDirectory");
- $regex = "/^.*\\".$this->yellow->system->get("coreContentExtension")."$/";
- $fileNames = $this->yellow->toolbox->getDirectoryEntries($pathTrash, $regex, false, false);
- foreach ($this->yellow->toolbox->getDirectoryEntries($pathTrash, "/.*/", false, true) as $entry) {
- $fileName = $entry."/".$this->yellow->system->get("coreContentDefaultFile");
- if (is_file($fileName)) array_push($fileNames, $fileName);
- }
- foreach ($fileNames as $fileName) {
- $rawDataOriginal = $this->yellow->toolbox->readFile($fileName, 4096);
- $locationOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalLocation");
- if ($location==$locationOriginal) {
- $found = true;
- break;
- }
- }
- return $found;
- }
-
- // 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 login with restriction
- public function isLoginRestriction() {
- return $this->yellow->system->get("editLoginRestriction");
- }
-
- // Check if user is logged in
- public function isUser() {
- return !is_string_empty($this->userEmail);
- }
-
- // Check if user with access
- public function isUserAccess($action, $location = "") {
- $userHome = $this->yellow->user->getUser("home", $this->userEmail);
- $tokens = preg_split("/\s*,\s*/", $this->yellow->user->getUser("access", $this->userEmail));
- return in_array($action, $tokens) && (is_string_empty($location) || substru($location, 0, strlenu($userHome))==$userHome);
- }
-}
-
-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 ($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 = " ";
- }
- for (;$posYours<count($diffYours); ++$posYours) {
- array_push($diff, $diffYours[$posYours]);
- $typeYours = $diffYours[$posYours][0];
- $typeMine = " ";
- }
- 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/generate.php b/system/extensions/generate.php
@@ -1,456 +0,0 @@
-<?php
-// Generate extension, https://github.com/annaesvensson/yellow-generate
-
-class YellowGenerate {
- const VERSION = "0.8.52";
- public $yellow; // access to API
- public $files; // number of files
- 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("generateStaticUrl", "auto");
- $this->yellow->system->setDefault("generateStaticDirectory", "public/");
- $this->yellow->system->setDefault("generateStaticDefaultFile", "index.html");
- $this->yellow->system->setDefault("generateStaticErrorFile", "404.html");
- }
-
- // Handle update
- public function onUpdate($action) {
- if ($action=="install") {
- if ($this->yellow->system->isExisting("commandStaticUrl")) { //TODO: remove later, for backwards compatibility
- $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
- $settings = array(
- "generateStaticUrl" => $this->yellow->system->get("commandStaticUrl"),
- "generateStaticDirectory" => $this->yellow->system->get("commandStaticDirectory"),
- "generateStaticDefaultFile" => $this->yellow->system->get("commandStaticDefaultFile"),
- "generateStaticErrorFile" => $this->yellow->system->get("commandStaticErrorFile"));
- if (!$this->yellow->system->save($fileName, $settings)) {
- $this->yellow->toolbox->log("error", "Can't write file '$fileName'!");
- }
- $this->yellow->toolbox->log("info", "Import settings for 'Generate ".YellowGenerate::VERSION."'");
- }
- }
- }
-
- // 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 "generate": $statusCode = $this->processCommandGenerate($command, $text); break;
- case "clean": $statusCode = $this->processCommandClean($command, $text); break;
- default: $statusCode = 0;
- }
- return $statusCode;
- }
-
- // Handle command help
- public function onCommandHelp() {
- return array("generate [directory location]", "clean [directory location]");
- }
-
- // Process command to generate static website
- public function processCommandGenerate($command, $text) {
- $statusCode = 0;
- list($path, $location) = $this->yellow->toolbox->getTextArguments($text);
- if (is_string_empty($location) || substru($location, 0, 1)=="/") {
- if ($this->checkStaticSettings()) {
- $statusCode = $this->generateStatic($path, $location);
- } else {
- $statusCode = 500;
- $this->files = 0;
- $this->errors = 1;
- $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
- echo "ERROR generating files: Please configure GenerateStaticUrl 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;
- }
-
- // Generate static website
- public function generateStatic($path, $location) {
- $statusCode = 200;
- $this->files = $this->errors = 0;
- $path = rtrim(is_string_empty($path) ? $this->yellow->system->get("generateStaticDirectory") : $path, "/");
- if (is_string_empty($location)) {
- $statusCode = $this->cleanStatic($path, $location);
- foreach ($this->yellow->extension->data as $key=>$value) {
- if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("clean");
- }
- }
- $statusCode = max($statusCode, $this->generateStaticContent($path, $location, "\rGenerating static website", 5, 95));
- $statusCode = max($statusCode, $this->generateStaticMedia($path, $location));
- echo "\rGenerating static website 100%... done\n";
- return $statusCode;
- }
-
- // Generate static content
- public function generateStaticContent($path, $locationFilter, $progressText, $increments, $max) {
- $statusCode = 200;
- $this->locationsArguments = $this->locationsArgumentsPagination = array();
- $staticUrl = $this->yellow->system->get("generateStaticUrl");
- list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl);
- $locations = $this->getContentLocations();
- $filesEstimated = count($locations);
- foreach ($locations as $location) {
- echo "$progressText ".$this->getProgressPercent($this->files, $filesEstimated, $increments, $max/1.5)."%... ";
- if (!preg_match("#^$base$locationFilter#", "$base$location")) continue;
- $statusCode = max($statusCode, $this->generateStaticFile($path, $location, true));
- }
- foreach ($this->locationsArguments as $location) {
- echo "$progressText ".$this->getProgressPercent($this->files, $filesEstimated, $increments, $max/1.5)."%... ";
- if (!preg_match("#^$base$locationFilter#", "$base$location")) continue;
- $statusCode = max($statusCode, $this->generateStaticFile($path, $location, true));
- }
- $filesEstimated = $this->files + count($this->locationsArguments) + count($this->locationsArgumentsPagination);
- foreach ($this->locationsArgumentsPagination as $location) {
- echo "$progressText ".$this->getProgressPercent($this->files, $filesEstimated, $increments, $max)."%... ";
- if (!preg_match("#^$base$locationFilter#", "$base$location")) continue;
- if (substru($location, -1)!=$this->yellow->toolbox->getLocationArgumentsSeparator()) {
- $statusCode = max($statusCode, $this->generateStaticFile($path, $location, false, true));
- }
- for ($pageNumber=2; $pageNumber<=999; ++$pageNumber) {
- $statusCodeLocation = $this->generateStaticFile($path, $location.$pageNumber, false, true);
- $statusCode = max($statusCode, $statusCodeLocation);
- if ($statusCodeLocation==100) break;
- }
- }
- echo "$progressText ".$this->getProgressPercent(100, 100, $increments, $max)."%... ";
- return $statusCode;
- }
-
- // Generate static media
- public function generateStaticMedia($path, $locationFilter) {
- $statusCode = 200;
- if (is_string_empty($locationFilter)) {
- foreach ($this->getMediaLocations() as $location) {
- $statusCode = max($statusCode, $this->generateStaticFile($path, $location));
- }
- foreach ($this->getExtraLocations($path) as $location) {
- $statusCode = max($statusCode, $this->generateStaticFile($path, $location));
- }
- $statusCode = max($statusCode, $this->generateStaticFile($path, "/error/", false, false, true));
- }
- return $statusCode;
- }
-
- // Generate static file
- public function generateStaticFile($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("generateStaticUrl");
- 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 generating location '$location', ".$this->yellow->page->getStatusCode(true)."\n";
- }
- if ($this->yellow->system->get("coreDebugMode")>=1) {
- echo "YellowGenerate::generateStaticFile 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 (is_string_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->errorMessage = "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->errorMessage = "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;
- if (substru($location, strlenu($base), 1)!="/") 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 ($this->yellow->system->get("coreDebugMode")>=2) {
- echo "YellowGenerate::analyseLocations detected location:$location<br/>\n";
- }
- }
- } else {
- $location = rtrim($location, "0..9");
- if (!isset($this->locationsArgumentsPagination[$location])) {
- $this->locationsArgumentsPagination[$location] = $location;
- if ($this->yellow->system->get("coreDebugMode")>=2) {
- echo "YellowGenerate::analyseLocations detected location:$location<br/>\n";
- }
- }
- }
- }
- }
-
- // Process command to clean static website
- public function processCommandClean($command, $text) {
- $statusCode = 0;
- list($path, $location) = $this->yellow->toolbox->getTextArguments($text);
- if (is_string_empty($location) || substru($location, 0, 1)=="/") {
- $statusCode = $this->cleanStatic($path, $location);
- echo "Yellow $command: Static website";
- echo " ".($statusCode!=200 ? "not " : "")."cleaned\n";
- } else {
- $statusCode = 400;
- echo "Yellow $command: Invalid arguments\n";
- }
- return $statusCode;
- }
-
- // Clean static website
- public function cleanStatic($path, $location) {
- $statusCode = 200;
- $path = rtrim(is_string_empty($path) ? $this->yellow->system->get("generateStaticDirectory") : $path, "/");
- if (is_string_empty($location)) {
- $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;
- }
-
- // 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("generateStaticDefaultFile");
- if (is_file($fileName) && is_readable($fileName) && !$this->yellow->lookup->isCommandLine()) {
- $statusCode = $this->yellow->sendFile(200, $fileName, true);
- }
- }
- return $statusCode;
- }
-
- // Check static settings
- public function checkStaticSettings() {
- return preg_match("/^(http|https):/", $this->yellow->system->get("generateStaticUrl"));
- }
-
- // Check static directory
- public function checkStaticDirectory($path) {
- $ok = false;
- if (!is_string_empty($path)) {
- if ($path==rtrim($this->yellow->system->get("generateStaticDirectory"), "/")) $ok = true;
- if ($path==rtrim($this->yellow->system->get("coreCacheDirectory"), "/")) $ok = true;
- if ($path==rtrim($this->yellow->system->get("coreTrashDirectory"), "/")) $ok = true;
- if (is_file("$path/".$this->yellow->system->get("generateStaticDefaultFile"))) $ok = true;
- if (is_file("$path/yellow.php")) $ok = false;
- }
- return $ok;
- }
-
- // Return progress in percent
- public function getProgressPercent($now, $total, $increments, $max) {
- $max = intval($max/$increments) * $increments;
- $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("generateStaticDefaultFile");
- } elseif ($statusCode==404) {
- $fileName = $path."/".$this->yellow->system->get("generateStaticErrorFile");
- } else {
- $fileName = $path."/error.html";
- }
- return $fileName;
- }
-
- // 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("generateStaticUrl");
- list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl);
- $this->yellow->page->setRequestInformation($scheme, $address, $base, "", "", false);
- foreach ($this->yellow->content->index(true, true) as $page) {
- if (preg_match("/exclude/i", $page->get("generate")) && !$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();
- $mediaPath = $this->yellow->system->get("coreMediaDirectory");
- $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($mediaPath, "/.*/", false, false);
- foreach ($fileNames as $fileName) {
- array_push($locations, $this->yellow->lookup->findMediaLocationFromFile($fileName));
- }
- $extensionPath = $this->yellow->system->get("coreExtensionDirectory");
- $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($extensionPath, "/.*/", false, false);
- foreach ($fileNames as $fileName) {
- array_push($locations, $this->yellow->lookup->findMediaLocationFromFile($fileName));
- }
- $themePath = $this->yellow->system->get("coreThemeDirectory");
- $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($themePath, "/.*/", false, false);
- foreach ($fileNames as $fileName) {
- array_push($locations, $this->yellow->lookup->findMediaLocationFromFile($fileName));
- }
- return array_diff($locations, $this->getMediaLocationsIgnore());
- }
-
- // Return media locations to ignore
- public function getMediaLocationsIgnore() {
- $locations = array("");
- $extensionPath = $this->yellow->system->get("coreExtensionDirectory");
- $extensionDirectoryLength = strlenu($this->yellow->system->get("coreExtensionDirectory"));
- if ($this->yellow->extension->isExisting("bundle")) {
- foreach ($this->yellow->toolbox->getDirectoryEntries($extensionPath, "/^bundle-(.*)/", false, false) as $entry) {
- list($locationsBundle) = $this->yellow->extension->get("bundle")->getBundleInformation($entry);
- $locations = array_merge($locations, $locationsBundle);
- }
- }
- if ($this->yellow->extension->isExisting("edit")) {
- foreach ($this->yellow->toolbox->getDirectoryEntries($extensionPath, "/^edit\.(.*)/", false, false) as $entry) {
- $location = $this->yellow->system->get("coreExtensionLocation").substru($entry, $extensionDirectoryLength);
- array_push($locations, $location);
- }
- }
- return array_unique($locations);
- }
-
- // Return extra locations
- public function getExtraLocations($path) {
- $locations = array();
- $pathIgnore = "($path/|".
- $this->yellow->system->get("generateStaticDirectory")."|".
- $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;
- }
-}
diff --git a/system/extensions/image.php b/system/extensions/image.php
@@ -1,199 +0,0 @@
-<?php
-// Image extension, https://github.com/annaesvensson/yellow-image
-
-class YellowImage {
- const VERSION = "0.8.19";
- 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("imageThumbnailJpgQuality", "80");
- }
-
- // Handle update
- public function onUpdate($action) {
- if ($action=="clean") {
- $statusCode = 200;
- $path = $this->yellow->lookup->findMediaDirectory("coreThumbnailLocation");
- foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, false) as $entry) {
- if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500;
- }
- if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!");
- }
- }
-
- // 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 (is_string_empty($alt)) $alt = $this->yellow->language->getText("imageDefaultAlt");
- if (is_string_empty($width)) $width = "100%";
- if (is_string_empty($height)) $height = $width;
- $path = $this->yellow->lookup->findMediaDirectory("coreImageLocation");
- list($src, $width, $height) = $this->getImageInformation($path.$name, $width, $height);
- } else {
- if (is_string_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 (!is_string_empty($alt)) $output .= " alt=\"".htmlspecialchars($alt)."\" title=\"".htmlspecialchars($alt)."\"";
- if (!is_string_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, $orientation, $type) =
- $this->yellow->toolbox->detectImageInformation($fileName, $file->get("type"));
- $widthMax = $this->yellow->system->get("imageUploadWidthMax");
- $heightMax = $this->yellow->system->get("imageUploadHeightMax");
- if ($type=="gif" || $type=="jpg" || $type=="png") {
- if ($widthInput>$widthMax || $heightInput>$heightMax) {
- 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, $orientation);
- if (!$this->saveImage($image, $fileName, $type, $this->yellow->system->get("imageUploadJpgQuality"))) {
- $file->error(500, "Can't write file '$fileName'!");
- }
- } elseif ($orientation>1) {
- $image = $this->loadImage($fileName, $type);
- $image = $this->orientImage($image, $orientation);
- if (!$this->saveImage($image, $fileName, $type, $this->yellow->system->get("imageUploadJpgQuality"))) {
- $file->error(500, "Can't write file '$fileName'!");
- }
- }
- }
- }
- }
-
- // Return image information, create thumbnail on demand
- public function getImageInformation($fileName, $widthOutput, $heightOutput) {
- $fileNameShort = substru($fileName, strlenu($this->yellow->lookup->findMediaDirectory("coreImageLocation")));
- list($widthInput, $heightInput, $orientation, $type) = $this->yellow->toolbox->detectImageInformation($fileName);
- $widthOutput = $this->convertValueAndUnit($widthOutput, $widthInput);
- $heightOutput = $this->convertValueAndUnit($heightOutput, $heightInput);
- if (($widthInput==$widthOutput && $heightInput==$heightOutput) || $type=="svg" || $type=="") {
- $src = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreImageLocation").$fileNameShort;
- $width = $widthOutput;
- $height = $heightOutput;
- } else {
- $pathThumb = $this->yellow->lookup->findMediaDirectory("coreThumbnailLocation");
- $fileNameThumb = ltrim(str_replace(array("/", "\\", "."), "-", dirname($fileNameShort)."/".pathinfo($fileName, PATHINFO_FILENAME)), "-");
- $fileNameThumb .= "-".$widthOutput."x".$heightOutput;
- $fileNameThumb .= ".".pathinfo($fileName, PATHINFO_EXTENSION);
- $fileNameOutput = $pathThumb.$fileNameThumb;
- if ($this->isFileNotUpdated($fileName, $fileNameOutput)) {
- $image = $this->loadImage($fileName, $type);
- $image = $this->resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput);
- $image = $this->orientImage($image, $orientation);
- 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("coreThumbnailLocation").$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, $orientation) {
- switch ($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
@@ -1,4060 +0,0 @@
-<?php
-// Markdown extension, https://github.com/annaesvensson/yellow-markdown
-
-class YellowMarkdown {
- const VERSION = "0.8.26";
- 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);
- $text = $markdown->transform($text);
- $text = $this->yellow->lookup->normaliseData($text, "html");
- return $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: 
- // 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: 
- // 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->getPage("main")->location);
- };
- $this->span_gamut += array("doStrikethrough" => 55);
- $this->block_gamut += array("doNoticeBlocks" => 65);
- $this->document_gamut += array("doFootnotesLinks" => 55);
- $this->escape_chars .= "~";
- parent::__construct();
- }
-
- // Handle striketrough
- public function doStrikethrough($text) {
- $parts = preg_split("/(?<![~])(~~)(?![~])/", $text, -1, 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 = is_string_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 (is_string_empty($attr) && $level>=2) $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 (is_string_empty($attr) && $level>=2) $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 (!is_string_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 (!is_string_empty($alt)) $output .= " alt=\"".$this->encodeAttribute($alt)."\"";
- if (!is_string_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 blockquotes, CommonMark compatible
- public function doBlockQuotes($text) {
- return preg_replace_callback("/((?>^[ ]*>[ ]?.+\n(.+\n)*)+)/m", array($this, "_doBlockQuotes_callback"), $text);
- }
-
- // 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 (!is_string_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";
- }
-
- // Handle footnotes links, normalise ids and links
- public function doFootnotesLinks($text) {
- if (!is_null($this->footnotes_assembled)) {
- $callbackId = function ($matches) {
- $id = str_replace(":", "-", $matches[2]);
- return "<$matches[1] id=\"$id\" $matches[3]>";
- };
- $text = preg_replace_callback("/<(li|sup) id=\"(fn:\d+)\"(.*?)>/", $callbackId, $text);
- $text = preg_replace_callback("/<(li|sup) id=\"(fnref\d*:\d+)\"(.*?)>/", $callbackId, $text);
- $callbackHref = function ($matches) {
- $href = $this->page->base.$this->page->location.str_replace(":", "-", $matches[2]);
- return "<$matches[1] href=\"$href\" $matches[3]>";
- };
- $text = preg_replace_callback("/<(a) href=\"(#fn:\d+)\"(.*?)>/", $callbackHref, $text);
- $text = preg_replace_callback("/<(a) href=\"(#fnref\d*:\d+)\"(.*?)>/", $callbackHref, $text);
- }
- return $text;
- }
-
- // 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/serve.php b/system/extensions/serve.php
@@ -1,61 +0,0 @@
-<?php
-// Serve extension, https://github.com/annaesvensson/yellow-serve
-
-class YellowServe {
- const VERSION = "0.8.24";
- public $yellow; // access to API
-
- // Handle initialisation
- public function onLoad($yellow) {
- $this->yellow = $yellow;
- }
-
- // Handle command
- public function onCommand($command, $text) {
- switch ($command) {
- case "serve": $statusCode = $this->processCommandServe($command, $text); break;
- default: $statusCode = 0;
- }
- return $statusCode;
- }
-
- // Handle command help
- public function onCommandHelp() {
- return "serve [url]";
- }
-
- // Process command to start web server
- public function processCommandServe($command, $text) {
- list($url) = $this->yellow->toolbox->getTextArguments($text);
- if (is_string_empty($url)) $url = "http://localhost:8000/";
- list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($url);
- if ($scheme=="http" && !is_string_empty($address) && is_string_empty($base)) {
- if (!preg_match("/\:\d+$/", $address)) $address .= ":8000";
- if ($this->checkServerSettings("$scheme://$address/")) {
- echo "Starting web server. Open a web browser and go to $scheme://$address/\n";
- echo "Press Ctrl+C to quit...\n";
- exec(PHP_BINARY." -S $address yellow.php 2>&1", $outputLines, $returnStatus);
- $statusCode = $returnStatus!=0 ? 500 : 200;
- if ($statusCode!=200) {
- $output = !is_array_empty($outputLines) ? end($outputLines) : "Please check arguments!";
- if (preg_match("/^\[(.*?)\]\s*(.*)$/", $output, $matches)) $output = $matches[2];
- echo "ERROR starting web server: $output\n";
- }
- } else {
- $statusCode = 400;
- $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
- echo "ERROR starting web server: Please configure `CoreServerUrl: auto` in file '$fileName'!\n";
- }
- } else {
- $statusCode = 400;
- echo "Yellow $command: Invalid arguments\n";
- }
- return $statusCode;
- }
-
- // Check server settings
- public function checkServerSettings($url) {
- return $this->yellow->system->get("coreServerUrl")=="auto" ||
- $this->yellow->system->get("coreServerUrl")==$url;
- }
-}
diff --git a/system/extensions/stockholm.php b/system/extensions/stockholm.php
@@ -1,22 +0,0 @@
-<?php
-// Stockholm extension, https://github.com/annaesvensson/yellow-stockholm
-
-class YellowStockholm {
- const VERSION = "0.8.14";
- 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("coreExtensionDirectory").$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") {
- $this->yellow->system->save($fileName, array("theme" => $this->yellow->system->getDifferent("theme")));
- }
- }
-}
diff --git a/system/extensions/update-available.ini b/system/extensions/update-available.ini
@@ -0,0 +1,832 @@
+# Datenstrom Yellow update settings for available extensions
+
+Extension: Anchor
+Version: 0.9.2
+Description: Show anchor links next to headings.
+Developer: Robert Pfotenhauer
+Tag: anchors, headings, feature
+DownloadUrl: https://github.com/pftnhr/yellow-anchor/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/pftnhr/yellow-anchor
+Published: 2024-08-19 10:43:22
+Status: available
+system/workers/anchor.php: anchor.php, create, update
+system/workers/anchor.css: anchor.css, create, update
+system/workers/anchor-stack.svg: anchor-stack.svg, create, update
+
+Extension: Backtotop
+Version: 0.9.1
+Description: Back-to-top link.
+Developer: Giovanni Salmeri
+Tag: feature
+DownloadUrl: https://github.com/GiovanniSalmeri/yellow-backtotop/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/GiovanniSalmeri/yellow-backtotop
+Published: 2024-04-25 20:30:00
+Status: available
+system/workers/backtotop.php: backtotop.php, create, update
+system/workers/backtotop.js: backtotop.js, create, update
+system/workers/backtotop.css: backtotop.css, create, update
+
+Extension: Berlin
+Version: 0.9.6
+Description: Berlin is a theme inspired by Dieter Rams.
+Designer: Anna Svensson
+Tag: theme
+DownloadUrl: https://github.com/annaesvensson/yellow-berlin/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-berlin
+DocumentationLanguage: en, de, sv
+Published: 2026-02-23 09:49:59
+Status: available
+system/workers/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.9.4
+Description: Blog for your website.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-blog/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-blog
+DocumentationLanguage: en, de, sv
+Published: 2024-05-09 16:44:32
+Status: available
+system/workers/blog.php: blog.php, create, update
+system/layouts/blog.html: blog.html, create, update, careful
+system/layouts/blog-start.html: blog-start.html, create, update, careful
+content/shared/page-new-blog.md: page-new-blog.md, create, optional
+content/3-blog/page.md: page.md, create, optional
+content/3-blog/2020-04-07-blog-example-page.md: 2020-04-07-blog-example-page.md, create, optional
+content/3-blog/2020-12-06-made-for-people.md: 2020-12-06-made-for-people.md, create, optional
+
+Extension: Breadcrumb
+Version: 0.9.1
+Description: Breadcrumb navigation.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-breadcrumb/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-breadcrumb
+DocumentationLanguage: en, de, sv
+Published: 2024-04-05 00:26:47
+Status: available
+system/workers/breadcrumb.php: breadcrumb.php, create, update
+
+Extension: Bundle
+Version: 0.9.7
+Description: Bundle website files.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-bundle/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-bundle
+DocumentationLanguage: en, de, sv
+Published: 2025-11-26 15:18:47
+Status: available
+system/workers/bundle.php: bundle.php, create, update
+
+Extension: Catalan
+Version: 0.9.5
+Description: Catalan language.
+Translator: Andreu Ferrer
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/catalan.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/catalan
+Published: 2025-12-19 01:24:45
+Status: available
+system/workers/catalan.php: catalan.php, create, update
+
+Extension: Check
+Version: 0.9.7
+Description: Find broken links.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-check/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-check
+DocumentationLanguage: en, de, sv
+Published: 2026-02-17 15:57:34
+Status: available
+system/workers/check.php: check.php, create, update
+
+Extension: Chinese
+Version: 0.9.5
+Description: Chinese language.
+Translator: Hyson Lee
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/chinese.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/chinese
+Published: 2025-12-19 01:25:19
+Status: available
+system/workers/chinese.php: chinese.php, create, update
+
+Extension: Contact
+Version: 0.9.1
+Description: Email contact page.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-contact/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-contact
+DocumentationLanguage: en, de, sv
+Published: 2024-04-05 00:15:27
+Status: available
+system/workers/contact.php: contact.php, create, update
+system/layouts/contact.html: contact.html, create, update, careful
+content/contact/page.md: page.md, create, optional
+
+Extension: Copenhagen
+Version: 0.9.6
+Description: Copenhagen is a beautiful theme.
+Designer: Anna Svensson
+Tag: theme
+DownloadUrl: https://github.com/annaesvensson/yellow-copenhagen/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-copenhagen
+DocumentationLanguage: en, de, sv
+Published: 2026-02-23 09:49:58
+Status: available
+system/workers/copenhagen.php: copenhagen.php, create, update
+system/themes/copenhagen.css: copenhagen.css, create, update, careful
+system/themes/copenhagen.png: copenhagen.png, create
+
+Extension: Core
+Version: 0.9.22
+Description: Core functionality of your website.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-core/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-core
+DocumentationLanguage: en, de, sv
+Published: 2026-02-21 19:43:11
+Status: available
+system/workers/core.php: core.php, create, update
+system/extensions/core.php: corepatch.txt, update
+system/layouts/default.html: default.html, create, update, careful
+system/layouts/error.html: error.html, create, update, careful
+system/layouts/header.html: header.html, create, update, careful
+system/layouts/footer.html: footer.html, create, update, careful
+system/layouts/navigation.html: navigation.html, create, update, careful
+system/layouts/pagination.html: pagination.html, create, update, careful
+
+Extension: Czech
+Version: 0.9.5
+Description: Czech language.
+Translator: Ufo Vyhuleny
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/czech.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/czech
+Published: 2025-12-19 01:25:37
+Status: available
+system/workers/czech.php: czech.php, create, update
+
+Extension: Danish
+Version: 0.9.5
+Description: Danish language.
+Translator: David Garcia
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/danish.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/danish
+Published: 2025-12-19 01:24:05
+Status: available
+system/workers/danish.php: danish.php, create, update
+
+Extension: Draft
+Version: 0.9.1
+Description: Support for draft pages.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-draft/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-draft
+DocumentationLanguage: en, de, sv
+Published: 2024-04-05 00:26:47
+Status: available
+system/workers/draft.php: draft.php, create, update
+
+Extension: Dutch
+Version: 0.9.5
+Description: Dutch language.
+Translator: Robin Vannieuwenhuijse
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/dutch.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/dutch
+Published: 2025-12-19 01:24:05
+Status: available
+system/workers/dutch.php: dutch.php, create, update
+
+Extension: Edit
+Version: 0.9.15
+Description: Edit your website in a web browser.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-edit/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-edit
+DocumentationLanguage: en, de, sv
+Published: 2026-02-17 16:02:00
+Status: available
+system/workers/edit.php: edit.php, create, update
+system/workers/edit.css: edit.css, create, update
+system/workers/edit.js: edit.js, create, update
+system/workers/edit-stack.svg: edit-stack.svg, create, update
+content/shared/page-new-default.md: page-new-default.md, create, optional
+
+Extension: Emoji
+Version: 0.9.4
+Description: Lots and lots of emoji.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-emoji/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-emoji
+DocumentationLanguage: en, de, sv
+Published: 2024-05-07 07:21:20
+Status: available
+system/workers/emoji.php: emoji.php, create, update
+system/workers/emoji.css: emoji.css, create, update
+system/workers/emoji-default-stack.svg: emoji-default-stack.svg, create, update
+system/workers/emoji-extra1-stack.svg: emoji-extra1-stack.svg, create, update
+system/workers/emoji-extra2-stack.svg: emoji-extra2-stack.svg, create, update
+system/workers/emoji-extra3-stack.svg: emoji-extra3-stack.svg, create, update
+system/workers/emoji-extra4-stack.svg: emoji-extra4-stack.svg, create, update
+system/workers/emoji-extra5-stack.svg: emoji-extra5-stack.svg, create, update
+system/workers/emoji-extra6-stack.svg: emoji-extra6-stack.svg, create, update
+system/workers/emoji-extra7-stack.svg: emoji-extra7-stack.svg, create, update
+system/workers/emoji-flags-stack.svg: emoji-flags-stack.svg, create, update
+
+Extension: English
+Version: 0.9.5
+Description: English language.
+Translator: Mark Seuffert
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/english.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/english
+Published: 2025-12-19 01:21:34
+Status: available
+system/workers/english.php: english.php, create, update
+
+Extension: Feed
+Version: 0.9.3
+Description: Feed with recent changes.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-feed/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-feed
+DocumentationLanguage: en, de, sv
+Published: 2024-05-09 16:28:24
+Status: available
+system/workers/feed.php: feed.php, create, update
+system/layouts/feed.html: feed.html, create, update, careful
+content/feed/page.md: page.md, create, optional
+
+Extension: French
+Version: 0.9.5
+Description: French language.
+Translator: Juh Nibreh
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/french.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/french
+Published: 2025-12-19 01:24:05
+Status: available
+system/workers/french.php: french.php, create, update
+
+Extension: Gallery
+Version: 0.9.4
+Description: Image gallery with popup.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-gallery/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-gallery
+DocumentationLanguage: en, de, sv
+Published: 2025-08-22 17:48:35
+Status: available
+system/workers/gallery.php: gallery.php, create, update
+system/workers/gallery.js: gallery.js, create, update
+system/workers/gallery.css: gallery.css, create, update
+system/workers/gallery-photoswipe.min.js: gallery-photoswipe.min.js, create, update
+system/workers/gallery-default-skin.png: gallery-default-skin.png, create, update
+system/workers/gallery-default-skin.svg: gallery-default-skin.svg, create, update
+system/workers/gallery-preloader.gif: gallery-preloader.gif, create, update
+
+Extension: Generate
+Version: 0.9.8
+Description: Generate a static website.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-generate/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-generate
+DocumentationLanguage: en, de, sv
+Published: 2026-02-17 15:57:09
+Status: available
+system/workers/generate.php: generate.php, create, update
+
+Extension: German
+Version: 0.9.5
+Description: German language.
+Translator: David Fehrmann
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/german.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/german
+Published: 2025-12-19 01:22:34
+Status: available
+system/workers/german.php: german.php, create, update
+
+Extension: Help
+Version: 0.9.9
+Description: Documentation of the basics.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-help/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-help
+DocumentationLanguage: en, de, sv
+Published: 2026-02-25 22:49:41
+Status: available
+system/workers/help.php: help.php, create, update
+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/how-to-change-the-content.md: how-to-change-the-content.md, create, optional, multi-language
+content/9-help/how-to-change-the-media.md: how-to-change-the-media.md, create, optional, multi-language
+content/9-help/how-to-change-the-system.md: how-to-change-the-system.md, create, optional, multi-language
+content/9-help/how-to-customise-languages.md: how-to-customise-languages.md, create, optional, multi-language
+content/9-help/how-to-customise-html-and-css: how-to-customise-html-and-css.md, create, optional, multi-language
+content/9-help/how-to-get-started.md: how-to-get-started.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/page.md: page.md, create, optional, multi-language
+content/9-help/troubleshooting.md: troubleshooting.md, create, optional, multi-language
+content/9-help/what-s-new.md: what-s-new.md, create, optional, multi-language
+media/images/help-photo.jpg: help-photo.jpg, create, optional
+media/images/language-de.png: language-de.png, create, optional
+media/images/language-en.png: language-en.png, create, optional
+media/images/language-sv.png: language-sv.png, create, optional
+
+Extension: Highlight
+Version: 0.9.2
+Description: Highlight code blocks.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-highlight/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-highlight
+DocumentationLanguage: en, de, sv
+Published: 2024-04-09 19:50:39
+Status: available
+system/workers/highlight.php: highlight.php, create, update
+system/workers/highlight.css: highlight.css, create, update
+system/workers/highlight-cpp.json: highlight-cpp.json, create, update
+system/workers/highlight-css.json: highlight-css.json, create, update
+system/workers/highlight-javascript.json: highlight-javascript.json, create, update
+system/workers/highlight-json.json: highlight-json.json, create, update
+system/workers/highlight-lua.json: highlight-lua.json, create, update
+system/workers/highlight-php.json: highlight-php.json, create, update
+system/workers/highlight-python.json: highlight-python.json, create, update
+system/workers/highlight-xml.json: highlight-xml.json, create, update
+system/workers/highlight-yaml.json: highlight-yaml.json, create, update
+
+Extension: Hungarian
+Version: 0.9.5
+Description: Hungarian language.
+Translator: Ádám Tuba
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/hungarian.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/hungarian
+Published: 2025-12-19 01:25:57
+Status: available
+system/workers/hungarian.php: hungarian.php, create, update
+
+Extension: Icon
+Version: 0.9.2
+Description: Icons and symbols.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-icon/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-icon
+DocumentationLanguage: en, de, sv
+Published: 2024-04-09 19:50:43
+Status: available
+system/workers/icon.php: icon.php, create, update
+system/workers/icon.css: icon.css, create, update
+system/workers/icon.woff: icon.woff, create, update
+
+Extension: Image
+Version: 0.9.3
+Description: Add images and thumbnails.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-image/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-image
+DocumentationLanguage: en, de, sv
+Published: 2024-04-23 11:44:38
+Status: available
+system/workers/image.php: image.php, create, update
+media/images/photo.jpg: photo.jpg, create, optional
+media/thumbnails/photo-100x40.jpg: photo-100x40.jpg, create, optional
+
+Extension: Include
+Version: 0.9.2
+Description: Include shared pages.
+Developer: Steffen Schultz
+Tag: feature, content, page
+DownloadUrl: https://github.com/schulle4u/yellow-include/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/schulle4u/yellow-include
+DocumentationLanguage: en, de
+Published: 2026-02-17 17:28:29
+Status: available
+system/workers/include.php: include.php, create, update
+
+Extension: Instagram
+Version: 0.9.1
+Description: Embed Instagram photos.
+Developer: Giovanni Salmeri
+Tag: feature
+DownloadUrl: https://github.com/GiovanniSalmeri/yellow-instagram/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/GiovanniSalmeri/yellow-instagram
+Published: 2024-04-25 20:30:00
+Status: available
+system/workers/instagram.php: instagram.php, create, update
+system/workers/instagram.js: instagram.js, create, update
+
+Extension: Italian
+Version: 0.9.5
+Description: Italian language.
+Translator: Giovanni Salmeri
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/italian.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/italian
+Published: 2025-12-19 01:26:16
+Status: available
+system/workers/italian.php: italian.php, create, update
+
+Extension: Japanese
+Version: 0.9.5
+Description: Japanese language.
+Translator: Yuhko Senuma, Tomonori Ikeda
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/japanese.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/japanese
+Published: 2025-12-19 01:26:51
+Status: available
+system/workers/japanese.php: japanese.php, create, update
+
+Extension: Karlskrona
+Version: 0.9.1
+Description: Karlskrona is a semantic theme.
+Designer: Robert Pfotenhauer
+Tag: theme
+DownloadUrl: https://github.com/pftnhr/yellow-karlskrona/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/pftnhr/yellow-karlskrona
+Published: 2024-03-02 13:51:13
+Status: available
+system/workers/karlskrona.php: karlskrona.php, create, update
+system/layouts/karlskrona-blog-start.html: karlskrona-blog-start.html, create, update, careful
+system/layouts/karlskrona-blog.html: karlskrona-blog.html, create, update, careful
+system/layouts/karlskrona-default.html: karlskrona-default.html, create, update, careful
+system/layouts/karlskrona-error.html: karlskrona-error.html, create, update, careful
+system/layouts/karlskrona-footer.html: karlskrona-footer.html, create, update, careful
+system/layouts/karlskrona-header.html: karlskrona-header.html, create, update, careful
+system/layouts/karlskrona-navigation.html: karlskrona-navigation.html, create, update, careful
+system/layouts/karlskrona-pagination.html: karlskrona-pagination.html, create, update, careful
+system/layouts/karlskrona-wiki-start.html: karlskrona-wiki-start.html, create, update, careful
+system/layouts/karlskrona-wiki.html: karlskrona-wiki.html, create, update, careful
+system/themes/karlskrona.css: karlskrona.css, create, update, careful
+system/themes/karlskrona.png: karlskrona.png, create
+
+Extension: Markdown
+Version: 0.9.8
+Description: Text formatting for humans.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-markdown/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-markdown
+DocumentationLanguage: en, de, sv
+Published: 2026-02-24 18:30:33
+Status: available
+system/workers/markdown.php: markdown.php, create, update
+
+Extension: Meta
+Version: 0.9.3
+Description: Meta data for humans and machines.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-meta/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-meta
+DocumentationLanguage: en, de, sv
+Published: 2024-04-24 16:42:53
+Status: available
+system/workers/meta.php: meta.php, create, update
+
+Extension: Norwegian
+Version: 0.9.5
+Description: Norwegian language.
+Translator: Per Arne Solvik
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/norwegian.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/norwegian
+Published: 2025-12-19 01:24:05
+Status: available
+system/workers/norwegian.php: norwegian.php, create, update
+
+Extension: Paris
+Version: 0.9.6
+Description: Paris is an elegant theme.
+Designer: Anna Svensson
+Tag: theme
+DownloadUrl: https://github.com/annaesvensson/yellow-paris/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-paris
+DocumentationLanguage: en, de, sv
+Published: 2026-02-23 09:49:56
+Status: available
+system/workers/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: Polish
+Version: 0.9.5
+Description: Polish language.
+Translator: Paweł Klockiewicz, Kanbeq
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/polish.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/polish
+Published: 2025-12-19 01:28:31
+Status: available
+system/workers/polish.php: polish.php, create, update
+
+Extension: Portuguese
+Version: 0.9.5
+Description: Portuguese language.
+Translator: Al Garcia
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/portuguese.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/portuguese
+Published: 2025-12-19 01:28:31
+Status: available
+system/workers/portuguese.php: portuguese.php, create, update
+
+Extension: Previousnext
+Version: 0.9.1
+Description: Show links to previous/next page.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-previousnext/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-previousnext
+DocumentationLanguage: en, de, sv
+Published: 2024-04-05 00:26:47
+Status: available
+system/workers/previousnext.php: previousnext.php, create, update
+
+Extension: Private
+Version: 0.9.1
+Description: Support for password-protected pages.
+Developer: Steffen Schultz
+Tag: feature, page, private, security
+DownloadUrl: https://github.com/schulle4u/yellow-private/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/schulle4u/yellow-private
+DocumentationLanguage: en, de
+Published: 2024-04-05 15:57:08
+Status: available
+system/workers/private.php: private.php, create, update
+
+Extension: Publish
+Version: 0.9.10
+Description: Make and publish extensions.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-publish/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-publish
+DocumentationLanguage: en, de, sv
+Published: 2026-02-17 15:56:59
+Status: available
+system/workers/publish.php: publish.php, create, update
+
+Extension: Readingtime
+Version: 0.9.1
+Description: Show estimated reading time for page content.
+Developer: Steffen Schultz
+Tag: feature, page, blog, readingtime
+DownloadUrl: https://github.com/schulle4u/yellow-readingtime/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/schulle4u/yellow-readingtime
+DocumentationLanguage: en, de
+Published: 2024-04-05 17:22:52
+Status: available
+system/workers/readingtime.php: readingtime.php, create, update
+
+Extension: Readingtime
+Version: 0.8.22
+Description: Show estimated reading time for page content.
+DownloadUrl: https://github.com/schulle4u/yellow-readingtime/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/schulle4u/yellow-readingtime
+DocumentationLanguage: en, de
+Published: 2023-05-23 13:25:56
+Developer: Steffen Schultz
+Tag: feature, page, blog, readingtime
+system/extensions/readingtime.php: readingtime.php, create, update
+
+Extension: Russian
+Version: 0.9.5
+Description: Russian language.
+Translator: Сергей Ворон
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/russian.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/russian
+Published: 2025-12-19 01:28:30
+Status: available
+system/workers/russian.php: russian.php, create, update
+
+Extension: Search
+Version: 0.9.2
+Description: Full-text search.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-search/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-search
+DocumentationLanguage: en, de, sv
+Published: 2024-04-25 13:09:04
+Status: available
+system/workers/search.php: search.php, create, update
+system/layouts/search.html: search.html, create, update, careful
+content/search/page.md: page.md, create, optional
+
+Extension: Serve
+Version: 0.9.1
+Description: Built-in web server.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-serve/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-serve
+DocumentationLanguage: en, de, sv
+Published: 2024-04-04 15:00:12
+Status: available
+system/workers/serve.php: serve.php, create, update
+
+Extension: Sitemap
+Version: 0.9.3
+Description: Sitemap with all pages.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-sitemap/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-sitemap
+DocumentationLanguage: en, de, sv
+Published: 2024-05-09 16:28:15
+Status: available
+system/workers/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.9.4
+Description: Image gallery with slider.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-slider/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-slider
+DocumentationLanguage: en, de, sv
+Published: 2025-08-22 17:52:06
+Status: available
+system/workers/slider.php: slider.php, create, update
+system/workers/slider.js: slider.js, create, update
+system/workers/slider.css: slider.css, create, update
+system/workers/slider-splide.min.js: slider-splide.min.js, create, update
+
+Extension: Slovak
+Version: 0.9.5
+Description: Slovak language.
+Translator: Ádám Tuba
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/slovak.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/slovak
+Published: 2025-12-19 01:24:05
+Status: available
+system/workers/slovak.php: slovak.php, create, update
+
+Extension: Spanish
+Version: 0.9.5
+Description: Spanish language.
+Translator: Al Garcia, David Garcia
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/spanish.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/spanish
+Published: 2025-12-19 01:23:50
+Status: available
+system/workers/spanish.php: spanish.php, create, update
+
+Extension: Stockholm
+Version: 0.9.6
+Description: Stockholm is a clean theme.
+Designer: Anna Svensson
+Tag: example, theme
+DownloadUrl: https://github.com/annaesvensson/yellow-stockholm/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-stockholm
+DocumentationLanguage: en, de, sv
+Published: 2026-02-23 09:49:55
+Status: available
+system/workers/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.9.5
+Description: Swedish language.
+Translator: Anna Svensson
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/swedish.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/swedish
+Published: 2025-12-19 01:22:34
+Status: available
+system/workers/swedish.php: swedish.php, create, update
+
+Extension: Toc
+Version: 0.9.1
+Description: Table of contents.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-toc/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-toc
+DocumentationLanguage: en, de, sv
+Published: 2024-04-05 00:26:47
+Status: available
+system/workers/toc.php: toc.php, create, update
+
+Extension: Traffic
+Version: 0.9.3
+Description: Create traffic analytics from log files.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-traffic/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-traffic
+DocumentationLanguage: en, de, sv
+Published: 2026-02-17 15:56:42
+Status: available
+system/workers/traffic.php: traffic.php, create, update
+
+Extension: Turkish
+Version: 0.9.5
+Description: Turkish language.
+Translator: Osman Kars
+Tag: language
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/turkish.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/turkish
+Published: 2025-12-19 01:28:30
+Status: available
+system/workers/turkish.php: turkish.php, create, update
+
+Extension: Update
+Version: 0.9.9
+Description: Keep your extensions up to date.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-update/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-update
+DocumentationLanguage: en, de, sv
+Published: 2026-02-17 16:14:10
+Status: available
+system/workers/update.php: update.php, create, update
+system/workers/updatepatch.bin: updatepatch.php, create, additional
+
+Extension: Wiki
+Version: 0.9.4
+Description: Wiki for your website.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-wiki/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-wiki
+DocumentationLanguage: en, de, sv
+Published: 2024-05-09 15:43:40
+Status: available
+system/workers/wiki.php: wiki.php, create, update
+system/layouts/wiki.html: wiki.html, create, update, careful
+system/layouts/wiki-start.html: wiki-start.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-page.md: wiki-example-page.md, create, optional
+
+Extension: Wittstock
+Version: 0.9.4
+Description: Wittstock is a classless theme.
+Designer: Steffen Schultz
+Tag: classless, theme
+DownloadUrl: https://github.com/schulle4u/yellow-wittstock/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/schulle4u/yellow-wittstock
+DocumentationLanguage: en, de
+Published: 2025-09-16 10:59:26
+Status: available
+system/workers/wittstock.php: wittstock.php, create, update
+system/themes/wittstock.css: wittstock.css, create, update, careful
+system/layouts/wittstock-blog.html: wittstock-blog.html, create, update, careful
+system/layouts/wittstock-blog-start.html: wittstock-blog-start.html, create, update, careful
+system/layouts/wittstock-contact.html: wittstock-contact.html, create, update, careful
+system/layouts/wittstock-default.html: wittstock-default.html, create, update, careful
+system/layouts/wittstock-error.html: wittstock-error.html, create, update, careful
+system/layouts/wittstock-feed.html: wittstock-feed.html, create, update, careful
+system/layouts/wittstock-footer.html: wittstock-footer.html, create, update, careful
+system/layouts/wittstock-header.html: wittstock-header.html, create, update, careful
+system/layouts/wittstock-navigation.html: wittstock-navigation.html, create, update, careful
+system/layouts/wittstock-pagination.html: wittstock-pagination.html, create, update, careful
+system/layouts/wittstock-search.html: wittstock-search.html, create, update, careful
+system/layouts/wittstock-sitemap.html: wittstock-sitemap.html, create, update, careful
+system/layouts/wittstock-wiki.html: wittstock-wiki.html, create, update, careful
+system/layouts/wittstock-wiki-start.html: wittstock-wiki-start.html, create, update, careful
diff --git a/system/extensions/update-current.ini b/system/extensions/update-current.ini
@@ -1,155 +0,0 @@
-# Datenstrom Yellow update settings
-
-Extension: Blog
-Version: 0.8.30
-Description: Blog for your website.
-DownloadUrl: https://github.com/annaesvensson/yellow-blog/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-blog
-DocumentationLanguage: en, de, sv
-Published: 2023-11-01 17:49:02
-Developer: Anna Svensson
-Tag: feature
-system/extensions/blog.php: blog.php, create, update
-system/layouts/blog.html: blog.html, create, update, careful
-system/layouts/blog-start.html: blog-start.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/2020-04-07-blog-example-page.md: 2020-04-07-blog-example-page.md, create, optional
-content/2-blog/2020-12-06-made-for-people.md: 2020-12-06-made-for-people.md, create, optional
-
-Extension: Core
-Version: 0.8.125
-Description: Core functionality of your website.
-DownloadUrl: https://github.com/annaesvensson/yellow-core/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-core
-DocumentationLanguage: en, de, sv
-Published: 2023-10-27 17:02:46
-Developer: Anna Svensson
-Tag: feature
-system/extensions/core.php: core.php, create, update
-system/layouts/default.html: default.html, create, update, careful
-system/layouts/error.html: error.html, create, update, careful
-system/layouts/header.html: header.html, create, update, careful
-system/layouts/footer.html: footer.html, create, update, careful
-system/layouts/navigation.html: navigation.html, create, update, careful
-system/layouts/pagination.html: pagination.html, create, update, careful
-
-Extension: Edit
-Version: 0.8.77
-Description: Edit your website in a web browser.
-DownloadUrl: https://github.com/annaesvensson/yellow-edit/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-edit
-DocumentationLanguage: en, de, sv
-Published: 2023-12-20 23:19:46
-Developer: Anna Svensson
-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-stack.svg: edit-stack.svg, create, update
-system/extensions/edit.woff: edit.woff, delete
-content/shared/page-new-default.md: page-new-default.md, create, optional
-
-Extension: English
-Version: 0.8.43
-Description: English language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/english.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/english
-Published: 2023-04-13 22:54:43
-Translator: Mark Seuffert
-Tag: language
-system/extensions/english.php: english.php, create, update
-
-Extension: Generate
-Version: 0.8.52
-Description: Generate a static website.
-DownloadUrl: https://github.com/annaesvensson/yellow-generate/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-generate
-DocumentationLanguage: en, de, sv
-Published: 2023-06-09 15:56:36
-Developer: Anna Svensson
-Tag: feature
-system/extensions/generate.php: generate.php, create, update
-
-Extension: German
-Version: 0.8.43
-Description: German language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/german.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/german
-Published: 2023-04-13 22:54:43
-Translator: David Fehrmann
-Tag: language
-system/extensions/german.php: german.php, create, update
-
-Extension: Image
-Version: 0.8.19
-Description: Add images and thumbnails.
-DownloadUrl: https://github.com/annaesvensson/yellow-image/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-image
-DocumentationLanguage: en, de, sv
-Published: 2023-04-16 23:50:53
-Developer: Anna Svensson
-Tag: feature
-system/extensions/image.php: image.php, create, update
-media/images/photo.jpg: photo.jpg, create, optional
-media/thumbnails/photo-100x40.jpg: photo-100x40.jpg, create, optional
-
-Extension: Markdown
-Version: 0.8.26
-Description: Text formatting for humans.
-DownloadUrl: https://github.com/annaesvensson/yellow-markdown/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-markdown
-DocumentationLanguage: en, de, sv
-Published: 2023-09-18 20:49:33
-Developer: Anna Svensson
-Tag: feature
-system/extensions/markdown.php: markdown.php, create, update
-
-Extension: Serve
-Version: 0.8.24
-Description: Built-in web server.
-DownloadUrl: https://github.com/annaesvensson/yellow-serve/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-serve
-DocumentationLanguage: en, de, sv
-Published: 2023-05-25 22:35:15
-Developer: Anna Svensson
-Tag: feature
-system/extensions/serve.php: serve.php, create, update
-
-Extension: Stockholm
-Version: 0.8.14
-Description: Stockholm is a clean theme.
-DownloadUrl: https://github.com/annaesvensson/yellow-stockholm/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-stockholm
-DocumentationLanguage: en, de, sv
-Published: 2022-10-20 12:44:02
-Designer: Anna Svensson
-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.43
-Description: Swedish language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/swedish.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/swedish
-Published: 2023-04-13 22:54:43
-Translator: Anna Svensson
-Tag: language
-system/extensions/swedish.php: swedish.php, create, update
-
-Extension: Update
-Version: 0.8.97
-Description: Keep your website up to date.
-DownloadUrl: https://github.com/annaesvensson/yellow-update/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-update
-DocumentationLanguage: en, de, sv
-Published: 2023-11-24 21:13:15
-Developer: Anna Svensson
-Tag: feature
-system/extensions/update.php: update.php, create, update
-system/extensions/updatepatch.bin: updatepatch.php, create, additional
diff --git a/system/extensions/update-installed.ini b/system/extensions/update-installed.ini
@@ -0,0 +1,188 @@
+# Datenstrom Yellow update settings for installed extensions
+
+Extension: Blog
+Version: 0.8.30
+Description: Blog for your website.
+DownloadUrl: https://github.com/annaesvensson/yellow-blog/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-blog
+DocumentationLanguage: en, de, sv
+Published: 2023-11-01 17:49:02
+Developer: Anna Svensson
+Tag: feature
+system/extensions/blog.php: blog.php, create, update
+system/layouts/blog.html: blog.html, create, update, careful
+system/layouts/blog-start.html: blog-start.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/2020-04-07-blog-example-page.md: 2020-04-07-blog-example-page.md, create, optional
+content/2-blog/2020-12-06-made-for-people.md: 2020-12-06-made-for-people.md, create, optional
+
+Extension: Core
+Version: 0.9.22
+Description: Core functionality of your website.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-core/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-core
+DocumentationLanguage: en, de, sv
+Published: 2026-02-21 19:43:11
+Status: available
+system/workers/core.php: core.php, create, update
+system/extensions/core.php: corepatch.txt, update
+system/layouts/default.html: default.html, create, update, careful
+system/layouts/error.html: error.html, create, update, careful
+system/layouts/header.html: header.html, create, update, careful
+system/layouts/footer.html: footer.html, create, update, careful
+system/layouts/navigation.html: navigation.html, create, update, careful
+system/layouts/pagination.html: pagination.html, create, update, careful
+
+Extension: Edit
+Version: 0.9.15
+Description: Edit your website in a web browser.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-edit/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-edit
+DocumentationLanguage: en, de, sv
+Published: 2026-02-17 16:02:00
+Status: available
+system/workers/edit.php: edit.php, create, update
+system/workers/edit.css: edit.css, create, update
+system/workers/edit.js: edit.js, create, update
+system/workers/edit-stack.svg: edit-stack.svg, create, update
+content/shared/page-new-default.md: page-new-default.md, create, optional
+
+Extension: English
+Version: 0.8.43
+Description: English language.
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/english.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/english
+Published: 2023-04-13 22:54:43
+Translator: Mark Seuffert
+Tag: language
+system/extensions/english.php: english.php, create, update
+
+Extension: Generate
+Version: 0.9.8
+Description: Generate a static website.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-generate/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-generate
+DocumentationLanguage: en, de, sv
+Published: 2026-02-17 15:57:09
+Status: available
+system/workers/generate.php: generate.php, create, update
+
+Extension: German
+Version: 0.8.43
+Description: German language.
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/german.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/german
+Published: 2023-04-13 22:54:43
+Translator: David Fehrmann
+Tag: language
+system/extensions/german.php: german.php, create, update
+
+Extension: Image
+Version: 0.9.3
+Description: Add images and thumbnails.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-image/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-image
+DocumentationLanguage: en, de, sv
+Published: 2024-04-23 11:44:38
+Status: available
+system/workers/image.php: image.php, create, update
+media/images/photo.jpg: photo.jpg, create, optional
+media/thumbnails/photo-100x40.jpg: photo-100x40.jpg, create, optional
+
+Extension: Install
+Version: 0.9.10
+Description: Install a brand new website.
+Developer: Anna Svensson
+DownloadUrl: https://github.com/annaesvensson/yellow-install/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-install
+DocumentationLanguage: en, de, sv
+Published: 2026-02-19 22:47:16
+Status: unassembled
+system/workers/install.php: install.php, create
+system/workers/install-language.bin: install-language.bin, compress @source/yellow-language/, create
+system/workers/install-wiki.bin: install-wiki.bin, compress @source/yellow-wiki/, create
+system/workers/install-blog.bin: install-blog.bin, compress @source/yellow-blog/, create
+system/extensions/yellow-system.ini: yellow-system.ini, create
+system/extensions/yellow-user.ini: yellow-user.ini, create
+system/extensions/yellow-language.ini: yellow-language.ini, create
+content/1-home/page.md: 1-home-page.md, create
+content/9-about/page.md: 9-about-page.md, create
+content/shared/page-error-404.md: page-error-404.md, create
+media/downloads/yellow-english.pdf: yellow-english.pdf, create
+media/downloads/yellow-deutsch.pdf: yellow-deutsch.pdf, create
+media/downloads/yellow-svenska.pdf: yellow-svenska.pdf, create
+./yellow.php: yellow.php, create
+./robots.txt: robots.txt, create
+
+Extension: Markdown
+Version: 0.9.8
+Description: Text formatting for humans.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-markdown/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-markdown
+DocumentationLanguage: en, de, sv
+Published: 2026-02-24 18:30:33
+Status: available
+system/workers/markdown.php: markdown.php, create, update
+
+Extension: Serve
+Version: 0.9.1
+Description: Built-in web server.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-serve/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-serve
+DocumentationLanguage: en, de, sv
+Published: 2024-04-04 15:00:12
+Status: available
+system/workers/serve.php: serve.php, create, update
+
+Extension: Stockholm
+Version: 0.9.6
+Description: Stockholm is a clean theme.
+Designer: Anna Svensson
+Tag: example, theme
+DownloadUrl: https://github.com/annaesvensson/yellow-stockholm/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-stockholm
+DocumentationLanguage: en, de, sv
+Published: 2026-02-23 09:49:55
+Status: available
+system/workers/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.43
+Description: Swedish language.
+DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/swedish.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/swedish
+Published: 2023-04-13 22:54:43
+Translator: Anna Svensson
+Tag: language
+system/extensions/swedish.php: swedish.php, create, update
+
+Extension: Update
+Version: 0.9.9
+Description: Keep your extensions up to date.
+Developer: Anna Svensson
+Tag: feature
+DownloadUrl: https://github.com/annaesvensson/yellow-update/archive/refs/heads/main.zip
+DocumentationUrl: https://github.com/annaesvensson/yellow-update
+DocumentationLanguage: en, de, sv
+Published: 2026-02-17 16:14:10
+Status: available
+system/workers/update.php: update.php, create, update
+system/workers/updatepatch.bin: updatepatch.php, create, additional
diff --git a/system/extensions/update-latest.ini b/system/extensions/update-latest.ini
@@ -1,786 +0,0 @@
-# Datenstrom Yellow update settings
-
-Extension: Berlin
-Version: 0.8.14
-Description: Berlin is a theme inspired by Dieter Rams.
-DownloadUrl: https://github.com/annaesvensson/yellow-berlin/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-berlin
-DocumentationLanguage: en, de, sv
-Published: 2022-10-20 12:44:35
-Designer: Anna Svensson
-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.30
-Description: Blog for your website.
-DownloadUrl: https://github.com/annaesvensson/yellow-blog/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-blog
-DocumentationLanguage: en, de, sv
-Published: 2023-11-01 17:49:02
-Developer: Anna Svensson
-Tag: feature
-system/extensions/blog.php: blog.php, create, update
-system/layouts/blog.html: blog.html, create, update, careful
-system/layouts/blog-start.html: blog-start.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/2020-04-07-blog-example-page.md: 2020-04-07-blog-example-page.md, create, optional
-content/2-blog/2020-12-06-made-for-people.md: 2020-12-06-made-for-people.md, create, optional
-
-Extension: Breadcrumb
-Version: 0.8.10
-Description: Breadcrumb navigation.
-DownloadUrl: https://github.com/annaesvensson/yellow-breadcrumb/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-breadcrumb
-DocumentationLanguage: en, de, sv
-Published: 2022-10-24 17:50:24
-Developer: Anna Svensson
-Tag: feature
-system/extensions/breadcrumb.php: breadcrumb.php, create, update
-
-Extension: Bundle
-Version: 0.8.31
-Description: Bundle website files.
-DownloadUrl: https://github.com/annaesvensson/yellow-bundle/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-bundle
-DocumentationLanguage: en, de, sv
-Published: 2023-11-28 16:26:34
-Developer: Anna Svensson
-Tag: feature
-system/extensions/bundle.php: bundle.php, create, update
-
-Extension: Catalan
-Version: 0.8.43
-Description: Catalan language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/catalan.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/catalan
-Published: 2023-04-13 22:54:43
-Translator: Andreu Ferrer
-Tag: language
-system/extensions/catalan.php: catalan.php, create, update
-
-Extension: Check
-Version: 0.8.2
-Description: Find broken links.
-DownloadUrl: https://github.com/annaesvensson/yellow-check/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-check
-DocumentationLanguage: en, de, sv
-Published: 2023-06-09 15:55:12
-Developer: Anna Svensson
-Tag: feature
-system/extensions/check.php: check.php, create, update
-
-Extension: Chinese
-Version: 0.8.43
-Description: Chinese language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/chinese.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/chinese
-Published: 2023-04-13 22:53:25
-Translator: Hyson Lee
-Tag: language
-system/extensions/chinese.php: chinese.php, create, update
-
-Extension: Contact
-Version: 0.8.23
-Description: Email contact page.
-DownloadUrl: https://github.com/annaesvensson/yellow-contact/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-contact
-DocumentationLanguage: en, de, sv
-Published: 2023-05-18 17:59:29
-Developer: Anna Svensson
-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: Copenhagen
-Version: 0.8.15
-Description: Copenhagen is a beautiful theme.
-DownloadUrl: https://github.com/annaesvensson/yellow-copenhagen/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-copenhagen
-DocumentationLanguage: en, de, sv
-Published: 2023-08-21 09:04:08
-Designer: Anna Svensson
-Tag: theme
-system/extensions/copenhagen.php: copenhagen.php, create, update
-system/themes/copenhagen.css: copenhagen.css, create, update, careful
-system/themes/copenhagen.png: copenhagen.png, create
-
-Extension: Core
-Version: 0.8.125
-Description: Core functionality of your website.
-DownloadUrl: https://github.com/annaesvensson/yellow-core/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-core
-DocumentationLanguage: en, de, sv
-Published: 2023-10-27 17:02:46
-Developer: Anna Svensson
-Tag: feature
-system/extensions/core.php: core.php, create, update
-system/layouts/default.html: default.html, create, update, careful
-system/layouts/error.html: error.html, create, update, careful
-system/layouts/header.html: header.html, create, update, careful
-system/layouts/footer.html: footer.html, create, update, careful
-system/layouts/navigation.html: navigation.html, create, update, careful
-system/layouts/pagination.html: pagination.html, create, update, careful
-
-Extension: Czech
-Version: 0.8.43
-Description: Czech language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/czech.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/czech
-Published: 2023-04-13 22:59:43
-Translator: Ufo Vyhuleny
-Tag: language
-system/extensions/czech.php: czech.php, create, update
-
-Extension: Danish
-Version: 0.8.43
-Description: Danish language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/danish.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/danish
-Published: 2023-04-13 23:07:01
-Translator: David Garcia
-Tag: language
-system/extensions/danish.php: danish.php, create, update
-
-Extension: Draft
-Version: 0.8.18
-Description: Support for draft pages.
-DownloadUrl: https://github.com/annaesvensson/yellow-draft/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-draft
-DocumentationLanguage: en, de, sv
-Published: 2023-05-19 13:21:26
-Developer: Anna Svensson
-Tag: feature
-system/extensions/draft.php: draft.php, create, update
-
-Extension: Dutch
-Version: 0.8.43
-Description: Dutch language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/dutch.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/dutch
-Published: 2023-04-13 23:07:33
-Translator: Robin Vannieuwenhuijse
-Tag: language
-system/extensions/dutch.php: dutch.php, create, update
-
-Extension: Edit
-Version: 0.8.77
-Description: Edit your website in a web browser.
-DownloadUrl: https://github.com/annaesvensson/yellow-edit/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-edit
-DocumentationLanguage: en, de, sv
-Published: 2023-12-20 23:19:46
-Developer: Anna Svensson
-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-stack.svg: edit-stack.svg, create, update
-system/extensions/edit.woff: edit.woff, delete
-content/shared/page-new-default.md: page-new-default.md, create, optional
-
-Extension: Emoji
-Version: 0.8.14
-Description: Lots and lots of emoji.
-DownloadUrl: https://github.com/annaesvensson/yellow-emoji/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-emoji
-DocumentationLanguage: en, de, sv
-Published: 2022-11-16 10:24:57
-Developer: Anna Svensson
-Tag: feature
-system/extensions/emoji.php: emoji.php, create, update
-system/extensions/emoji.css: emoji.css, create, update
-system/extensions/emoji-default-stack.svg: emoji-default-stack.svg, create, update
-system/extensions/emoji-extra1-stack.svg: emoji-extra1-stack.svg, create, update
-system/extensions/emoji-extra2-stack.svg: emoji-extra2-stack.svg, create, update
-system/extensions/emoji-extra3-stack.svg: emoji-extra3-stack.svg, create, update
-system/extensions/emoji-extra4-stack.svg: emoji-extra4-stack.svg, create, update
-system/extensions/emoji-extra5-stack.svg: emoji-extra5-stack.svg, create, update
-system/extensions/emoji-extra6-stack.svg: emoji-extra6-stack.svg, create, update
-system/extensions/emoji-extra7-stack.svg: emoji-extra7-stack.svg, create, update
-system/extensions/emoji-flags-stack.svg: emoji-flags-stack.svg, create, update
-
-Extension: English
-Version: 0.8.43
-Description: English language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/english.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/english
-Published: 2023-04-13 22:54:43
-Translator: Mark Seuffert
-Tag: language
-system/extensions/english.php: english.php, create, update
-
-Extension: Feed
-Version: 0.8.25
-Description: Feed with recent changes.
-DownloadUrl: https://github.com/annaesvensson/yellow-feed/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-feed
-DocumentationLanguage: en, de, sv
-Published: 2023-11-04 01:58:26
-Developer: Anna Svensson
-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: French
-Version: 0.8.43
-Description: French language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/french.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/french
-Published: 2023-04-13 23:07:59
-Translator: Juh Nibreh
-Tag: language
-system/extensions/french.php: french.php, create, update
-
-Extension: Gallery
-Version: 0.8.18
-Description: Image gallery with popup.
-DownloadUrl: https://github.com/annaesvensson/yellow-gallery/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-gallery
-DocumentationLanguage: en, de, sv
-Published: 2022-11-04 09:39:17
-Developer: Anna Svensson
-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
-system/extensions/gallery-preloader.gif: gallery-preloader.gif, create, update
-
-Extension: Generate
-Version: 0.8.52
-Description: Generate a static website.
-DownloadUrl: https://github.com/annaesvensson/yellow-generate/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-generate
-DocumentationLanguage: en, de, sv
-Published: 2023-06-09 15:56:36
-Developer: Anna Svensson
-Tag: feature
-system/extensions/generate.php: generate.php, create, update
-
-Extension: German
-Version: 0.8.43
-Description: German language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/german.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/german
-Published: 2023-04-13 22:54:43
-Translator: David Fehrmann
-Tag: language
-system/extensions/german.php: german.php, create, update
-
-Extension: Googlecalendar
-Version: 0.8.17
-Description: Embed Google calendar.
-DownloadUrl: https://github.com/annaesvensson/yellow-googlecalendar/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-googlecalendar
-DocumentationLanguage: en, de, sv
-Published: 2023-04-18 01:21:56
-Developer: Anna Svensson
-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.9
-Description: Embed Google map.
-DownloadUrl: https://github.com/annaesvensson/yellow-googlemap/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-googlemap
-DocumentationLanguage: en, de, sv
-Published: 2023-04-18 01:19:46
-Developer: Anna Svensson
-Tag: feature
-system/extensions/googlemap.php: googlemap.php, create, update
-
-Extension: Helloworld
-Version: 0.8.15
-Description: Make animated text.
-DownloadUrl: https://github.com/schulle4u/yellow-helloworld/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/schulle4u/yellow-helloworld
-Published: 2020-08-13 16:12:30
-Developer: Steffen Schultz
-Tag: example, feature
-system/extensions/helloworld.php: helloworld.php, create, update
-system/extensions/helloworld.js: helloworld.js, create, update
-system/extensions/helloworld.css: helloworld.css, create, update
-
-Extension: Help
-Version: 0.8.23
-Description: Help for your website.
-DownloadUrl: https://github.com/annaesvensson/yellow-help/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-help
-DocumentationLanguage: en, de, sv
-Published: 2023-05-18 20:09:11
-Developer: Anna Svensson
-Tag: feature
-system/extensions/help.php: help.php, create, update
-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/how-to-change-the-content.md: how-to-change-the-content.md, create, optional, multi-language
-content/9-help/how-to-change-the-media.md: how-to-change-the-media.md, create, optional, multi-language
-content/9-help/how-to-change-the-system.md: how-to-change-the-system.md, create, optional, multi-language
-content/9-help/how-to-customise-a-language.md: how-to-customise-a-language.md, create, optional, multi-language
-content/9-help/how-to-customise-a-layout.md: how-to-customise-a-layout.md, create, optional, multi-language
-content/9-help/how-to-customise-a-theme.md: how-to-customise-a-theme.md, create, optional, multi-language
-content/9-help/how-to-get-started.md: how-to-get-started.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/page.md: page.md, create, optional, multi-language
-content/9-help/troubleshooting.md: troubleshooting.md, create, optional, multi-language
-content/9-help/what-s-new.md: what-s-new.md, create, optional, multi-language
-media/images/help-photo.jpg: help-photo.jpg, create, optional
-media/images/language-de.png: language-de.png, create, optional
-media/images/language-en.png: language-en.png, create, optional
-media/images/language-sv.png: language-sv.png, create, optional
-
-Extension: Highlight
-Version: 0.8.16
-Description: Highlight source code.
-DownloadUrl: https://github.com/annaesvensson/yellow-highlight/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-highlight
-DocumentationLanguage: en, de, sv
-Published: 2022-12-22 16:37:25
-Developer: Anna Svensson
-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-lua.json: highlight-lua.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.43
-Description: Hungarian language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/hungarian.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/hungarian
-Published: 2023-04-13 23:08:22
-Translator: Ádám Tuba
-Tag: language
-system/extensions/hungarian.php: hungarian.php, create, update
-
-Extension: Icon
-Version: 0.8.14
-Description: Icons and symbols.
-DownloadUrl: https://github.com/annaesvensson/yellow-icon/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-icon
-DocumentationLanguage: en, de, sv
-Published: 2022-11-16 11:09:07
-Developer: Anna Svensson
-Tag: feature
-system/extensions/icon.php: icon.php, create, update
-system/extensions/icon.css: icon.css, create, update
-system/extensions/icon.woff: icon.woff, create, update
-
-Extension: Image
-Version: 0.8.19
-Description: Add images and thumbnails.
-DownloadUrl: https://github.com/annaesvensson/yellow-image/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-image
-DocumentationLanguage: en, de, sv
-Published: 2023-04-16 23:50:53
-Developer: Anna Svensson
-Tag: feature
-system/extensions/image.php: image.php, create, update
-media/images/photo.jpg: photo.jpg, create, optional
-media/thumbnails/photo-100x40.jpg: photo-100x40.jpg, create, optional
-
-Extension: Instagram
-Version: 0.8.7
-Description: Embed Instagram photos.
-DocumentationUrl: https://github.com/GiovanniSalmeri/yellow-instagram
-DownloadUrl: https://github.com/GiovanniSalmeri/yellow-instagram/archive/refs/heads/main.zip
-Published: 2022-10-15 17:54:00
-Developer: Giovanni Salmeri
-Tag: feature
-system/extensions/instagram.php: instagram.php, create, update
-system/extensions/instagram.js: instagram.js, create, update
-
-Extension: Italian
-Version: 0.8.43
-Description: Italian language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/italian.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/italian
-Published: 2023-04-13 23:08:39
-Translator: Giovanni Salmeri
-Tag: language
-system/extensions/italian.php: italian.php, create, update
-
-Extension: Japanese
-Version: 0.8.43
-Description: Japanese language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/japanese.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/japanese
-Published: 2023-04-13 23:03:24
-Translator: Yuhko Senuma, Tomonori Ikeda
-Tag: language
-system/extensions/japanese.php: japanese.php, create, update
-
-Extension: Karlskrona
-Version: 0.8.18
-Description: Karlskrona is a semantic theme.
-DownloadUrl: https://github.com/pftnhr/yellow-karlskrona/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/pftnhr/yellow-karlskrona
-Published: 2023-09-11 20:00:13
-Designer: Robert Pfotenhauer
-Tag: theme
-system/extensions/karlskrona.php: karlskrona.php, create, update
-system/layouts/karlskrona-blog-start.html: karlskrona-blog-start.html, create, update, careful
-system/layouts/karlskrona-blog.html: karlskrona-blog.html, create, update, careful
-system/layouts/karlskrona-default.html: karlskrona-default.html, create, update, careful
-system/layouts/karlskrona-error.html: karlskrona-error.html, create, update, careful
-system/layouts/karlskrona-footer.html: karlskrona-footer.html, create, update, careful
-system/layouts/karlskrona-header.html: karlskrona-header.html, create, update, careful
-system/layouts/karlskrona-navigation.html: karlskrona-navigation.html, create, update, careful
-system/layouts/karlskrona-pagination.html: karlskrona-pagination.html, create, update, careful
-system/layouts/karlskrona-wiki-start.html: karlskrona-wiki-start.html, create, update, careful
-system/layouts/karlskrona-wiki.html: karlskrona-wiki.html, create, update, careful
-system/themes/karlskrona.css: karlskrona.css, create, update, careful
-system/themes/karlskrona.png: karlskrona.png, create
-
-Extension: Markdown
-Version: 0.8.26
-Description: Text formatting for humans.
-DownloadUrl: https://github.com/annaesvensson/yellow-markdown/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-markdown
-DocumentationLanguage: en, de, sv
-Published: 2023-09-18 20:49:33
-Developer: Anna Svensson
-Tag: feature
-system/extensions/markdown.php: markdown.php, create, update
-
-Extension: Meta
-Version: 0.8.17
-Description: Meta data for humans and machines.
-DownloadUrl: https://github.com/annaesvensson/yellow-meta/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-meta
-DocumentationLanguage: en, de, sv
-Published: 2023-05-19 00:58:40
-Developer: Anna Svensson
-Tag: feature
-system/extensions/meta.php: meta.php, create, update
-
-Extension: Norwegian
-Version: 0.8.43
-Description: Norwegian language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/norwegian.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/norwegian
-Published: 2023-04-13 23:08:53
-Translator: Per Arne Solvik
-Tag: language
-system/extensions/norwegian.php: norwegian.php, create, update
-
-Extension: Paris
-Version: 0.8.14
-Description: Paris is an elegant theme.
-DownloadUrl: https://github.com/annaesvensson/yellow-paris/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-paris
-DocumentationLanguage: en, de, sv
-Published: 2022-10-20 12:45:32
-Designer: Anna Svensson
-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: Parsedown
-Version: 0.8.26
-Description: Text formatting for humans.
-DownloadUrl: https://github.com/annaesvensson/yellow-parsedown/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-parsedown
-DocumentationLanguage: en, de, sv
-Published: 2023-09-18 20:49:49
-Developer: Anna Svensson
-Tag: feature
-system/extensions/parsedown.php: parsedown.php, create, update
-
-Extension: Polish
-Version: 0.8.43
-Description: Polish language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/polish.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/polish
-Published: 2023-04-13 23:09:08
-Translator: Paweł Klockiewicz, Kanbeq
-Tag: language
-system/extensions/polish.php: polish.php, create, update
-
-Extension: Portuguese
-Version: 0.8.43
-Description: Portuguese language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/portuguese.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/portuguese
-Published: 2023-04-13 23:09:38
-Translator: Al Garcia
-Tag: language
-system/extensions/portuguese.php: portuguese.php, create, update
-
-Extension: Previousnext
-Version: 0.8.18
-Description: Show links to previous/next page.
-DownloadUrl: https://github.com/annaesvensson/yellow-previousnext/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-previousnext
-DocumentationLanguage: en, de, sv
-Published: 2023-04-03 09:37:00
-Developer: Anna Svensson
-Tag: feature
-system/extensions/previousnext.php: previousnext.php, create, update
-
-Extension: Private
-Version: 0.8.13
-Tag: feature, page, private, security
-Description: Support for password-protected pages.
-DownloadUrl: https://github.com/schulle4u/yellow-private/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/schulle4u/yellow-private
-DocumentationLanguage: en, de
-Published: 2023-05-22 14:35:25
-Developer: Steffen Schultz
-system/extensions/private.php: private.php, create, update
-
-Extension: Publish
-Version: 0.8.70
-Description: Make and publish extensions.
-DownloadUrl: https://github.com/annaesvensson/yellow-publish/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-publish
-DocumentationLanguage: en, de, sv
-Published: 2023-12-11 18:37:01
-Developer: Anna Svensson
-Tag: feature
-system/extensions/publish.php: publish.php, create, update
-
-Extension: Readingtime
-Version: 0.8.22
-Description: Show estimated reading time for page content.
-DownloadUrl: https://github.com/schulle4u/yellow-readingtime/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/schulle4u/yellow-readingtime
-DocumentationLanguage: en, de
-Published: 2023-05-23 13:25:56
-Developer: Steffen Schultz
-Tag: feature, page, blog, readingtime
-system/extensions/readingtime.php: readingtime.php, create, update
-
-Extension: Russian
-Version: 0.8.43
-Description: Russian language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/russian.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/russian
-Published: 2023-04-13 23:09:57
-Translator: Сергей Ворон
-Tag: language
-system/extensions/russian.php: russian.php, create, update
-
-Extension: Search
-Version: 0.8.29
-Description: Full-text search.
-DownloadUrl: https://github.com/annaesvensson/yellow-search/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-search
-DocumentationLanguage: en, de, sv
-Published: 2023-10-13 12:38:07
-Developer: Anna Svensson
-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: Serve
-Version: 0.8.24
-Description: Built-in web server.
-DownloadUrl: https://github.com/annaesvensson/yellow-serve/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-serve
-DocumentationLanguage: en, de, sv
-Published: 2023-05-25 22:35:15
-Developer: Anna Svensson
-Tag: feature
-system/extensions/serve.php: serve.php, create, update
-
-Extension: Sitemap
-Version: 0.8.15
-Description: Sitemap with all pages.
-DownloadUrl: https://github.com/annaesvensson/yellow-sitemap/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-sitemap
-DocumentationLanguage: en, de, sv
-Published: 2023-10-26 16:26:39
-Developer: Anna Svensson
-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.18
-Description: Image gallery with slider.
-DownloadUrl: https://github.com/annaesvensson/yellow-slider/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-slider
-DocumentationLanguage: en, de, sv
-Published: 2022-11-04 09:39:38
-Developer: Anna Svensson
-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-splide.min.js: slider-splide.min.js, create, update
-
-Extension: Slovak
-Version: 0.8.43
-Description: Slovak language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/slovak.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/slovak
-Published: 2023-04-13 23:10:38
-Translator: Ádám Tuba
-Tag: language
-system/extensions/slovak.php: slovak.php, create, update
-
-Extension: Spanish
-Version: 0.8.43
-Description: Spanish language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/spanish.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/spanish
-Published: 2023-04-13 23:11:01
-Translator: Al Garcia, David Garcia
-Tag: language
-system/extensions/spanish.php: spanish.php, create, update
-
-Extension: Stockholm
-Version: 0.8.14
-Description: Stockholm is a clean theme.
-DownloadUrl: https://github.com/annaesvensson/yellow-stockholm/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-stockholm
-DocumentationLanguage: en, de, sv
-Published: 2022-10-20 12:44:02
-Designer: Anna Svensson
-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.43
-Description: Swedish language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/swedish.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/swedish
-Published: 2023-04-13 22:54:43
-Translator: Anna Svensson
-Tag: language
-system/extensions/swedish.php: swedish.php, create, update
-
-Extension: Toc
-Version: 0.8.11
-Description: Table of contents.
-DownloadUrl: https://github.com/annaesvensson/yellow-toc/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-toc
-DocumentationLanguage: en, de, sv
-Published: 2023-11-01 14:32:36
-Developer: Anna Svensson
-Tag: feature
-system/extensions/toc.php: toc.php, create, update
-
-Extension: Traffic
-Version: 0.8.32
-Description: Create traffic analytics from log files.
-DownloadUrl: https://github.com/annaesvensson/yellow-traffic/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-traffic
-DocumentationLanguage: en, de, sv
-Published: 2023-06-09 15:38:36
-Developer: Anna Svensson
-Tag: feature
-system/extensions/traffic.php: traffic.php, create, update
-
-Extension: Turkish
-Version: 0.8.43
-Description: Turkish language.
-DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/turkish.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/turkish
-Published: 2023-04-13 23:11:16
-Translator: Osman Kars
-Tag: language
-system/extensions/turkish.php: turkish.php, create, update
-
-Extension: Update
-Version: 0.8.97
-Description: Keep your website up to date.
-DownloadUrl: https://github.com/annaesvensson/yellow-update/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-update
-DocumentationLanguage: en, de, sv
-Published: 2023-11-24 21:13:15
-Developer: Anna Svensson
-Tag: feature
-system/extensions/update.php: update.php, create, update
-system/extensions/updatepatch.bin: updatepatch.php, create, additional
-
-Extension: Wiki
-Version: 0.8.30
-Description: Wiki for your website.
-DownloadUrl: https://github.com/annaesvensson/yellow-wiki/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-wiki
-DocumentationLanguage: en, de, sv
-Published: 2023-11-01 17:49:10
-Developer: Anna Svensson
-Tag: feature
-system/extensions/wiki.php: wiki.php, create, update
-system/layouts/wiki.html: wiki.html, create, update, careful
-system/layouts/wiki-start.html: wiki-start.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-page.md: wiki-example-page.md, create, optional
-
-Extension: Wittstock
-Version: 0.8.28
-Description: Wittstock is a classless theme.
-DownloadUrl: https://github.com/schulle4u/yellow-wittstock/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/schulle4u/yellow-wittstock
-DocumentationLanguage: en, de
-Published: 2023-10-27 13:55:23
-Designer: Steffen Schultz
-Tag: classless, theme
-system/extensions/wittstock.php: wittstock.php, create, update
-system/themes/wittstock.css: wittstock.css, create, update, careful
-system/layouts/wittstock-blog.html: wittstock-blog.html, create, update, careful
-system/layouts/wittstock-blog-start.html: wittstock-blog-start.html, create, update, careful
-system/layouts/wittstock-contact.html: wittstock-contact.html, create, update, careful
-system/layouts/wittstock-default.html: wittstock-default.html, create, update, careful
-system/layouts/wittstock-error.html: wittstock-error.html, create, update, careful
-system/layouts/wittstock-feed.html: wittstock-feed.html, create, update, careful
-system/layouts/wittstock-footer.html: wittstock-footer.html, create, update, careful
-system/layouts/wittstock-header.html: wittstock-header.html, create, update, careful
-system/layouts/wittstock-navigation.html: wittstock-navigation.html, create, update, careful
-system/layouts/wittstock-pagination.html: wittstock-pagination.html, create, update, careful
-system/layouts/wittstock-search.html: wittstock-search.html, create, update, careful
-system/layouts/wittstock-sitemap.html: wittstock-sitemap.html, create, update, careful
-system/layouts/wittstock-wiki.html: wittstock-wiki.html, create, update, careful
-system/layouts/wittstock-wiki-start.html: wittstock-wiki-start.html, create, update, careful
-
-Extension: Youtube
-Version: 0.8.7
-Description: Embed Youtube videos.
-DownloadUrl: https://github.com/annaesvensson/yellow-youtube/archive/refs/heads/main.zip
-DocumentationUrl: https://github.com/annaesvensson/yellow-youtube
-DocumentationLanguage: en, de, sv
-Published: 2023-04-18 01:25:49
-Developer: Anna Svensson
-Tag: feature
-system/extensions/youtube.php: youtube.php, create, update
diff --git a/system/extensions/update.php b/system/extensions/update.php
@@ -1,939 +0,0 @@
-<?php
-// Update extension, https://github.com/annaesvensson/yellow-update
-
-class YellowUpdate {
- const VERSION = "0.8.97";
- const PRIORITY = "2";
- public $yellow; // access to API
- public $extensions; // number of extensions
-
- // Handle initialisation
- public function onLoad($yellow) {
- $this->yellow = $yellow;
- $this->yellow->system->setDefault("updateCurrentRelease", "none");
- $this->yellow->system->setDefault("updateLatestUrl", "auto");
- $this->yellow->system->setDefault("updateLatestFile", "update-latest.ini");
- $this->yellow->system->setDefault("updateCurrentFile", "update-current.ini");
- $this->yellow->system->setDefault("updateExtensionFile", "extension.ini");
- $this->yellow->system->setDefault("updateEventPending", "none");
- $this->yellow->system->setDefault("updateEventDaily", "0");
- $this->yellow->system->setDefault("updateTrashTimeout", "7776660");
- }
-
- // Handle update
- public function onUpdate($action) {
- if ($action=="clean" || $action=="daily") {
- $statusCode = 200;
- $path = $this->yellow->system->get("coreExtensionDirectory");
- foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.download$/", false, false) as $entry) {
- if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500;
- }
- if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!");
- $statusCode = 200;
- $path = $this->yellow->system->get("coreTrashDirectory");
- foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, false) as $entry) {
- $expire = $this->yellow->toolbox->getFileDeleted($entry) + $this->yellow->system->get("updateTrashTimeout");
- if ($expire<=time() && !$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500;
- }
- foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, true) as $entry) {
- $expire = $this->yellow->toolbox->getFileDeleted($entry) + $this->yellow->system->get("updateTrashTimeout");
- if ($expire<=time() && !$this->yellow->toolbox->deleteDirectory($entry)) $statusCode = 500;
- }
- if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!");
- }
- }
-
- // Handle request
- public function onRequest($scheme, $address, $base, $location, $fileName) {
- return $this->processRequestPending($scheme, $address, $base, $location, $fileName);
- }
-
- // Handle command
- public function onCommand($command, $text) {
- $statusCode = $this->processCommandPending();
- if ($statusCode==0) {
- switch ($command) {
- case "about": $statusCode = $this->processCommandAbout($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() {
- return array("about [extension]", "install [extension]", "uninstall [extension]", "update [extension]");
- }
-
- // Parse page content shortcut
- public function onParseContentShortcut($page, $name, $text, $type) {
- $output = null;
- if ($name=="yellow" && $type=="inline") {
- if ($text=="about") {
- list($dummy, $settingsCurrent) = $this->getExtensionSettings(false);
- $output = "Datenstrom Yellow ".YellowCore::RELEASE."<br />\n";
- foreach ($settingsCurrent as $key=>$value) {
- $output .= ucfirst($key)." ".$value->get("version")."<br />\n";
- }
- }
- if ($text=="release") $output = "Datenstrom Yellow ".YellowCore::RELEASE;
- if ($text=="log") {
- $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreWebsiteFile");
- $fileHandle = @fopen($fileName, "rb");
- if ($fileHandle) {
- clearstatcache(true, $fileName);
- if (flock($fileHandle, LOCK_SH)) {
- $dataBufferSize = 1024;
- 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;
- }
- flock($fileHandle, LOCK_UN);
- }
- fclose($fileHandle);
- }
- $output = str_replace("\n", "<br />\n", htmlspecialchars($dataBuffer));
- }
- }
- return $output;
- }
-
- // Process command to show current version
- public function processCommandAbout($command, $text) {
- $statusCode = 200;
- $extensions = $this->getExtensionsFromText($text);
- if (!is_array_empty($extensions)) {
- list($statusCode, $settings) = $this->getExtensionAboutInformation($extensions);
- if ($statusCode==200) {
- foreach ($settings as $key=>$value) {
- echo ucfirst($key)." ".$value->get("version")." - ".$this->getExtensionDescription($key, $value)."\n";
- if ($value->isExisting("documentationUrl")) echo "Read more at ".$value->get("documentationUrl")."\n";
- }
- }
- if ($statusCode>=400) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n";
- } else {
- echo "Datenstrom Yellow ".YellowCore::RELEASE."\n";
- list($statusCode, $settingsCurrent) = $this->getExtensionSettings(false);
- foreach ($settingsCurrent as $key=>$value) {
- echo ucfirst($key)." ".$value->get("version")."\n";
- }
- }
- return $statusCode;
- }
-
- // Process command to install extensions
- public function processCommandInstall($command, $text) {
- $extensions = $this->getExtensionsFromText($text);
- if (!is_array_empty($extensions)) {
- $this->extensions = 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->errorMessage."\n";
- echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated";
- echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." installed\n";
- } else {
- list($statusCode, $settingsLatest) = $this->getExtensionSettings(true);
- foreach ($settingsLatest as $key=>$value) {
- echo ucfirst($key)." - ".$this->getExtensionDescription($key, $value)."\n";
- }
- if ($statusCode!=200) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n";
- }
- return $statusCode;
- }
-
- // Process command to uninstall extensions
- public function processCommandUninstall($command, $text) {
- $extensions = $this->getExtensionsFromText($text);
- if (!is_array_empty($extensions)) {
- $this->extensions = 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->errorMessage."\n";
- echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated";
- echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." uninstalled\n";
- } else {
- list($statusCode, $settingsCurrent) = $this->getExtensionSettings(false);
- foreach ($settingsCurrent as $key=>$value) {
- echo ucfirst($key)." - ".$this->getExtensionDescription($key, $value)."\n";
- }
- if ($statusCode!=200) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n";
- }
- return $statusCode;
- }
-
- // Process command to update website
- public function processCommandUpdate($command, $text) {
- $extensions = $this->getExtensionsFromText($text);
- if (!is_array_empty($extensions)) {
- list($statusCode, $settings) = $this->getExtensionUpdateInformation($extensions);
- if ($statusCode!=200 || !is_array_empty($settings)) {
- $this->extensions = 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->errorMessage."\n";
- echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated";
- echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." updated\n";
- } else {
- echo "Your website is up to date\n";
- }
- } else {
- list($statusCode, $settings) = $this->getExtensionUpdateInformation(array("all"));
- if (!is_array_empty($settings)) {
- foreach ($settings as $key=>$value) {
- echo ucfirst($key)." ".$value->get("version")."\n";
- }
- echo "Yellow $command: Updates are available. Please type 'php yellow.php update all'.\n";
- } elseif ($statusCode!=200) {
- echo "ERROR updating files: ".$this->yellow->page->errorMessage."\n";
- } else {
- echo "Your website is up to date\n";
- }
- }
- return $statusCode;
- }
-
- // Process command for pending events
- public function processCommandPending() {
- $statusCode = 0;
- $this->extensions = 0;
- $this->updatePatchPending();
- $this->updateEventPending();
- $statusCode = $this->updateExtensionPending();
- if ($statusCode==303) {
- echo "Detected ZIP file".($this->extensions!=1 ? "s" : "");
- echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." installed. Please run command again.\n";
- }
- return $statusCode;
- }
-
- // Process request for pending events
- public function processRequestPending($scheme, $address, $base, $location, $fileName) {
- $statusCode = 0;
- if ($this->yellow->lookup->isContentFile($fileName)) {
- $this->updatePatchPending();
- $this->updateEventPending();
- $statusCode = $this->updateExtensionPending();
- if ($statusCode==303) {
- $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
- $statusCode = $this->yellow->sendStatus(303, $location);
- }
- }
- return $statusCode;
- }
-
- // Download extensions
- public function downloadExtensions($settings) {
- $statusCode = 200;
- $path = $this->yellow->system->get("coreExtensionDirectory");
- 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 ($statusCode==200 && !$this->yellow->toolbox->createFile($fileName.".download", $fileData)) {
- $statusCode = 500;
- $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
- }
- if ($statusCode!=200) 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.".download", $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 ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUpdate::updateExtensionArchive file:$path<br/>\n";
- $pathBase = "";
- if (preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1];
- $fileData = $zip->getFromName($pathBase.$this->yellow->system->get("updateExtensionFile"));
- $settings = $this->yellow->toolbox->getTextSettings($fileData, "");
- list($extension, $version, $newModified, $oldModified) = $this->getExtensionInformation($settings);
- if (!is_string_empty($extension) && !is_string_empty($version)) {
- $statusCode = max($statusCode, $this->updateExtensionSettings($extension, $action, $settings));
- $paths = $this->getExtensionDirectories($zip, $pathBase);
- foreach ($this->getExtensionFileNames($settings) as $fileName) {
- list($entry, $flags) = $this->yellow->toolbox->getTextList($settings[$fileName], ",", 2);
- if (!$this->yellow->lookup->isContentFile($fileName)) {
- $fileNameSource = $pathBase.$entry;
- $fileData = $zip->getFromName($fileNameSource);
- $lastModified = $this->yellow->toolbox->getFileModified($fileName);
- $statusCode = max($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, $paths, $page);
- $fileData = $zip->getFromName($fileNameSource);
- $lastModified = $this->yellow->toolbox->getFileModified($fileNameDestination);
- $statusCode = max($statusCode, $this->updateExtensionFile($fileNameDestination, $fileData,
- $newModified, $oldModified, $lastModified, $flags, $extension));
- }
- }
- }
- $statusCode = max($statusCode, $this->updateExtensionNotification($extension, $action));
- $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'");
- ++$this->extensions;
- } else {
- $statusCode = 500;
- $this->yellow->page->error($statusCode, "Can't detect file '$path'!");
- }
- $zip->close();
- } else {
- $statusCode = 500;
- $this->yellow->page->error($statusCode, "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->lookup->normalisePath($fileName);
- if ($this->yellow->lookup->isValidFile($fileName)) {
- $create = $update = $delete = false;
- if (preg_match("/create/i", $flags) && !is_file($fileName) && !is_string_empty($fileData)) $create = true;
- if (preg_match("/update/i", $flags) && is_file($fileName) && !is_string_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 ($this->yellow->system->get("coreDebugMode")>=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;
- }
-
- // Update pending patches
- public function updatePatchPending() {
- $fileName = $this->yellow->system->get("coreExtensionDirectory")."updatepatch.bin";
- if (is_file($fileName)) {
- if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUpdate::updatePatchPending file:$fileName<br/>\n";
- if (!$this->yellow->extension->isExisting("updatepatch")) {
- require_once($fileName);
- $this->yellow->extension->register("updatepatch", "YellowUpdatePatch");
- }
- if ($this->yellow->extension->isExisting("updatepatch")) {
- $value = $this->yellow->extension->data["updatepatch"];
- if (method_exists($value["object"], "onLoad")) $value["object"]->onLoad($this->yellow);
- if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("patch");
- }
- unset($this->yellow->extension->data["updatepatch"]);
- if (function_exists("opcache_reset")) opcache_reset();
- if (!$this->yellow->toolbox->deleteFile($fileName)) {
- $this->yellow->toolbox->log("error", "Can't delete file '$fileName'!");
- }
- }
- }
-
- // Update pending events
- public function updateEventPending() {
- if ($this->yellow->system->get("updateCurrentRelease")!="none") {
- if ($this->yellow->system->get("updateCurrentRelease")!=YellowCore::RELEASE) {
- $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
- if (!$this->yellow->system->save($fileName, array("updateCurrentRelease" => YellowCore::RELEASE))) {
- $this->yellow->toolbox->log("error", "Can't write file '$fileName'!");
- } else {
- list($name, $version, $os) = $this->yellow->toolbox->detectServerInformation();
- $product = "Datenstrom Yellow ".YellowCore::RELEASE;
- $this->yellow->toolbox->log("info", "Update $product, PHP ".PHP_VERSION.", $name $version, $os");
- }
- }
- if ($this->yellow->system->get("updateEventPending")!="none") {
- foreach (explode(",", $this->yellow->system->get("updateEventPending")) as $token) {
- list($extension, $action) = $this->yellow->toolbox->getTextList($token, "/", 2);
- if ($this->yellow->extension->isExisting($extension) && $action!="uninstall") {
- $value = $this->yellow->extension->data[$extension];
- if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate($action);
- }
- }
- $this->updateSystemSettings("all", $action);
- $this->updateLanguageSettings("all", $action);
- $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
- if (!$this->yellow->system->save($fileName, array("updateEventPending" => "none"))) {
- $this->yellow->toolbox->log("error", "Can't write file '$fileName'!");
- }
- }
- if ($this->yellow->system->get("updateEventDaily")<=time()) {
- foreach ($this->yellow->extension->data as $key=>$value) {
- if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("daily");
- }
- $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
- if (!$this->yellow->system->save($fileName, array("updateEventDaily" => $this->getTimestampDaily()))) {
- $this->yellow->toolbox->log("error", "Can't write file '$fileName'!");
- }
- }
- }
- }
-
- // Update pending extensions
- public function updateExtensionPending() {
- $statusCode = 0;
- $path = $this->yellow->system->get("coreExtensionDirectory");
- if (!is_array_empty($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", false, false))) {
- $statusCode = $this->updateExtensions("install");
- if ($statusCode==200) $statusCode = 303;
- if ($statusCode>=400) {
- $this->yellow->toolbox->log("error", $this->yellow->page->errorMessage);
- $this->yellow->page->statusCode = 0;
- $this->yellow->page->errorMessage = "";
- $statusCode = 303;
- }
- }
- return $statusCode;
- }
-
- // Update extension settings
- public function updateExtensionSettings($extension, $action, $settings) {
- $statusCode = 200;
- $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateCurrentFile");
- $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName);
- if ($action=="install" || $action=="update") {
- $settingsCurrent = $this->yellow->toolbox->getTextSettings($fileData, "extension");
- $settingsCurrent[$extension] = new YellowArray();
- foreach ($settings as $key=>$value) $settingsCurrent[$extension][$key] = $value;
- $settingsCurrent->uksort("strnatcasecmp");
- $fileDataNew = "";
- foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
- if (preg_match("/^\#/", $line)) $fileDataNew = $line;
- break;
- }
- foreach ($settingsCurrent as $extension=>$block) {
- if (!is_string_empty($fileDataNew)) $fileDataNew .= "\n";
- foreach ($block as $key=>$value) {
- $fileDataNew .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n";
- }
- }
- } elseif ($action=="uninstall") {
- $fileDataNew = $this->yellow->toolbox->unsetTextSettings($fileData, "extension", $extension);
- }
- if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) {
- $statusCode = 500;
- $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
- }
- return $statusCode;
- }
-
- // Update system settings
- public function updateSystemSettings($extension, $action) {
- $statusCode = 200;
- $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
- $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName);
- if ($action=="install" || $action=="update") {
- $fileDataStart = $fileDataSettings = "";
- $settings = new YellowArray();
- $settings->exchangeArray($this->yellow->system->settingsDefaults->getArrayCopy());
- foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
- if (preg_match("/^\#/", $line)) {
- if (is_string_empty($fileDataStart)) $fileDataStart = $line."\n";
- continue;
- }
- if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
- if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
- $settings[$matches[1]] = $matches[2];
- }
- }
- }
- foreach ($settings as $key=>$value) {
- $fileDataSettings .= ucfirst($key).(is_string_empty($value) ? ":\n" : ": $value\n");
- }
- $fileDataNew = $fileDataStart.$fileDataSettings;
- } elseif ($action=="uninstall") {
- if (!is_string_empty($extension)) {
- $fileDataNew = "";
- $regex = "/^".ucfirst($extension)."[A-Z]+/";
- foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
- if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
- if (!is_string_empty($matches[1]) && preg_match($regex, $matches[1])) continue;
- }
- $fileDataNew .= $line;
- }
- }
- }
- if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) {
- $statusCode = 500;
- $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
- }
- return $statusCode;
- }
-
- // Update language settings
- public function updateLanguageSettings($extension, $action) {
- $statusCode = 200;
- $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreLanguageFile");
- $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName);
- if ($action=="install" || $action=="update") {
- $fileDataStart = $fileDataSettings = $language = "";
- $settings = new YellowArray();
- foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
- if (preg_match("/^\#/", $line)) {
- if (is_string_empty($fileDataStart)) $fileDataStart = $line."\n";
- continue;
- }
- if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
- if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
- if (lcfirst($matches[1])=="language") {
- if (!is_array_empty($settings)) {
- if (!is_string_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;
- $settings["languageLocale"] = "n/a";
- $settings["languageDescription"] = "n/a";
- $settings["languageTranslator"] = "Unknown";
- foreach ($this->yellow->language->settingsDefaults as $key=>$value) {
- $require = preg_match("/^([a-z]*)[A-Z]+/", $key, $tokens) ? $tokens[1] : "core";
- if ($require=="language") $require = "core";
- if ($this->yellow->extension->isExisting($require)) {
- if ($this->yellow->language->isText($key, $language)) {
- $settings[$key] = $this->yellow->language->getText($key, $language);
- } else {
- $settings[$key] = $this->yellow->language->getText($key, "en");
- }
- }
- }
- }
- if (!is_string_empty($language)) {
- $settings[$matches[1]] = $matches[2];
- }
- }
- }
- }
- if (!is_array_empty($settings)) {
- if (!is_string_empty($fileDataSettings)) $fileDataSettings .= "\n";
- foreach ($settings as $key=>$value) {
- $fileDataSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n";
- }
- }
- $fileDataNew = $fileDataStart.$fileDataSettings;
- } elseif ($action=="uninstall") {
- if (!is_string_empty($extension) && ucfirst($extension)!="Language") {
- $fileDataNew = "";
- $regex = "/^".ucfirst($extension)."[A-Z]+/";
- foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
- if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
- if (!is_string_empty($matches[1]) && preg_match($regex, $matches[1])) continue;
- }
- $fileDataNew .= $line;
- }
- }
- }
- if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) {
- $statusCode = 500;
- $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
- }
- return $statusCode;
- }
-
- // Update extension notification
- public function updateExtensionNotification($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);
- }
- $updateEventPending = $this->yellow->system->get("updateEventPending");
- if ($updateEventPending=="none") $updateEventPending = "";
- if (!is_string_empty($updateEventPending)) $updateEventPending .= ",";
- $updateEventPending .= "$extension/$action";
- $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
- if (!$this->yellow->system->save($fileName, array("updateEventPending" => $updateEventPending))) {
- $statusCode = 500;
- $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
- }
- 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, "uninstall", $block));
- }
- return $statusCode;
- }
-
- // Remove extension archive
- public function removeExtensionArchive($extension, $action, $settings) {
- $statusCode = 200;
- $fileNames = $this->getExtensionFileNames($settings, true);
- if (!is_array_empty($fileNames)) {
- $statusCode = max($statusCode, $this->updateExtensionNotification($extension, $action));
- foreach ($fileNames as $fileName) {
- $statusCode = max($statusCode, $this->removeExtensionFile($fileName));
- }
- if ($statusCode==200) {
- $statusCode = max($statusCode, $this->updateExtensionSettings($extension, $action, $settings));
- $statusCode = max($statusCode, $this->updateSystemSettings($extension, $action));
- $statusCode = max($statusCode, $this->updateLanguageSettings($extension, $action));
- }
- $version = $settings->get("version");
- $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'");
- ++$this->extensions;
- } else {
- $statusCode = 500;
- $this->yellow->page->error($statusCode, "Please delete extension '$extension' manually!");
- }
- return $statusCode;
- }
-
- // Remove extension file
- public function removeExtensionFile($fileName) {
- $statusCode = 200;
- $fileName = $this->yellow->lookup->normalisePath($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 ($this->yellow->system->get("coreDebugMode")>=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 about information
- public function getExtensionAboutInformation($extensions) {
- $settings = array();
- list($statusCode, $settingsCurrent) = $this->getExtensionSettings(false);
- $settingsCurrent["Datenstrom Yellow"] = new YellowArray();
- $settingsCurrent["Datenstrom Yellow"]["version"] = YellowCore::RELEASE;
- $settingsCurrent["Datenstrom Yellow"]["description"] = "Datenstrom Yellow is for people who make small websites.";
- $settingsCurrent["Datenstrom Yellow"]["documentationUrl"] = "https://datenstrom.se/yellow/";
- foreach ($extensions as $extension) {
- $found = false;
- if (strtoloweru($extension)=="yellow") $extension = "Datenstrom Yellow";
- 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'!");
- }
- }
- return array($statusCode, $settings);
- }
-
- // 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 about 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 (in_array("all", $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 {
- foreach ($extensions as $extension) {
- $found = false;
- foreach ($settingsCurrent as $key=>$value) {
- if (strtoloweru($key)==strtoloweru($extension) && $settingsLatest->isExisting($key)) {
- $versionCurrent = $settingsCurrent[$key]->get("version");
- $versionLatest = $settingsLatest[$key]->get("version");
- if (strnatcasecmp($versionCurrent, $versionLatest)<0) {
- $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 settings
- public function getExtensionSettings($latest) {
- $statusCode = 200;
- $settings = array();
- if (!$latest) {
- $fileNameCurrent = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateCurrentFile");
- $fileData = $this->yellow->toolbox->readFile($fileNameCurrent);
- $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension");
- foreach ($settings->getArrayCopy() 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("updateLatestUrl");
- if ($url=="auto") $url = "https://raw.githubusercontent.com/datenstrom/yellow/main/system/extensions/update-latest.ini";
- 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 information
- public function getExtensionInformation($settings) {
- $extension = lcfirst($settings->get("extension"));
- $version = $settings->get("version");
- $newModified = strtotime($settings->get("published"));
- $oldModified = 0;
- $invalid = false;
- foreach ($settings as $key=>$value) {
- if (strposu($key, "/")) {
- $fileName = $this->yellow->lookup->normalisePath($key);
- if (!$this->yellow->lookup->isValidFile($fileName)) $invalid = true;
- if ($oldModified==0) $oldModified = $this->yellow->toolbox->getFileModified($fileName);
- }
- }
- if ($invalid) $extension = $version = "";
- return array($extension, $version, $newModified, $oldModified);
- }
-
- // Return extension directories
- public function getExtensionDirectories($zip, $pathBase) {
- $paths = array();
- for ($index=0; $index<$zip->numFiles; ++$index) {
- $entry = substru($zip->getNameIndex($index), strlenu($pathBase));
- if (preg_match("#^(.*\/).*?$#", $entry, $matches)) {
- array_push($paths, $matches[1]);
- }
- }
- return array_unique($paths);
- }
-
- // 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, $paths, $page) {
- if (preg_match("/multi-language/i", $flags)) {
- $pathMultiLanguage = "";
- $languagesWanted = array($page->get("language"), "en");
- foreach ($languagesWanted as $language) {
- foreach ($paths as $path) {
- if ($this->yellow->lookup->normaliseToken(rtrim($path, "/"))==$language) {
- $pathMultiLanguage = $path;
- break;
- }
- }
- if (!is_string_empty($pathMultiLanguage)) break;
- }
- $fileNameSource = $pathBase.$pathMultiLanguage.$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 description including responsible developer/designer/translator
- public function getExtensionDescription($key, $value) {
- $description = $responsible = "";
- if ($value->isExisting("description")) $description = $value->get("description");
- if ($value->isExisting("developer")) $responsible = "Developed by ".$value["developer"].".";
- if ($value->isExisting("designer")) $responsible = "Designed by ".$value["designer"].".";
- if ($value->isExisting("translator")) $responsible = "Translated by ".$value["translator"].".";
- if (is_string_empty($description)) $description = "No description available.";
- return "$description $responsible";
- }
-
- // Return extension file
- public function getExtensionFile($url) {
- $curlHandle = curl_init();
- curl_setopt($curlHandle, CURLOPT_URL, $this->getExtensionDownloadUrl($url));
- 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);
- $fileData = curl_exec($curlHandle);
- $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
- $redirectUrl = ($statusCode>=300 && $statusCode<=399) ? curl_getinfo($curlHandle, CURLINFO_REDIRECT_URL) : "";
- curl_close($curlHandle);
- if ($statusCode==0) {
- $statusCode = 450;
- $this->yellow->page->error($statusCode, "Can't connect to the update server!");
- }
- if ($statusCode!=450 && $statusCode!=200) {
- $statusCode = 500;
- $this->yellow->page->error($statusCode, "Can't download file '$url'!");
- }
- if ($this->yellow->system->get("coreDebugMode")>=2 && !is_string_empty($redirectUrl)) {
- echo "YellowUpdate::getExtensionFile redirected to url:$redirectUrl<br/>\n";
- }
- if ($this->yellow->system->get("coreDebugMode")>=2) {
- echo "YellowUpdate::getExtensionFile status:$statusCode url:$url<br/>\n";
- }
- return array($statusCode, $fileData);
- }
-
- // Return extension download URL, redirect to known URL if necessary
- public function getExtensionDownloadUrl($url) {
- if (preg_match("#^https://github.com/(.+)/archive/refs/heads/main.zip$#", $url, $matches)) {
- $url = "https://codeload.github.com/".$matches[1]."/zip/refs/heads/main";
- }
- if (preg_match("#^https://github.com/(.+)/raw/main/(.+)$#", $url, $matches)) {
- $url = "https://raw.githubusercontent.com/".$matches[1]."/main/".$matches[2];
- }
- return $url;
- }
-
- // Return time of next daily update
- public function getTimestampDaily() {
- $timeOffset = 0;
- foreach (str_split($this->yellow->system->get("sitename")) as $char) {
- $timeOffset = ($timeOffset+ord($char)) % 60;
- }
- return mktime(0, 0, 0) + 60*60*24 + $timeOffset;
- }
-}
diff --git a/system/themes/stockholm.css b/system/themes/stockholm.css
@@ -5,18 +5,14 @@
:root {
--bg: #fff;
--code-bg: #f7f7f7;
- --notice1-bg: #fffbf0;
- --notice2-bg: #fdf0f0;
- --notice3-bg: #f0f8fe;
+ --important-bg: #f0f8fe;
--heading: #111;
- --text: #666;
+ --text: #333;
--code: #666;
--link: #07d;
--link-active: #29f;
--blockquote-accent: #29f;
- --notice1-accent: #fb0;
- --notice2-accent: #d00;
- --notice3-accent: #08e;
+ --important-accent: #08e;
--separator: #ddd;
--border: #bbb;
--font: "Open Sans", Helvetica, sans-serif;
@@ -112,15 +108,20 @@ a:hover {
border-spacing: 0;
border-collapse: collapse;
}
-.content th {
+.content th:not([align]) {
text-align: left;
- padding: 0.3em;
- border-bottom: 1px solid var(--separator);
}
+.content th,
.content td {
- text-align: left;
padding: 0.3em;
- padding-right: 2em;
+ padding-left: 2em;
+}
+.content th:first-child,
+.content td:first-child {
+ padding: 0.3em;
+}
+.content th {
+ border-bottom: 1px solid var(--separator);
}
.content code,
.content pre {
@@ -156,26 +157,18 @@ a:hover {
font-weight: bold;
border-left: 3px solid var(--blockquote-accent);
}
-.content .notice1 {
- margin: 1em 0;
- padding: 10px 1em;
- background-color: var(--notice1-bg);
- border-left: 10px solid var(--notice1-accent);
+.content details summary {
+ padding-left: inherit;
+ cursor: pointer;
}
-.content .notice2 {
- margin: 1em 0;
- padding: 10px 1em;
- background-color: var(--notice2-bg);
- border-left: 10px solid var(--notice2-accent);
+.content details > * {
+ padding-left: 1em;
}
-.content .notice3,
-.content .notice4,
-.content .notice5,
-.content .notice6 {
+.content .important {
margin: 1em 0;
padding: 10px 1em;
- background-color: var(--notice3-bg);
- border-left: 10px solid var(--notice3-accent);
+ background-color: var(--important-bg);
+ border-left: 10px solid var(--important-accent);
}
.content .flexible {
position: relative;
@@ -389,6 +382,8 @@ a:hover {
border: none !important;
}
}
+
+/* Custom */
.latex sub {
vertical-align: -0.6ex;
margin-left: -0.125em;
@@ -407,4 +402,3 @@ a:hover {
margin-left: -0.20em;
margin-right: -0.2em;
}
-
diff --git a/system/workers/core.php b/system/workers/core.php
@@ -0,0 +1,4005 @@
+<?php
+// Core extension, https://github.com/annaesvensson/yellow-core
+
+class YellowCore {
+ const VERSION = "0.9.22";
+ const RELEASE = "0.9";
+ public $content; // content files
+ public $media; // media files
+ public $system; // system settings
+ public $language; // language settings
+ public $user; // user settings
+ public $extension; // extensions
+ public $lookup; // lookup and normalisation methods
+ public $toolbox; // toolbox with helper methods
+ public $page; // current page
+
+ public function __construct() {
+ $this->content = new YellowContent($this);
+ $this->media = new YellowMedia($this);
+ $this->system = new YellowSystem($this);
+ $this->language = new YellowLanguage($this);
+ $this->user = new YellowUser($this);
+ $this->extension = new YellowExtension($this);
+ $this->lookup = new YellowLookup($this);
+ $this->toolbox = new YellowToolbox($this);
+ $this->page = new YellowPage($this);
+ $this->checkRequirements();
+ $this->system->setDefault("sitename", "Localhost");
+ $this->system->setDefault("author", "Datenstrom");
+ $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("coreServerUrl", "auto");
+ $this->system->setDefault("coreTimezone", "UTC");
+ $this->system->setDefault("coreContentExtension", ".md");
+ $this->system->setDefault("coreContentDefaultFile", "page.md");
+ $this->system->setDefault("coreContentErrorFile", "page-error-(.*).md");
+ $this->system->setDefault("coreLanguageFile", "yellow-language.ini");
+ $this->system->setDefault("coreUserFile", "yellow-user.ini");
+ $this->system->setDefault("coreWebsiteFile", "yellow-website.log");
+ $this->system->setDefault("coreAssetLocation", "/assets/");
+ $this->system->setDefault("coreMediaLocation", "/media/");
+ $this->system->setDefault("coreDownloadLocation", "/media/downloads/");
+ $this->system->setDefault("coreImageLocation", "/media/images/");
+ $this->system->setDefault("coreThumbnailLocation", "/media/thumbnails/");
+ $this->system->setDefault("coreMultiLanguageMode", "0");
+ $this->system->setDefault("coreDebugMode", "0");
+ }
+
+ public function __destruct() {
+ $this->shutdown();
+ }
+
+ // Check requirements
+ public function checkRequirements() {
+ if (!version_compare(PHP_VERSION, "7.0", ">=")) $this->exitFatalError("Datenstrom Yellow requires PHP 7.0 or higher!");
+ if (!extension_loaded("curl")) $this->exitFatalError("Datenstrom Yellow requires PHP curl extension!");
+ if (!extension_loaded("gd")) $this->exitFatalError("Datenstrom Yellow requires PHP gd extension!");
+ if (!extension_loaded("mbstring")) $this->exitFatalError("Datenstrom Yellow requires PHP mbstring extension!");
+ if (!extension_loaded("zip")) $this->exitFatalError("Datenstrom Yellow requires PHP zip extension!");
+ mb_internal_encoding("UTF-8");
+ }
+
+ // Handle initialisation
+ public function load() {
+ $this->system->load("system/extensions/yellow-system.ini");
+ $this->system->set("coreSystemFile", "yellow-system.ini");
+ $this->system->set("coreContentDirectory", "content/");
+ $this->system->set("coreMediaDirectory", $this->lookup->findMediaDirectory("coreMediaLocation"));
+ $this->system->set("coreSystemDirectory", "system/");
+ $this->system->set("coreCacheDirectory", "system/cache/");
+ $this->system->set("coreExtensionDirectory", "system/extensions/");
+ $this->system->set("coreLayoutDirectory", "system/layouts/");
+ $this->system->set("coreThemeDirectory", "system/themes/");
+ $this->system->set("coreTrashDirectory", "system/trash/");
+ $this->system->set("coreWorkerDirectory", "system/workers/");
+ list($pathInstall, $pathRoot, $pathHome) = $this->lookup->findFileSystemInformation();
+ $this->system->set("coreServerInstallDirectory", $pathInstall);
+ $this->system->set("coreServerRootDirectory", $pathRoot);
+ $this->system->set("coreServerHomeDirectory", $pathHome);
+ register_shutdown_function(array($this, "processFatalError"));
+ if ($this->system->get("coreDebugMode")>=1) {
+ ini_set("display_errors", 1);
+ error_reporting(E_ALL);
+ }
+ date_default_timezone_set($this->system->get("coreTimezone"));
+ $this->extension->load($this->system->get("coreWorkerDirectory"));
+ $this->language->load($this->system->get("coreExtensionDirectory").$this->system->get("coreLanguageFile"));
+ $this->user->load($this->system->get("coreExtensionDirectory").$this->system->get("coreUserFile"));
+ $this->startup();
+ }
+
+ // Handle request from web browser
+ public function request() {
+ $statusCode = 0;
+ $this->toolbox->timerStart($time);
+ ob_start();
+ list($scheme, $address, $base, $location, $fileName) = $this->lookup->getRequestInformation();
+ $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName, true);
+ 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->isError()) $statusCode = $this->processRequestError();
+ ob_end_flush();
+ $this->toolbox->timerStop($time);
+ if ($this->system->get("coreDebugMode")>=1 && ($this->lookup->isContentFile($fileName) || $this->page->isError())) {
+ 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)) {
+ $statusCode = $this->sendPage($scheme, $address, $base, $location, $fileName, $cacheable, true);
+ } elseif (!is_string_empty($fileName)) {
+ $statusCode = $this->sendFile(200, $fileName, $cacheable);
+ }
+ if (!is_readable($fileName)) $this->page->error(404);
+ }
+ if ($this->system->get("coreDebugMode")>=1 && ($this->lookup->isContentFile($fileName) || $this->page->isError())) {
+ echo "YellowCore::processRequest file:$fileName<br />\n";
+ }
+ return $statusCode;
+ }
+
+ // Process request with error
+ public function processRequestError() {
+ ob_clean();
+ $statusCode = $this->sendPage($this->page->scheme, $this->page->address, $this->page->base,
+ $this->page->location, $this->page->fileName, false, false);
+ if ($this->system->get("coreDebugMode")>=1) echo "YellowCore::processRequestError file:".$this->page->fileName."<br />\n";
+ return $statusCode;
+ }
+
+ // Process fatal runtime error
+ public function processFatalError() {
+ $error = error_get_last();
+ if (!is_null($error) && isset($error["type"]) && ($error["type"]==E_ERROR || $error["type"]==E_PARSE)) {
+ $fileNameAbsolute = isset($error["file"]) ? $error["file"] : "";
+ $fileName = substru($fileNameAbsolute, strlenu($this->system->get("coreServerInstallDirectory")));
+ $this->toolbox->log("error", "Process file '$fileName' with fatal error!");
+ $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted(500));
+ $troubleshooting = PHP_SAPI!="cli" ?
+ "<a href=\"".$this->toolbox->getTroubleshootingUrl()."\">See troubleshooting</a>." : "See ".$this->toolbox->getTroubleshootingUrl();
+ echo "<br />\nDatenstrom Yellow stopped with fatal error. Activate the debug mode for more information. $troubleshooting\n";
+ }
+ }
+
+ // Show error message and terminate immediately
+ public function exitFatalError($errorMessage = "") {
+ $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted(500));
+ $troubleshooting = PHP_SAPI!="cli" ?
+ "<a href=\"".$this->toolbox->getTroubleshootingUrl()."\">See troubleshooting</a>." : "See ".$this->toolbox->getTroubleshootingUrl();
+ echo "$errorMessage $troubleshooting\n";
+ exit(1);
+ }
+
+ // Send page response
+ public function sendPage($scheme, $address, $base, $location, $fileName, $cacheable, $showSource) {
+ $rawData = $showSource ? $this->toolbox->readFile($fileName) : $this->page->getRawDataError();
+ $statusCode = max($this->page->statusCode, 200);
+ $errorMessage = $this->page->errorMessage;
+ $this->page = new YellowPage($this);
+ $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName, $cacheable);
+ $this->page->parseMeta($rawData, $statusCode, $errorMessage);
+ $this->language->set($this->page->get("language"));
+ $this->page->parseContent();
+ $this->page->parsePage();
+ $statusCode = $this->sendData($this->page->statusCode, $this->page->headerData, $this->page->outputData);
+ if ($this->system->get("coreDebugMode")>=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 data response
+ public function sendData($statusCode, $headerData, $outputData) {
+ $lastModifiedFormatted = isset($headerData["Last-Modified"]) ? $headerData["Last-Modified"] : "";
+ if ($statusCode==200 && !isset($headerData["Cache-Control"]) && $this->toolbox->isNotModified($lastModifiedFormatted)) {
+ $statusCode = 304;
+ $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode));
+ } else {
+ $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode));
+ foreach ($headerData as $key=>$value) {
+ $this->toolbox->sendHttpHeader("$key: $value");
+ }
+ if (!is_null($outputData)) echo $outputData;
+ }
+ 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;
+ $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode));
+ } else {
+ $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode));
+ if (!$cacheable) $this->toolbox->sendHttpHeader("Cache-Control: no-cache, no-store");
+ $this->toolbox->sendHttpHeader("Content-Type: ".$this->toolbox->getMimeContentType($fileName));
+ $this->toolbox->sendHttpHeader("Last-Modified: ".$lastModifiedFormatted);
+ echo $this->toolbox->readFile($fileName);
+ }
+ return $statusCode;
+ }
+
+ // Send status response
+ public function sendStatus($statusCode, $location = "") {
+ if (!is_string_empty($location)) $this->page->status($statusCode, $location);
+ $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode));
+ foreach ($this->page->headerData as $key=>$value) {
+ $this->toolbox->sendHttpHeader("$key: $value");
+ }
+ return $statusCode;
+ }
+
+ // Handle command from command line
+ public function command($line = "") {
+ $statusCode = 0;
+ $this->toolbox->timerStart($time);
+ list($command, $text) = $this->lookup->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 && is_string_empty($command)) {
+ $lines = array();
+ foreach ($this->extension->data as $key=>$value) {
+ if (method_exists($value["object"], "onCommandHelp")) {
+ $this->lookup->commandHandler = $key;
+ $output = $value["object"]->onCommandHelp();
+ if (!is_null($output)) {
+ $lines = array_merge($lines, is_array($output) ? $output : array($output));
+ }
+ }
+ }
+ usort($lines, "strnatcasecmp");
+ $this->showCommandHelp($lines);
+ $statusCode = 200;
+ }
+ if ($statusCode==0) {
+ $this->lookup->commandHandler = "core";
+ $statusCode = 400;
+ echo "Yellow $command: Command not found\n";
+ }
+ $this->toolbox->timerStop($time);
+ if ($this->system->get("coreDebugMode")>=1) {
+ echo "YellowCore::command status:$statusCode time:$time ms<br />\n";
+ }
+ return $statusCode<400 ? 0 : 1;
+ }
+
+ // Show command help
+ public function showCommandHelp($lines) {
+ echo "Datenstrom Yellow is for people who make small websites. https://datenstrom.se/yellow/\n";
+ $lineCounter = 0;
+ foreach ($lines as $line) {
+ echo(++$lineCounter>1 ? " " : "Syntax: ")."php yellow.php $line\n";
+ }
+ }
+
+ // Handle startup
+ public function startup() {
+ if (isset($this->extension->data)) {
+ foreach ($this->extension->data as $key=>$value) {
+ if (method_exists($value["object"], "onStartup")) $value["object"]->onStartup();
+ }
+ }
+ }
+
+ // Handle shutdown
+ public function shutdown() {
+ if (isset($this->extension->data)) {
+ foreach ($this->extension->data as $key=>$value) {
+ if (method_exists($value["object"], "onShutdown")) $value["object"]->onShutdown();
+ }
+ }
+ }
+
+ // 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);
+ }
+}
+
+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])) {
+ $this->pages[$location] = array();
+ $scheme = $this->yellow->page->scheme;
+ $address = $this->yellow->page->address;
+ $base = $this->yellow->page->base;
+ if (is_string_empty($location)) {
+ $rootLocations = $this->yellow->lookup->findContentRootLocations();
+ foreach ($rootLocations as $rootLocation=>$rootFileName) {
+ $page = new YellowPage($this->yellow);
+ $page->setRequestInformation($scheme, $address, $base, $rootLocation, $rootFileName, false);
+ $page->parseMeta("");
+ array_push($this->pages[$location], $page);
+ }
+ } else {
+ if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowContent::scanLocation location:$location<br />\n";
+ $fileNames = $this->yellow->lookup->findChildrenFromContentLocation($location);
+ foreach ($fileNames as $fileName) {
+ $page = new YellowPage($this->yellow);
+ $page->setRequestInformation($scheme, $address, $base,
+ $this->yellow->lookup->findContentLocationFromFile($fileName), $fileName, false);
+ $page->parseMeta($this->yellow->toolbox->readFile($fileName, 4096));
+ 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) {
+ $found = true;
+ break;
+ }
+ }
+ return $found ? $page : null;
+ }
+
+ // Return page collection with pages of the website
+ public function index($showInvisible = false) {
+ $rootLocation = $this->getRootLocation($this->yellow->page->location);
+ return $this->getChildrenRecursive($rootLocation, $showInvisible);
+ }
+
+ // Return page collection with top-level navigation
+ public function top($showInvisible = false) {
+ $rootLocation = $this->getRootLocation($this->yellow->page->location);
+ return $this->getChildren($rootLocation, $showInvisible);
+ }
+
+ // 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));
+ $page = null;
+ while (!$this->yellow->lookup->isRootLocation($location)) {
+ $page = $this->find($location);
+ if ($page) $pages->prepend($page);
+ $location = $this->getParentLocation($location);
+ }
+ if ($page) {
+ $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)) {
+ $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();
+ if ($this->yellow->system->get("coreMultiLanguageMode")) {
+ 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)) {
+ $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)) {
+ $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);
+ $sharedLocation = $this->getHomeLocation($location)."shared/";
+ return $pages->merge($this->scanLocation($sharedLocation));
+ }
+
+ // 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) {
+ $parentLocation = "";
+ $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 (is_string_empty($parentLocation)) $parentLocation = "root$token/";
+ return $parentLocation;
+ }
+
+ // Return top-level location
+ public function getParentTopLocation($location) {
+ $parentTopLocation = "";
+ $token = rtrim(substru($this->getRootLocation($location), 4), "/");
+ if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1];
+ if (is_string_empty($parentTopLocation)) $parentTopLocation = "$token/";
+ return $parentTopLocation;
+ }
+}
+
+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])) {
+ $this->files[$location] = array();
+ $scheme = $this->yellow->page->scheme;
+ $address = $this->yellow->page->address;
+ $base = $this->yellow->system->get("coreServerBase");
+ if (is_string_empty($location)) {
+ $fileNames = array($this->yellow->system->get("coreMediaDirectory"));
+ } else {
+ if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowMedia::scanLocation location:$location<br />\n";
+ $fileNames = $this->yellow->lookup->findChildrenFromMediaLocation($location);
+ }
+ foreach ($fileNames as $fileName) {
+ $file = new YellowPage($this->yellow);
+ $file->setRequestInformation($scheme, $address, $base,
+ $this->yellow->lookup->findMediaLocationFromFile($fileName), $fileName, false);
+ $file->parseMeta(null);
+ 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) {
+ $found = true;
+ break;
+ }
+ }
+ return $found ? $file : null;
+ }
+
+ // Return page collection with media files
+ public function index($showInvisible = false) {
+ return $this->getChildrenRecursive("", $showInvisible);
+ }
+
+ // 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)) {
+ $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)) {
+ $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) {
+ $parentLocation = "";
+ $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];
+ }
+ return $parentLocation;
+ }
+
+ // Return top-level location
+ public function getParentTopLocation($location) {
+ $parentTopLocation = "";
+ $token = rtrim($this->yellow->system->get("coreMediaLocation"), "/");
+ if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1];
+ if (is_string_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) {
+ $this->modified = $this->yellow->toolbox->getFileModified($fileName);
+ $fileData = $this->yellow->toolbox->readFile($fileName);
+ $this->settings = $this->yellow->toolbox->getTextSettings($fileData, "");
+ if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowSystem::load file:$fileName<br />\n";
+ if ($this->yellow->system->get("coreDebugMode")>=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 (!is_string_empty($key) && !is_string_empty($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->writeFile($fileName, $fileData);
+ }
+
+ // Set default system setting
+ public function setDefault($key, $value) {
+ $this->settingsDefaults[$key] = $value;
+ }
+
+ // Set default system settings
+ public function setDefaults($lines) {
+ foreach ($lines as $line) {
+ if (preg_match("/^\#/", $line)) continue;
+ if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
+ if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
+ $this->settingsDefaults[$matches[1]] = $matches[2];
+ }
+ }
+ }
+ }
+
+ // 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 different value for system setting
+ public function getDifferent($key) {
+ $array = array_diff($this->yellow->toolbox->enumerate($key), array($this->get($key)));
+ return reset($array);
+ }
+
+ // TODO: Remove later, this is only for backwards compatibility
+ public function getAvailable($key) { return $this->yellow->toolbox->enumerate($key); }
+
+ // Return system settings
+ public function getSettings($filterStart = "", $filterEnd = "") {
+ $settings = array();
+ if (is_string_empty($filterStart) && is_string_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 (!is_string_empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value;
+ if (!is_string_empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $settings[$key] = $value;
+ }
+ }
+ return $settings;
+ }
+
+ // 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 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) {
+ $this->modified = $this->yellow->toolbox->getFileModified($fileName);
+ $fileData = $this->yellow->toolbox->readFile($fileName);
+ $settings = $this->yellow->toolbox->getTextSettings($fileData, "language");
+ foreach ($settings as $language=>$block) {
+ if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray();
+ foreach ($block as $key=>$value) {
+ $this->settings[$language][$key] = $value;
+ }
+ }
+ if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowLanguage::load file:$fileName<br />\n";
+ foreach ($this->settings->getArrayCopy() as $key=>$value) {
+ if (!isset($this->settings[$key]["languageDescription"])) {
+ unset($this->settings[$key]);
+ }
+ }
+ $callback = function ($a, $b) {
+ return strnatcmp($a["languageDescription"], $b["languageDescription"]);
+ };
+ $this->settings->uasort($callback);
+ }
+
+ // Set current language
+ public function set($language) {
+ $this->language = $language;
+ }
+
+ // Set default language setting
+ public function setDefault($key, $value, $language) {
+ if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray();
+ $this->settings[$language][$key] = $value;
+ $this->settingsDefaults[$key] = true;
+ }
+
+ // Set default language settings
+ public function setDefaults($lines) {
+ $language = "";
+ foreach ($lines as $line) {
+ if (preg_match("/^\#/", $line)) continue;
+ if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
+ if (lcfirst($matches[1])=="language" && !is_string_empty($matches[2])) {
+ $language = $matches[2];
+ if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray();
+ }
+ if (!is_string_empty($language) && !is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
+ $this->settings[$language][$matches[1]] = $matches[2];
+ $this->settingsDefaults[$matches[1]] = 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 (is_string_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 text as language specific date, convert to one of the standard formats
+ public function getDateStandard($text, $language = "") {
+ if (preg_match("/^\d+$/", $text)) {
+ $output = $text;
+ } elseif (preg_match("/^\d+\-\d+$/", $text)) {
+ $format = $this->getText("coreDateFormatShort", $language);
+ $output = $this->getDateFormatted(strtotime($text), $format, $language);
+ } elseif (preg_match("/^\d+\-\d+\-\d+$/", $text)) {
+ $format = $this->getText("coreDateFormatMedium", $language);
+ $output = $this->getDateFormatted(strtotime($text), $format, $language);
+ } else {
+ $format = $this->getText("coreDateFormatLong", $language);
+ $output = $this->getDateFormatted(strtotime($text), $format, $language);
+ }
+ return $output;
+ }
+
+ // Return Unix time as date, relative to today
+ public function getDateRelative($timestamp, $format, $daysLimit, $language = "") {
+ $timeDifference = mktime(0, 0, 0) - strtotime(date("Y-m-d", $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 Unix time as date
+ public function getDateFormatted($timestamp, $format, $language = "") {
+ $dateMonthsNominative = preg_split("/\s*,\s*/", $this->getText("coreDateMonthsNominative", $language));
+ $dateMonthsGenitive = preg_split("/\s*,\s*/", $this->getText("coreDateMonthsGenitive", $language));
+ $dateWeekdays = preg_split("/\s*,\s*/", $this->getText("coreDateWeekdays", $language));
+ $monthNominative = $dateMonthsNominative[date("n", $timestamp) - 1];
+ $monthGenitive = $dateMonthsGenitive[date("n", $timestamp) - 1];
+ $weekday = $dateWeekdays[date("N", $timestamp) - 1];
+ $timeZone = $this->yellow->system->get("coreTimezone");
+ $timeZoneHelper = new DateTime("now", new DateTimeZone($timeZone));
+ $timeZoneOffset = $timeZoneHelper->getOffset();
+ $timeZoneAbbreviation = "GMT".($timeZoneOffset<0 ? "-" : "+").abs(intval($timeZoneOffset/3600));
+ $format = preg_replace("/(?<!\\\)F/", addcslashes($monthNominative, "A..Za..z"), $format);
+ $format = preg_replace("/(?<!\\\)V/", addcslashes($monthGenitive, "A..Za..z"), $format);
+ $format = preg_replace("/(?<!\\\)M/", addcslashes(substru($monthNominative, 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 language settings
+ public function getSettings($filterStart = "", $filterEnd = "", $language = "") {
+ $settings = array();
+ if (is_string_empty($language)) $language = $this->language;
+ if (isset($this->settings[$language])) {
+ if (is_string_empty($filterStart) && is_string_empty($filterEnd)) {
+ $settings = $this->settings[$language]->getArrayCopy();
+ } else {
+ foreach ($this->settings[$language] as $key=>$value) {
+ if (!is_string_empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value;
+ if (!is_string_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;
+ }
+
+ // Check if language setting exists
+ public function isText($key, $language = "") {
+ if (is_string_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 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) {
+ $this->modified = $this->yellow->toolbox->getFileModified($fileName);
+ $fileData = $this->yellow->toolbox->readFile($fileName);
+ $this->settings = $this->yellow->toolbox->getTextSettings($fileData, "email");
+ if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUser::load file:$fileName<br />\n";
+ }
+
+ // 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 (!is_string_empty($key) && !is_string_empty($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->writeFile($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->writeFile($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 (is_string_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 (is_string_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 (is_string_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 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) {
+ $this->modified = max($this->modified, $this->yellow->toolbox->getFileModified($entry));
+ require_once($entry);
+ $name = $this->yellow->lookup->normaliseName(basename($entry), true, true);
+ $this->register(lcfirst($name), "Yellow".ucfirst($name));
+ if ($this->yellow->system->get("coreDebugMode")>=3) echo "YellowExtension::load file:$entry<br />\n";
+ }
+ $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);
+ }
+ }
+
+ // 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;
+ }
+
+ // Return file system information
+ public function findFileSystemInformation() {
+ $pathInstall = substru(__DIR__, 0, 1-strlenu($this->yellow->system->get("coreWorkerDirectory")));
+ $pathBase = $this->yellow->system->get("coreContentDirectory");
+ $pathRoot = $this->yellow->system->get("coreMultiLanguageMode") ? "default/" : "";
+ $pathHome = "home/";
+ if (!is_string_empty($pathRoot)) {
+ $firstRoot = "";
+ $token = $root = rtrim($pathRoot, "/");
+ foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) {
+ if (is_string_empty($firstRoot)) $firstRoot = $token = $entry;
+ if ($this->normaliseToken($entry)==$root) {
+ $token = $entry;
+ break;
+ }
+ }
+ $pathRoot = $this->normaliseToken($token)."/";
+ $pathBase .= "$firstRoot/";
+ }
+ if (!is_string_empty($pathHome)) {
+ $firstHome = "";
+ $token = $home = rtrim($pathHome, "/");
+ foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) {
+ if (is_string_empty($firstHome)) $firstHome = $token = $entry;
+ if ($this->normaliseToken($entry)==$home) {
+ $token = $entry;
+ break;
+ }
+ }
+ $pathHome = $this->normaliseToken($token)."/";
+ }
+ return array($pathInstall, $pathRoot, $pathHome);
+ }
+
+ // Return content language
+ public function findContentLanguage($fileName, $languageDefault) {
+ $language = $languageDefault;
+ $pathBase = $this->yellow->system->get("coreContentDirectory");
+ $pathRoot = $this->yellow->system->get("coreServerRootDirectory");
+ if (!is_string_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 content root locations
+ public function findContentRootLocations() {
+ $rootLocations = array();
+ $pathBase = $this->yellow->system->get("coreContentDirectory");
+ $pathRoot = $this->yellow->system->get("coreServerRootDirectory");
+ if (!is_string_empty($pathRoot)) {
+ foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) {
+ $token = $this->normaliseToken($entry)."/";
+ if ($token==$pathRoot) $token = "";
+ $rootLocations["root/$token"] = "$pathBase$entry/";
+ }
+ } else {
+ $rootLocations["root/"] = "$pathBase";
+ }
+ if ($this->yellow->system->get("coreDebugMode")>=3) {
+ foreach ($rootLocations as $key=>$key) {
+ echo "YellowLookup::findContentRootLocations $key -> $value<br />\n";
+ }
+ }
+ return $rootLocations;
+ }
+
+ // Return content location from file path
+ public function findContentLocationFromFile($fileName) {
+ $invalid = false;
+ $location = "/";
+ $pathBase = $this->yellow->system->get("coreContentDirectory");
+ $pathRoot = $this->yellow->system->get("coreServerRootDirectory");
+ $pathHome = $this->yellow->system->get("coreServerHomeDirectory");
+ $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 (!is_string_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);
+ if ($token!=$fileDefault) {
+ $location .= $this->normaliseToken($tokens[$i], $fileExtension, true);
+ }
+ $extension = ($pos = strrposu($fileName, ".")) ? substru($fileName, $pos) : "";
+ if ($extension!=$fileExtension) $invalid = true;
+ } else {
+ $invalid = true;
+ }
+ if ($this->yellow->system->get("coreDebugMode")>=2) {
+ $debug = ($invalid ? "INVALID" : $location)." <- $pathBase$fileName";
+ echo "YellowLookup::findContentLocationFromFile $debug<br />\n";
+ }
+ return $invalid ? "" : $location;
+ }
+
+ // Return file path from content location
+ public function findFileFromContentLocation($location, $directory = false) {
+ $found = $invalid = false;
+ $path = $this->yellow->system->get("coreContentDirectory");
+ $pathRoot = $this->yellow->system->get("coreServerRootDirectory");
+ $pathHome = $this->yellow->system->get("coreServerHomeDirectory");
+ $fileDefault = $this->yellow->system->get("coreContentDefaultFile");
+ $fileExtension = $this->yellow->system->get("coreContentExtension");
+ $tokens = explode("/", $location);
+ if ($this->isRootLocation($location)) {
+ if (!is_string_empty($pathRoot)) {
+ $token = (count($tokens)>2) ? $tokens[1] : rtrim($pathRoot, "/");
+ $path .= $this->findFileDirectory($path, $token, "", true, true, $found, $invalid);
+ }
+ } else {
+ if (!is_string_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 (!is_string_empty($tokens[$i])) {
+ $token = $tokens[$i].$fileExtension;
+ if ($token==$fileDefault) $invalid = true;
+ $path .= $this->findFileDirectory($path, $token, $fileExtension, false, true, $found, $invalid);
+ } else {
+ $path .= $this->findFileDefault($path, $fileDefault, $fileExtension, false);
+ }
+ if ($this->yellow->system->get("coreDebugMode")>=2) {
+ $debug = "$location -> ".($invalid ? "INVALID" : $path);
+ echo "YellowLookup::findFileFromContentLocation $debug<br />\n";
+ }
+ }
+ }
+ return $invalid ? "" : $path;
+ }
+
+ // Return children from content location
+ public function findChildrenFromContentLocation($location) {
+ $fileNames = array();
+ if (!$this->isFileLocation($location)) {
+ $path = $this->findFileFromContentLocation($location, true);
+ $fileDefault = $this->yellow->system->get("coreContentDefaultFile");
+ $fileExtension = $this->yellow->system->get("coreContentExtension");
+ 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)) {
+ $regex = "/^.*\\".$fileExtension."$/";
+ foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) {
+ if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) continue;
+ array_push($fileNames, $path.$entry);
+ }
+ }
+ }
+ return $fileNames;
+ }
+
+ // Return media location from file path
+ public function findMediaLocationFromFile($fileName) {
+ $location = "";
+ $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory"));
+ if (substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory")) {
+ $location = "/".$fileName;
+ }
+ return $location;
+ }
+
+ // Return file path from media location
+ public function findFileFromMediaLocation($location) {
+ $fileName = "";
+ $mediaLocationLength = strlenu($this->yellow->system->get("coreMediaLocation"));
+ if (substru($location, 0, $mediaLocationLength)==$this->yellow->system->get("coreMediaLocation")) {
+ $fileName = substru($location, 1);
+ }
+ return $fileName;
+ }
+
+ // Return children from media location
+ public function findChildrenFromMediaLocation($location) {
+ $fileNames = array();
+ if (!$this->isFileLocation($location)) {
+ $path = $this->findFileFromMediaLocation($location);
+ 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);
+ }
+ }
+ return $fileNames;
+ }
+
+ // Return media directory from a system setting
+ public function findMediaDirectory($key) {
+ return substru($key, -8, 8)=="Location" ? $this->findFileFromMediaLocation($this->yellow->system->get($key)) : "";
+ }
+
+ // Return system location from file path, for virtually mapped system files
+ public function findSystemLocationFromFile($fileName) {
+ $location = "";
+ $layoutDirectoryLength = strlenu($this->yellow->system->get("coreLayoutDirectory"));
+ $themeDirectoryLength = strlenu($this->yellow->system->get("coreThemeDirectory"));
+ $workerDirectoryLength = strlenu($this->yellow->system->get("coreWorkerDirectory"));
+ if (substru($fileName, 0, $layoutDirectoryLength)==$this->yellow->system->get("coreLayoutDirectory")) {
+ if ($this->isSafeFile($fileName)) {
+ $location = $this->yellow->system->get("coreAssetLocation").substru($fileName, $layoutDirectoryLength);
+ }
+ } elseif (substru($fileName, 0, $themeDirectoryLength)==$this->yellow->system->get("coreThemeDirectory")) {
+ if ($this->isSafeFile($fileName)) {
+ $location = $this->yellow->system->get("coreAssetLocation").substru($fileName, $themeDirectoryLength);
+ }
+ } elseif (substru($fileName, 0, $workerDirectoryLength)==$this->yellow->system->get("coreWorkerDirectory")) {
+ if ($this->isSafeFile($fileName)) {
+ $location = $this->yellow->system->get("coreAssetLocation").substru($fileName, $workerDirectoryLength);
+ }
+ }
+ return $location;
+ }
+
+ // Return file path from system location, for virtually mapped system files
+ public function findFileFromSystemLocation($location) {
+ $fileName = "";
+ $assetLocationLength = strlenu($this->yellow->system->get("coreAssetLocation"));
+ if (substru($location, 0, $assetLocationLength)==$this->yellow->system->get("coreAssetLocation")) {
+ if ($this->isSafeFile($location)) {
+ $fileNameLayout = $this->yellow->system->get("coreLayoutDirectory").substru($location, $assetLocationLength);
+ $fileNameTheme = $this->yellow->system->get("coreThemeDirectory").substru($location, $assetLocationLength);
+ $fileNameWorker = $this->yellow->system->get("coreWorkerDirectory").substru($location, $assetLocationLength);
+ if (is_file($fileNameLayout)) {
+ $fileName = $fileNameLayout;
+ } elseif (is_file($fileNameTheme)) {
+ $fileName = $fileNameTheme;
+ } elseif (is_file($fileNameWorker)) {
+ $fileName = $fileNameWorker;
+ }
+ }
+ }
+ return $fileName;
+ }
+
+ // 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)) {
+ $regex = "/^[\d\-\_\.]*($fileDefault)$/";
+ foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) {
+ if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) {
+ $token = $entry;
+ break;
+ }
+ }
+ }
+ return $includePath ? "$path/$token" : $token;
+ }
+
+ // Normalise file/directory token
+ public function normaliseToken($text, $fileExtension = "", $removeExtension = false) {
+ if (!is_string_empty($fileExtension)) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text;
+ if (preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !is_string_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) && !is_string_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 (!is_string_empty($prefix) && !preg_match("/[\-\_\.]$/", $prefix)) $prefix .= "-";
+ return $prefix;
+ }
+
+ // 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", "picture", "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", "allow", "allowfullscreen", "alt", "autocomplete", "autoplay", "background", "bgcolor", "border", "cellpadding", "cellspacing", "charset", "checked", "cite", "class", "clear", "color", "cols", "colspan", "content", "contenteditable", "controls", "coords", "crossorigin", "datetime", "default", "dir", "disabled", "download", "enctype", "face", "for", "frameborder", "headers", "height", "hidden", "high", "href", "hreflang", "id", "integrity", "ismap", "label", "lang", "list", "loading", "loop", "low", "max", "maxlength", "media", "method", "min", "multiple", "muted", "name", "noshade", "novalidate", "nowrap", "open", "optimum", "pattern", "placeholder", "poster", "prefix", "preload", "property", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "sandbox", "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");
+ $attributesAllowEmptyString = array("alt", "download", "open", "sandbox", "value");
+ $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, $attributesAllowEmptyString);
+ 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) && !$this->isSafeUrl($href)) {
+ $elementAttributes["href"] = "error-xss-filter";
+ }
+ $href = isset($elementAttributes["xlink:href"]) ? $elementAttributes["xlink:href"] : "";
+ if (preg_match("/^\w+:/", $href) && !$this->isSafeUrl($href)) {
+ $elementAttributes["xlink:href"] = "error-xss-filter";
+ }
+ }
+ $output .= "<$elementStart$elementName";
+ foreach ($elementAttributes as $key=>$value) $output .= " $key=\"$value\"";
+ if (!is_string_empty($elementEnd)) $output .= " ";
+ $output .= "$elementEnd>";
+ }
+ if (!$elementFound) break;
+ $offsetBytes = $matches[0][1] + strlenb($matches[0][0]);
+ }
+ return $output;
+ }
+
+ // Normalise name and email for a single address
+ public function normaliseAddress($input, $type = "mail", $filterStrict = true) {
+ $output = "";
+ if ($type=="mail") {
+ if (preg_match("/^(.*?)(\s*)<(.*?)>$/", $input, $matches)) {
+ $name = $matches[1];
+ $email = $matches[3];
+ } else {
+ $name = "";
+ $email = $input;
+ }
+ $name = preg_replace("/[^\pL\d\-\. ]/u", "", $name);
+ $name = preg_replace("/\s+/s", " ", $name);
+ if ($filterStrict && !preg_match("/^[\w\+\-\.\@]+$/", $email)) {
+ $email = "error-mail-address";
+ }
+ $output = is_string_empty($name) ? "<$email>" : "$name <$email>";
+ }
+ return $output;
+ }
+
+ // Normalise fields in MIME headers
+ public function normaliseHeaders($input, $type = "mime", $filterStrict = true) {
+ $output = "";
+ if ($type=="mime") {
+ $keysMixedEncoding = array("To", "From", "Reply-To", "Cc", "Bcc");
+ foreach ($input as $key=>$value) {
+ $key = ucwords(preg_replace("/[^a-zA-Z\-]/u", "-", $key), "-");
+ if (in_array($key, $keysMixedEncoding)) {
+ $text = "$key: ";
+ foreach (preg_split("/\s*,\s*/", $value) as $email) {
+ if (!preg_match("/^(.*?)(\s*)<(.*?)>$/", $email, $matches)) {
+ $matches[1] = $matches[2] = "";
+ $matches[3] = $email;
+ }
+ if (!is_string_empty($matches[1]) && !preg_match("/^[\pL\d\-\. ]+$/u", $matches[1])) {
+ $matches[1] = $matches[2] = "";
+ $matches[3] = "error-mail-address";
+ }
+ if ($filterStrict && !preg_match("/^[\w\+\-\.\@]+$/", $matches[3])) {
+ $matches[3] = "error-mail-address";
+ }
+ if (substru($text, -2, 2)!=": ") $text .= ",\r\n ";
+ $text = $this->getMimeHeader($text, $matches[1]);
+ $text = $this->getMimeHeader($text, "$matches[2]<$matches[3]>", false);
+ }
+ $text .= "\r\n";
+ } else {
+ $text = $this->getMimeHeader("$key: ", $value)."\r\n";
+ }
+ $output .= $text;
+ }
+ }
+ return $output;
+ }
+
+ // Normalise CSS class
+ public function normaliseClass($text) {
+ return str_replace(array(" ", "_"), array("-", "-"), strtoloweru($text));
+ }
+
+ // Normalise relative path tokens
+ public function normalisePath($text) {
+ $textFiltered = "";
+ $textLength = strlenb($text);
+ for ($pos=0; $pos<$textLength; ++$pos) {
+ if ($text[$pos]=="." && ($pos==0 || $text[$pos-1]=="/")) {
+ while ($text[$pos]==".") ++$pos;
+ if ($text[$pos]=="/") ++$pos;
+ --$pos;
+ continue;
+ }
+ $textFiltered .= $text[$pos];
+ }
+ return $textFiltered;
+ }
+
+ // 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;
+ }
+
+ // 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;
+ }
+ } else {
+ $location = $pageBase.$pageLocation.$location;
+ }
+ $location = str_replace("/./", "/", $location);
+ $location = str_replace(":", $this->yellow->toolbox->getLocationArgumentsSeparator(), $location);
+ } else {
+ if ($filterStrict && !$this->isSafeUrl($location)) $location = "error-xss-filter";
+ }
+ return $location;
+ }
+
+ // Normalise location arguments
+ public function normaliseArguments($text, $filterStrict = true) {
+ if ($filterStrict) $text = str_replace(" ", "-", strtoloweru($text));
+ $separator = $this->yellow->toolbox->getLocationArgumentsSeparator();
+ $text = str_replace(":", $separator, $text);
+ if (preg_match("/^(.*\/)?page$separator.*$/", $text)) {
+ $text = rtrim($text, "/");
+ } else {
+ $text = rtrim($text, "/")."/";
+ }
+ return str_replace(array("%2F","%3A","%3D"), array("/",":","="), rawurlencode($text));
+ }
+
+ // 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 && !$this->isSafeUrl($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 request information
+ public function getRequestInformation($scheme = "", $address = "", $base = "") {
+ if (is_string_empty($scheme) && is_string_empty($address) && is_string_empty($base)) {
+ $url = $this->yellow->system->get("coreServerUrl");
+ if ($url=="auto" || $this->isCommandLine()) $url = $this->yellow->toolbox->detectServerUrl();
+ list($scheme, $address, $base) = $this->getUrlInformation($url);
+ $this->yellow->system->set("coreServerScheme", $scheme);
+ $this->yellow->system->set("coreServerAddress", $address);
+ $this->yellow->system->set("coreServerBase", $base);
+ if ($this->yellow->system->get("coreDebugMode")>=3) {
+ echo "YellowLookup::getRequestInformation $scheme://$address$base<br />\n";
+ }
+ }
+ $location = substru($this->yellow->toolbox->detectServerLocation(), strlenu($base));
+ $fileName = "";
+ if (is_string_empty($fileName)) $fileName = $this->findFileFromSystemLocation($location);
+ if (is_string_empty($fileName)) $fileName = $this->findFileFromMediaLocation($location);
+ if (is_string_empty($fileName)) $fileName = $this->findFileFromContentLocation($location);
+ return array($scheme, $address, $base, $location, $fileName);
+ }
+
+ // Return command information
+ public function getCommandInformation($line = "") {
+ if (is_string_empty($line)) {
+ $line = $this->yellow->toolbox->getTextString(array_slice($this->yellow->toolbox->getServer("argv"), 1));
+ if ($this->yellow->system->get("coreDebugMode")>=3) {
+ echo "YellowLookup::getCommandInformation $line<br />\n";
+ }
+ }
+ return $this->yellow->toolbox->getTextList($line, " ", 2);
+ }
+
+ // Return request handler
+ public function getRequestHandler() {
+ return $this->requestHandler;
+ }
+
+ // Return command handler
+ public function getCommandHandler() {
+ return $this->commandHandler;
+ }
+
+ // Return attributes from text
+ public function getTextAttributes($text, $attributesAllowEmptyString) {
+ $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 (!is_string_empty($key) && (!is_string_empty($value) || in_array(strtolower($key), $attributesAllowEmptyString))) {
+ $attributes[$key] = $value;
+ }
+ }
+ return $attributes;
+ }
+
+ // Return HTML attributes from generic Markdown attributes
+ public function getHtmlAttributes($text) {
+ $htmlAttributes = "";
+ $htmlAttributesData = array();
+ foreach (explode(" ", $text) as $token) {
+ if (substru($token, 0, 1)==".") {
+ if (!isset($htmlAttributesData["class"])) {
+ $htmlAttributesData["class"] = substru($token, 1);
+ } else {
+ $htmlAttributesData["class"] .= " ".substru($token, 1);
+ }
+ }
+ if (substru($token, 0, 1)=="#") $htmlAttributesData["id"] = substru($token, 1);
+ if (preg_match("/^([\w]+)=(.+)/", $token, $matches)) $htmlAttributesData[$matches[1]] = $matches[2];
+ }
+ foreach ($htmlAttributesData as $key=>$value) {
+ $htmlAttributes .= " $key=\"".htmlspecialchars($value)."\"";
+ }
+ return $htmlAttributes;
+ }
+
+ // Return MIME header field, encode and fold if necessary
+ public function getMimeHeader($text, $field, $allowEncode = true) {
+ if ($allowEncode) {
+ $encode = preg_match("/[\x7F-\xFF]/", $field);
+ $fieldPos = 0;
+ while (true) {
+ $textPos = strlenb($text)-(($pos = strrposb($text, "\n")) ? $pos+1 : 0);
+ $bytesAvailable = max(0, 78-$textPos);
+ $fragment = substrb($field, $fieldPos);
+ if ($encode && !is_string_empty($fragment)) $fragment = "=?UTF-8?B?".base64_encode($fragment)."?=";
+ if ($bytesAvailable<strlenb($fragment)) {
+ $bytesHandled = $bytesAvailable;
+ if (!$encode) {
+ for ($pos=$bytesHandled;$pos>0;--$pos) {
+ if ($field[$fieldPos+$pos]==" ") {
+ $fragment = substrb($field, $fieldPos, $pos);
+ $bytesHandled = $pos+1;
+ break;
+ }
+ }
+ if ($pos==0) $encode = true;
+ }
+ if ($encode) {
+ while (true) {
+ $fragment = substrb($field, $fieldPos, $bytesHandled);
+ if (!is_string_empty($fragment)) $fragment = "=?UTF-8?B?".base64_encode($fragment)."?=";
+ if ($bytesAvailable>=strlenb($fragment) || $bytesHandled==0) break;
+ --$bytesHandled;
+ }
+ }
+ $text .= $fragment."\r\n ";
+ $fieldPos += $bytesHandled;
+ } else {
+ $text .= $fragment;
+ break;
+ }
+ }
+ } else {
+ $textPos = strlenb($text)-(($pos = strrposb($text, "\n")) ? $pos+1 : 0);
+ $bytesAvailable = max(0, 78-$textPos);
+ if ($bytesAvailable<strlenb($field)) {
+ $text .= "\r\n ".ltrim($field);
+ } else {
+ $text .= $field;
+ }
+ }
+ return $text;
+ }
+
+ // 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->findFileFromContentLocation("$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 (!is_array_empty($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false))) $nested = true;
+ }
+ return $nested;
+ }
+
+ // Check if location is within shared directory
+ public function isSharedLocation($location) {
+ $sharedLocation = $this->yellow->content->getHomeLocation($location)."shared/";
+ return substru($location, 0, strlenu($sharedLocation))==$sharedLocation;
+ }
+
+ // 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 URL is a well-known URL scheme
+ public function isSafeUrl($url) {
+ return preg_match("/^(http|https|ftp|mailto|tel):/", $url);
+ }
+
+ // Check if file is a well-known file type
+ public function isSafeFile($fileName) {
+ return preg_match("/\.(css|gif|ico|js|jpeg|jpg|json|map|png|scss|svg|webmanifest|woff|woff2)$/", $fileName);
+ }
+
+ // 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 strposu($fileName, "/")===false ||
+ 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");
+ }
+
+ // Check if running at command line
+ public function isCommandLine() {
+ return isset($this->commandHandler);
+ }
+}
+
+class YellowToolbox {
+ public $yellow; // access to API
+
+ public function __construct($yellow) {
+ $this->yellow = $yellow;
+ }
+
+ // 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 (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
+ if (!is_string_empty($locationArguments)) $locationArguments .= "/";
+ $locationArguments .= "$matches[1]:$matches[2]";
+ }
+ }
+ }
+ if (!$found && !is_string_empty($key) && !is_string_empty($value)) {
+ if (!is_string_empty($locationArguments)) $locationArguments .= "/";
+ $locationArguments .= "$key:$value";
+ }
+ if (!is_string_empty($locationArguments)) {
+ $locationArguments = $this->yellow->lookup->normaliseArguments($locationArguments, false);
+ }
+ 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 (!is_string_empty($key) && !is_string_empty($value)) {
+ if (!is_string_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 (!is_string_empty($locationArguments)) {
+ $locationArguments = $this->yellow->lookup->normaliseArguments($locationArguments, false);
+ }
+ 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 420: $text = "Not public"; break;
+ case 430: $text = "Login failed"; break;
+ case 434: $text = "Can create"; break;
+ case 435: $text = "Can restore"; break;
+ case 450: $text = "Update error"; 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",
+ "jpeg" => "image/jpeg",
+ "jpg" => "image/jpeg",
+ "md" => "text/markdown",
+ "png" => "image/png",
+ "scss" => "text/x-scss",
+ "svg" => "image/svg+xml",
+ "txt" => "text/plain",
+ "webmanifest" => "application/manifest+json",
+ "woff" => "application/font-woff",
+ "woff2" => "application/font-woff2",
+ "xml" => "text/xml; charset=utf-8");
+ $fileType = $this->getFileType($fileName);
+ if (is_string_empty($fileType)) {
+ $contentType = $contentTypes["html"];
+ } elseif (array_key_exists($fileType, $contentTypes)) {
+ $contentType = $contentTypes[$fileType];
+ }
+ return $contentType;
+ }
+
+ // Send HTTP header
+ public function sendHttpHeader($text) {
+ if (!headers_sent()) header($text);
+ }
+
+ // Return files and directories
+ public function getDirectoryEntries($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true) {
+ return $this->getDirectoryEntriesRecursive($path, $regex, $sort, $directories, $includePath, 1);
+ }
+
+ // Return files and directories recursively
+ public function getDirectoryEntriesRecursive($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true, $levelMax = 0) {
+ --$levelMax;
+ $entries = array();
+ $directoryHandle = @opendir($path);
+ if ($directoryHandle) {
+ $path = rtrim($path, "/");
+ $directoryEntries = array();
+ while (($entry = readdir($directoryHandle))!==false) {
+ if (substru($entry, 0, 1)==".") continue;
+ $entry = $this->yellow->lookup->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 (is_dir("$path/$entry") && $levelMax!=0) array_push($directoryEntries, "$path/$entry");
+ }
+ if ($sort) {
+ natcasesort($entries);
+ natcasesort($directoryEntries);
+ }
+ closedir($directoryHandle);
+ foreach ($directoryEntries as $directoryEntry) {
+ $entries = array_merge($entries, $this->getDirectoryEntriesRecursive($directoryEntry, $regex, $sort, $directories, $includePath, $levelMax));
+ }
+ }
+ return $entries;
+ }
+
+ // Return directory information, modification date and file count
+ public function getDirectoryInformation($path) {
+ return $this->getDirectoryInformationRecursive($path, 1);
+ }
+
+ // Return directory information recursively, modification date and file count
+ public function getDirectoryInformationRecursive($path, $levelMax = 0) {
+ --$levelMax;
+ $modified = $fileCount = 0;
+ $directoryHandle = @opendir($path);
+ if ($directoryHandle) {
+ $path = rtrim($path, "/");
+ $directoryEntries = array();
+ while (($entry = readdir($directoryHandle))!==false) {
+ if (substru($entry, 0, 1)==".") continue;
+ $modified = max($modified, $this->getFileModified("$path/$entry"));
+ if (is_file("$path/$entry")) ++$fileCount;
+ if (is_dir("$path/$entry") && $levelMax!=0) array_push($directoryEntries, "$path/$entry");
+ }
+ closedir($directoryHandle);
+ foreach ($directoryEntries as $directoryEntry) {
+ list($modifiedBelow, $fileCountBelow) = $this->getDirectoryInformationRecursive($directoryEntry, $levelMax);
+ $modified = max($modified, $modifiedBelow);
+ $fileCount += $fileCountBelow;
+ }
+ }
+ return array($modified, $fileCount);
+ }
+
+ // Read file, empty string if not found
+ public function readFile($fileName, $sizeMax = 0) {
+ $fileData = "";
+ $fileHandle = @fopen($fileName, "rb");
+ if ($fileHandle) {
+ clearstatcache(true, $fileName);
+ if (flock($fileHandle, LOCK_SH)) {
+ $fileSize = $sizeMax ? $sizeMax : filesize($fileName);
+ if ($fileSize) $fileData = fread($fileHandle, $fileSize);
+ flock($fileHandle, LOCK_UN);
+ }
+ fclose($fileHandle);
+ }
+ return $fileData;
+ }
+
+ // Write file
+ public function writeFile($fileName, $fileData, $mkdir = false) {
+ $ok = false;
+ if ($mkdir) {
+ $path = dirname($fileName);
+ if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true);
+ }
+ $fileHandle = @fopen($fileName, "cb");
+ if ($fileHandle) {
+ clearstatcache(true, $fileName);
+ if (flock($fileHandle, LOCK_EX)) {
+ ftruncate($fileHandle, 0);
+ fwrite($fileHandle, $fileData);
+ fflush($fileHandle);
+ 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 (!is_string_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);
+ fflush($fileHandle);
+ 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 (!is_string_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 (!is_string_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 (is_string_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"));
+ $fileNameDestination .= ".".pathinfo($fileName, PATHINFO_EXTENSION);
+ $ok = @rename($fileName, $fileNameDestination);
+ }
+ return $ok;
+ }
+
+ // Delete directory
+ public function deleteDirectory($path, $pathTrash = "") {
+ clearstatcache();
+ if (is_string_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"));
+ $ok = @rename($path, $pathDestination);
+ }
+ return $ok;
+ }
+
+ // Set file/directory modification date, Unix time
+ public function modifyFile($fileName, $modified) {
+ clearstatcache(true, $fileName);
+ return @touch($fileName, $modified);
+ }
+
+ // Return file/directory modification date, Unix time
+ public function getFileModified($fileName) {
+ return (is_file($fileName) || is_dir($fileName)) ? filemtime($fileName) : 0;
+ }
+
+ // Return file/directory deletion date, Unix time
+ public function getFileDeleted($fileName) {
+ $deleted = 0;
+ $text = basename($fileName);
+ $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text;
+ if (preg_match("#^(.+)-(\d\d\d\d-\d\d-\d\d)-(\d\d)-(\d\d)-(\d\d)$#", $text, $matches)) {
+ $deleted = strtotime("$matches[2] $matches[3]:$matches[4]:$matches[5]");
+ }
+ return $deleted;
+ }
+
+ // Return file size
+ public function getFileSize($fileName) {
+ return is_file($fileName) ? filesize($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 number of bytes
+ public function getNumberBytes($text) {
+ $bytes = intval($text);
+ switch (strtoupperu(substru($text, -1))) {
+ case "G": $bytes *= 1024*1024*1024; break;
+ case "M": $bytes *= 1024*1024; break;
+ case "K": $bytes *= 1024; break;
+ }
+ return $bytes;
+ }
+
+ // Return lines from text, including newline
+ public function getTextLines($text) {
+ $lines = preg_split("/\n/", $text);
+ foreach ($lines as &$line) {
+ $line = $line."\n";
+ }
+ if (is_string_empty($text) || substru($text, -1, 1)=="\n") array_pop($lines);
+ return $lines;
+ }
+
+ // Return settings from text
+ function getTextSettings($text, $blockStart) {
+ $settings = new YellowArray();
+ if (is_string_empty($blockStart)) {
+ foreach ($this->getTextLines($text) as $line) {
+ if (preg_match("/^\#/", $line)) continue;
+ if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
+ if (!is_string_empty($matches[1]) && !is_string_empty($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 && !is_string_empty($matches[2])) {
+ $blockKey = $matches[2];
+ $settings[$blockKey] = new YellowArray();
+ }
+ if (!is_string_empty($blockKey) && !is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
+ $settings[$blockKey][$matches[1]] = $matches[2];
+ }
+ }
+ }
+ }
+ return $settings;
+ }
+
+ // Set settings in text
+ function setTextSettings($text, $blockStart, $blockKey, $settings) {
+ $textNew = "";
+ if (is_string_empty($blockStart)) {
+ foreach ($this->getTextLines($text) as $line) {
+ if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
+ if (!is_string_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 && !is_string_empty($matches[2])) {
+ $scan = lcfirst($matches[2])==lcfirst($blockKey);
+ }
+ }
+ if (!$scan && is_string_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 (!is_string_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 (!is_string_empty($textMiddle)) {
+ $textMiddle = $textSettings;
+ if (!is_string_empty($textEnd)) $textMiddle .= "\n";
+ } else {
+ if (!is_string_empty($textStart)) $textEnd .= "\n";
+ $textEnd .= $textSettings;
+ }
+ $textNew = $textStart.$textMiddle.$textEnd;
+ }
+ return $textNew;
+ }
+
+ // Remove settings from text
+ function unsetTextSettings($text, $blockStart, $blockKey) {
+ $textNew = "";
+ if (!is_string_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 && !is_string_empty($matches[2])) {
+ $scan = lcfirst($matches[2])==lcfirst($blockKey);
+ }
+ }
+ if (!$scan && is_string_empty($textMiddle)) {
+ $textStart .= $line;
+ } elseif ($scan) {
+ $textMiddle .= $line;
+ } else {
+ $textEnd .= $line;
+ }
+ }
+ $textNew = rtrim($textStart.$textEnd)."\n";
+ }
+ return $textNew;
+ }
+
+ // Return array of specific size from text
+ public function getTextList($text, $separator, $size) {
+ $tokens = explode($separator, $text, $size);
+ return array_pad($tokens, $size, "");
+ }
+
+ // Return array of variable size 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 (is_null($value) || $value==$optional) $tokens[$key] = "";
+ }
+ return array_pad($tokens, $sizeMin, "");
+ }
+
+ // Return text from array, space separated
+ public function getTextString($tokens, $optional = "-") {
+ $text = "";
+ foreach ($tokens as $token) {
+ if (preg_match("/\s/", $token)) $token = "\"$token\"";
+ if (is_string_empty($token)) $token = $optional;
+ if (!is_string_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) {
+ $hiddenLevel = 0;
+ $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] : "";
+ $elementAttributes = isset($matches[3][0]) ? $matches[3][0] : "";
+ $elementEnd = isset($matches[4][0]) ? $matches[4][0] : "";
+ if (!is_string_empty($elementBefore) && !$hiddenLevel) {
+ $rawText = preg_replace("/\s+/s", " ", html_entity_decode($elementBefore, ENT_QUOTES, "UTF-8"));
+ if (is_string_empty($elementStart) && in_array(strtolower($elementName), $elementsBlock)) $rawText = rtrim($rawText)." ";
+ if (substru($rawText, 0, 1)==" " && (is_string_empty($output) || substru($output, -1)==" ")) $rawText = ltrim($rawText);
+ $output .= $this->getTextTruncated($rawText, $lengthMax);
+ $lengthMax -= strlenu($rawText);
+ }
+ if (!is_string_empty($elementRawData) && $elementRawData==$endMarker) {
+ $output .= $endMarkerText;
+ $lengthMax = 0;
+ }
+ if ($lengthMax<=0 || !$elementFound) break;
+ if ($hiddenLevel>0 ||
+ preg_match("/aria-hidden=\"true\"/i", $elementAttributes) ||
+ preg_match("/role=\"doc-noteref\"/i", $elementAttributes)) {
+ if (!is_string_empty($elementName) && is_string_empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) {
+ if (is_string_empty($elementStart)) {
+ ++$hiddenLevel;
+ } else {
+ --$hiddenLevel;
+ }
+ }
+ }
+ $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 (!is_string_empty($elementBefore)) {
+ $output .= $this->getTextTruncated($elementBefore, $lengthMax);
+ $lengthMax -= strlenu($elementBefore);
+ }
+ if (!is_string_empty($elementRawData) && $elementRawData==$endMarker) {
+ $output .= $endMarkerText;
+ $lengthMax = 0;
+ }
+ if ($lengthMax<=0 || !$elementFound) break;
+ if (!is_string_empty($elementName) && is_string_empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) {
+ if (is_string_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 (is_string_empty($dataBuffer) && function_exists("random_bytes")) {
+ $dataBuffer = @random_bytes($dataBufferSize);
+ }
+ if (is_string_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 (is_string_empty($salt) || strlenb($hash)!=60) $hash = "";
+ break;
+ case "sha256": $prefix = "$5y$";
+ $salt = $this->createSalt(32);
+ $hash = "$prefix$salt".hash("sha256", $salt.$text);
+ if (is_string_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 && !is_string_empty($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;
+ }
+
+ // Remove meta data in raw data
+ public function unsetMetaData($rawData, $key) {
+ if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) {
+ $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) continue;
+ }
+ $rawDataMiddle .= $line;
+ }
+ $rawDataNew = $parts[1]."---\n".$rawDataMiddle."---\n".$parts[3];
+ } else {
+ $rawDataNew = $rawData;
+ }
+ return $rawDataNew;
+ }
+
+ // Return troubleshooting URL
+ public function getTroubleshootingUrl() {
+ return "https://datenstrom.se/yellow/help/troubleshooting";
+ }
+
+ // 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->yellow->lookup->normalisePath($location);
+ if (substru($location, 0, 1)!="/") $location = "/".$location;
+ $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 (!is_string_empty($matches[1]) && !is_string_empty($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 sitename
+ public function detectServerSitename() {
+ $sitename = "Localhost";
+ if (preg_match("#^(www\.)?([\w\-]+)#", $this->getServer("SERVER_NAME"), $matches)) {
+ $sitename = ucfirst($matches[2]);
+ }
+ return $sitename;
+ }
+
+ // Detect server timezone
+ public function detectServerTimezone() {
+ $timezone = ini_get("date.timezone");
+ if (is_string_empty($timezone)) {
+ if (PHP_OS=="Darwin") {
+ if (preg_match("#zoneinfo/(.*)#", @readlink("/etc/localtime"), $matches)) $timezone = $matches[1];
+ } else {
+ if (preg_match("/^(\S+)\/(\S+)/", $this->readFile("/etc/timezone"), $matches)) $timezone = $matches[1];
+ }
+ }
+ if (!in_array($timezone, timezone_identifiers_list())) $timezone = "UTC";
+ return $timezone;
+ }
+
+ // Detect server name, version and operating system
+ public function detectServerInformation() {
+ $name = "Unknown";
+ $version = "x.x.x";
+ $os = PHP_OS;
+ if (preg_match("/^(\S+)\/(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) {
+ $name = $matches[1];
+ $version = $matches[2];
+ } elseif (preg_match("/^(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) {
+ $name = $matches[1];
+ }
+ if (PHP_SAPI=="cli" || PHP_SAPI=="cli-server") {
+ $name = "Built-in";
+ $version = PHP_VERSION;
+ }
+ if (PHP_OS=="Darwin") {
+ $os = "Mac";
+ } elseif (strtoupperu(substru(PHP_OS, 0, 3))=="WIN") {
+ $os = "Windows";
+ }
+ return array($name, $version, $os);
+ }
+
+ // Detect browser language
+ public function detectBrowserLanguage($languages, $languageDefault) {
+ $languageFound = $languageDefault;
+ foreach (preg_split("/\s*,\s*/", $this->getServer("HTTP_ACCEPT_LANGUAGE")) as $text) {
+ list($language, $dummy) = $this->getTextList($text, ";", 2);
+ if (!is_string_empty($language) && in_array($language, $languages)) {
+ $languageFound = $language;
+ break;
+ }
+ }
+ return $languageFound;
+ }
+
+ // Detect terminal width and height
+ public function detectTerminalInformation() {
+ $width = $height = 0;
+ if (strtoupperu(substru(PHP_OS, 0, 3))=="WIN") {
+ exec("powershell \$Host.UI.RawUI.WindowSize.Width", $outputLines, $returnStatus);
+ if ($returnStatus==0 && !is_array_empty($outputLines)) {
+ $width = intval(end($outputLines));
+ }
+ exec("powershell \$Host.UI.RawUI.WindowSize.Height", $outputLines, $returnStatus);
+ if ($returnStatus==0 && !is_array_empty($outputLines)) {
+ $height = intval(end($outputLines));
+ }
+ } else {
+ exec("stty size", $outputLines, $returnStatus);
+ if ($returnStatus==0 && preg_match("/^(\d+)\s+(\d+)/", implode("\n", $outputLines), $matches)) {
+ $width = intval($matches[2]);
+ $height = intval($matches[1]);
+ }
+ }
+ return array($width, $height);
+ }
+
+ // Detect image width, height, orientation and type for GIF/JPEG/PNG/SVG
+ public function detectImageInformation($fileName, $fileType = "") {
+ $width = $height = $orientation = 0;
+ $type = "";
+ $fileHandle = @fopen($fileName, "rb");
+ if ($fileHandle) {
+ if (is_string_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=="jpeg" || $fileType=="jpg") {
+ $dataBufferSizeMax = filesize($fileName);
+ $dataBufferSize = min($dataBufferSizeMax, 4096);
+ if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize);
+ $dataSignature = substrb($dataBuffer, 0, 2);
+ if (!feof($fileHandle) && $dataSignature=="\xff\xd8") {
+ for ($pos=2; $pos+8<$dataBufferSize; $pos+=$length) {
+ if ($dataBuffer[$pos]!="\xff") break;
+ $dataMarker = $dataBuffer[$pos+1];
+ if ($dataMarker=="\xe1") {
+ $orientation = $this->getImageOrientationFromBuffer($dataBuffer, $pos+4, $dataBufferSize);
+ }
+ if (($dataMarker>="\xc0" && $dataMarker<="\xc3") ||
+ ($dataMarker>="\xc5" && $dataMarker<="\xc7") ||
+ ($dataMarker>="\xc9" && $dataMarker<="\xcb") ||
+ ($dataMarker>="\xcd" && $dataMarker<="\xcf")) {
+ $width = (ord($dataBuffer[$pos+7])<<8) + ord($dataBuffer[$pos+8]);
+ $height = (ord($dataBuffer[$pos+5])<<8) + ord($dataBuffer[$pos+6]);
+ $type = "jpeg";
+ 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, $orientation, $type);
+ }
+
+ // Return image orientation from Exif
+ public function getImageOrientationFromBuffer($dataBuffer, $pos, $size) {
+ $orientation = 0;
+ $dataSignature = substrb($dataBuffer, $pos, 6);
+ if ($dataSignature=="\x45\x78\x69\x66\x00\x00" && $pos+14<=$size) {
+ $startPos = $pos+6;
+ $bigEndian = $dataBuffer[$startPos]=="M";
+ $ifdOffset = $this->getLongFromBuffer($dataBuffer, $startPos+4, $bigEndian);
+ $ifdStartPos = $startPos+$ifdOffset;
+ $ifdCount = $ifdStartPos+2<=$size ? $this->getShortFromBuffer($dataBuffer, $ifdStartPos, $bigEndian) : 0;
+ $pos = $ifdStartPos+2;
+ while ($ifdCount && $pos+12<=$size) {
+ $ifdTag = $this->getShortFromBuffer($dataBuffer, $pos, $bigEndian);
+ $ifdFormat = $this->getShortFromBuffer($dataBuffer, $pos+2, $bigEndian);
+ if ($ifdTag==0x8769 && $ifdFormat==4) {
+ $ifdOffset = $this->getLongFromBuffer($dataBuffer, $pos+8, $bigEndian);
+ $ifdStartPos = $startPos+$ifdOffset;
+ $ifdCount = $ifdStartPos+2<=$size ? $this->getShortFromBuffer($dataBuffer, $ifdStartPos, $bigEndian) : 0;
+ $pos = $ifdStartPos+2;
+ continue;
+ }
+ if ($ifdTag==0x0112 && $ifdFormat==3) {
+ $orientation = $this->getShortFromBuffer($dataBuffer, $pos+8, $bigEndian);
+ break;
+ }
+ --$ifdCount;
+ $pos += 12;
+ }
+ }
+ return $orientation;
+ }
+
+ // Return unsigned short value from buffer
+ public function getShortFromBuffer($dataBuffer, $pos, $bigEndian) {
+ if ($bigEndian) {
+ $value = (ord($dataBuffer[$pos])<<8) + ord($dataBuffer[$pos+1]);
+ } else {
+ $value = (ord($dataBuffer[$pos+1])<<8) + ord($dataBuffer[$pos]);
+ }
+ return $value;
+ }
+
+ // Return unsigned long value from buffer
+ public function getLongFromBuffer($dataBuffer, $pos, $bigEndian) {
+ if ($bigEndian) {
+ $value = (ord($dataBuffer[$pos])<<24) + (ord($dataBuffer[$pos+1])<<16) +
+ (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]);
+ } else {
+ $value = (ord($dataBuffer[$pos+3])<<24) + (ord($dataBuffer[$pos+2])<<16) +
+ (ord($dataBuffer[$pos+1])<<8) + ord($dataBuffer[$pos]);
+ }
+ return $value;
+ }
+
+ // Return possible values
+ public function enumerate($action, $context = "") {
+ $values = array();
+ foreach ($this->yellow->extension->data as $key=>$value) {
+ if (method_exists($value["object"], "onEnumerate")) {
+ $output = $value["object"]->onEnumerate($action, $context);
+ if (!is_null($output)) {
+ $values = array_merge($values, is_array($output) ? $output : array($output));
+ }
+ }
+ }
+ if ($action=="email") {
+ foreach ($this->yellow->user->settings as $userKey=>$userValue) {
+ array_push($values, $userKey);
+ }
+ } elseif ($action=="language") {
+ foreach ($this->yellow->language->settings as $languageKey=>$languageValue) {
+ array_push($values, $languageKey);
+ }
+ } elseif ($action=="theme") {
+ $path = $this->yellow->system->get("coreThemeDirectory");
+ foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.css$/", true, false, false) as $entry) {
+ array_push($values, substru($entry, 0, -4));
+ }
+ }
+ usort($values, "strnatcasecmp");
+ return $values;
+ }
+
+ // Send email message
+ public function mail($action, $headers, $message) {
+ $statusCode = 0;
+ foreach ($this->yellow->extension->data as $key=>$value) {
+ if (method_exists($value["object"], "onMail")) {
+ $statusCode = $value["object"]->onMail($action, $headers, $message);
+ if ($statusCode!=0) break;
+ }
+ }
+ if ($statusCode==0) {
+ $text = $this->yellow->lookup->normaliseHeaders($headers, "mime");
+ $to = $subject = $remaining = $key = "";
+ foreach (preg_split("/\r\n/", $text) as $line) {
+ if (preg_match("/^(.*?):\s*(.*?)$/", $line, $matches) && !is_string_empty($matches[1])) {
+ $key = $matches[1];
+ $fragment = $matches[2];
+ } else {
+ $fragment = $line;
+ }
+ if ($key=="To") { $to .= $fragment; continue; }
+ if ($key=="Subject") { $subject .= $fragment; continue; }
+ $remaining .= $line."\r\n";
+ }
+ $statusCode = mail($to, $subject, $message, $remaining) ? 200 : 500;
+ }
+ return $statusCode==200;
+ }
+
+ // Write information to log file
+ public function log($action, $message) {
+ $statusCode = 0;
+ foreach ($this->yellow->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->appendFile($this->yellow->system->get("coreServerInstallDirectory").
+ $this->yellow->system->get("coreExtensionDirectory").
+ $this->yellow->system->get("coreWebsiteFile"), $line);
+ }
+ }
+
+ // 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 (is_string_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\d+$/", $location);
+ }
+
+ // Check if unmodified since last HTTP request
+ public function isNotModified($lastModifiedFormatted) {
+ return $this->getServer("HTTP_IF_MODIFIED_SINCE")==$lastModifiedFormatted;
+ }
+}
+
+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 $statusCode; // status code
+ public $errorMessage; // error message
+ public $lastModified; // last modification date
+ public $available; // page is available? (boolean)
+ public $visible; // page is visible location? (boolean)
+ public $cacheable; // page is cacheable? (boolean)
+
+ public function __construct($yellow) {
+ $this->yellow = $yellow;
+ $this->scheme = "";
+ $this->address = "";
+ $this->base = "";
+ $this->location = "";
+ $this->fileName = "";
+ $this->metaData = new YellowArray();
+ $this->pageCollections = array();
+ $this->sharedPages = array();
+ $this->headerData = array();
+ }
+
+ // Set request information
+ public function setRequestInformation($scheme, $address, $base, $location, $fileName, $cacheable) {
+ $this->scheme = $scheme;
+ $this->address = $address;
+ $this->base = $base;
+ $this->location = $location;
+ $this->fileName = $fileName;
+ $this->cacheable = $cacheable;
+ }
+
+ // Parse page meta
+ public function parseMeta($rawData, $statusCode = 0, $errorMessage = "") {
+ $this->rawData = $rawData;
+ $this->parser = null;
+ $this->parserData = "";
+ $this->statusCode = $statusCode;
+ $this->errorMessage = $errorMessage;
+ $this->lastModified = 0;
+ $this->available = true;
+ $this->visible = true;
+ $this->parseMetaData();
+ }
+
+ // Parse page meta update
+ public function parseMetaUpdate() {
+ if ($this->statusCode==0) {
+ $this->rawData = $this->yellow->toolbox->readFile($this->fileName);
+ $this->statusCode = 200;
+ $this->parseMetaData();
+ }
+ }
+
+ // Parse page meta data
+ public function parseMetaData() {
+ $this->metaData = new YellowArray();
+ $this->metaDataOffsetBytes = 0;
+ if (!is_null($this->rawData)) {
+ $this->set("title", $this->yellow->toolbox->createTextTitle($this->location));
+ $this->set("language", $this->yellow->lookup->findContentLanguage($this->fileName, $this->yellow->system->get("language")));
+ $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName)));
+ $this->parseMetaDataRaw(array("sitename", "author", "layout", "theme", "parser", "status"));
+ $this->parseMetaDataShared();
+ $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->yellow->lookup->isRootLocation($this->location) || !is_readable($this->fileName)) $this->available = false;
+ if ($this->get("status")=="shared") $this->available = false;
+ if ($this->get("status")=="unlisted") $this->visible = false;
+ } else {
+ $this->set("size", $this->yellow->toolbox->getFileSize($this->fileName));
+ $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 (!$this->yellow->lookup->isFileLocation($this->location)) $this->available = false;
+ }
+ foreach ($this->yellow->extension->data as $key=>$value) {
+ if (method_exists($value["object"], "onParseMetaData")) $value["object"]->onParseMetaData($this);
+ }
+ }
+
+ // Parse page meta data from raw data
+ public function parseMetaDataRaw($defaultKeys) {
+ foreach ($defaultKeys as $key) {
+ $value = $this->yellow->system->get($key);
+ if (!is_string_empty($key) && !is_string_empty($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 (!is_string_empty($matches[1]) && !is_string_empty($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 meta data for shared pages
+ public function parseMetaDataShared() {
+ $this->sharedPages["main"] = $this;
+ if (!$this->yellow->lookup->isSharedLocation($this->location) && $this->statusCode!=0) {
+ foreach ($this->yellow->content->getShared($this->location) as $page) {
+ $this->sharedPages[basename($page->location)] = $page;
+ $page->sharedPages["main"] = $this;
+ }
+ }
+ if ($this->yellow->lookup->isSharedLocation($this->location)) {
+ $this->set("status", "shared");
+ }
+ }
+
+ // Parse page content on demand
+ public function parseContent() {
+ 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->getContentRaw();
+ $this->parserData = $this->parser->onParseContentRaw($this, $this->parserData);
+ 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->getContentRaw();
+ $this->parserData = preg_replace("/\[yellow error\]/i", $this->errorMessage, $this->parserData);
+ }
+ if (!$this->isExisting("description")) {
+ $description = $this->yellow->toolbox->createTextDescription($this->parserData, 150);
+ $this->set("description", !is_string_empty($description) ? $description : $this->get("title"));
+ }
+ if ($this->yellow->system->get("coreDebugMode")>=3) {
+ echo "YellowPage::parseContent location:".$this->location."<br />\n";
+ }
+ }
+ }
+
+ // Parse page content element, experimental
+ public function parseContentElement($name, $text, $attributes, $type) {
+ $output = null;
+ foreach ($this->yellow->extension->data as $key=>$value) {
+ if (method_exists($value["object"], "onParseContentElement")) {
+ $output = $value["object"]->onParseContentElement($this, $name, $text, $attributes, $type);
+ if (!is_null($output)) break;
+ }
+ }
+ if (is_null($output)) {
+ if ($name=="yellow" && $type=="inline" && $text=="error") {
+ $output = $this->errorMessage;
+ }
+ }
+ if ($this->yellow->system->get("coreDebugMode")>=3 && !is_string_empty($name)) {
+ echo "YellowPage::parseContentElement 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));
+ $theme = $this->yellow->lookup->normaliseName($this->get("theme"));
+ if (!is_file($this->yellow->system->get("coreThemeDirectory").$theme.".css") &&
+ !in_array($theme, $this->yellow->toolbox->enumerate("theme"))) {
+ $this->error(500, "Theme '".$this->get("theme")."' does not exist!");
+ }
+ if (!$this->yellow->language->isExisting($this->get("language"))) {
+ $this->error(500, "Language '".$this->get("language")."' does not exist!");
+ }
+ if (!is_object($this->parser)) {
+ $this->error(500, "Parser '".$this->get("parser")."' 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->lookup->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->status(301, $location);
+ }
+ if ($this->yellow->lookup->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) {
+ $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 ($this->yellow->system->get("coreDebugMode")>=2) {
+ echo "YellowPage::includeLayout file:$fileNameLayoutTheme<br />\n";
+ }
+ $this->setLastModified(filemtime($fileNameLayoutTheme));
+ require($fileNameLayoutTheme);
+ } elseif (is_file($fileNameLayoutNormal)) {
+ if ($this->yellow->system->get("coreDebugMode")>=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 (!is_string_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 (!is_string_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 data, raw format
+ public function getContentRaw() {
+ $this->parseMetaUpdate();
+ return substrb($this->rawData, $this->metaDataOffsetBytes);
+ }
+
+ // Return page content data, HTML encoded or raw format
+ public function getContentHtml() {
+ $this->parseContent();
+ return $this->parserData;
+ }
+
+ // Return page extra data, HTML encoded
+ public function getExtraHtml($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") {
+ $assetLocation = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreAssetLocation");
+ $fileNameTheme = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".css";
+ if (is_file($fileNameTheme)) {
+ $fileLocation = $assetLocation.$this->yellow->lookup->normaliseName($this->get("theme")).".css";
+ $output .= "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"$fileLocation\" />\n";
+ }
+ $fileNameScript = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".js";
+ if (is_file($fileNameScript)) {
+ $fileLocation = $assetLocation.$this->yellow->lookup->normaliseName($this->get("theme")).".js";
+ $output .= "<script type=\"text/javascript\" src=\"$fileLocation\"></script>\n";
+ }
+ $fileNameFavicon = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".png";
+ if (is_file($fileNameFavicon)) {
+ $fileLocation = $assetLocation.$this->yellow->lookup->normaliseName($this->get("theme")).".png";
+ $output .= "<link rel=\"icon\" type=\"image/png\" href=\"$fileLocation\" />\n";
+ }
+ }
+ return $output;
+ }
+
+ // 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($canonicalUrl = false) {
+ if ($canonicalUrl) {
+ $scheme = $this->yellow->system->get("coreServerScheme");
+ $address = $this->yellow->system->get("coreServerAddress");
+ $location = $this->yellow->system->get("coreServerBase").$this->location;
+ } else {
+ $scheme = $this->scheme;
+ $address = $this->address;
+ $location = $this->base.$this->location;
+ }
+ return "$scheme://$address$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] : "";
+ }
+
+ // 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 raw data for error page
+ public function getRawDataError() {
+ $statusCode = $this->statusCode;
+ $sharedLocation = $this->yellow->content->getHomeLocation($this->location)."shared/";
+ $fileNameError = $this->yellow->lookup->findFileFromContentLocation($sharedLocation, true).$this->yellow->system->get("coreContentErrorFile");
+ $fileNameError = str_replace("(.*)", $statusCode, $fileNameError);
+ $languageError = $this->yellow->lookup->findContentLanguage($this->fileName, $this->yellow->system->get("language"));
+ if (is_file($fileNameError)) {
+ $rawData = $this->yellow->toolbox->readFile($fileNameError);
+ } elseif ($this->yellow->language->isText("coreError{$statusCode}Title", $languageError)) {
+ $rawData = "---\nTitle: ".$this->yellow->language->getText("coreError{$statusCode}Title", $languageError)."\n";
+ $rawData .= "Layout: error\n---\n".$this->yellow->language->getText("coreError{$statusCode}Text", $languageError);
+ } else {
+ $rawData = "---\nTitle:".$this->yellow->toolbox->getHttpStatusFormatted($statusCode, true)."\n";
+ $rawData .= "Layout:error\n---\n".$this->errorMessage;
+ }
+ return $rawData;
+ }
+
+ // 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 (!is_string_empty($this->errorMessage)) $statusCode .= ": ".$this->errorMessage;
+ }
+ return $statusCode;
+ }
+
+ // Respond with status code, no page content
+ public function status($statusCode, $location = "") {
+ if ($statusCode>0 && !$this->isExisting("pageClean")) {
+ $this->statusCode = $statusCode;
+ $this->lastModified = 0;
+ $this->headerData = array();
+ if (!is_string_empty($location)) {
+ $this->setHeader("Location", $location);
+ $this->setHeader("Cache-Control", "no-cache, no-store");
+ }
+ $this->set("pageClean", (string)$statusCode);
+ }
+ }
+
+ // Respond with error page
+ public function error($statusCode, $errorMessage = "") {
+ if ($statusCode>=400 && is_string_empty($this->errorMessage)) {
+ $this->statusCode = $statusCode;
+ $this->errorMessage = is_string_empty($errorMessage) ? "Page error!" : $errorMessage;
+ }
+ }
+
+ // 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->yellow->lookup->isActiveLocation($this->location, $this->yellow->page->location);
+ }
+
+ // 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;
+ }
+
+ // Append page to end of page collection
+ #[\ReturnTypeWillChange]
+ public function append($page) {
+ parent::append($page);
+ }
+
+ // Prepend page to start of page collection
+ #[\ReturnTypeWillChange]
+ public function prepend($page) {
+ $array = $this->getArrayCopy();
+ array_unshift($array, $page);
+ $this->exchangeArray($array);
+ }
+
+ // Remove page from page collection
+ public function remove($page): YellowPageCollection {
+ $array = array();
+ $location = $page->location;
+ foreach ($this->getArrayCopy() as $page) {
+ if ($page->location!=$location) array_push($array, $page);
+ }
+ $this->exchangeArray($array);
+ return $this;
+ }
+
+ // Filter page collection by page setting
+ public function filter($key, $value, $exactMatch = true): YellowPageCollection {
+ $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 (is_string_empty($this->filterValue)) $this->filterValue = substru($pageValue, 0, $pageValueLength);
+ array_push($array, $page);
+ break;
+ }
+ }
+ }
+ }
+ $this->exchangeArray($array);
+ return $this;
+ }
+
+ // Filter page collection by location or file
+ public function match($regex = "/.*/", $filterByLocation = true): YellowPageCollection {
+ $array = array();
+ $this->filterValue = $regex;
+ foreach ($this->getArrayCopy() as $page) {
+ if (preg_match($regex, $filterByLocation ? $page->location : $page->fileName)) array_push($array, $page);
+ }
+ $this->exchangeArray($array);
+ return $this;
+ }
+
+ // Sort page collection by settings similarity
+ public function similar($page): YellowPageCollection {
+ $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 (!is_array_empty($tokens)) {
+ $array = array();
+ foreach ($this->getArrayCopy() as $page) {
+ $sortScore = 0;
+ foreach ($tokens as $token) {
+ if (stristr($page->get("title"), $token)) $sortScore += 50;
+ if (stristr($page->get("tag"), $token)) $sortScore += 5;
+ if (stristr($page->get("author"), $token)) $sortScore += 2;
+ }
+ if ($page->location!=$location) {
+ $page->set("sortScore", $sortScore);
+ array_push($array, $page);
+ }
+ }
+ $this->exchangeArray($array);
+ $this->sort("modified", false)->sort("sortScore", false);
+ }
+ return $this;
+ }
+
+ // Sort page collection by page setting
+ public function sort($key, $ascendingOrder = true): YellowPageCollection {
+ $array = $this->getArrayCopy();
+ $sortIndex = 0;
+ $sortKeys = array();
+ foreach ($array as $page) {
+ $sortKeys[$page->location] = $page->get($key)." ".++$sortIndex;
+ }
+ $callback = function ($a, $b) use ($sortKeys, $ascendingOrder) {
+ return $ascendingOrder ?
+ strnatcasecmp($sortKeys[$a->location], $sortKeys[$b->location]) :
+ strnatcasecmp($sortKeys[$b->location], $sortKeys[$a->location]);
+ };
+ usort($array, $callback);
+ $this->exchangeArray($array);
+ return $this;
+ }
+
+ // Group page collection by page setting, return array with multiple collections
+ public function group($key, $ascendingOrder = true, $format = ""): array {
+ $array = array();
+ $groupByInitial = $format=="initial";
+ $groupByDate = !is_string_empty($format) && $format!="count" && $format!="initial";
+ foreach ($this->getIterator() as $page) {
+ if ($page->isExisting($key)) {
+ foreach (preg_split("/\s*,\s*/", $page->get($key)) as $group) {
+ if ($groupByInitial) {
+ $group = strtoupperu(substru($group, 0, 1));
+ } elseif ($groupByDate) {
+ $group = $this->yellow->language->getDateFormatted(strtotime($group), $format);
+ }
+ if (!is_string_empty($group)) {
+ if (!isset($array[$group])) {
+ $groupSearch = strtoloweru($group);
+ foreach (array_keys($array) as $groupFound) {
+ if (strtoloweru($groupFound)==$groupSearch) {
+ $group = $groupFound;
+ break;
+ }
+ }
+ if (!isset($array[$group])) $array[$group] = new YellowPageCollection($this->yellow);
+ }
+ $array[$group]->append($page);
+ }
+ }
+ }
+ }
+ $callbackString = function ($a, $b) use ($ascendingOrder) {
+ return $ascendingOrder ? strnatcasecmp($a, $b) : strnatcasecmp($b, $a);
+ };
+ $callbackCollection = function ($a, $b) use ($ascendingOrder) {
+ return $ascendingOrder ? count($a)-count($b) : count($b)-count($a);
+ };
+ if ($format!="count") {
+ uksort($array, $callbackString);
+ } else {
+ uasort($array, $callbackCollection);
+ }
+ return $array;
+ }
+
+ // Calculate union, merge page collection
+ public function merge($input): YellowPageCollection {
+ $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): YellowPageCollection {
+ $callback = function ($a, $b) {
+ return strcmp($a->location, $b->location);
+ };
+ $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): YellowPageCollection {
+ $callback = function ($a, $b) {
+ return strcmp($a->location, $b->location);
+ };
+ $this->exchangeArray(array_udiff($this->getArrayCopy(), (array)$input, $callback));
+ return $this;
+ }
+
+ // Limit the number of pages in page collection
+ public function limit($pagesMax): YellowPageCollection {
+ $this->exchangeArray(array_slice($this->getArrayCopy(), 0, $pagesMax));
+ return $this;
+ }
+
+ // Reverse page collection
+ public function reverse(): YellowPageCollection {
+ $this->exchangeArray(array_reverse($this->getArrayCopy()));
+ return $this;
+ }
+
+ // Randomize page collection
+ public function shuffle(): YellowPageCollection {
+ $array = $this->getArrayCopy();
+ shuffle($array);
+ $this->exchangeArray($array);
+ return $this;
+ }
+
+ // Paginate page collection
+ public function paginate($limit): YellowPageCollection {
+ if (!$this->isPagination() && $limit!=0) {
+ $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<0 || $this->paginationNumber>$this->paginationCount) $this->paginationNumber = 0;
+ if ($this->paginationNumber) {
+ $this->exchangeArray(array_slice($this->getArrayCopy(), ($this->paginationNumber - 1) * $limit, $limit));
+ } else {
+ $this->yellow->page->error(404);
+ }
+ }
+ 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;
+ }
+
+ // Check if page collection is empty
+ public function isEmpty() {
+ return empty($this->getArrayCopy());
+ }
+}
+
+class YellowArray extends ArrayObject {
+ public function __construct($array = []) {
+ 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
+ #[\ReturnTypeWillChange]
+ public function offsetGet($key) {
+ if (is_string($key)) $key = lcfirst($key);
+ return parent::offsetGet($key);
+ }
+
+ // Set array element
+ #[\ReturnTypeWillChange]
+ public function offsetSet($key, $value) {
+ if (is_string($key)) $key = lcfirst($key);
+ parent::offsetSet($key, $value);
+ }
+
+ // Remove array element
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($key) {
+ if (is_string($key)) $key = lcfirst($key);
+ parent::offsetUnset($key);
+ }
+
+ // Check if array element exists
+ #[\ReturnTypeWillChange]
+ public function offsetExists($key) {
+ if (is_string($key)) $key = lcfirst($key);
+ return parent::offsetExists($key);
+ }
+
+ // Check if array is empty
+ public function isEmpty() {
+ return empty($this->getArrayCopy());
+ }
+}
+
+// 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 is_string_empty($string) {
+ return is_null($string) || $string==="";
+}
+
+// Check if array is empty
+function is_array_empty($array) {
+ return is_null($array) || (is_array($array) ? empty($array) : empty($array->getArrayCopy()));
+}
diff --git a/system/extensions/edit-stack.svg b/system/workers/edit-stack.svg
diff --git a/system/workers/edit.css b/system/workers/edit.css
@@ -0,0 +1,599 @@
+/* Edit extension, https://github.com/annaesvensson/yellow-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 .yellow-toolbar-disabled,
+.yellow-toolbar .yellow-toolbar-disabled:hover {
+ background-color: inherit;
+ border-color: #c1c1c1 #c1c1c1 #aaa;
+ color: #aaa;
+}
+.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 #aaa;
+ 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-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-emoji {
+ padding: 10px;
+ width: 14em;
+}
+#yellow-popup-emoji a {
+ padding: 0.2em;
+}
+#yellow-popup-emoji .yellow-dropdown li {
+ display: inline-block;
+}
+#yellow-popup-icon {
+ padding: 10px;
+ width: 13em;
+}
+#yellow-popup-icon a {
+ padding: 0.18em 0.3em;
+ min-width: 1em;
+ text-align: center;
+}
+#yellow-popup-icon .yellow-dropdown li {
+ display: inline-block;
+}
+
+/* Icons */
+
+.yellow-icon {
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ background-color: currentcolor;
+ background-size: 1em 1em;
+ background-repeat: no-repeat;
+ background-position: center center;
+}
+.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 {
+ -webkit-mask: url("edit-stack.svg#preview");
+ mask: url("edit-stack.svg#preview");
+}
+.yellow-icon-format,
+.yellow-icon-paragraph {
+ -webkit-mask: url("edit-stack.svg#format");
+ mask: url("edit-stack.svg#format");
+}
+.yellow-icon-heading {
+ -webkit-mask: url("edit-stack.svg#heading");
+ mask: url("edit-stack.svg#heading");
+}
+.yellow-icon-h1 {
+ -webkit-mask: url("edit-stack.svg#h1");
+ mask: url("edit-stack.svg#h1");
+}
+.yellow-icon-h2 {
+ -webkit-mask: url("edit-stack.svg#h2");
+ mask: url("edit-stack.svg#h2");
+}
+.yellow-icon-h3 {
+ -webkit-mask: url("edit-stack.svg#h3");
+ mask: url("edit-stack.svg#h3");
+}
+.yellow-icon-bold {
+ -webkit-mask: url("edit-stack.svg#bold");
+ mask: url("edit-stack.svg#bold");
+}
+.yellow-icon-italic {
+ -webkit-mask: url("edit-stack.svg#italic");
+ mask: url("edit-stack.svg#italic");
+}
+.yellow-icon-strikethrough {
+ -webkit-mask: url("edit-stack.svg#strikethrough");
+ mask: url("edit-stack.svg#strikethrough");
+}
+.yellow-icon-quote {
+ -webkit-mask: url("edit-stack.svg#quote");
+ mask: url("edit-stack.svg#quote");
+}
+.yellow-icon-code,
+.yellow-icon-pre {
+ -webkit-mask: url("edit-stack.svg#code");
+ mask: url("edit-stack.svg#code");
+}
+.yellow-icon-link {
+ -webkit-mask: url("edit-stack.svg#link");
+ mask: url("edit-stack.svg#link");
+}
+.yellow-icon-file {
+ -webkit-mask: url("edit-stack.svg#file");
+ mask: url("edit-stack.svg#file");
+}
+.yellow-icon-list,
+.yellow-icon-ul {
+ -webkit-mask: url("edit-stack.svg#list");
+ mask: url("edit-stack.svg#list");
+}
+.yellow-icon-ol {
+ -webkit-mask: url("edit-stack.svg#ol");
+ mask: url("edit-stack.svg#ol");
+}
+.yellow-icon-tl {
+ -webkit-mask: url("edit-stack.svg#tl");
+ mask: url("edit-stack.svg#tl");
+}
+.yellow-icon-hr {
+ -webkit-mask: url("edit-stack.svg#hr");
+ mask: url("edit-stack.svg#hr");
+}
+.yellow-icon-table {
+ -webkit-mask: url("edit-stack.svg#table");
+ mask: url("edit-stack.svg#table");
+}
+.yellow-icon-emoji {
+ -webkit-mask: url("edit-stack.svg#emoji");
+ mask: url("edit-stack.svg#emoji");
+}
+.yellow-icon-icon {
+ -webkit-mask: url("edit-stack.svg#icon");
+ mask: url("edit-stack.svg#icon");
+}
+.yellow-icon-status {
+ -webkit-mask: url("edit-stack.svg#status");
+ mask: url("edit-stack.svg#status");
+}
+.yellow-icon-undo {
+ -webkit-mask: url("edit-stack.svg#undo");
+ mask: url("edit-stack.svg#undo");
+}
+.yellow-icon-redo {
+ -webkit-mask: url("edit-stack.svg#redo");
+ mask: url("edit-stack.svg#redo");
+}
+.yellow-icon-spinner {
+ -webkit-mask: url("edit-stack.svg#spinner");
+ mask: url("edit-stack.svg#spinner");
+}
+.yellow-icon-select {
+ -webkit-mask: url("edit-stack.svg#select");
+ mask: url("edit-stack.svg#select");
+}
+.yellow-icon-search {
+ -webkit-mask: url("edit-stack.svg#search");
+ mask: url("edit-stack.svg#search");
+}
+.yellow-icon-close {
+ -webkit-mask: url("edit-stack.svg#close");
+ mask: url("edit-stack.svg#close");
+}
+.yellow-icon-help {
+ -webkit-mask: url("edit-stack.svg#help");
+ mask: url("edit-stack.svg#help");
+}
+.yellow-icon-logo {
+ -webkit-mask: url("edit-stack.svg#logo");
+ mask: url("edit-stack.svg#logo");
+}
diff --git a/system/workers/edit.js b/system/workers/edit.js
@@ -0,0 +1,1512 @@
+// Edit extension, https://github.com/annaesvensson/yellow-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("yellowcsrftoken")) {
+ 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.coreDebugMode) 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\" aria-label=\""+this.getText("CancelButton")+"\"></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\" aria-label=\""+this.getText("CancelButton")+"\"></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\" aria-label=\""+this.getText("CancelButton")+"\"></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\" aria-label=\""+this.getText("CancelButton")+"\"></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\" aria-label=\""+this.getText("CancelButton")+"\"></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=\"yellowcsrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("yellowcsrftoken"))+"\" />"+
+ "<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\" aria-label=\""+this.getText("CancelButton")+"\"></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=\"yellowcsrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("yellowcsrftoken"))+"\" />"+
+ "<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-configure":
+ elementDiv.innerHTML =
+ "<form method=\"post\">"+
+ "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\" aria-label=\""+this.getText("CancelButton")+"\"></i></a>"+
+ "<div class=\"yellow-title\"><h1 id=\"yellow-pane-configure-title\">"+this.getText("ConfigureTitle")+"</h1></div>"+
+ "<div class=\"yellow-status\"><p id=\"yellow-pane-configure-status\" class=\""+paneStatus+"\">"+this.getText("ConfigureStatus", "", paneStatus)+"</p></div>"+
+ "<div class=\"yellow-settings\">"+
+ "<div id=\"yellow-pane-configure-settings-actions\" class=\"yellow-settings-left\"><p>"+this.getRawDataSettingsActions(paneAction)+"</p></div>"+
+ "<div id=\"yellow-pane-configure-settings-separator\" class=\"yellow-settings-left yellow-settings-separator\"> </div>"+
+ "<div id=\"yellow-pane-configure-settings-fields\" class=\"yellow-settings-right yellow-fields\">"+
+ "<input type=\"hidden\" name=\"action\" value=\"configure\" />"+
+ "<input type=\"hidden\" name=\"yellowcsrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("yellowcsrftoken"))+"\" />"+
+ "<p><label for=\"yellow-pane-configure-sitename\">"+this.getText("ConfigureSitename")+"</label><br /><input class=\"yellow-form-control\" name=\"sitename\" id=\"yellow-pane-configure-sitename\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("sitename"))+"\" /></p>"+
+ "<p><label for=\"yellow-pane-configure-author\">"+this.getText("ConfigureAuthor")+"</label><br /><input class=\"yellow-form-control\" name=\"author\" id=\"yellow-pane-configure-author\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("author"))+"\" /></p>"+
+ "<p><label for=\"yellow-pane-configure-email\">"+this.getText("ConfigureEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-configure-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+
+ "<p>"+this.getText("ConfigureInformation")+"</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\" aria-label=\""+this.getText("CancelButton")+"\"></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\" aria-label=\""+this.getText("CancelButton")+"\"></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\" aria-label=\""+this.getText("CancelButton")+"\"></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 = "";
+ if (document.getElementById("yellow-pane-account-"+yellow.user.language)) {
+ document.getElementById("yellow-pane-account-"+yellow.user.language).checked = true;
+ }
+ }
+ break;
+ case "yellow-pane-configure":
+ if (paneStatus=="none") {
+ document.getElementById("yellow-pane-configure-status").innerHTML = this.getText("ConfigureStatusNone");
+ document.getElementById("yellow-pane-configure-sitename").value = yellow.system.sitename;
+ document.getElementById("yellow-pane-configure-author").value = yellow.system.author;
+ document.getElementById("yellow-pane-configure-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")) {
+ document.getElementById(paneId+"-text").readOnly = true;
+ var elements = document.getElementsByClassName("yellow-toolbar-btn-icon");
+ for (var i=0, l=elements.length; i<l; i++) {
+ yellow.toolbox.addClass(elements[i], "yellow-toolbar-disabled");
+ }
+ yellow.toolbox.setVisible(document.getElementById(paneId+"-submit"), false);
+ }
+ }
+ 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) {
+ if (document.getElementById(paneId)) {
+ 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-configure":
+ 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.coreDebugMode) 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.coreDebugMode) 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.coreDebugMode) 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 "configure": 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 "toolbar": this.processToolbar(status, arguments); break;
+ case "settings": this.processSettings(arguments); break;
+ case "submit": this.processSubmit(arguments); break;
+ case "restore": this.processSubmit("action:"+action); break;
+ case "help": this.processHelp(); break;
+ case "close": this.processClose(); break;
+ }
+ }
+ },
+
+ // Process toolbar
+ processToolbar: function(status, arguments) {
+ if (yellow.system.coreDebugMode) 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 "important": yellow.editor.setMarkdown(elementText, "! ", "insert-multiline-block", "important", 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 (this.isExpandable(status)) {
+ this.showPopup("yellow-popup-"+status, status);
+ } else {
+ this.hidePopup(this.popupId);
+ }
+ }
+ if (!elementText.readOnly) {
+ if (status=="preview") this.showPreview(elementText, elementPreview);
+ if (status=="save" && this.paneAction!="delete") this.processSubmit("action:"+this.paneAction);
+ if (status=="help") window.open(this.getText("YellowHelpUrl"), "_blank");
+ }
+ },
+
+ // 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.coreDebugMode) 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", "yellowcsrftoken":this.getCookie("yellowcsrftoken") };
+ 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");
+ },
+
+ // Process close
+ processClose: function() {
+ this.hidePane(this.paneId);
+ if (yellow.page.action=="login") {
+ var url = yellow.system.coreServerScheme+"://"+
+ yellow.system.coreServerAddress+
+ yellow.system.coreServerBase+
+ yellow.page.location;
+ window.open(url, "_self");
+ }
+ },
+
+ // Create popup
+ createPopup: function(popupId) {
+ if (yellow.system.coreDebugMode) 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-important\" data-action=\"toolbar\" data-status=\"important\">"+this.getText("ToolbarImportant")+"</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-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-emoji":
+ var rawDataEmojis = "";
+ if (yellow.system.emojiToolbarButtons && yellow.system.emojiToolbarButtons!="none") {
+ var tokens = yellow.system.emojiToolbarButtons.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=\"emoji emoji-"+yellow.toolbox.encodeHtml(className)+"\"></i></a></li>";
+ }
+ }
+ elementDiv.innerHTML = "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+rawDataEmojis+"</ul>";
+ break;
+ case "yellow-popup-icon":
+ var rawDataIcons = "";
+ if (yellow.system.iconToolbarButtons && yellow.system.iconToolbarButtons!="none") {
+ var tokens = yellow.system.iconToolbarButtons.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=\"icon "+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.coreDebugMode) 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.coreDebugMode) 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("yellowcsrftoken", this.getCookie("yellowcsrftoken"));
+ 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("DOMContentLoaded"));
+ } 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("yellowcsrftoken", this.getCookie("yellowcsrftoken"));
+ 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").indexOf("#data-action-")!=-1) {
+ var position = elements[i].getAttribute("href").indexOf("#data-action-");
+ var action = elements[i].getAttribute("href").substring(position+13);
+ var href = elements[i].getAttribute("href").substring(0, position);
+ if (href=="" || href==yellow.page.base+yellow.page.location) elements[i].setAttribute("data-action", action);
+ }
+ 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 || yellow.page.statusCode==435) 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=="emoji" || name=="icon");
+ },
+
+ // Check if extension exists
+ isExtension: function(name) {
+ return name in yellow.system.coreExtensions;
+ }
+};
+
+yellow.editor = {
+
+ // Set Markdown formatting
+ setMarkdown: function(element, prefix, type, name, toggle, callback) {
+ var information = this.getMarkdownInformation(element, prefix, type, name);
+ 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":
+ case "remove-multiline-block":
+ textSelectionNew = this.getMarkdownMultilineBlock(textSelection, information);
+ selectionStartNew = information.top;
+ selectionEndNew = information.bottom + this.getMarkdownDifference(textSelection, textSelectionNew);
+ break;
+ case "insert-fenced-block":
+ textSelectionNew = this.getMarkdownFencedBlock(textSelection, information);
+ selectionStartNew = information.top + information.prefix.length;
+ selectionEndNew = information.bottom + this.getMarkdownDifference(textSelection, textSelectionNew) - information.prefix.length;
+ break;
+ case "remove-fenced-block":
+ textSelectionNew = this.getMarkdownFencedBlock(textSelection, information);
+ selectionStartNew = information.top - information.prefix.length;
+ selectionEndNew = information.bottom + 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.coreDebugMode) console.log("yellow.editor.setMarkdown type:"+information.type);
+ },
+
+ // Return Markdown formatting information
+ getMarkdownInformation: function(element, prefix, type, name) {
+ 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 attributes = name ? prefix+" {."+name+"}\n" : "";
+ 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, "attributes":attributes, "start":start, "end":end, "top":top, "bottom":bottom, "found":found };
+ },
+
+ // Return Markdown length difference
+ getMarkdownDifference: function(textSelection, textSelectionNew) {
+ var position = textSelection.indexOf("\n");
+ var positionNew = textSelectionNew.indexOf("\n");
+ var textSelectionLength = position!=-1 ? textSelection.length : textSelection.length+1;
+ var 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) {
+ var attributesOnly = lines[i].match(/^\s*!\s*\{\./);
+ if (!attributesOnly) textSelectionNew += lines[i].substring(matches[0].length);
+ } else {
+ textSelectionNew += lines[i];
+ }
+ }
+ textSelection = textSelectionNew;
+ if (information.type.indexOf("remove")==-1) {
+ textSelectionNew = information.attributes;
+ 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.coreDebugMode) 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/workers/edit.php b/system/workers/edit.php
@@ -0,0 +1,2026 @@
+<?php
+// Edit extension, https://github.com/annaesvensson/yellow-edit
+
+class YellowEdit {
+ const VERSION = "0.9.15";
+ public $yellow; // access to API
+ public $response; // web response
+ public $merge; // text merge
+ public $editable; // page can be edited? (boolean)
+
+ // Handle initialisation
+ public function onLoad($yellow) {
+ $this->yellow = $yellow;
+ $this->response = new YellowEditResponse($yellow);
+ $this->merge = new YellowEditMerge($yellow);
+ $this->yellow->system->setDefault("editSiteEmail", "noreply");
+ $this->yellow->system->setDefault("editLocation", "/edit/");
+ $this->yellow->system->setDefault("editUploadNewLocation", "/media/@group/@filename");
+ $this->yellow->system->setDefault("editUploadExtensions", ".gif, .jpeg, .jpg, .mp3, .mp4, .ogg, .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("editUserAccess", "create, edit, delete, restore, upload");
+ $this->yellow->system->setDefault("editUserHome", "/");
+ $this->yellow->system->setDefault("editLoginRestriction", "0");
+ $this->yellow->system->setDefault("editLoginSessionTimeout", "2592000");
+ $this->yellow->system->setDefault("editBruteForceProtection", "25");
+ }
+
+ // Handle update
+ public function onUpdate($action) {
+ if ($action=="clean" || $action=="daily") {
+ $cleanup = false;
+ $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$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" && !is_string_empty($matches[2])) {
+ $status = $this->yellow->user->getUser("status", $matches[2]);
+ $reserved = strtotime($this->yellow->user->getUser("modified", $matches[2])) + 60*60*24;
+ $cleanup = $status!="active" && $status!="inactive" && $reserved<=time();
+ }
+ }
+ if (!$cleanup) $fileDataNew .= $line;
+ }
+ $fileDataNew = rtrim($fileDataNew)."\n";
+ if ($fileData!=$fileDataNew && !$this->yellow->toolbox->writeFile($fileNameUser, $fileDataNew)) {
+ $this->yellow->toolbox->log("error", "Can't write file '$fileNameUser'!");
+ }
+ }
+ }
+
+ // Handle request
+ public function onRequest($scheme, $address, $base, $location, $fileName) {
+ $statusCode = 0;
+ if ($this->isEditLocation($location)) {
+ $this->editable = true;
+ $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->lookup->getRequestInformation($scheme, $address, $base);
+ $this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName, false);
+ $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName);
+ }
+ return $statusCode;
+ }
+
+ // 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]";
+ }
+
+ // Handle page meta data
+ public function onParseMetaData($page) {
+ $page->set("editPageUrl", $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"), "/").$page->location));
+ }
+
+ // Handle page content element
+ public function onParseContentElement($page, $name, $text, $attributes, $type) {
+ $output = null;
+ if ($name=="edit" && $type=="inline") {
+ list($target, $description) = $this->yellow->toolbox->getTextList($text, " ", 2);
+ if (is_string_empty($target) || $target=="-") $target = "main";
+ if (is_string_empty($description)) $description = ucfirst($name);
+ $pageTarget = $target=="main" ? $page->getPage("main") : $page->getPage("main")->getPage($target);
+ $output = "<a href=\"".$pageTarget->get("editPageUrl")."\">".htmlspecialchars($description)."</a>";
+ }
+ return $output;
+ }
+
+ // Handle page layout
+ public function onParsePageLayout($page, $name) {
+ if ($this->editable) {
+ $this->response->processPageData($page);
+ }
+ }
+
+ // Handle page extra data
+ public function onParsePageExtra($page, $name) {
+ $output = null;
+ if ($this->editable && $name=="header") {
+ $assetLocation = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreAssetLocation");
+ $output = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"{$assetLocation}edit.css\" />\n";
+ $output .= "<script type=\"text/javascript\" src=\"{$assetLocation}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;
+ }
+
+ // 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) {
+ $data[$key] = "$value[email] - User account by $value[name].";
+ }
+ uksort($data, "strnatcasecmp");
+ foreach ($data as $line) echo "$line\n";
+ if (is_array_empty($data)) echo "Yellow $command: No user accounts\n";
+ return 200;
+ }
+
+ // Add user account
+ public function userAdd($command, $text) {
+ $status = "ok";
+ list($option, $email, $password) = $this->yellow->toolbox->getTextArguments($text);
+ if (is_string_empty($email) || is_string_empty($password)) $status = $this->response->status = "incomplete";
+ 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") {
+ $name = $this->yellow->system->get("sitename");
+ $userLanguage = $this->yellow->system->get("language");
+ $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile");
+ $settings = array(
+ "name" => $name,
+ "description" => $this->yellow->language->getText("editUserDescription", $userLanguage),
+ "language" => $userLanguage,
+ "access" => $this->yellow->system->get("editUserAccess"),
+ "home" => $this->yellow->system->get("editUserHome"),
+ "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->toolbox->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) = $this->yellow->toolbox->getTextArguments($text);
+ if (is_string_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("coreExtensionDirectory").$this->yellow->system->get("coreUserFile");
+ $settings = array(
+ "hash" => is_string_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);
+ if (is_string_empty($email)) $status = $this->response->status = "invalid";
+ 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") {
+ $name = $this->yellow->user->getUser("name", $email);
+ $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$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->toolbox->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 "configure": $statusCode = $this->processRequestConfigure($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 "restore": $statusCode = $this->processRequestRestore($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->getRedirectLocation($location);
+ $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
+ $statusCode = $this->yellow->sendStatus(301, $location);
+ } else {
+ $statusCode = 404;
+ if ($this->response->isUserAccess("create", $location)) $statusCode = 434;
+ if ($this->response->isUserAccess("restore", $location) && $this->response->isDeletedLocation($location)) {
+ $statusCode = 435;
+ }
+ $this->yellow->page->error($statusCode);
+ $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("coreExtensionDirectory").$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 (is_string_empty($name) || is_string_empty($email) || is_string_empty($password) || is_string_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") {
+ $userLanguage = $this->yellow->lookup->findContentLanguage($fileName, $this->yellow->system->get("language"));
+ $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile");
+ $settings = array(
+ "name" => $name,
+ "description" => $this->yellow->language->getText("editUserDescription", $userLanguage),
+ "language" => $userLanguage,
+ "access" => $this->yellow->system->get("editUserAccess"),
+ "home" => $this->yellow->system->get("editUserHome"),
+ "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("coreExtensionDirectory").$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("coreExtensionDirectory").$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->toolbox->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 (is_string_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("coreExtensionDirectory").$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("coreExtensionDirectory").$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("coreExtensionDirectory").$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) || is_string_empty($hash)) $this->response->status = "done";
+ }
+ if ($this->response->status=="ok") {
+ $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$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("coreExtensionDirectory").$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 (is_string_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("coreExtensionDirectory").$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->toolbox->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("coreExtensionDirectory").$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 || !is_string_empty($password)) {
+ if (is_string_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("coreExtensionDirectory").$this->yellow->system->get("coreUserFile");
+ $settings = array(
+ "name" => $name,
+ "description" => $this->yellow->user->getUser("description", $emailSource),
+ "language" => $language,
+ "access" => $this->yellow->user->getUser("access", $emailSource),
+ "home" => $this->yellow->user->getUser("home", $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("coreExtensionDirectory").$this->yellow->system->get("coreUserFile");
+ $settings = array(
+ "name" => $name,
+ "language" => $language,
+ "pending" => $email.":".(is_string_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("coreExtensionDirectory").$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 settings
+ public function processRequestConfigure($scheme, $address, $base, $location, $fileName) {
+ $statusCode = 0;
+ if ($this->response->isUserAccess("configure")) {
+ $this->response->action = "configure";
+ $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 (is_string_empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) $this->response->status = "invalid";
+ }
+ if ($this->response->status=="ok") {
+ $fileNameSystem = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
+ $settings = array("sitename" => $sitename, "author" => $author, "email" => $email);
+ $file = $this->response->getFileSystem($scheme, $address, $base, $location, $fileNameSystem, $settings);
+ $this->response->status = (!$file->isError() && $this->yellow->system->save($fileNameSystem, $settings)) ? "done" : "error";
+ if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameSystem'!");
+ }
+ 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 = is_string_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 all")==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) && !is_string_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->writeFile($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->errorMessage);
+ $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) && !is_string_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)) {
+ $ok = $this->yellow->toolbox->renameFile($fileName, $page->fileName, true) &&
+ $this->yellow->toolbox->writeFile($page->fileName, $page->rawData);
+ } else {
+ $ok = $this->yellow->toolbox->renameDirectory(dirname($fileName), dirname($page->fileName), true) &&
+ $this->yellow->toolbox->writeFile($page->fileName, $page->rawData);
+ }
+ if ($ok) {
+ $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->errorMessage);
+ $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()) {
+ if ($this->yellow->lookup->isFileLocation($location)) {
+ $ok = $this->response->deleteFileLocation($location, $fileName);
+ } else {
+ $ok = $this->response->deleteDirectoryLocation($location, $fileName);
+ }
+ if ($ok) {
+ $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->errorMessage);
+ $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+ }
+ }
+ return $statusCode;
+ }
+
+ // Process request to restore deleted page
+ public function processRequestRestore($scheme, $address, $base, $location, $fileName) {
+ $statusCode = 0;
+ if ($this->response->isUserAccess("restore", $location) && !is_file($fileName)) {
+ $page = $this->response->getPageRestore($scheme, $address, $base, $location, $fileName);
+ if (!$page->isError()) {
+ if ($this->yellow->lookup->isFileLocation($location)) {
+ $ok = $this->response->restoreFileLocation($location);
+ } else {
+ $ok = $this->response->restoreDirectoryLocation($location);
+ }
+ if ($ok) {
+ $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
+ $statusCode = $this->yellow->sendStatus(303, $location);
+ } else {
+ $this->yellow->page->error(500, "Can't restore file '$fileName'!");
+ $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+ }
+ } else {
+ $this->yellow->page->error(500, $page->errorMessage);
+ $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"));
+ $page->headerData = array(
+ "Cache-Control"=>"no-cache, no-store",
+ "Content-Type"=>$this->yellow->toolbox->getMimeContentType("a.html"),
+ "Last-Modified"=>$this->yellow->toolbox->getHttpDateFormatted(time()));
+ $statusCode = $this->yellow->sendData($page->statusCode, $page->headerData, $page->outputData);
+ if ($this->yellow->system->get("coreDebugMode")>=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'!";
+ }
+ $headerData = array(
+ "Cache-Control"=>"no-cache, no-store",
+ "Content-Type"=>$this->yellow->toolbox->getMimeContentType("a.json"),
+ "Last-Modified"=>$this->yellow->toolbox->getHttpDateFormatted(time()));
+ return $this->yellow->sendData(isset($data["error"]) ? 500 : 200, $headerData, json_encode($data));
+ }
+
+ // Check user authentication
+ public function checkUserAuth($scheme, $address, $base, $location, $fileName) {
+ $action = $this->yellow->page->getRequest("action");
+ $authToken = $this->yellow->toolbox->getCookie("yellowauthtoken");
+ $csrfToken = $this->yellow->toolbox->getCookie("yellowcsrftoken");
+ if (is_string_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 (!is_string_empty($authToken) && !is_string_empty($csrfToken)) {
+ $csrfTokenReceived = isset($_POST["yellowcsrftoken"]) ? $_POST["yellowcsrftoken"] : "";
+ $csrfTokenIrrelevant = is_string_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 (is_string_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 (!is_string_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("coreExtensionDirectory").$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 (!is_string_empty($password) && strlenu($password)<$this->yellow->system->get("editUserPasswordMinLength")) $status = "short";
+ if (!is_string_empty($password) && $password==$email) $status = "weak";
+ if (!is_string_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 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;
+ }
+
+ // 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 edit location
+ public function isEditLocation($location) {
+ $locationLength = strlenu($this->yellow->system->get("editLocation"));
+ return substru($location, 0, $locationLength)==$this->yellow->system->get("editLocation");
+ }
+}
+
+class YellowEditResponse {
+ public $yellow; // access to API
+ public $extension; // access to extension
+ 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");
+ $this->userEmail = "";
+ }
+
+ // Process page data
+ public function processPageData($page) {
+ if ($this->isUser()) {
+ if (is_string_empty($this->rawDataSource)) $this->rawDataSource = $page->rawData;
+ if (is_string_empty($this->rawDataEdit)) $this->rawDataEdit = $page->rawData;
+ if (is_string_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 || $page->statusCode==435) {
+ $this->rawDataEdit = $this->getRawDataNew($page, true);
+ $this->rawDataReadonly = false;
+ }
+ }
+ if (is_string_empty($this->language)) $this->language = $page->get("language");
+ if (is_string_empty($this->action)) $this->action = $this->isUser() ? "none" : "login";
+ if (is_string_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->lookup->normaliseLines($rawData, $endOfLine);
+ $page = new YellowPage($this->yellow);
+ $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false);
+ $page->parseMeta($rawData, 200);
+ $this->editContentFile($page, "precreate", $this->userEmail);
+ if ($this->yellow->content->find($page->location)) {
+ $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("editNewLocation"));
+ $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("editNewPrefix"));
+ $pageCounter = 0;
+ while ($this->yellow->content->find($page->location) || is_string_empty($page->fileName)) {
+ $page->rawData = $this->yellow->toolbox->setMetaData($page->rawData, "title", $this->getTitleNext($page->rawData));
+ $page->rawData = $this->yellow->lookup->normaliseLines($page->rawData, $endOfLine);
+ $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("editNewLocation"));
+ $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("editNewPrefix"));
+ if (++$pageCounter>999) break;
+ }
+ if ($this->yellow->content->find($page->location) || is_string_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!");
+ }
+ $this->editContentFile($page, "create", $this->userEmail);
+ return $page;
+ }
+
+ // Return modified page
+ public function getPageEdit($scheme, $address, $base, $location, $fileName, $rawDataSource, $rawDataEdit, $rawDataFile, $endOfLine) {
+ $rawDataSource = $this->yellow->lookup->normaliseLines($rawDataSource, $endOfLine);
+ $rawDataEdit = $this->yellow->lookup->normaliseLines($rawDataEdit, $endOfLine);
+ $rawDataFile = $this->yellow->lookup->normaliseLines($rawDataFile, $endOfLine);
+ $rawData = $this->extension->merge->merge($rawDataSource, $rawDataEdit, $rawDataFile);
+ $page = new YellowPage($this->yellow);
+ $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false);
+ $page->parseMeta($rawData, 200);
+ $this->editContentFile($page, "preedit", $this->userEmail);
+ $pageSource = new YellowPage($this->yellow);
+ $pageSource->setRequestInformation($scheme, $address, $base, $location, $fileName, false);
+ $pageSource->parseMeta($rawDataSource, 200);
+ if ($this->isMetaModified($pageSource, $page) && $page->location!=$this->yellow->content->getHomeLocation($page->location)) {
+ $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("editNewLocation"), true);
+ $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("editNewPrefix"));
+ if ($page->location!=$pageSource->location && ($this->yellow->content->find($page->location) || is_string_empty($page->fileName))) {
+ $page->error(500, "Page '".$page->get("title")."' is not possible!");
+ }
+ }
+ if (is_string_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!");
+ }
+ $this->editContentFile($page, "edit", $this->userEmail);
+ return $page;
+ }
+
+ // Return deleted page
+ public function getPageDelete($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) {
+ $rawData = $this->yellow->lookup->normaliseLines($rawData, $endOfLine);
+ $page = new YellowPage($this->yellow);
+ $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false);
+ $page->parseMeta($rawData, 200);
+ if (!$this->isUserAccess("delete", $page->location)) {
+ $page->error(500, "Page '".$page->get("title")."' is restricted!");
+ }
+ $this->editContentFile($page, "delete", $this->userEmail);
+ return $page;
+ }
+
+ // Return restored page
+ public function getPageRestore($scheme, $address, $base, $location, $fileName) {
+ $page = new YellowPage($this->yellow);
+ $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false);
+ $page->parseMeta("");
+ if (!$this->isUserAccess("restore", $page->location)) {
+ $page->error(500, "Page '".$page->get("title")."' is restricted!");
+ }
+ $this->editContentFile($page, "restore", $this->userEmail);
+ return $page;
+ }
+
+ // Return preview page
+ public function getPagePreview($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) {
+ $rawData = $this->yellow->lookup->normaliseLines($rawData, $endOfLine);
+ $page = new YellowPage($this->yellow);
+ $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false);
+ $page->parseMeta($rawData, 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->getContentHtml();
+ $output .= "</div></div></div>";
+ $page->statusCode = 200;
+ $page->outputData = $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, "/".$fileNameShort, $fileNameShort, false);
+ $file->parseMeta(null);
+ $file->set("fileNameTemp", $fileNameTemp);
+ $file->set("fileNameShort", $fileNameShort);
+ if ($file->get("type")=="html" || $file->get("type")=="svg") {
+ $fileData = $this->yellow->toolbox->readFile($fileNameTemp);
+ $fileData = $this->yellow->lookup->normaliseData($fileData, $file->get("type"));
+ if (is_string_empty($fileData) || !$this->yellow->toolbox->writeFile($fileNameTemp, $fileData)) {
+ $file->error(500, "Can't write file '$fileNameTemp'!");
+ }
+ }
+ $this->editMediaFile($file, "upload", $this->userEmail);
+ $fileNameShort = basename($file->fileName);
+ $file->location = $this->getFileNewLocation($fileNameShort, $pageLocation, $file->get("fileNewLocation"));
+ $file->fileName = substru($file->location, 1);
+ $fileCounter = 0;
+ 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, $fileNameSystem, $settings) {
+ $file = new YellowPage($this->yellow);
+ $file->setRequestInformation($scheme, $address, $base, "/".$fileNameSystem, $fileNameSystem, false);
+ $file->parseMeta(null);
+ foreach ($settings as $key=>$value) $file->set($key, $value);
+ $this->editSystemFile($file, "configure", $this->userEmail);
+ return $file;
+ }
+
+ // Return page data including status information
+ public function getPageData($page) {
+ $data = array();
+ $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->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;
+ }
+ 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["coreDebugMode"] = $this->yellow->system->get("coreDebugMode");
+ $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->toolbox->enumerate("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["emojiToolbarButtons"] = $this->yellow->system->get("emojiToolbarButtons");
+ $data["iconToolbarButtons"] = $this->yellow->system->get("iconToolbarButtons");
+ if ($this->isUserAccess("configure")) {
+ $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());
+ }
+ 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["description"] = $this->yellow->user->getUser("description", $this->userEmail);
+ $data["language"] = $this->yellow->user->getUser("language", $this->userEmail);
+ $data["status"] = $this->yellow->user->getUser("status", $this->userEmail);
+ $data["access"] = $this->yellow->user->getUser("access", $this->userEmail);
+ $data["home"] = $this->yellow->user->getUser("home", $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=="yellowauthtoken" || $key=="yellowcsrftoken" ||
+ 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("configure")) $settingsActions .= ", configure";
+ if ($this->isUserAccess("update")) $settingsActions .= ", update";
+ return $settingsActions=="account" ? "none" : $settingsActions;
+ }
+
+ // Return toolbar buttons
+ public function getToolbarButtons() {
+ $toolbarButtons = $this->yellow->system->get("editToolbarButtons");
+ if ($toolbarButtons=="auto") {
+ $toolbarButtons = "format, bold, italic, strikethrough, code, separator, list, link, file";
+ if ($this->yellow->extension->isExisting("emoji")) $toolbarButtons .= ", emoji";
+ if ($this->yellow->extension->isExisting("icon")) $toolbarButtons .= ", icon";
+ $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 = is_string_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(true);
+ list($statusCodeLatest, $settingsLatest) = $this->yellow->extension->get("update")->getExtensionSettings(false);
+ $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 (!is_string_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)."shared/";
+ $fileName = $this->yellow->lookup->findFileFromContentLocation($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)."shared/";
+ $fileName = $this->yellow->lookup->findFileFromContentLocation($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, $editNewLocation, $pageMatchLocation = false) {
+ $location = is_string_empty($editNewLocation) ? "@title" : $editNewLocation;
+ $location = preg_replace("/@title/i", $this->getPageNewTitle($rawData), $location);
+ $location = preg_replace("/@timestamp/i", $this->getPageNewData($rawData, "published", "U"), $location);
+ $location = preg_replace("/@date/i", $this->getPageNewData($rawData, "published", "Y-m-d"), $location);
+ $location = preg_replace("/@year/i", $this->getPageNewData($rawData, "published", "Y"), $location);
+ $location = preg_replace("/@month/i", $this->getPageNewData($rawData, "published", "m"), $location);
+ $location = preg_replace("/@day/i", $this->getPageNewData($rawData, "published", "d"), $location);
+ $location = preg_replace("/@tag/i", $this->getPageNewData($rawData, "tag"), $location);
+ $location = preg_replace("/@author/i", $this->getPageNewData($rawData, "author"), $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 (preg_match("/\d/", $location)) {
+ $locationNew = "";
+ $tokens = explode("/", $location);
+ for ($i=1; $i<count($tokens); ++$i) {
+ $locationNew .= "/".$this->yellow->lookup->normaliseToken($tokens[$i]);
+ }
+ $location = $locationNew;
+ }
+ 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 = is_string_empty($titleSlug) ? $title : $titleSlug;
+ $value = $this->yellow->lookup->normaliseName($value, false, false, true);
+ return trim(preg_replace("/-+/", "-", $value), "-");
+ }
+
+ // Return data for new/modified page
+ public function getPageNewData($rawData, $key, $dateFormat = "") {
+ $value = $this->yellow->toolbox->getMetaData($rawData, $key);
+ if (preg_match("/^(.*?)\,(.*)$/", $value, $matches)) $value = $matches[1];
+ if (!is_string_empty($dateFormat)) $value = date($dateFormat, strtotime($value));
+ if (is_string_empty($value)) $value = "none";
+ $value = $this->yellow->lookup->normaliseName($value, false, false, true);
+ return trim(preg_replace("/-+/", "-", $value), "-");
+ }
+
+ // Return file name for new/modified page
+ public function getPageNewFile($location, $pageFileName = "", $pagePrefix = "") {
+ $fileName = $this->yellow->lookup->findFileFromContentLocation($location);
+ if (!is_string_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 = is_string_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 (is_string_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 = is_string_empty($fileNewLocation) ? $this->yellow->system->get("editUploadNewLocation") : $fileNewLocation;
+ $location = preg_replace("/@timestamp/i", time(), $location);
+ $location = preg_replace("/@date/i", date("Y-m-d"), $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";
+ $fileType = $this->yellow->toolbox->getFileType($fileNameShort);
+ $locationMedia = $this->yellow->system->get("coreMediaLocation");
+ $locationGroup = $this->yellow->system->get("coreDownloadLocation");
+ if (preg_match("/(gif|jpeg|jpg|png|svg)$/", $fileType)) {
+ $locationGroup = $this->yellow->system->get("coreImageLocation");
+ }
+ if (preg_match("#^$locationMedia(.+?)\/#", $locationGroup, $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 = is_string_empty($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 = is_string_empty($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);
+ $locationArguments = "/action:$action/email:$email/expire:$expire/language:$userLanguage/actiontoken:$actionToken/";
+ $url = "$scheme://$address$base".$this->yellow->lookup->normaliseArguments($locationArguments, false);
+ }
+ $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");
+ $siteEmail = $this->yellow->system->get("editSiteEmail");
+ $subject = $this->yellow->language->getText("{$prefix}Subject", $userLanguage);
+ $footer = $this->yellow->language->getText("editMailFooter", $userLanguage);
+ $footer = str_replace("\\n", "\r\n", $footer);
+ $footer = preg_replace("/@sitename/i", $sitename, $footer);
+ $mailHeaders = array(
+ "To" => $this->yellow->lookup->normaliseAddress("$userName <$userEmail>"),
+ "From" => $this->yellow->lookup->normaliseAddress("$sitename <$siteEmail>"),
+ "Subject" => $subject,
+ "Date" => date(DATE_RFC2822),
+ "Mime-Version" => "1.0",
+ "Content-Type" => "text/plain; charset=utf-8",
+ "X-Request-Url" => "$scheme://$address$base");
+ $mailMessage = "$message\r\n\r\n$url\r\n-- \r\n$footer";
+ return $this->yellow->toolbox->mail($action, $mailHeaders, $mailMessage);
+ }
+
+ // 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("yellowauthtoken", $authToken, $expire, "$base/", "", $scheme=="https", true);
+ setcookie("yellowcsrftoken", $csrfToken, $expire, "$base/", "", $scheme=="https", false);
+ }
+
+ // Destroy browser cookies
+ public function destroyCookies($scheme, $address, $base) {
+ setcookie("yellowauthtoken", "", 1, "$base/");
+ setcookie("yellowcsrftoken", "", 1, "$base/");
+ }
+
+ // 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 (is_string_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 (is_string_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 (is_string_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 (is_string_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);
+ }
+ }
+ }
+
+ // Delete file
+ public function deleteFileLocation($location, $fileName) {
+ $rawData = $this->yellow->toolbox->readFile($fileName);
+ $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalLocation", $location);
+ $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalFileName", $fileName);
+ return $this->yellow->toolbox->writeFile($fileName, $rawData) &&
+ $this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory"));
+ }
+
+ // Delete directory
+ public function deleteDirectoryLocation($location, $fileName) {
+ $rawData = $this->yellow->toolbox->readFile($fileName);
+ $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalLocation", $location);
+ $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalFileName", $fileName);
+ return $this->yellow->toolbox->writeFile($fileName, $rawData) &&
+ $this->yellow->toolbox->deleteDirectory(dirname($fileName), $this->yellow->system->get("coreTrashDirectory"));
+ }
+
+ // Restore deleted file from trash
+ public function restoreFileLocation($location) {
+ $fileNameDeleted = $fileNameRestored = "";
+ $deleted = 0;
+ $pathTrash = $this->yellow->system->get("coreTrashDirectory");
+ $regex = "/^.*\\".$this->yellow->system->get("coreContentExtension")."$/";
+ foreach ($this->yellow->toolbox->getDirectoryEntries($pathTrash, $regex, false, false) as $entry) {
+ $rawDataOriginal = $this->yellow->toolbox->readFile($entry);
+ $locationOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalLocation");
+ $fileNameOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalFileName");
+ $deletedOriginal = $this->yellow->toolbox->getFileDeleted($entry);
+ if ($location==$locationOriginal && $deleted<=$deletedOriginal) {
+ $fileNameDeleted = $entry;
+ $fileNameRestored = $fileNameOriginal;
+ $rawDataRestored = $rawDataOriginal;
+ $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalLocation");
+ $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalFileName");
+ $deleted = $deletedOriginal;
+ }
+ }
+ return !is_string_empty($fileNameDeleted) && $this->yellow->lookup->isContentFile($fileNameRestored) &&
+ $this->yellow->toolbox->renameFile($fileNameDeleted, $fileNameRestored, true) &&
+ $this->yellow->toolbox->writeFile($fileNameRestored, $rawDataRestored);
+ }
+
+ // Restore deleted directory from trash
+ public function restoreDirectoryLocation($location) {
+ $pathDeleted = $fileNameRestored = "";
+ $deleted = 0;
+ $pathTrash = $this->yellow->system->get("coreTrashDirectory");
+ foreach ($this->yellow->toolbox->getDirectoryEntries($pathTrash, "/.*/", false, true) as $entry) {
+ $fileName = $entry."/".$this->yellow->system->get("coreContentDefaultFile");
+ if (!is_file($fileName)) continue;
+ $rawDataOriginal = $this->yellow->toolbox->readFile($fileName);
+ $locationOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalLocation");
+ $fileNameOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalFileName");
+ $deletedOriginal = $this->yellow->toolbox->getFileDeleted($entry);
+ if ($location==$locationOriginal && $deleted<=$deletedOriginal) {
+ $pathDeleted = $entry;
+ $fileNameRestored = $fileNameOriginal;
+ $rawDataRestored = $rawDataOriginal;
+ $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalLocation");
+ $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalFileName");
+ $deleted = $deletedOriginal;
+ }
+ }
+ return !is_string_empty($pathDeleted) && $this->yellow->lookup->isContentFile($fileNameRestored) &&
+ $this->yellow->toolbox->renameDirectory($pathDeleted, dirname($fileNameRestored), true) &&
+ $this->yellow->toolbox->writeFile($fileNameRestored, $rawDataRestored);
+ }
+
+ // Check if location has been deleted
+ public function isDeletedLocation($location) {
+ $found = false;
+ $pathTrash = $this->yellow->system->get("coreTrashDirectory");
+ $regex = "/^.*\\".$this->yellow->system->get("coreContentExtension")."$/";
+ $fileNames = $this->yellow->toolbox->getDirectoryEntries($pathTrash, $regex, false, false);
+ foreach ($this->yellow->toolbox->getDirectoryEntries($pathTrash, "/.*/", false, true) as $entry) {
+ $fileName = $entry."/".$this->yellow->system->get("coreContentDefaultFile");
+ if (is_file($fileName)) array_push($fileNames, $fileName);
+ }
+ foreach ($fileNames as $fileName) {
+ $rawDataOriginal = $this->yellow->toolbox->readFile($fileName, 4096);
+ $locationOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalLocation");
+ if ($location==$locationOriginal) {
+ $found = true;
+ break;
+ }
+ }
+ return $found;
+ }
+
+ // 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 login with restriction
+ public function isLoginRestriction() {
+ return $this->yellow->system->get("editLoginRestriction");
+ }
+
+ // Check if user is logged in
+ public function isUser() {
+ return !is_string_empty($this->userEmail);
+ }
+
+ // Check if user with access
+ public function isUserAccess($action, $location = "") {
+ $userHome = $this->yellow->user->getUser("home", $this->userEmail);
+ $tokens = preg_split("/\s*,\s*/", $this->yellow->user->getUser("access", $this->userEmail));
+ return in_array($action, $tokens) && (is_string_empty($location) || substru($location, 0, strlenu($userHome))==$userHome);
+ }
+}
+
+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 ($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 = " ";
+ }
+ for (;$posYours<count($diffYours); ++$posYours) {
+ array_push($diff, $diffYours[$posYours]);
+ $typeYours = $diffYours[$posYours][0];
+ $typeMine = " ";
+ }
+ 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/workers/generate.php b/system/workers/generate.php
@@ -0,0 +1,450 @@
+<?php
+// Generate extension, https://github.com/annaesvensson/yellow-generate
+
+class YellowGenerate {
+ const VERSION = "0.9.8";
+ public $yellow; // access to API
+ public $files; // number of files
+ public $errors; // number of errors
+ public $locationsWithArguments; // locations with arguments detected
+ public $locationsWithPagination; // locations with pagination detected
+
+ // Handle initialisation
+ public function onLoad($yellow) {
+ $this->yellow = $yellow;
+ $this->yellow->system->setDefault("generateStaticUrl", "auto");
+ $this->yellow->system->setDefault("generateStaticDirectory", "public/");
+ $this->yellow->system->setDefault("generateStaticDefaultFile", "index.html");
+ $this->yellow->system->setDefault("generateStaticErrorFile", "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 "generate": $statusCode = $this->processCommandGenerate($command, $text); break;
+ case "clean": $statusCode = $this->processCommandClean($command, $text); break;
+ default: $statusCode = 0;
+ }
+ return $statusCode;
+ }
+
+ // Handle command help
+ public function onCommandHelp() {
+ return array("generate [directory location]", "clean [directory location]");
+ }
+
+ // Process command to generate static website
+ public function processCommandGenerate($command, $text) {
+ $statusCode = 0;
+ list($path, $location) = $this->yellow->toolbox->getTextArguments($text);
+ if (is_string_empty($location) || substru($location, 0, 1)=="/") {
+ if ($this->checkStaticSettings()) {
+ $statusCode = $this->generateStatic($path, $location);
+ } else {
+ $statusCode = 500;
+ $this->files = 0;
+ $this->errors = 1;
+ $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
+ echo "ERROR generating files: Please configure GenerateStaticUrl 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;
+ }
+
+ // Generate static website
+ public function generateStatic($path, $location) {
+ $statusCode = 200;
+ $this->files = $this->errors = 0;
+ $path = rtrim(is_string_empty($path) ? $this->yellow->system->get("generateStaticDirectory") : $path, "/");
+ if (is_string_empty($location)) {
+ $statusCode = $this->cleanStatic($path, $location);
+ foreach ($this->yellow->extension->data as $key=>$value) {
+ if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("clean");
+ }
+ }
+ $statusCode = max($statusCode, $this->generateStaticContent($path, $location, "\rGenerating static website", 5, 95));
+ $statusCode = max($statusCode, $this->generateStaticMedia($path, $location));
+ $statusCode = max($statusCode, $this->generateStaticSystem($path, $location));
+ echo "\rGenerating static website 100%... done\n";
+ return $statusCode;
+ }
+
+ // Generate static content
+ public function generateStaticContent($path, $locationFilter, $progressText, $increments, $max) {
+ $statusCode = 200;
+ $this->locationsWithArguments = $this->locationsWithPagination = array();
+ $staticUrl = $this->yellow->system->get("generateStaticUrl");
+ list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl);
+ $locations = $this->getContentLocations();
+ $filesEstimated = count($locations);
+ foreach ($locations as $location) {
+ echo "$progressText ".$this->getProgressPercent($this->files, $filesEstimated, $increments, $max/1.5)."%... ";
+ if (!preg_match("#^$base$locationFilter#", "$base$location")) continue;
+ $statusCode = max($statusCode, $this->generateStaticFile($path, $location, true));
+ }
+ foreach ($this->locationsWithArguments as $location) {
+ echo "$progressText ".$this->getProgressPercent($this->files, $filesEstimated, $increments, $max/1.5)."%... ";
+ if (!preg_match("#^$base$locationFilter#", "$base$location")) continue;
+ $statusCode = max($statusCode, $this->generateStaticFile($path, $location, true));
+ }
+ $filesEstimated = $this->files + count($this->locationsWithArguments) + count($this->locationsWithPagination);
+ foreach ($this->locationsWithPagination as $location) {
+ echo "$progressText ".$this->getProgressPercent($this->files, $filesEstimated, $increments, $max)."%... ";
+ if (!preg_match("#^$base$locationFilter#", "$base$location")) continue;
+ for ($pageNumber=2; $pageNumber<=999; ++$pageNumber) {
+ $statusCodeLocation = $this->generateStaticFile($path, $location.$pageNumber, false, true);
+ $statusCode = max($statusCode, $statusCodeLocation);
+ if ($statusCodeLocation==100) break;
+ }
+ }
+ echo "$progressText ".$this->getProgressPercent(100, 100, $increments, $max)."%... ";
+ return $statusCode;
+ }
+
+ // Generate static media
+ public function generateStaticMedia($path, $locationFilter) {
+ $statusCode = 200;
+ if (is_string_empty($locationFilter)) {
+ foreach ($this->getMediaLocations() as $location) {
+ $statusCode = max($statusCode, $this->generateStaticFile($path, $location));
+ }
+ }
+ return $statusCode;
+ }
+
+ // Generate static system
+ public function generateStaticSystem($path, $locationFilter) {
+ $statusCode = 200;
+ if (is_string_empty($locationFilter)) {
+ foreach ($this->getSystemLocations($path) as $location) {
+ $statusCode = max($statusCode, $this->generateStaticFile($path, $location));
+ }
+ $statusCode = max($statusCode, $this->generateStaticFile($path, "/error/", false, false, true));
+ }
+ return $statusCode;
+ }
+
+ // Generate static file
+ public function generateStaticFile($path, $location, $analyse = false, $probe = false, $error = false) {
+ $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("generateStaticUrl");
+ 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->analyseStaticLocations($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 generating location '$location', ".$this->yellow->page->getStatusCode(true)."\n";
+ }
+ if ($this->yellow->system->get("coreDebugMode")>=2) {
+ echo "YellowGenerate::generateStaticFile 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 (is_string_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->writeFile($fileName, $fileData, true) ||
+ !$this->yellow->toolbox->modifyFile($fileName, $modified)) {
+ $statusCode = 500;
+ $this->yellow->page->statusCode = $statusCode;
+ $this->yellow->page->errorMessage = "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->errorMessage = "Can't write file '$fileName'!";
+ }
+ return $statusCode;
+ }
+
+ // Analyse static locations with arguments
+ public function analyseStaticLocations($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;
+ if (substru($location, strlenu($base), 1)!="/") continue;
+ $location = substru($location, strlenu($base));
+ if (!$this->yellow->toolbox->isLocationArguments($location)) continue;
+ if (!$this->yellow->toolbox->isLocationArgumentsPagination($location)) {
+ if (!isset($this->locationsWithArguments[$location])) {
+ $this->locationsWithArguments[$location] = $location;
+ if ($this->yellow->system->get("coreDebugMode")>=2) {
+ echo "YellowGenerate::analyseStaticLocations detected location:$location<br />\n";
+ }
+ }
+ } else {
+ $location = rtrim($location, "0..9");
+ if (!isset($this->locationsWithPagination[$location])) {
+ $this->locationsWithPagination[$location] = $location;
+ if ($this->yellow->system->get("coreDebugMode")>=2) {
+ echo "YellowGenerate::analyseStaticLocations detected location:$location<br />\n";
+ }
+ }
+ }
+ }
+ }
+
+ // Process command to clean static website
+ public function processCommandClean($command, $text) {
+ $statusCode = 0;
+ list($path, $location) = $this->yellow->toolbox->getTextArguments($text);
+ if (is_string_empty($location) || substru($location, 0, 1)=="/") {
+ $statusCode = $this->cleanStatic($path, $location);
+ echo "Yellow $command: Static website";
+ echo " ".($statusCode!=200 ? "not " : "")."cleaned\n";
+ } else {
+ $statusCode = 400;
+ echo "Yellow $command: Invalid arguments\n";
+ }
+ return $statusCode;
+ }
+
+ // Clean static website
+ public function cleanStatic($path, $location) {
+ $statusCode = 200;
+ $path = rtrim(is_string_empty($path) ? $this->yellow->system->get("generateStaticDirectory") : $path, "/");
+ if (is_string_empty($location)) {
+ $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;
+ }
+
+ // 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("generateStaticDefaultFile");
+ if (is_file($fileName) && is_readable($fileName) && !$this->yellow->lookup->isCommandLine()) {
+ $statusCode = $this->yellow->sendFile(200, $fileName, true);
+ }
+ }
+ return $statusCode;
+ }
+
+ // Check static settings
+ public function checkStaticSettings() {
+ return preg_match("/^(http|https):/", $this->yellow->system->get("generateStaticUrl"));
+ }
+
+ // Check static directory
+ public function checkStaticDirectory($path) {
+ $ok = false;
+ if (!is_string_empty($path)) {
+ if ($path==rtrim($this->yellow->system->get("generateStaticDirectory"), "/")) $ok = true;
+ if ($path==rtrim($this->yellow->system->get("coreCacheDirectory"), "/")) $ok = true;
+ if ($path==rtrim($this->yellow->system->get("coreTrashDirectory"), "/")) $ok = true;
+ if (is_file("$path/".$this->yellow->system->get("generateStaticDefaultFile"))) $ok = true;
+ if (is_file("$path/yellow.php")) $ok = false;
+ }
+ return $ok;
+ }
+
+ // Return progress in percent
+ public function getProgressPercent($now, $total, $increments, $max) {
+ $max = intval($max/$increments) * $increments;
+ $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("generateStaticDefaultFile");
+ } elseif ($statusCode==404) {
+ $fileName = $path."/".$this->yellow->system->get("generateStaticErrorFile");
+ } else {
+ $fileName = $path."/error.html";
+ }
+ return $fileName;
+ }
+
+ // 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() {
+ $locations = array();
+ $staticUrl = $this->yellow->system->get("generateStaticUrl");
+ list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl);
+ $this->yellow->page->setRequestInformation($scheme, $address, $base, "", "", false);
+ foreach ($this->yellow->content->getChildrenRecursive("", true) as $page) {
+ if (preg_match("/exclude/i", $page->get("generate"))) 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();
+ $path = $this->yellow->system->get("coreMediaDirectory");
+ $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($path, "/.*/", false, false);
+ foreach ($fileNames as $fileName) {
+ $location = $this->yellow->lookup->findMediaLocationFromFile($fileName);
+ if (!is_string_empty($location)) array_push($locations, $location);
+ }
+ return $locations;
+ }
+
+ // Return system locations
+ public function getSystemLocations($pathIgnore) {
+ $locations = array();
+ $path = $this->yellow->system->get("coreWorkerDirectory");
+ $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($path, "/.*/", false, false);
+ foreach ($fileNames as $fileName) {
+ $location = $this->yellow->lookup->findSystemLocationFromFile($fileName);
+ if (!is_string_empty($location)) array_push($locations, $location);
+ }
+ $path = $this->yellow->system->get("coreThemeDirectory");
+ $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($path, "/.*/", false, false);
+ foreach ($fileNames as $fileName) {
+ $location = $this->yellow->lookup->findSystemLocationFromFile($fileName);
+ if (!is_string_empty($location)) array_push($locations, $location);
+ }
+ $path = $this->yellow->system->get("coreLayoutDirectory");
+ $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($path, "/.*/", false, false);
+ foreach ($fileNames as $fileName) {
+ $location = $this->yellow->lookup->findSystemLocationFromFile($fileName);
+ if (!is_string_empty($location)) array_push($locations, $location);
+ }
+ $regexIgnore = "#^(/".
+ $this->yellow->system->get("coreContentDirectory")."|/".
+ $this->yellow->system->get("coreMediaDirectory")."|/".
+ $this->yellow->system->get("coreSystemDirectory")."|/".
+ $this->yellow->system->get("generateStaticDirectory")."|/$pathIgnore/|/yellow.php)#";
+ $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive(".", "/.*/", false, false);
+ foreach ($fileNames as $fileName) {
+ $location = substru($fileName, 1);
+ if (!preg_match($regexIgnore, $location)) array_push($locations, $location);
+ }
+ return array_diff($locations, $this->getSystemLocationsIgnore());
+ }
+
+ // Return system locations to ignore
+ public function getSystemLocationsIgnore() {
+ $locations = array();
+ $path = $this->yellow->system->get("coreWorkerDirectory");
+ $workerDirectoryLength = strlenu($this->yellow->system->get("coreWorkerDirectory"));
+ if ($this->yellow->extension->isExisting("bundle")) {
+ foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^bundle-(.*)/", false, false) as $entry) {
+ list($locationsBundle) = $this->yellow->extension->get("bundle")->getBundleInformation($entry);
+ $locations = array_merge($locations, $locationsBundle);
+ }
+ }
+ if ($this->yellow->extension->isExisting("edit")) {
+ foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^edit(\-|\.)(.*)/", false, false) as $entry) {
+ $location = $this->yellow->system->get("coreAssetLocation").substru($entry, $workerDirectoryLength);
+ array_push($locations, $location);
+ }
+ }
+ return array_unique($locations);
+ }
+}
diff --git a/system/workers/image.php b/system/workers/image.php
@@ -0,0 +1,212 @@
+<?php
+// Image extension, https://github.com/annaesvensson/yellow-image
+
+class YellowImage {
+ const VERSION = "0.9.3";
+ 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("imageUploadJpegQuality", "80");
+ $this->yellow->system->setDefault("imageThumbnailJpegQuality", "80");
+ $this->yellow->system->setDefault("imageJpegExtension", "auto");
+ }
+
+ // Handle update
+ public function onUpdate($action) {
+ if ($action=="clean") {
+ $statusCode = 200;
+ $path = $this->yellow->lookup->findMediaDirectory("coreThumbnailLocation");
+ foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, false) as $entry) {
+ if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500;
+ }
+ if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!");
+ }
+ }
+
+ // Handle page content element
+ public function onParseContentElement($page, $name, $text, $attributes, $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 (is_string_empty($alt)) $alt = $this->yellow->language->getText("imageDefaultAlt");
+ if (is_string_empty($width)) $width = "100%";
+ if (is_string_empty($height)) $height = $width;
+ $path = $this->yellow->lookup->findMediaDirectory("coreImageLocation");
+ list($src, $width, $height) = $this->getImageInformation($path.$name, $width, $height);
+ } else {
+ if (is_string_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 (!is_string_empty($alt)) $output .= " alt=\"".htmlspecialchars($alt)."\" title=\"".htmlspecialchars($alt)."\"";
+ if (!is_string_empty($style)) $output .= " class=\"".htmlspecialchars($style)."\"";
+ $output .= " />";
+ }
+ return $output;
+ }
+
+ // Handle media file changes
+ public function onEditMediaFile($file, $action, $email) {
+ if ($action=="upload") {
+ $fileName = $file->get("fileNameTemp");
+ list($widthInput, $heightInput, $orientation, $type) =
+ $this->yellow->toolbox->detectImageInformation($fileName, $file->get("type"));
+ $widthMax = $this->yellow->system->get("imageUploadWidthMax");
+ $heightMax = $this->yellow->system->get("imageUploadHeightMax");
+ if ($type=="gif" || $type=="jpeg" || $type=="png") {
+ if ($widthInput>$widthMax || $heightInput>$heightMax) {
+ 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, $orientation);
+ if (!$this->saveImage($image, $fileName, $type, $this->yellow->system->get("imageUploadJpegQuality"))) {
+ $file->error(500, "Can't write file '$fileName'!");
+ }
+ } elseif ($orientation>1) {
+ $image = $this->loadImage($fileName, $type);
+ $image = $this->orientImage($image, $orientation);
+ if (!$this->saveImage($image, $fileName, $type, $this->yellow->system->get("imageUploadJpegQuality"))) {
+ $file->error(500, "Can't write file '$fileName'!");
+ }
+ }
+ }
+ if ($type=="jpeg") {
+ $file->fileName = dirname($file->fileName)."/".pathinfo($file->fileName, PATHINFO_FILENAME).$this->getImageExtension($file->fileName, $type);
+ $file->set("type", $this->yellow->toolbox->getFileType($file->fileName));
+ }
+ }
+ }
+
+ // Return image information, create thumbnail on demand
+ public function getImageInformation($fileName, $widthOutput, $heightOutput) {
+ $fileNameShort = substru($fileName, strlenu($this->yellow->lookup->findMediaDirectory("coreImageLocation")));
+ list($widthInput, $heightInput, $orientation, $type) = $this->yellow->toolbox->detectImageInformation($fileName);
+ $widthOutput = $this->convertValueAndUnit($widthOutput, $widthInput);
+ $heightOutput = $this->convertValueAndUnit($heightOutput, $heightInput);
+ if (($widthInput==$widthOutput && $heightInput==$heightOutput) || $type=="svg" || $type=="") {
+ $src = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreImageLocation").$fileNameShort;
+ $width = $widthOutput;
+ $height = $heightOutput;
+ } else {
+ $pathThumb = $this->yellow->lookup->findMediaDirectory("coreThumbnailLocation");
+ $fileNameThumb = ltrim(str_replace(array("/", "\\", "."), "-", dirname($fileNameShort)."/".pathinfo($fileName, PATHINFO_FILENAME)), "-");
+ $fileNameThumb .= "-".$widthOutput."x".$heightOutput;
+ $fileNameThumb .= $this->getImageExtension($fileName, $type);
+ $fileNameOutput = $pathThumb.$fileNameThumb;
+ if ($this->isFileNotUpdated($fileName, $fileNameOutput)) {
+ $image = $this->loadImage($fileName, $type);
+ $image = $this->resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput);
+ $image = $this->orientImage($image, $orientation);
+ if (is_file($fileNameOutput)) $this->yellow->toolbox->deleteFile($fileNameOutput);
+ if (!$this->saveImage($image, $fileNameOutput, $type, $this->yellow->system->get("imageThumbnailJpegQuality")) ||
+ !$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("coreThumbnailLocation").$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));
+ }
+
+ // Return image extension
+ public function getImageExtension($fileName, $type) {
+ $jpegExtension = $this->yellow->system->get("imageJpegExtension");
+ $fileExtension = ".".pathinfo($fileName, PATHINFO_EXTENSION);
+ if ($jpegExtension!="auto" && $type=="jpeg") $fileExtension = $jpegExtension;
+ return $fileExtension;
+ }
+
+ // Load image from file
+ public function loadImage($fileName, $type) {
+ $image = false;
+ switch ($type) {
+ case "gif": $image = @imagecreatefromgif($fileName); break;
+ case "jpeg": $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 "jpeg": $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, $orientation) {
+ switch ($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/workers/install-blog.bin b/system/workers/install-blog.bin
Binary files differ.
diff --git a/system/workers/install-language.bin b/system/workers/install-language.bin
Binary files differ.
diff --git a/system/workers/install-wiki.bin b/system/workers/install-wiki.bin
Binary files differ.
diff --git a/system/workers/install.php b/system/workers/install.php
@@ -0,0 +1,581 @@
+<?php
+// Install extension, https://github.com/annaesvensson/yellow-install
+
+class YellowInstall {
+ const VERSION = "0.9.10";
+ const PRIORITY = "1";
+ public $yellow; // access to API
+
+ // Handle initialisation
+ public function onLoad($yellow) {
+ $this->yellow = $yellow;
+ }
+
+ // Handle request
+ public function onRequest($scheme, $address, $base, $location, $fileName) {
+ return $this->processRequestInstall($scheme, $address, $base, $location, $fileName);
+ }
+
+ // Handle command
+ public function onCommand($command, $text) {
+ return $this->processCommandInstall($command, $text);
+ }
+
+ // Process request to install website
+ public function processRequestInstall($scheme, $address, $base, $location, $fileName) {
+ $statusCode = 0;
+ if ($this->yellow->lookup->isContentFile($fileName) || is_string_empty($fileName)) {
+ if ($this->yellow->system->get("updateCurrentRelease")=="none") {
+ $this->checkServerRequirements();
+ $author = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $this->yellow->page->getRequest("author")));
+ $email = trim($this->yellow->page->getRequest("email"));
+ $password = trim($this->yellow->page->getRequest("password"));
+ $language = trim($this->yellow->page->getRequest("language"));
+ $extension = trim($this->yellow->page->getRequest("extension"));
+ $status = trim($this->yellow->page->getRequest("status"));
+ $statusCode = $this->updateLog();
+ $statusCode = max($statusCode, $this->updateLanguages("small"));
+ $errorMessage = $this->yellow->page->errorMessage;
+ $this->yellow->content->pages["root/"] = array();
+ $this->yellow->page = new YellowPage($this->yellow);
+ $this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName, false);
+ $this->yellow->page->parseMeta($this->getRawDataInstall(), $statusCode, $errorMessage);
+ $this->yellow->page->parseContent();
+ $this->yellow->page->parsePage();
+ if ($status=="install") $status = $this->updateExtensions("small", $extension)==200 ? "ok" : "error";
+ if ($status=="ok") $status = $this->updateUser($email, $password, $author, $language)==200 ? "ok" : "error";
+ if ($status=="ok") $status = $this->updateAuthentication($scheme, $address, $base, $email)==200 ? "ok" : "error";
+ if ($status=="ok") $status = $this->updateContent($language, "installHome", "/")==200 ? "ok" : "error";
+ if ($status=="ok") $status = $this->updateContent($language, "installAbout", "/about/")==200 ? "ok" : "error";
+ if ($status=="ok") $status = $this->updateContent($language, "installDefault", "/shared/page-new-default")==200 ? "ok" : "error";
+ if ($status=="ok") $status = $this->updateContent($language, "installWiki", "/shared/page-new-wiki")==200 ? "ok" : "error";
+ if ($status=="ok") $status = $this->updateContent($language, "installBlog", "/shared/page-new-blog")==200 ? "ok" : "error";
+ if ($status=="ok") $status = $this->updateContent($language, "coreError404", "/shared/page-error-404")==200 ? "ok" : "error";
+ if ($status=="ok") $status = $this->updateSettings()==200 ? "ok" : "error";
+ if ($status=="ok") $status = $this->removeInstall()==200 ? "done" : "error";
+ } else {
+ $status = $this->removeInstall(true)==200 ? "done" : "error";
+ }
+ if ($status=="done") {
+ $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, "/");
+ $statusCode = $this->yellow->sendStatus(303, $location);
+ } else {
+ $statusCode = $this->yellow->sendData($this->yellow->page->statusCode, $this->yellow->page->headerData, $this->yellow->page->outputData);
+ }
+ }
+ return $statusCode;
+ }
+
+ // Process command to install website
+ public function processCommandInstall($command, $text) {
+ $statusCode = 0;
+ if ($this->yellow->system->get("updateCurrentRelease")=="none") {
+ $this->checkCommandRequirements();
+ list($installation, $option) = $this->yellow->toolbox->getTextArguments($text);
+ if (is_string_empty($command)) {
+ $statusCode = 200;
+ echo "Datenstrom Yellow is for people who make small websites. https://datenstrom.se/yellow/\n";
+ echo "Syntax: php yellow.php\n";
+ echo " php yellow.php about [extension]\n";
+ echo " php yellow.php serve [url]\n";
+ echo " php yellow.php skip installation [option]\n";
+ } elseif ($command=="about" || $command=="serve") {
+ $statusCode = 0;
+ } elseif ($command=="skip" && $installation=="installation") {
+ $statusCode = $this->updateLog();
+ if ($statusCode==200) $statusCode = $this->updateLanguages($option);
+ if ($statusCode==200) $statusCode = $this->updateExtensions($option, "");
+ if ($statusCode==200) $statusCode = $this->updateSettings(true);
+ if ($statusCode==200) $statusCode = $this->removeInstall();
+ if ($statusCode>=400) {
+ echo "ERROR installing files: ".$this->yellow->page->errorMessage."\n";
+ echo "The installation has not been completed. Please run command again.\n";
+ } else {
+ $extensionsCount = $this->getExtensionsCount();
+ echo "Yellow $command: $extensionsCount extension".($extensionsCount!=1 ? "s" : "").", 0 errors\n";
+ }
+ } else {
+ $statusCode = 500;
+ echo "The installation has not been completed. Please type 'php yellow.php serve' or 'php yellow.php skip installation`.\n";
+ }
+ } else {
+ $statusCode = $this->removeInstall(true);
+ if ($statusCode==200) $statusCode = 0;
+ if ($statusCode>=400) {
+ echo "ERROR installing files: ".$this->yellow->page->errorMessage."\n";
+ echo "Detected ZIP files, 0 extensions installed. Please run command again.\n";
+ }
+ }
+ return $statusCode;
+ }
+
+ // Update log file
+ public function updateLog() {
+ $statusCode = 200;
+ $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreWebsiteFile");
+ if (!is_file($fileName)) {
+ list($name, $version, $os) = $this->yellow->toolbox->detectServerInformation();
+ $product = "Datenstrom Yellow ".YellowCore::RELEASE;
+ $this->yellow->toolbox->log("info", "Install $product, PHP ".PHP_VERSION.", $name $version, $os");
+ foreach ($this->yellow->extension->data as $key=>$value) {
+ if ($key=="install") continue;
+ $this->yellow->toolbox->log("info", "Install extension '".ucfirst($key)." $value[version]'");
+ }
+ if (!is_file($fileName)) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+ }
+ }
+ return $statusCode;
+ }
+
+ // Update languages
+ public function updateLanguages($option) {
+ $statusCode = 200;
+ $path = $this->yellow->system->get("coreWorkerDirectory")."install-language.bin";
+ $zip = new ZipArchive();
+ if ($zip->open($path)===true) {
+ $pathBase = "";
+ if (preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1];
+ $fileData = $zip->getFromName($pathBase.$this->yellow->system->get("updateExtensionFile"));
+ foreach ($this->getLanguageExtensionsRequired($fileData, $option) as $extension) {
+ $fileDataPhp = $zip->getFromName($pathBase."translations/$extension/$extension.php");
+ $fileDataIni = $zip->getFromName($pathBase."translations/$extension/extension.ini");
+ $statusCode = max($statusCode, $this->updateLanguageArchive($fileDataPhp, $fileDataIni, $pathBase, "install"));
+ }
+ $this->yellow->extension->load($this->yellow->system->get("coreWorkerDirectory"));
+ $this->yellow->language->load($this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreLanguageFile"));
+ $zip->close();
+ } else {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't open file '$path'!");
+ }
+ return $statusCode;
+ }
+
+ // Update language archive
+ public function updateLanguageArchive($fileDataPhp, $fileDataIni, $pathBase, $action) {
+ $statusCode = 200;
+ if ($this->yellow->extension->isExisting("update")) {
+ $settings = $this->yellow->toolbox->getTextSettings($fileDataIni, "");
+ $extension = lcfirst($settings->get("extension"));
+ $version = $settings->get("version");
+ $modified = strtotime($settings->get("published"));
+ $fileNamePhp = $this->yellow->system->get("coreWorkerDirectory").$extension.".php";
+ if (!is_string_empty($extension) && !is_string_empty($version) && !is_file($fileNamePhp)) {
+ $statusCode = max($statusCode, $this->yellow->extension->get("update")->updateExtensionSettings($extension, $action, $fileDataIni));
+ $statusCode = max($statusCode, $this->yellow->extension->get("update")->updateExtensionFile(
+ $fileNamePhp, $fileDataPhp, $modified, 0, 0, "create", $extension));
+ $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'");
+ }
+ }
+ return $statusCode;
+ }
+
+ // Update extensions
+ public function updateExtensions($option, $extension) {
+ $statusCode = 200;
+ if ($this->yellow->extension->isExisting("update")) {
+ if (!is_string_empty($option)) {
+ if ($option=="medium" || $option=="large") {
+ $path = $this->yellow->system->get("coreExtensionDirectory");
+ $fileData = $this->yellow->toolbox->readFile($path.$this->yellow->system->get("updateAvailableFile"));
+ $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension");
+ $extensions = $this->getAvailableExtensionsRequired($settings, $option);
+ $statusCode = $this->downloadExtensionsAvailable($settings, $extensions);
+ $path = $this->yellow->system->get("coreWorkerDirectory");
+ foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^install-.*\.bin$/", true, false) as $entry) {
+ if (basename($entry)=="install-language.bin") continue;
+ if (preg_match("/^install-(.*?)\.bin/", basename($entry), $matches) && !in_array($matches[1], $extensions)) continue;
+ $statusCode = max($statusCode, $this->yellow->extension->get("update")->updateExtensionArchive($entry, "install"));
+ }
+ }
+ if (!($option=="small" || $option=="medium" || $option=="large")) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Option '$option' not supported!");
+ }
+ }
+ if (!is_string_empty($extension)) {
+ $path = $this->yellow->system->get("coreWorkerDirectory")."install-".$extension.".bin";
+ if (is_file($path)) {
+ $statusCode = $this->yellow->extension->get("update")->updateExtensionArchive($path, "install");
+ }
+ }
+ }
+ return $statusCode;
+ }
+
+ // Update user
+ public function updateUser($email, $password, $name, $language) {
+ $statusCode = 200;
+ if ($this->yellow->extension->isExisting("edit") && !is_string_empty($email) && !is_string_empty($password)) {
+ if (is_string_empty($name)) $name = $this->yellow->system->get("sitename");
+ $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile");
+ $settings = array(
+ "name" => $name,
+ "description" => $this->yellow->language->getText("editUserDescription", $language),
+ "language" => $language,
+ "access" => "create, edit, delete, restore, upload, configure, update",
+ "home" => "/",
+ "hash" => $this->yellow->extension->get("edit")->response->createHash($password),
+ "stamp" => $this->yellow->extension->get("edit")->response->createStamp(),
+ "pending" => "none",
+ "failed" => "0",
+ "modified" => date("Y-m-d H:i:s", time()),
+ "status" => "active");
+ if (!$this->yellow->user->save($fileNameUser, $email, $settings)) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't write file '$fileNameUser'!");
+ }
+ $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", "Add user '".strtok($name, " ")."'");
+ }
+ return $statusCode;
+ }
+
+ // Update authentication
+ public function updateAuthentication($scheme, $address, $base, $email) {
+ if ($this->yellow->extension->isExisting("edit") && $this->yellow->user->isExisting($email)) {
+ $base = rtrim($base.$this->yellow->system->get("editLocation"), "/");
+ $this->yellow->extension->get("edit")->response->createCookies($scheme, $address, $base, $email);
+ }
+ return 200;
+ }
+
+ // Update content
+ public function updateContent($language, $name, $location) {
+ $statusCode = 200;
+ $fileName = $this->yellow->lookup->findFileFromContentLocation($location);
+ $fileData = str_replace("\r\n", "\n", $this->yellow->toolbox->readFile($fileName));
+ if (!is_string_empty($fileData) && $language!="en") {
+ $titleOld = "Title: ".$this->yellow->language->getText("{$name}Title", "en")."\n";
+ $titleNew = "Title: ".$this->yellow->language->getText("{$name}Title", $language)."\n";
+ $fileData = str_replace($titleOld, $titleNew, $fileData);
+ $textOld = str_replace("\\n", "\n", $this->yellow->language->getText("{$name}Text", "en"));
+ $textNew = str_replace("\\n", "\n", $this->yellow->language->getText("{$name}Text", $language));
+ $fileData = str_replace($textOld, $textNew, $fileData);
+ if (!$this->yellow->toolbox->writeFile($fileName, $fileData)) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+ }
+ }
+ return $statusCode;
+ }
+
+ // Update settings
+ public function updateSettings($skipInstallation = false) {
+ $statusCode = 200;
+ $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
+ if (!$this->yellow->system->save($fileName, $this->getSystemSettings($skipInstallation))) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+ }
+ $language = $this->yellow->system->get("language");
+ $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreLanguageFile");
+ $fileData = $this->yellow->toolbox->readFile($fileName);
+ if (strposu($fileData, "Language:")===false) {
+ if (!is_string_empty($fileData)) $fileData .= "\n";
+ $fileData .= "Language: $language\n";
+ $fileData .= "media/images/photo.jpg: ".$this->yellow->language->getText("installExampleImage", $language)."\n";
+ if (!$this->yellow->toolbox->writeFile($fileName, $fileData)) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+ }
+ }
+ return $statusCode;
+ }
+
+ // Remove files used by installation
+ public function removeInstall($log = false) {
+ $statusCode = 200;
+ if (function_exists("opcache_reset")) opcache_reset();
+ $this->yellow->toolbox->deleteFile("license.md");
+ $fileName = $this->yellow->system->get("coreWorkerDirectory")."install.php";
+ if (!$this->yellow->toolbox->deleteFile($fileName)) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!");
+ }
+ $path = $this->yellow->system->get("coreWorkerDirectory");
+ foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^install-.*\.bin$/", true, false) as $entry) {
+ if (!$this->yellow->toolbox->deleteFile($entry)) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't delete file '$entry'!");
+ }
+ }
+ if ($statusCode==200) unset($this->yellow->extension->data["install"]);
+ $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateInstalledFile");
+ $fileData = $this->yellow->toolbox->readFile($fileName);
+ $fileDataNew = $this->yellow->toolbox->unsetTextSettings($fileData, "extension", "install");
+ if ($statusCode==200 && !$this->yellow->toolbox->writeFile($fileName, $fileDataNew)) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+ }
+ if ($log) $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", "Uninstall extension 'Install ".YellowInstall::VERSION."'");
+ return $statusCode;
+ }
+
+ // Check web server requirements
+ public function checkServerRequirements() {
+ if ($this->yellow->system->get("coreDebugMode")>=1) {
+ list($name, $version, $os) = $this->yellow->toolbox->detectServerInformation();
+ echo "YellowInstall::checkServerRequirements for $name $version, $os<br />\n";
+ }
+ if (!$this->checkServerComplete()) $this->yellow->exitFatalError("Datenstrom Yellow requires complete upload!");
+ if (!$this->checkServerWrite()) $this->yellow->exitFatalError("Datenstrom Yellow requires write access!");
+ if (!$this->checkServerHtaccess()) $this->yellow->exitFatalError("Datenstrom Yellow requires htaccess file!");
+ if (!$this->checkServerRewrite()) $this->yellow->exitFatalError("Datenstrom Yellow requires rewrite support!");
+ }
+
+ // Check command line requirements
+ public function checkCommandRequirements() {
+ if ($this->yellow->system->get("coreDebugMode")>=1) {
+ list($name, $version, $os) = $this->yellow->toolbox->detectServerInformation();
+ echo "YellowInstall::checkCommandRequirements for $name $version, $os<br />\n";
+ }
+ if (!$this->checkServerComplete()) $this->yellow->exitFatalError("Datenstrom Yellow requires complete upload!");
+ if (!$this->checkServerWrite()) $this->yellow->exitFatalError("Datenstrom Yellow requires write access!");
+ }
+
+ // Check web server complete upload
+ public function checkServerComplete() {
+ $complete = true;
+ $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateInstalledFile");
+ $fileData = $this->yellow->toolbox->readFile($fileName);
+ $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension");
+ $fileNames = array($fileName);
+ foreach ($settings as $extension=>$block) {
+ foreach ($block as $key=>$value) {
+ if (strposu($key, "/")) {
+ list($entry, $flags) = $this->yellow->toolbox->getTextList($value, ",", 2);
+ if (!preg_match("/create/i", $flags)) continue;
+ if (preg_match("/delete/i", $flags)) continue;
+ if (preg_match("/additional/i", $flags)) continue;
+ array_push($fileNames, $key);
+ }
+ }
+ }
+ foreach ($fileNames as $fileName) {
+ if (!is_file($fileName) || filesize($fileName)==0) {
+ $complete = false;
+ if ($this->yellow->system->get("coreDebugMode")>=1) {
+ echo "YellowInstall::checkServerComplete detected missing file:$fileName<br />\n";
+ }
+ }
+ }
+ return $complete;
+ }
+
+ // Check web server write access
+ public function checkServerWrite() {
+ $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
+ return $this->yellow->system->save($fileName, array());
+ }
+
+ // Check web server htaccess file
+ public function checkServerHtaccess() {
+ list($name) = $this->yellow->toolbox->detectServerInformation();
+ return strtoloweru($name)!="apache" || is_file(".htaccess");
+ }
+
+ // Check web server rewrite support
+ public function checkServerRewrite() {
+ $rewrite = true;
+ if (!$this->isServerBuiltin()) {
+ $curlHandle = curl_init();
+ list($scheme, $address, $base) = $this->yellow->lookup->getRequestInformation();
+ $location = $this->yellow->system->get("coreAssetLocation").$this->yellow->lookup->normaliseName($this->yellow->system->get("theme")).".css";
+ $url = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
+ curl_setopt($curlHandle, CURLOPT_URL, $url);
+ curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; YellowInstall/".YellowInstall::VERSION).")";
+ curl_setopt($curlHandle, CURLOPT_NOBODY, 1);
+ curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30);
+ curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, false);
+ curl_exec($curlHandle);
+ $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
+ if (PHP_VERSION_ID<80000) curl_close($curlHandle);
+ if ($statusCode!=200) {
+ $rewrite = false;
+ if ($this->yellow->system->get("coreDebugMode")>=1 && !$rewrite) {
+ echo "YellowInstall::checkServerRewrite detected failed url:$url<br />\n";
+ }
+ }
+ }
+ return $rewrite;
+ }
+
+ // Download available extension files
+ public function downloadExtensionsAvailable($settings, $extensions) {
+ $statusCode = 200;
+ if ($this->yellow->extension->isExisting("update")) {
+ $path = $this->yellow->system->get("coreWorkerDirectory");
+ $extensionsNow = 0;
+ $extensionsTotal = count($extensions);
+ $curlHandle = curl_init();
+ foreach ($extensions as $extension) {
+ echo "\rDownloading available extensions ".$this->getProgressPercent(++$extensionsNow, $extensionsTotal, 5, 95)."%... ";
+ $fileName = $path."install-".$this->yellow->lookup->normaliseName($extension, true, false, true).".bin";
+ if (is_file($fileName)) continue;
+ $url = $settings[$extension]->get("downloadUrl");
+ curl_setopt($curlHandle, CURLOPT_URL, $this->yellow->extension->get("update")->getExtensionDownloadUrl($url));
+ curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; YellowInstall/".YellowInstall::VERSION).")";
+ curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30);
+ $fileData = curl_exec($curlHandle);
+ $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
+ $redirectUrl = ($statusCode>=300 && $statusCode<=399) ? curl_getinfo($curlHandle, CURLINFO_REDIRECT_URL) : "";
+ if ($statusCode==0) {
+ $statusCode = 450;
+ $this->yellow->page->error($statusCode, "Can't connect to the update server!");
+ }
+ if ($statusCode!=450 && $statusCode!=200) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't download file '$url'!");
+ }
+ if ($statusCode==200 && !$this->yellow->toolbox->writeFile($fileName, $fileData)) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+ }
+ if ($this->yellow->system->get("coreDebugMode")>=2 && !is_string_empty($redirectUrl)) {
+ echo "YellowInstall::downloadExtensionsAvailable redirected to url:$redirectUrl<br />\n";
+ }
+ if ($this->yellow->system->get("coreDebugMode")>=2) {
+ echo "YellowInstall::downloadExtensionsAvailable status:$statusCode url:$url<br />\n";
+ }
+ if ($statusCode!=200) break;
+ }
+ if (PHP_VERSION_ID<80000) curl_close($curlHandle);
+ echo "\rDownloading available extensions 100%... done\n";
+ }
+ return $statusCode;
+ }
+
+ // Return available extensions required
+ public function getAvailableExtensionsRequired($settings, $option) {
+ $extensions = array();
+ if ($option=="medium") {
+ $text = "help highlight search toc";
+ $extensions = array_unique(array_filter($this->yellow->toolbox->getTextArguments($text), "strlen"));
+ } elseif ($option=="large") {
+ foreach ($settings as $key=>$value) {
+ if (preg_match("/language/i", $value->get("tag"))) continue;
+ array_push($extensions, strtoloweru($key));
+ }
+ }
+ return $extensions;
+ }
+
+ // Return language extensions required
+ public function getLanguageExtensionsRequired($fileData, $option) {
+ $extensions = array();
+ $languages = array();
+ foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+ if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
+ if (!is_string_empty($matches[1]) && !is_string_empty($matches[2]) && strposu($matches[1], "/")) {
+ $extension = basename($matches[1]);
+ $extension = $this->yellow->lookup->normaliseName($extension, true, true);
+ list($entry, $flags) = $this->yellow->toolbox->getTextList($matches[2], ",", 2);
+ $arguments = preg_split("/\s*,\s*/", trim($flags));
+ $language = array_pop($arguments);
+ if (preg_match("/^(.*)\.php$/", basename($entry))) {
+ $languages[$language] = $extension;
+ }
+ }
+ }
+ }
+ if ($option=="large") {
+ foreach ($languages as $language=>$extension) {
+ array_push($extensions, $extension);
+ }
+ } else {
+ foreach ($this->getSystemLanguages("en, de, sv") as $language) {
+ if (isset($languages[$language])) array_push($extensions, $languages[$language]);
+ }
+ $extensions = array_slice($extensions, 0, 3);
+ }
+ return $extensions;
+ }
+
+ // Return extensions installed
+ public function getExtensionsCount() {
+ $fileNameCurrent = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateInstalledFile");
+ $fileData = $this->yellow->toolbox->readFile($fileNameCurrent);
+ $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension");
+ return count($settings);
+ }
+
+ // Return system languages
+ public function getSystemLanguages($languagesDefault) {
+ $languages = array();
+ foreach (preg_split("/\s*,\s*/", $this->yellow->toolbox->getServer("HTTP_ACCEPT_LANGUAGE")) as $string) {
+ list($language, $dummy) = $this->yellow->toolbox->getTextList($string, ";", 2);
+ if (!is_string_empty($language)) array_push($languages, $language);
+ }
+ foreach (preg_split("/\s*,\s*/", $languagesDefault) as $language) {
+ if (!is_string_empty($language)) array_push($languages, $language);
+ }
+ return array_unique($languages);
+ }
+
+ // Return system settings
+ public function getSystemSettings($skipInstallation) {
+ $settings = array();
+ foreach ($_REQUEST as $key=>$value) {
+ if (!$this->yellow->system->isExisting($key)) continue;
+ if ($key=="password" || $key=="status") continue;
+ $settings[$key] = trim($value);
+ }
+ if ($this->yellow->system->get("sitename")=="Datenstrom Yellow") $settings["sitename"] = $this->yellow->toolbox->detectServerSitename();
+ if ($this->yellow->system->get("generateStaticUrl")=="auto" && getenv("URL")!==false) $settings["generateStaticUrl"] = getenv("URL");
+ if ($this->yellow->system->get("generateStaticUrl")=="auto" && $skipInstallation) $settings["generateStaticUrl"] = "http://localhost:8000/";
+ if ($this->yellow->system->get("coreTimezone")=="UTC") $settings["coreTimezone"] = $this->yellow->toolbox->detectServerTimezone();
+ if ($this->yellow->system->get("updateEventPending")=="none") {
+ $settings["updateEventPending"] = "website/install";
+ } else {
+ $themeStandard = ",".$this->yellow->system->get("theme")."/install";
+ $settings["updateEventPending"] = $this->yellow->system->get("updateEventPending").$themeStandard;
+ }
+ $settings["updateCurrentRelease"] = YellowCore::RELEASE;
+ return $settings;
+ }
+
+ // Return raw data for install page
+ public function getRawDataInstall() {
+ $languages = $this->yellow->toolbox->enumerate("language");
+ $language = $this->yellow->toolbox->detectBrowserLanguage($languages, $this->yellow->system->get("language"));
+ $this->yellow->language->set($language);
+ $rawData = "---\nTitle:".$this->yellow->language->getText("installTitle")."\nLanguage:$language\nNavigation:navigation\nHeader:none\nFooter:none\nSidebar:none\n---\n";
+ $rawData .= "<form class=\"install-form\" action=\"".$this->yellow->page->getLocation(true)."\" method=\"post\">\n";
+ $rawData .= "<p><label for=\"author\">".$this->yellow->language->getText("editSignupName")."</label><br /><input class=\"form-control\" type=\"text\" maxlength=\"64\" name=\"author\" id=\"author\" value=\"\" /></p>\n";
+ $rawData .= "<p><label for=\"email\">".$this->yellow->language->getText("editSignupEmail")."</label><br /><input class=\"form-control\" type=\"text\" maxlength=\"64\" name=\"email\" id=\"email\" value=\"\" /></p>\n";
+ $rawData .= "<p><label for=\"password\">".$this->yellow->language->getText("editSignupPassword")."</label><br /><input class=\"form-control\" type=\"password\" maxlength=\"64\" name=\"password\" id=\"password\" value=\"\" /></p>\n";
+ $rawData .= "<p>".$this->yellow->language->getText("installLanguage")."</p>\n<p>";
+ foreach ($languages as $language) {
+ $checked = $language==$this->yellow->language->language ? " checked=\"checked\"" : "";
+ $rawData .= "<label for=\"{$language}-language\"><input type=\"radio\" name=\"language\" id=\"{$language}-language\" value=\"$language\"$checked> ".$this->yellow->language->getTextHtml("languageDescription", $language)."</label><br />";
+ }
+ $rawData .= "</p>\n";
+ $rawData .= "<p>".$this->yellow->language->getText("installExtension")."</p>\n<p>";
+ foreach (array("website", "wiki", "blog") as $extension) {
+ $checked = $extension=="website" ? " checked=\"checked\"" : "";
+ $rawData .= "<label for=\"{$extension}-extension\"><input type=\"radio\" name=\"extension\" id=\"{$extension}-extension\" value=\"$extension\"$checked> ".$this->yellow->language->getTextHtml("installExtension".ucfirst($extension))."</label><br />";
+ }
+ $rawData .= "</p>\n";
+ $rawData .= "<input class=\"btn\" type=\"submit\" value=\"".$this->yellow->language->getText("installButton")."\" />\n";
+ $rawData .= "<input type=\"hidden\" name=\"status\" value=\"install\" />\n";
+ $rawData .= "</form>\n";
+ return $rawData;
+ }
+
+ // Return progress in percent
+ public function getProgressPercent($now, $total, $increments, $max) {
+ $max = intval($max/$increments) * $increments;
+ $percent = intval(($max/$total) * $now);
+ if ($increments>1) $percent = intval($percent/$increments) * $increments;
+ return min($max, $percent);
+ }
+
+ // Check if running built-in web server
+ public function isServerBuiltin() {
+ list($name) = $this->yellow->toolbox->detectServerInformation();
+ return strtoloweru($name)=="built-in";
+ }
+}
diff --git a/system/workers/markdown.php b/system/workers/markdown.php
@@ -0,0 +1,4109 @@
+<?php
+// Markdown extension, https://github.com/annaesvensson/yellow-markdown
+
+class YellowMarkdown {
+ const VERSION = "0.9.8";
+ 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);
+ $text = $markdown->transform($text);
+ $text = $this->yellow->lookup->normaliseData($text, "html");
+ return $text;
+ }
+}
+
+// PHP Markdown Lib
+// Copyright (c) 2004-2021 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.1";
+
+ /**
+ * 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|details|summary';
+
+ // 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: 
+ // 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|details|summary';
+
+ /**
+ * 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_quote =& $matches[6];
+ $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_quote) {
+ $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: 
+ // 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_quote =& $matches[6];
+ $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_quote) {
+ $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 function __construct($yellow, $page) {
+ $this->yellow = $yellow;
+ $this->page = $page;
+ $this->idAttributes = array();
+ $this->url_filter_func = function($url) use ($yellow, $page) {
+ return $yellow->lookup->normaliseLocation($url, $page->getPage("main")->location);
+ };
+ $this->span_gamut += array("doStrikethrough" => 55);
+ $this->block_gamut += array("doCollapsibleBlocks" => 65);
+ $this->block_gamut += array("doGeneralBlocks" => 75);
+ $this->document_gamut += array("doFootnotesLinks" => 55);
+ $this->escape_chars .= "~?";
+ parent::__construct();
+ }
+
+ // Handle striketrough
+ public function doStrikethrough($text) {
+ $parts = preg_split("/(?<![~])(~~)(?![~])/", $text, -1, 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]+)/", array($this, "_doAutoLinks_email_callback"), $text);
+ return $text;
+ }
+
+ // Handle shortcuts, block style
+ public function _doAutoLinks_shortcutBlock_callback($matches) {
+ $output = $this->page->parseContentElement($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->parseContentElement($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->parseContentElement("", $matches[1], "", "symbol");
+ return is_null($output) ? $matches[0] : $this->hashPart($output);
+ }
+
+ // Handle fenced code blocks
+ public function _doFencedCodeBlocks_callback($matches) {
+ $name = $this->getBlockName($matches[2], $matches[3]);
+ $text = $matches[4];
+ $attributes = $matches[3];
+ $output = $this->page->parseContentElement($name, $text, $attributes, "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 (is_string_empty($attr) && $level>=2) $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 (is_string_empty($attr) && $level>=2) $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 (!is_string_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 (!is_string_empty($alt)) $output .= " alt=\"".$this->encodeAttribute($alt)."\"";
+ if (!is_string_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 blockquotes, CommonMark compatible
+ public function doBlockQuotes($text) {
+ return preg_replace_callback("/((?>^[ ]*>[ ]?.+\n(.+\n)*)+)/m", array($this, "_doBlockQuotes_callback"), $text);
+ }
+
+ // Handle collapsible block elements
+ public function doCollapsibleBlocks($text) {
+ return preg_replace_callback("/((?>^[ ]*\?[ ]?.+\n(.+\n)*)+)/m", array($this, "_doCollapsibleBlocks_callback"), $text);
+ }
+
+ // Handle collapsible block elements over multiple lines
+ public function _doCollapsibleBlocks_callback($matches) {
+ $name = $attributes = $attr = "";
+ $text = preg_replace("/^[ ]*\?[ ]?/m", "", $matches[1]);
+ if (preg_match("/^[ ]*".$this->id_class_attr_catch_re."[ ]*\n([\S\s]*)$/m", $text, $parts)) {
+ $name = $this->getBlockName("", $parts[1]);
+ $text = $parts[2];
+ $attributes = $parts[1];
+ $attr = $this->doExtraAttributes("details", $parts[1]);
+ }
+ if (!is_string_empty($text)) {
+ $output = $this->page->parseContentElement($name, $text, $attributes, "collapsible");
+ if (is_null($output)) {
+ $summary = "";
+ if (preg_match("/^(.*?)\n\n(.*)$/s", $text, $parts)) {
+ $summary = $parts[1];
+ $text = $parts[2];
+ }
+ $output = "<details$attr>\n";
+ if (!is_string_empty($summary)) $output .= "<summary>".$this->runSpanGamut($summary)."</summary>\n";
+ $output .= $this->runBlockGamut($text)."\n</details>";
+ }
+ } else {
+ $output = "<details$attr></details>";
+ }
+ return "\n".$this->hashBlock($output)."\n\n";
+ }
+
+ // Handle general block elements
+ public function doGeneralBlocks($text) {
+ return preg_replace_callback("/((?>^[ ]*!(?!\[)[ ]?.+\n(.+\n)*)+)/m", array($this, "_doGeneralBlocks_callback"), $text);
+ }
+
+ // Handle general block elements over multiple lines
+ public function _doGeneralBlocks_callback($matches) {
+ $name = $attributes = $attr = "";
+ $text = preg_replace("/^[ ]*![ ]?/m", "", $matches[1]);
+ if (preg_match("/^[ ]*".$this->id_class_attr_catch_re."[ ]*\n([\S\s]*)$/m", $text, $parts)) {
+ $name = $this->getBlockName("", $parts[1]);
+ $text = $parts[2];
+ $attributes = $parts[1];
+ $attr = $this->doExtraAttributes("div", $parts[1]);
+ }
+ if (!is_string_empty($text)) {
+ $output = $this->page->parseContentElement($name, $text, $attributes, "general");
+ if (is_null($output)) {
+ $output = "<div$attr>\n".$this->runBlockGamut($text)."\n</div>";
+ }
+ } else {
+ $output = "<div$attr></div>";
+ }
+ return "\n".$this->hashBlock($output)."\n\n";
+ }
+
+ // Handle footnotes links, normalise ids and links
+ public function doFootnotesLinks($text) {
+ if (!is_null($this->footnotes_assembled)) {
+ $callbackId = function ($matches) {
+ $id = str_replace(":", "-", $matches[2]);
+ return "<$matches[1] id=\"$id\" $matches[3]>";
+ };
+ $text = preg_replace_callback("/<(li|sup) id=\"(fn:\d+)\"(.*?)>/", $callbackId, $text);
+ $text = preg_replace_callback("/<(li|sup) id=\"(fnref\d*:\d+)\"(.*?)>/", $callbackId, $text);
+ $callbackHref = function ($matches) {
+ $href = $this->page->base.$this->page->location.str_replace(":", "-", $matches[2]);
+ return "<$matches[1] href=\"$href\" $matches[3]>";
+ };
+ $text = preg_replace_callback("/<(a) href=\"(#fn:\d+)\"(.*?)>/", $callbackHref, $text);
+ $text = preg_replace_callback("/<(a) href=\"(#fnref\d*:\d+)\"(.*?)>/", $callbackHref, $text);
+ }
+ return $text;
+ }
+
+ // Return suitable name for block element
+ public function getBlockName($language, $attributes) {
+ if (!is_string_empty($language)) {
+ $name = ltrim($language, ".");
+ } else {
+ $name = "";
+ foreach (explode(" ", $attributes) as $token) {
+ if (substru($token, 0, 1)==".") { $name = substru($token, 1); break; }
+ }
+ }
+ return $name;
+ }
+
+ // 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\"";
+ } else {
+ $attr = " id=\"error-duplicate-heading\"";
+ }
+ return $attr;
+ }
+}
diff --git a/system/workers/serve.php b/system/workers/serve.php
@@ -0,0 +1,61 @@
+<?php
+// Serve extension, https://github.com/annaesvensson/yellow-serve
+
+class YellowServe {
+ const VERSION = "0.9.1";
+ public $yellow; // access to API
+
+ // Handle initialisation
+ public function onLoad($yellow) {
+ $this->yellow = $yellow;
+ }
+
+ // Handle command
+ public function onCommand($command, $text) {
+ switch ($command) {
+ case "serve": $statusCode = $this->processCommandServe($command, $text); break;
+ default: $statusCode = 0;
+ }
+ return $statusCode;
+ }
+
+ // Handle command help
+ public function onCommandHelp() {
+ return "serve [url]";
+ }
+
+ // Process command to start web server
+ public function processCommandServe($command, $text) {
+ list($url) = $this->yellow->toolbox->getTextArguments($text);
+ if (is_string_empty($url)) $url = "http://localhost:8000/";
+ list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($url);
+ if ($scheme=="http" && !is_string_empty($address) && is_string_empty($base)) {
+ if (!preg_match("/\:\d+$/", $address)) $address .= ":8000";
+ if ($this->checkServerSettings("$scheme://$address/")) {
+ echo "Starting web server. Open a web browser and go to $scheme://$address/\n";
+ echo "Press Ctrl+C to quit...\n";
+ exec(PHP_BINARY." -S $address yellow.php 2>&1", $outputLines, $returnStatus);
+ $statusCode = $returnStatus!=0 ? 500 : 200;
+ if ($statusCode!=200) {
+ $output = !is_array_empty($outputLines) ? end($outputLines) : "Please check arguments!";
+ if (preg_match("/^\[(.*?)\]\s*(.*)$/", $output, $matches)) $output = $matches[2];
+ echo "ERROR starting web server: $output\n";
+ }
+ } else {
+ $statusCode = 400;
+ $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
+ echo "ERROR starting web server: Please configure `CoreServerUrl: auto` in file '$fileName'!\n";
+ }
+ } else {
+ $statusCode = 400;
+ echo "Yellow $command: Invalid arguments\n";
+ }
+ return $statusCode;
+ }
+
+ // Check server settings
+ public function checkServerSettings($url) {
+ return $this->yellow->system->get("coreServerUrl")=="auto" ||
+ $this->yellow->system->get("coreServerUrl")==$url;
+ }
+}
diff --git a/system/workers/stockholm.php b/system/workers/stockholm.php
@@ -0,0 +1,22 @@
+<?php
+// Stockholm extension, https://github.com/annaesvensson/yellow-stockholm
+
+class YellowStockholm {
+ const VERSION = "0.9.6";
+ 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("coreExtensionDirectory").$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") {
+ $this->yellow->system->save($fileName, array("theme" => $this->yellow->system->getDifferent("theme")));
+ }
+ }
+}
diff --git a/system/workers/update.php b/system/workers/update.php
@@ -0,0 +1,924 @@
+<?php
+// Update extension, https://github.com/annaesvensson/yellow-update
+
+class YellowUpdate {
+ const VERSION = "0.9.9";
+ const PRIORITY = "2";
+ public $yellow; // access to API
+ public $extensions; // number of extensions
+
+ // Handle initialisation
+ public function onLoad($yellow) {
+ $this->yellow = $yellow;
+ $this->yellow->system->setDefault("updateCurrentRelease", "none");
+ $this->yellow->system->setDefault("updateAvailableUrl", "auto");
+ $this->yellow->system->setDefault("updateAvailableFile", "update-available.ini");
+ $this->yellow->system->setDefault("updateInstalledFile", "update-installed.ini");
+ $this->yellow->system->setDefault("updateExtensionFile", "extension.ini");
+ $this->yellow->system->setDefault("updateEventPending", "none");
+ $this->yellow->system->setDefault("updateEventDaily", "0");
+ $this->yellow->system->setDefault("updateTrashTimeout", "7776660");
+ }
+
+ // Handle update
+ public function onUpdate($action) {
+ if ($action=="clean" || $action=="daily") {
+ $statusCode = 200;
+ $path = $this->yellow->system->get("coreExtensionDirectory");
+ foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.download$/", false, false) as $entry) {
+ if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500;
+ }
+ if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!");
+ $statusCode = 200;
+ $path = $this->yellow->system->get("coreTrashDirectory");
+ foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, false) as $entry) {
+ $expire = $this->yellow->toolbox->getFileDeleted($entry) + $this->yellow->system->get("updateTrashTimeout");
+ if ($expire<=time() && !$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500;
+ }
+ foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, true) as $entry) {
+ $expire = $this->yellow->toolbox->getFileDeleted($entry) + $this->yellow->system->get("updateTrashTimeout");
+ if ($expire<=time() && !$this->yellow->toolbox->deleteDirectory($entry)) $statusCode = 500;
+ }
+ if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!");
+ }
+ }
+
+ // Handle request
+ public function onRequest($scheme, $address, $base, $location, $fileName) {
+ return $this->processRequestPending($scheme, $address, $base, $location, $fileName);
+ }
+
+ // Handle command
+ public function onCommand($command, $text) {
+ $statusCode = $this->processCommandPending();
+ if ($statusCode==0) {
+ switch ($command) {
+ case "about": $statusCode = $this->processCommandAbout($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() {
+ return array("about [extension]", "install [extension]", "uninstall [extension]", "update [extension]");
+ }
+
+ // Handle page content element
+ public function onParseContentElement($page, $name, $text, $attributes, $type) {
+ $output = null;
+ if ($name=="about" && $type=="inline") {
+ list($dummy, $settingsCurrent) = $this->getExtensionSettings(true);
+ $output = "Datenstrom Yellow ".YellowCore::RELEASE."<br />\n";
+ foreach ($settingsCurrent as $key=>$value) {
+ $output .= ucfirst($key)." ".$value->get("version")."<br />\n";
+ }
+ }
+ return $output;
+ }
+
+ // Process command to show current version
+ public function processCommandAbout($command, $text) {
+ $statusCode = 200;
+ $extensions = $this->getExtensionsFromText($text);
+ if (!is_array_empty($extensions)) {
+ list($statusCode, $settings) = $this->getExtensionAboutInformation($extensions);
+ if ($statusCode==200) {
+ foreach ($settings as $key=>$value) {
+ echo ucfirst($key)." ".$value->get("version")." - ".$this->getExtensionDescription($key, $value)."\n";
+ if ($value->isExisting("documentationUrl")) echo "Read more at ".$value->get("documentationUrl")."\n";
+ }
+ }
+ if ($statusCode>=400) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n";
+ } else {
+ echo "Datenstrom Yellow ".YellowCore::RELEASE."\n";
+ list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true);
+ foreach ($settingsCurrent as $key=>$value) {
+ echo ucfirst($key)." ".$value->get("version")."\n";
+ }
+ }
+ return $statusCode;
+ }
+
+ // Process command to install extensions
+ public function processCommandInstall($command, $text) {
+ $extensions = $this->getExtensionsFromText($text);
+ if (!is_array_empty($extensions)) {
+ $this->extensions = 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->errorMessage."\n";
+ echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated";
+ echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." installed\n";
+ } else {
+ list($statusCode, $settingsAvailable) = $this->getExtensionSettings(false);
+ foreach ($settingsAvailable as $key=>$value) {
+ echo ucfirst($key)." - ".$this->getExtensionDescription($key, $value)."\n";
+ }
+ if ($statusCode!=200) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n";
+ }
+ return $statusCode;
+ }
+
+ // Process command to uninstall extensions
+ public function processCommandUninstall($command, $text) {
+ $extensions = $this->getExtensionsFromText($text);
+ if (!is_array_empty($extensions)) {
+ $this->extensions = 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->errorMessage."\n";
+ echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated";
+ echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." uninstalled\n";
+ } else {
+ list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true);
+ foreach ($settingsCurrent as $key=>$value) {
+ echo ucfirst($key)." - ".$this->getExtensionDescription($key, $value)."\n";
+ }
+ if ($statusCode!=200) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n";
+ }
+ return $statusCode;
+ }
+
+ // Process command to update website
+ public function processCommandUpdate($command, $text) {
+ $extensions = $this->getExtensionsFromText($text);
+ if (!is_array_empty($extensions)) {
+ list($statusCode, $settings) = $this->getExtensionUpdateInformation($extensions);
+ if ($statusCode!=200 || !is_array_empty($settings)) {
+ $this->extensions = 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->errorMessage."\n";
+ echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated";
+ echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." updated\n";
+ } else {
+ echo "Your website is up to date\n";
+ }
+ } else {
+ list($statusCode, $settings) = $this->getExtensionUpdateInformation(array("all"));
+ if (!is_array_empty($settings)) {
+ foreach ($settings as $key=>$value) {
+ echo ucfirst($key)." ".$value->get("version")."\n";
+ }
+ echo "Yellow $command: Updates are available. Please type 'php yellow.php update all'.\n";
+ } elseif ($statusCode!=200) {
+ echo "ERROR updating files: ".$this->yellow->page->errorMessage."\n";
+ } else {
+ echo "Your website is up to date\n";
+ }
+ }
+ return $statusCode;
+ }
+
+ // Process command for pending events
+ public function processCommandPending() {
+ $statusCode = 0;
+ $this->extensions = 0;
+ $this->updatePatchPending();
+ $this->updateEventPending();
+ $statusCode = $this->updateExtensionPending();
+ if ($statusCode==303) {
+ echo "Yellow detected ZIP file".($this->extensions!=1 ? "s" : "");
+ echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." installed. Please run command again.\n";
+ }
+ return $statusCode;
+ }
+
+ // Process request for pending events
+ public function processRequestPending($scheme, $address, $base, $location, $fileName) {
+ $statusCode = 0;
+ if ($this->yellow->lookup->isContentFile($fileName)) {
+ $this->updatePatchPending();
+ $this->updateEventPending();
+ $statusCode = $this->updateExtensionPending();
+ if ($statusCode==303) {
+ $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
+ $statusCode = $this->yellow->sendStatus(303, $location);
+ }
+ }
+ return $statusCode;
+ }
+
+ // Download extensions
+ public function downloadExtensions($settings) {
+ $statusCode = 200;
+ $path = $this->yellow->system->get("coreExtensionDirectory");
+ 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 ($statusCode==200 && !$this->yellow->toolbox->writeFile($fileName.".download", $fileData)) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+ }
+ if ($statusCode!=200) 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.".download", $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();
+ $this->yellow->page->setHeader("Clear-Site-Data", "cache");
+ $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 ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUpdate::updateExtensionArchive file:$path<br />\n";
+ $pathBase = "";
+ if (preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1];
+ $fileData = $zip->getFromName($pathBase.$this->yellow->system->get("updateExtensionFile"));
+ $settings = $this->yellow->toolbox->getTextSettings($fileData, "");
+ list($extension, $version, $newModified, $oldModified) = $this->getExtensionInformation($settings);
+ if (!is_string_empty($extension) && !is_string_empty($version)) {
+ $statusCode = max($statusCode, $this->updateExtensionSettings($extension, $action, $fileData));
+ $paths = $this->getExtensionDirectories($zip, $pathBase);
+ foreach ($this->getExtensionFileNames($settings) as $fileName) {
+ list($entry, $flags) = $this->yellow->toolbox->getTextList($settings[$fileName], ",", 2);
+ if (!$this->yellow->lookup->isContentFile($fileName)) {
+ $fileNameSource = $pathBase.$entry;
+ $fileData = $zip->getFromName($fileNameSource);
+ $lastModified = $this->yellow->toolbox->getFileModified($fileName);
+ $statusCode = max($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, $paths, $page);
+ $fileData = $zip->getFromName($fileNameSource);
+ $lastModified = $this->yellow->toolbox->getFileModified($fileNameDestination);
+ $statusCode = max($statusCode, $this->updateExtensionFile($fileNameDestination, $fileData,
+ $newModified, $oldModified, $lastModified, $flags, $extension));
+ }
+ }
+ }
+ $statusCode = max($statusCode, $this->updateExtensionNotification($extension, $action));
+ $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'");
+ ++$this->extensions;
+ } else {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't detect file '$path'!");
+ }
+ $zip->close();
+ } else {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "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->lookup->normalisePath($fileName);
+ if ($this->yellow->lookup->isValidFile($fileName)) {
+ $create = $update = $delete = false;
+ if (preg_match("/create/i", $flags) && !is_file($fileName) && !is_string_empty($fileData)) $create = true;
+ if (preg_match("/update/i", $flags) && is_file($fileName) && !is_string_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->writeFile($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->writeFile($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 ($this->yellow->system->get("coreDebugMode")>=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;
+ }
+
+ // Update pending patches
+ public function updatePatchPending() {
+ $fileName = $this->yellow->system->get("coreWorkerDirectory")."updatepatch.bin";
+ if (is_file($fileName)) {
+ if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUpdate::updatePatchPending file:$fileName<br />\n";
+ if (!$this->yellow->extension->isExisting("updatepatch")) {
+ require_once($fileName);
+ $this->yellow->extension->register("updatepatch", "YellowUpdatePatch");
+ }
+ if ($this->yellow->extension->isExisting("updatepatch")) {
+ $value = $this->yellow->extension->data["updatepatch"];
+ if (method_exists($value["object"], "onLoad")) $value["object"]->onLoad($this->yellow);
+ if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("patch");
+ }
+ unset($this->yellow->extension->data["updatepatch"]);
+ if (function_exists("opcache_reset")) opcache_reset();
+ $this->yellow->page->setHeader("Clear-Site-Data", "cache");
+ if (!$this->yellow->toolbox->deleteFile($fileName)) {
+ $this->yellow->toolbox->log("error", "Can't delete file '$fileName'!");
+ }
+ }
+ }
+
+ // Update pending events
+ public function updateEventPending() {
+ if ($this->yellow->system->get("updateCurrentRelease")!="none") {
+ if ($this->yellow->system->get("updateCurrentRelease")!=YellowCore::RELEASE) {
+ $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
+ if (!$this->yellow->system->save($fileName, array("updateCurrentRelease" => YellowCore::RELEASE))) {
+ $this->yellow->toolbox->log("error", "Can't write file '$fileName'!");
+ } else {
+ list($name, $version, $os) = $this->yellow->toolbox->detectServerInformation();
+ $product = "Datenstrom Yellow ".YellowCore::RELEASE;
+ $this->yellow->toolbox->log("info", "Update $product, PHP ".PHP_VERSION.", $name $version, $os");
+ }
+ }
+ if ($this->yellow->system->get("updateEventPending")!="none") {
+ foreach (preg_split("/\s*,\s*/", $this->yellow->system->get("updateEventPending")) as $token) {
+ list($extension, $action) = $this->yellow->toolbox->getTextList($token, "/", 2);
+ if ($action!="uninstall") {
+ $this->updateSystemSettings($extension, $action);
+ $this->updateLanguageSettings($extension, $action);
+ if ($this->yellow->extension->isExisting($extension)) {
+ $value = $this->yellow->extension->data[$extension];
+ if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate($action);
+ }
+ }
+ }
+ $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
+ if (!$this->yellow->system->save($fileName, array("updateEventPending" => "none"))) {
+ $this->yellow->toolbox->log("error", "Can't write file '$fileName'!");
+ }
+ }
+ if ($this->yellow->system->get("updateEventDaily")<=time()) {
+ foreach ($this->yellow->extension->data as $key=>$value) {
+ if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("daily");
+ }
+ $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
+ if (!$this->yellow->system->save($fileName, array("updateEventDaily" => $this->getTimestampDaily()))) {
+ $this->yellow->toolbox->log("error", "Can't write file '$fileName'!");
+ }
+ }
+ }
+ }
+
+ // Update pending extensions
+ public function updateExtensionPending() {
+ $statusCode = 0;
+ $path = $this->yellow->system->get("coreExtensionDirectory");
+ if (!is_array_empty($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", false, false))) {
+ $statusCode = $this->updateExtensions("install");
+ if ($statusCode==200) $statusCode = 303;
+ if ($statusCode>=400) {
+ $this->yellow->toolbox->log("error", $this->yellow->page->errorMessage);
+ $this->yellow->page->statusCode = 0;
+ $this->yellow->page->errorMessage = "";
+ $statusCode = 303;
+ }
+ }
+ return $statusCode;
+ }
+
+ // Update extension settings
+ public function updateExtensionSettings($extension, $action, $text = "") {
+ $statusCode = 200;
+ $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateInstalledFile");
+ $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName);
+ if ($action=="install" || $action=="update") {
+ $settingsCurrent = $this->yellow->toolbox->getTextSettings($fileData, "extension");
+ $settingsCurrent[$extension] = new YellowArray();
+ $block = $this->yellow->toolbox->getTextSettings($text, "");
+ foreach ($block as $key=>$value) $settingsCurrent[$extension][$key] = $value;
+ $settingsCurrent->uksort("strnatcasecmp");
+ $fileDataNew = "";
+ foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+ if (preg_match("/^\#/", $line)) $fileDataNew = $line;
+ break;
+ }
+ foreach ($settingsCurrent as $extension=>$block) {
+ if (!is_string_empty($fileDataNew)) $fileDataNew .= "\n";
+ foreach ($block as $key=>$value) {
+ $fileDataNew .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n";
+ }
+ }
+ } elseif ($action=="uninstall") {
+ $fileDataNew = $this->yellow->toolbox->unsetTextSettings($fileData, "extension", $extension);
+ }
+ if ($fileData!=$fileDataNew && !$this->yellow->toolbox->writeFile($fileName, $fileDataNew)) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+ }
+ return $statusCode;
+ }
+
+ // Update system settings
+ public function updateSystemSettings($extension, $action) {
+ $statusCode = 200;
+ $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
+ $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName);
+ if (!is_string_empty($extension)) {
+ $regex = "/^".ucfirst($extension)."[A-Z]+/";
+ if ($action=="install" || $action=="update") {
+ $fileDataStart = $fileDataSettings = "";
+ $settings = new YellowArray();
+ $settings->exchangeArray($this->yellow->system->settingsDefaults->getArrayCopy());
+ foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+ if (preg_match("/^\#/", $line)) {
+ if (is_string_empty($fileDataStart)) $fileDataStart = $line."\n";
+ continue;
+ }
+ if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
+ if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
+ if (!preg_match($regex, $matches[1]) || $settings->isExisting($matches[1])) {
+ $settings[$matches[1]] = $matches[2];
+ }
+ }
+ }
+ }
+ foreach ($settings as $key=>$value) {
+ $fileDataSettings .= ucfirst($key).(is_string_empty($value) ? ":\n" : ": $value\n");
+ }
+ $fileDataNew = $fileDataStart.$fileDataSettings;
+ } elseif ($action=="uninstall") {
+ $fileDataNew = "";
+ foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+ if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
+ if (!is_string_empty($matches[1]) && preg_match($regex, $matches[1])) continue;
+ }
+ $fileDataNew .= $line;
+ }
+ }
+ }
+ if ($fileData!=$fileDataNew && !$this->yellow->toolbox->writeFile($fileName, $fileDataNew)) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+ }
+ return $statusCode;
+ }
+
+ // Update language settings
+ public function updateLanguageSettings($extension, $action) {
+ $statusCode = 200;
+ $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreLanguageFile");
+ $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName);
+ if (!is_string_empty($extension) && ucfirst($extension)!="Language") {
+ $regex = "/^".ucfirst($extension)."[A-Z]+/";
+ if ($action=="install" || $action=="update") {
+ $fileDataStart = $fileDataSettings = $language = "";
+ $settings = new YellowArray();
+ foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+ if (preg_match("/^\#/", $line)) {
+ if (is_string_empty($fileDataStart)) $fileDataStart = $line."\n";
+ continue;
+ }
+ if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
+ if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
+ if (lcfirst($matches[1])=="language") {
+ if (!is_array_empty($settings)) {
+ if (!is_string_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;
+ $settings["languageLocale"] = "n/a";
+ $settings["languageDescription"] = "n/a";
+ $settings["languageTranslator"] = "Unknown";
+ foreach ($this->yellow->language->settingsDefaults as $key=>$value) {
+ $require = preg_match("/^([a-z]*)[A-Z]+/", $key, $tokens) ? $tokens[1] : "core";
+ if ($require=="language") $require = "core";
+ if ($this->yellow->extension->isExisting($require)) {
+ if ($this->yellow->language->isText($key, $language)) {
+ $settings[$key] = $this->yellow->language->getText($key, $language);
+ } else {
+ $settings[$key] = $this->yellow->language->getText($key, "en");
+ }
+ }
+ }
+ }
+ if (!is_string_empty($language)) {
+ if (!preg_match($regex, $matches[1]) || $settings->isExisting($matches[1])) {
+ $settings[$matches[1]] = $matches[2];
+ }
+ }
+ }
+ }
+ }
+ if (!is_array_empty($settings)) {
+ if (!is_string_empty($fileDataSettings)) $fileDataSettings .= "\n";
+ foreach ($settings as $key=>$value) {
+ $fileDataSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n";
+ }
+ }
+ $fileDataNew = $fileDataStart.$fileDataSettings;
+ } elseif ($action=="uninstall") {
+ $fileDataNew = "";
+ foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+ if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
+ if (!is_string_empty($matches[1]) && preg_match($regex, $matches[1])) continue;
+ }
+ $fileDataNew .= $line;
+ }
+ }
+ }
+ if ($fileData!=$fileDataNew && !$this->yellow->toolbox->writeFile($fileName, $fileDataNew)) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+ }
+ return $statusCode;
+ }
+
+ // Update extension notification
+ public function updateExtensionNotification($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);
+ }
+ $updateEventPending = $this->yellow->system->get("updateEventPending");
+ if ($updateEventPending=="none") $updateEventPending = "";
+ if (!is_string_empty($updateEventPending)) $updateEventPending .= ",";
+ $updateEventPending .= "$extension/$action";
+ $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
+ if (!$this->yellow->system->save($fileName, array("updateEventPending" => $updateEventPending))) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+ }
+ return $statusCode;
+ }
+
+ // Remove extensions
+ public function removeExtensions($settings) {
+ $statusCode = 200;
+ if (function_exists("opcache_reset")) opcache_reset();
+ $this->yellow->page->setHeader("Clear-Site-Data", "cache");
+ foreach ($settings as $extension=>$block) {
+ $statusCode = max($statusCode, $this->removeExtensionArchive($extension, "uninstall", $block));
+ }
+ return $statusCode;
+ }
+
+ // Remove extension archive
+ public function removeExtensionArchive($extension, $action, $settings) {
+ $statusCode = 200;
+ $fileNames = $this->getExtensionFileNames($settings, true);
+ if (!is_array_empty($fileNames)) {
+ $statusCode = max($statusCode, $this->updateExtensionNotification($extension, $action));
+ foreach ($fileNames as $fileName) {
+ $statusCode = max($statusCode, $this->removeExtensionFile($fileName));
+ }
+ if ($statusCode==200) {
+ $statusCode = max($statusCode, $this->updateExtensionSettings($extension, $action));
+ $statusCode = max($statusCode, $this->updateSystemSettings($extension, $action));
+ $statusCode = max($statusCode, $this->updateLanguageSettings($extension, $action));
+ }
+ $version = $settings->get("version");
+ $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'");
+ ++$this->extensions;
+ } else {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Please delete extension '$extension' manually!");
+ }
+ return $statusCode;
+ }
+
+ // Remove extension file
+ public function removeExtensionFile($fileName) {
+ $statusCode = 200;
+ $fileName = $this->yellow->lookup->normalisePath($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 ($this->yellow->system->get("coreDebugMode")>=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 about information
+ public function getExtensionAboutInformation($extensions) {
+ $settings = array();
+ list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true);
+ $settingsCurrent["Datenstrom Yellow"] = new YellowArray();
+ $settingsCurrent["Datenstrom Yellow"]["version"] = YellowCore::RELEASE;
+ $settingsCurrent["Datenstrom Yellow"]["description"] = "Datenstrom Yellow is for people who make small websites.";
+ $settingsCurrent["Datenstrom Yellow"]["documentationUrl"] = "https://datenstrom.se/yellow/";
+ foreach ($extensions as $extension) {
+ $found = false;
+ if (strtoloweru($extension)=="yellow") $extension = "Datenstrom Yellow";
+ 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'!");
+ }
+ }
+ return array($statusCode, $settings);
+ }
+
+ // Return extension install information
+ public function getExtensionInstallInformation($extensions) {
+ $settings = array();
+ list($statusCodeCurrent, $settingsCurrent) = $this->getExtensionSettings(true);
+ list($statusCodeAvailable, $settingsAvailable) = $this->getExtensionSettings(false);
+ $statusCode = max($statusCodeCurrent, $statusCodeAvailable);
+ foreach ($extensions as $extension) {
+ $found = false;
+ foreach ($settingsAvailable as $key=>$value) {
+ if (strtoloweru($key)==strtoloweru($extension)) {
+ if (!$settingsCurrent->isExisting($key)) $settings[$key] = $settingsAvailable[$key];
+ $found = true;
+ break;
+ }
+ }
+ if (!$found) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't find extension '$extension'!");
+ }
+ }
+ return array($statusCode, $settings);
+ }
+
+ // Return extension about information
+ public function getExtensionUninstallInformation($extensions, $extensionsProtected = "") {
+ $settings = array();
+ list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true);
+ 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(true);
+ list($statusCodeAvailable, $settingsAvailable) = $this->getExtensionSettings(false);
+ $statusCode = max($statusCodeCurrent, $statusCodeAvailable);
+ if (in_array("all", $extensions)) {
+ foreach ($settingsCurrent as $key=>$value) {
+ if ($settingsAvailable->isExisting($key)) {
+ $versionCurrent = $settingsCurrent[$key]->get("version");
+ $versionAvailable = $settingsAvailable[$key]->get("version");
+ if (strnatcasecmp($versionCurrent, $versionAvailable)<0) {
+ $settings[$key] = $settingsAvailable[$key];
+ }
+ }
+ }
+ } else {
+ foreach ($extensions as $extension) {
+ $found = false;
+ foreach ($settingsCurrent as $key=>$value) {
+ if (strtoloweru($key)==strtoloweru($extension) && $settingsAvailable->isExisting($key)) {
+ $versionCurrent = $settingsCurrent[$key]->get("version");
+ $versionAvailable = $settingsAvailable[$key]->get("version");
+ if (strnatcasecmp($versionCurrent, $versionAvailable)<0) {
+ $settings[$key] = $settingsAvailable[$key];
+ }
+ $found = true;
+ break;
+ }
+ }
+ if (!$found) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't find extension '$extension'!");
+ }
+ }
+ }
+ return array($statusCode, $settings);
+ }
+
+ // Return extension settings
+ public function getExtensionSettings($current) {
+ $statusCode = 200;
+ $settings = array();
+ if ($current) {
+ $fileNameCurrent = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateInstalledFile");
+ $fileData = $this->yellow->toolbox->readFile($fileNameCurrent);
+ $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension");
+ foreach ($settings->getArrayCopy() 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 {
+ $fileNameAvailable = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateAvailableFile");
+ $expire = $this->yellow->toolbox->getFileModified($fileNameAvailable) + 60*10;
+ if ($expire<=time()) {
+ $url = $this->yellow->system->get("updateAvailableUrl");
+ if ($url=="auto") $url = "https://raw.githubusercontent.com/datenstrom/yellow/main/system/extensions/update-available.ini";
+ list($statusCode, $fileData) = $this->getExtensionFile($url);
+ if ($statusCode==200 && !$this->yellow->toolbox->writeFile($fileNameAvailable, $fileData)) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't write file '$fileNameAvailable'!");
+ }
+ }
+ $fileData = $this->yellow->toolbox->readFile($fileNameAvailable);
+ $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension");
+ }
+ $settings->uksort("strnatcasecmp");
+ return array($statusCode, $settings);
+ }
+
+ // Return extension information
+ public function getExtensionInformation($settings) {
+ $extension = lcfirst($settings->get("extension"));
+ $version = $settings->get("version");
+ $newModified = strtotime($settings->get("published"));
+ $oldModified = 0;
+ $invalid = false;
+ foreach ($settings as $key=>$value) {
+ if (strposu($key, "/")) {
+ $fileName = $this->yellow->lookup->normalisePath($key);
+ if (!$this->yellow->lookup->isValidFile($fileName)) $invalid = true;
+ if ($oldModified==0) $oldModified = $this->yellow->toolbox->getFileModified($fileName);
+ }
+ }
+ if ($invalid) $extension = $version = "";
+ return array($extension, $version, $newModified, $oldModified);
+ }
+
+ // Return extension directories
+ public function getExtensionDirectories($zip, $pathBase) {
+ $paths = array();
+ for ($index=0; $index<$zip->numFiles; ++$index) {
+ $entry = substru($zip->getNameIndex($index), strlenu($pathBase));
+ if (preg_match("#^(.*\/).*?$#", $entry, $matches)) {
+ array_push($paths, $matches[1]);
+ }
+ }
+ return array_unique($paths);
+ }
+
+ // 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() {
+ return $this->yellow->content->scanLocation("");
+ }
+
+ // Return extension files names for content files
+ public function getExtensionContentFileNames($fileName, $pathBase, $entry, $flags, $paths, $page) {
+ if (preg_match("/multi-language/i", $flags)) {
+ $pathMultiLanguage = "";
+ $languagesWanted = array($page->get("language"), "en");
+ foreach ($languagesWanted as $language) {
+ foreach ($paths as $path) {
+ if ($this->yellow->lookup->normaliseToken(rtrim($path, "/"))==$language) {
+ $pathMultiLanguage = $path;
+ break;
+ }
+ }
+ if (!is_string_empty($pathMultiLanguage)) break;
+ }
+ $fileNameSource = $pathBase.$pathMultiLanguage.$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 description including responsible developer/designer/translator
+ public function getExtensionDescription($key, $value) {
+ $description = $responsible = "";
+ if ($value->isExisting("description")) $description = $value->get("description");
+ if ($value->isExisting("developer")) $responsible = "Developed by ".$value["developer"].".";
+ if ($value->isExisting("designer")) $responsible = "Designed by ".$value["designer"].".";
+ if ($value->isExisting("translator")) $responsible = "Translated by ".$value["translator"].".";
+ if (is_string_empty($description)) $description = "No description available.";
+ return "$description $responsible";
+ }
+
+ // Return extension file
+ public function getExtensionFile($url) {
+ $curlHandle = curl_init();
+ curl_setopt($curlHandle, CURLOPT_URL, $this->getExtensionDownloadUrl($url));
+ 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);
+ $fileData = curl_exec($curlHandle);
+ $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
+ $redirectUrl = ($statusCode>=300 && $statusCode<=399) ? curl_getinfo($curlHandle, CURLINFO_REDIRECT_URL) : "";
+ if (PHP_VERSION_ID<80000) curl_close($curlHandle);
+ if ($statusCode==0) {
+ $statusCode = 450;
+ $this->yellow->page->error($statusCode, "Can't connect to the update server!");
+ }
+ if ($statusCode!=450 && $statusCode!=200) {
+ $statusCode = 500;
+ $this->yellow->page->error($statusCode, "Can't download file '$url'!");
+ }
+ if ($this->yellow->system->get("coreDebugMode")>=2 && !is_string_empty($redirectUrl)) {
+ echo "YellowUpdate::getExtensionFile redirected to url:$redirectUrl<br />\n";
+ }
+ if ($this->yellow->system->get("coreDebugMode")>=2) {
+ echo "YellowUpdate::getExtensionFile status:$statusCode url:$url<br />\n";
+ }
+ return array($statusCode, $fileData);
+ }
+
+ // Return extension download URL, redirect to known URL if necessary
+ public function getExtensionDownloadUrl($url) {
+ if (preg_match("#^https://github.com/(.+)/archive/refs/heads/main.zip$#", $url, $matches)) {
+ $url = "https://codeload.github.com/".$matches[1]."/zip/refs/heads/main";
+ }
+ if (preg_match("#^https://github.com/(.+)/raw/main/(.+)$#", $url, $matches)) {
+ $url = "https://raw.githubusercontent.com/".$matches[1]."/main/".$matches[2];
+ }
+ return $url;
+ }
+
+ // Return time of next daily update
+ public function getTimestampDaily() {
+ $timeOffset = 0;
+ foreach (str_split($this->yellow->system->get("sitename")) as $char) {
+ $timeOffset = ($timeOffset+ord($char)) % 60;
+ }
+ return mktime(0, 0, 0) + 60*60*24 + $timeOffset;
+ }
+}
diff --git a/yellow.php b/yellow.php
@@ -1,7 +1,7 @@
<?php
// Datenstrom Yellow, https://github.com/datenstrom/yellow
-require("system/extensions/core.php");
+require("system/workers/core.php");
if (PHP_SAPI!="cli") {
$yellow = new YellowCore();