summaryrefslogtreecommitdiff
path: root/platform/www/inc
diff options
context:
space:
mode:
Diffstat (limited to 'platform/www/inc')
-rw-r--r--platform/www/inc/.htaccess8
-rw-r--r--platform/www/inc/Action/AbstractAclAction.php25
-rw-r--r--platform/www/inc/Action/AbstractAction.php88
-rw-r--r--platform/www/inc/Action/AbstractAliasAction.php28
-rw-r--r--platform/www/inc/Action/AbstractUserAction.php25
-rw-r--r--platform/www/inc/Action/Admin.php45
-rw-r--r--platform/www/inc/Action/Backlink.php24
-rw-r--r--platform/www/inc/Action/Cancel.php25
-rw-r--r--platform/www/inc/Action/Check.php26
-rw-r--r--platform/www/inc/Action/Conflict.php34
-rw-r--r--platform/www/inc/Action/Denied.php23
-rw-r--r--platform/www/inc/Action/Diff.php35
-rw-r--r--platform/www/inc/Action/Draft.php39
-rw-r--r--platform/www/inc/Action/Draftdel.php38
-rw-r--r--platform/www/inc/Action/Edit.php91
-rw-r--r--platform/www/inc/Action/Exception/ActionAbort.php20
-rw-r--r--platform/www/inc/Action/Exception/ActionAclRequiredException.php17
-rw-r--r--platform/www/inc/Action/Exception/ActionDisabledException.php17
-rw-r--r--platform/www/inc/Action/Exception/ActionException.php66
-rw-r--r--platform/www/inc/Action/Exception/ActionUserRequiredException.php17
-rw-r--r--platform/www/inc/Action/Exception/FatalException.php26
-rw-r--r--platform/www/inc/Action/Exception/NoActionException.php15
-rw-r--r--platform/www/inc/Action/Export.php113
-rw-r--r--platform/www/inc/Action/Index.php25
-rw-r--r--platform/www/inc/Action/Locked.php25
-rw-r--r--platform/www/inc/Action/Login.php36
-rw-r--r--platform/www/inc/Action/Logout.php50
-rw-r--r--platform/www/inc/Action/Media.php24
-rw-r--r--platform/www/inc/Action/Plugin.php32
-rw-r--r--platform/www/inc/Action/Preview.php42
-rw-r--r--platform/www/inc/Action/Profile.php45
-rw-r--r--platform/www/inc/Action/ProfileDelete.php42
-rw-r--r--platform/www/inc/Action/Recent.php40
-rw-r--r--platform/www/inc/Action/Recover.php21
-rw-r--r--platform/www/inc/Action/Redirect.php65
-rw-r--r--platform/www/inc/Action/Register.php45
-rw-r--r--platform/www/inc/Action/Resendpwd.php177
-rw-r--r--platform/www/inc/Action/Revert.php60
-rw-r--r--platform/www/inc/Action/Revisions.php24
-rw-r--r--platform/www/inc/Action/Save.php60
-rw-r--r--platform/www/inc/Action/Search.php135
-rw-r--r--platform/www/inc/Action/Show.php36
-rw-r--r--platform/www/inc/Action/Sitemap.php66
-rw-r--r--platform/www/inc/Action/Source.php36
-rw-r--r--platform/www/inc/Action/Subscribe.php168
-rw-r--r--platform/www/inc/ActionRouter.php228
-rw-r--r--platform/www/inc/Ajax.php438
-rw-r--r--platform/www/inc/Cache/Cache.php240
-rw-r--r--platform/www/inc/Cache/CacheInstructions.php46
-rw-r--r--platform/www/inc/Cache/CacheParser.php64
-rw-r--r--platform/www/inc/Cache/CacheRenderer.php94
-rw-r--r--platform/www/inc/ChangeLog/ChangeLog.php666
-rw-r--r--platform/www/inc/ChangeLog/MediaChangeLog.php30
-rw-r--r--platform/www/inc/ChangeLog/PageChangeLog.php30
-rw-r--r--platform/www/inc/Debug/DebugHelper.php167
-rw-r--r--platform/www/inc/Debug/PropertyDeprecationHelper.php134
-rw-r--r--platform/www/inc/DifferenceEngine.php1544
-rw-r--r--platform/www/inc/Draft.php165
-rw-r--r--platform/www/inc/Extension/ActionPlugin.php22
-rw-r--r--platform/www/inc/Extension/AdminPlugin.php123
-rw-r--r--platform/www/inc/Extension/AuthPlugin.php461
-rw-r--r--platform/www/inc/Extension/CLIPlugin.php13
-rw-r--r--platform/www/inc/Extension/Event.php197
-rw-r--r--platform/www/inc/Extension/EventHandler.php108
-rw-r--r--platform/www/inc/Extension/Plugin.php13
-rw-r--r--platform/www/inc/Extension/PluginController.php393
-rw-r--r--platform/www/inc/Extension/PluginInterface.php162
-rw-r--r--platform/www/inc/Extension/PluginTrait.php256
-rw-r--r--platform/www/inc/Extension/RemotePlugin.php122
-rw-r--r--platform/www/inc/Extension/SyntaxPlugin.php132
-rw-r--r--platform/www/inc/FeedParser.php27
-rw-r--r--platform/www/inc/FeedParserFile.php62
-rw-r--r--platform/www/inc/Form/ButtonElement.php34
-rw-r--r--platform/www/inc/Form/CheckableElement.php62
-rw-r--r--platform/www/inc/Form/DropdownElement.php198
-rw-r--r--platform/www/inc/Form/Element.php151
-rw-r--r--platform/www/inc/Form/FieldsetCloseElement.php30
-rw-r--r--platform/www/inc/Form/FieldsetOpenElement.php36
-rw-r--r--platform/www/inc/Form/Form.php462
-rw-r--r--platform/www/inc/Form/HTMLElement.php29
-rw-r--r--platform/www/inc/Form/InputElement.php159
-rw-r--r--platform/www/inc/Form/LabelElement.php27
-rw-r--r--platform/www/inc/Form/LegacyForm.php181
-rw-r--r--platform/www/inc/Form/OptGroup.php106
-rw-r--r--platform/www/inc/Form/TagCloseElement.php88
-rw-r--r--platform/www/inc/Form/TagElement.php29
-rw-r--r--platform/www/inc/Form/TagOpenElement.php30
-rw-r--r--platform/www/inc/Form/TextareaElement.php51
-rw-r--r--platform/www/inc/Form/ValueElement.php45
-rw-r--r--platform/www/inc/HTTP/DokuHTTPClient.php77
-rw-r--r--platform/www/inc/HTTP/HTTPClient.php885
-rw-r--r--platform/www/inc/HTTP/HTTPClientException.php10
-rw-r--r--platform/www/inc/IXR_Library.php1135
-rw-r--r--platform/www/inc/Input/Get.php29
-rw-r--r--platform/www/inc/Input/Input.php287
-rw-r--r--platform/www/inc/Input/Post.php30
-rw-r--r--platform/www/inc/Input/Server.php19
-rw-r--r--platform/www/inc/JpegMeta.php3188
-rw-r--r--platform/www/inc/Mailer.class.php777
-rw-r--r--platform/www/inc/Manifest.php84
-rw-r--r--platform/www/inc/Menu/AbstractMenu.php96
-rw-r--r--platform/www/inc/Menu/DetailMenu.php21
-rw-r--r--platform/www/inc/Menu/Item/AbstractItem.php253
-rw-r--r--platform/www/inc/Menu/Item/Admin.php28
-rw-r--r--platform/www/inc/Menu/Item/Back.php29
-rw-r--r--platform/www/inc/Menu/Item/Backlink.php18
-rw-r--r--platform/www/inc/Menu/Item/Edit.php65
-rw-r--r--platform/www/inc/Menu/Item/ImgBackto.php24
-rw-r--r--platform/www/inc/Menu/Item/Index.php27
-rw-r--r--platform/www/inc/Menu/Item/Login.php29
-rw-r--r--platform/www/inc/Menu/Item/Media.php21
-rw-r--r--platform/www/inc/Menu/Item/MediaManager.php32
-rw-r--r--platform/www/inc/Menu/Item/Profile.php24
-rw-r--r--platform/www/inc/Menu/Item/Recent.php20
-rw-r--r--platform/www/inc/Menu/Item/Register.php24
-rw-r--r--platform/www/inc/Menu/Item/Resendpwd.php24
-rw-r--r--platform/www/inc/Menu/Item/Revert.php26
-rw-r--r--platform/www/inc/Menu/Item/Revisions.php21
-rw-r--r--platform/www/inc/Menu/Item/Subscribe.php24
-rw-r--r--platform/www/inc/Menu/Item/Top.php36
-rw-r--r--platform/www/inc/Menu/MenuInterface.php20
-rw-r--r--platform/www/inc/Menu/MobileMenu.php93
-rw-r--r--platform/www/inc/Menu/PageMenu.php23
-rw-r--r--platform/www/inc/Menu/SiteMenu.php20
-rw-r--r--platform/www/inc/Menu/UserMenu.php21
-rw-r--r--platform/www/inc/Parsing/Handler/AbstractRewriter.php39
-rw-r--r--platform/www/inc/Parsing/Handler/Block.php211
-rw-r--r--platform/www/inc/Parsing/Handler/CallWriter.php40
-rw-r--r--platform/www/inc/Parsing/Handler/CallWriterInterface.php30
-rw-r--r--platform/www/inc/Parsing/Handler/Lists.php186
-rw-r--r--platform/www/inc/Parsing/Handler/Nest.php82
-rw-r--r--platform/www/inc/Parsing/Handler/Preformatted.php49
-rw-r--r--platform/www/inc/Parsing/Handler/Quote.php86
-rw-r--r--platform/www/inc/Parsing/Handler/ReWriterInterface.php37
-rw-r--r--platform/www/inc/Parsing/Handler/Table.php320
-rw-r--r--platform/www/inc/Parsing/Lexer/Lexer.php349
-rw-r--r--platform/www/inc/Parsing/Lexer/ParallelRegex.php203
-rw-r--r--platform/www/inc/Parsing/Lexer/StateStack.php60
-rw-r--r--platform/www/inc/Parsing/Parser.php128
-rw-r--r--platform/www/inc/Parsing/ParserMode/AbstractMode.php40
-rw-r--r--platform/www/inc/Parsing/ParserMode/Acronym.php68
-rw-r--r--platform/www/inc/Parsing/ParserMode/Base.php31
-rw-r--r--platform/www/inc/Parsing/ParserMode/Camelcaselink.php23
-rw-r--r--platform/www/inc/Parsing/ParserMode/Code.php25
-rw-r--r--platform/www/inc/Parsing/ParserMode/Emaillink.php20
-rw-r--r--platform/www/inc/Parsing/ParserMode/Entity.php50
-rw-r--r--platform/www/inc/Parsing/ParserMode/Eol.php25
-rw-r--r--platform/www/inc/Parsing/ParserMode/Externallink.php44
-rw-r--r--platform/www/inc/Parsing/ParserMode/File.php25
-rw-r--r--platform/www/inc/Parsing/ParserMode/Filelink.php39
-rw-r--r--platform/www/inc/Parsing/ParserMode/Footnote.php50
-rw-r--r--platform/www/inc/Parsing/ParserMode/Formatting.php115
-rw-r--r--platform/www/inc/Parsing/ParserMode/Header.php24
-rw-r--r--platform/www/inc/Parsing/ParserMode/Hr.php19
-rw-r--r--platform/www/inc/Parsing/ParserMode/Html.php27
-rw-r--r--platform/www/inc/Parsing/ParserMode/Internallink.php20
-rw-r--r--platform/www/inc/Parsing/ParserMode/Linebreak.php19
-rw-r--r--platform/www/inc/Parsing/ParserMode/Listblock.php44
-rw-r--r--platform/www/inc/Parsing/ParserMode/Media.php20
-rw-r--r--platform/www/inc/Parsing/ParserMode/ModeInterface.php46
-rw-r--r--platform/www/inc/Parsing/ParserMode/Multiplyentity.php27
-rw-r--r--platform/www/inc/Parsing/ParserMode/Nocache.php19
-rw-r--r--platform/www/inc/Parsing/ParserMode/Notoc.php19
-rw-r--r--platform/www/inc/Parsing/ParserMode/Php.php27
-rw-r--r--platform/www/inc/Parsing/ParserMode/Plugin.php8
-rw-r--r--platform/www/inc/Parsing/ParserMode/Preformatted.php31
-rw-r--r--platform/www/inc/Parsing/ParserMode/Quote.php41
-rw-r--r--platform/www/inc/Parsing/ParserMode/Quotes.php51
-rw-r--r--platform/www/inc/Parsing/ParserMode/Rss.php19
-rw-r--r--platform/www/inc/Parsing/ParserMode/Smiley.php48
-rw-r--r--platform/www/inc/Parsing/ParserMode/Table.php47
-rw-r--r--platform/www/inc/Parsing/ParserMode/Unformatted.php28
-rw-r--r--platform/www/inc/Parsing/ParserMode/Windowssharelink.php31
-rw-r--r--platform/www/inc/Parsing/ParserMode/Wordblock.php52
-rw-r--r--platform/www/inc/PassHash.php808
-rw-r--r--platform/www/inc/Remote/AccessDeniedException.php10
-rw-r--r--platform/www/inc/Remote/Api.php410
-rw-r--r--platform/www/inc/Remote/ApiCore.php1025
-rw-r--r--platform/www/inc/Remote/RemoteException.php10
-rw-r--r--platform/www/inc/Remote/XmlRpcServer.php61
-rw-r--r--platform/www/inc/SafeFN.class.php158
-rw-r--r--platform/www/inc/Search/Indexer.php1214
-rw-r--r--platform/www/inc/Sitemap/Item.php66
-rw-r--r--platform/www/inc/Sitemap/Mapper.php164
-rw-r--r--platform/www/inc/StyleUtils.php194
-rw-r--r--platform/www/inc/Subscriptions/BulkSubscriptionSender.php261
-rw-r--r--platform/www/inc/Subscriptions/MediaSubscriptionSender.php47
-rw-r--r--platform/www/inc/Subscriptions/PageSubscriptionSender.php88
-rw-r--r--platform/www/inc/Subscriptions/RegistrationSubscriptionSender.php40
-rw-r--r--platform/www/inc/Subscriptions/SubscriberManager.php290
-rw-r--r--platform/www/inc/Subscriptions/SubscriberRegexBuilder.php70
-rw-r--r--platform/www/inc/Subscriptions/SubscriptionSender.php86
-rw-r--r--platform/www/inc/TaskRunner.php240
-rw-r--r--platform/www/inc/Ui/Admin.php167
-rw-r--r--platform/www/inc/Ui/Search.php647
-rw-r--r--platform/www/inc/Ui/SearchState.php141
-rw-r--r--platform/www/inc/Ui/Ui.php20
-rw-r--r--platform/www/inc/Utf8/Asian.php99
-rw-r--r--platform/www/inc/Utf8/Clean.php204
-rw-r--r--platform/www/inc/Utf8/Conversion.php162
-rw-r--r--platform/www/inc/Utf8/PhpString.php383
-rw-r--r--platform/www/inc/Utf8/Table.php93
-rw-r--r--platform/www/inc/Utf8/Unicode.php277
-rw-r--r--platform/www/inc/Utf8/tables/case.php659
-rw-r--r--platform/www/inc/Utf8/tables/loweraccents.php116
-rw-r--r--platform/www/inc/Utf8/tables/romanization.php1458
-rw-r--r--platform/www/inc/Utf8/tables/specials.php615
-rw-r--r--platform/www/inc/Utf8/tables/upperaccents.php114
-rw-r--r--platform/www/inc/actions.php64
-rw-r--r--platform/www/inc/auth.php1279
-rw-r--r--platform/www/inc/cache.php57
-rw-r--r--platform/www/inc/changelog.php403
-rw-r--r--platform/www/inc/cli.php656
-rw-r--r--platform/www/inc/common.php2132
-rw-r--r--platform/www/inc/compatibility.php83
-rw-r--r--platform/www/inc/config_cascade.php91
-rw-r--r--platform/www/inc/confutils.php474
-rw-r--r--platform/www/inc/defines.php65
-rw-r--r--platform/www/inc/deprecated.php570
-rw-r--r--platform/www/inc/farm.php150
-rw-r--r--platform/www/inc/fetch.functions.php196
-rw-r--r--platform/www/inc/form.php1105
-rw-r--r--platform/www/inc/fulltext.php933
-rw-r--r--platform/www/inc/html.php2380
-rw-r--r--platform/www/inc/httputils.php346
-rw-r--r--platform/www/inc/indexer.php369
-rw-r--r--platform/www/inc/infoutils.php527
-rw-r--r--platform/www/inc/init.php623
-rw-r--r--platform/www/inc/io.php781
-rw-r--r--platform/www/inc/lang/en/admin.txt3
-rw-r--r--platform/www/inc/lang/en/adminplugins.txt2
-rw-r--r--platform/www/inc/lang/en/backlinks.txt3
-rw-r--r--platform/www/inc/lang/en/conflict.txt5
-rw-r--r--platform/www/inc/lang/en/denied.txt3
-rw-r--r--platform/www/inc/lang/en/diff.txt3
-rw-r--r--platform/www/inc/lang/en/draft.txt5
-rw-r--r--platform/www/inc/lang/en/edit.txt1
-rw-r--r--platform/www/inc/lang/en/editrev.txt2
-rw-r--r--platform/www/inc/lang/en/index.txt3
-rw-r--r--platform/www/inc/lang/en/install.html7
-rw-r--r--platform/www/inc/lang/en/lang.php395
-rw-r--r--platform/www/inc/lang/en/locked.txt3
-rw-r--r--platform/www/inc/lang/en/login.txt3
-rw-r--r--platform/www/inc/lang/en/mailtext.txt15
-rw-r--r--platform/www/inc/lang/en/mailwrap.html13
-rw-r--r--platform/www/inc/lang/en/newpage.txt3
-rw-r--r--platform/www/inc/lang/en/norev.txt3
-rw-r--r--platform/www/inc/lang/en/onceexisted.txt3
-rw-r--r--platform/www/inc/lang/en/password.txt6
-rw-r--r--platform/www/inc/lang/en/preview.txt3
-rw-r--r--platform/www/inc/lang/en/pwconfirm.txt9
-rw-r--r--platform/www/inc/lang/en/read.txt1
-rw-r--r--platform/www/inc/lang/en/recent.txt3
-rw-r--r--platform/www/inc/lang/en/register.txt3
-rw-r--r--platform/www/inc/lang/en/registermail.txt10
-rw-r--r--platform/www/inc/lang/en/resendpwd.txt3
-rw-r--r--platform/www/inc/lang/en/resetpwd.txt3
-rw-r--r--platform/www/inc/lang/en/revisions.txt3
-rw-r--r--platform/www/inc/lang/en/searchpage.txt3
-rw-r--r--platform/www/inc/lang/en/showrev.txt2
-rw-r--r--platform/www/inc/lang/en/stopwords.txt39
-rw-r--r--platform/www/inc/lang/en/subscr_digest.txt16
-rw-r--r--platform/www/inc/lang/en/subscr_form.txt3
-rw-r--r--platform/www/inc/lang/en/subscr_list.txt13
-rw-r--r--platform/www/inc/lang/en/subscr_single.txt19
-rw-r--r--platform/www/inc/lang/en/updateprofile.txt3
-rw-r--r--platform/www/inc/lang/en/uploadmail.txt11
-rw-r--r--platform/www/inc/legacy.php18
-rw-r--r--platform/www/inc/load.php153
-rw-r--r--platform/www/inc/mail.php166
-rw-r--r--platform/www/inc/media.php2541
-rw-r--r--platform/www/inc/pageutils.php778
-rw-r--r--platform/www/inc/parser/code.php71
-rw-r--r--platform/www/inc/parser/handler.php1157
-rw-r--r--platform/www/inc/parser/metadata.php751
-rw-r--r--platform/www/inc/parser/parser.php99
-rw-r--r--platform/www/inc/parser/renderer.php910
-rw-r--r--platform/www/inc/parser/xhtml.php1999
-rw-r--r--platform/www/inc/parser/xhtmlsummary.php84
-rw-r--r--platform/www/inc/parserutils.php809
-rw-r--r--platform/www/inc/pluginutils.php151
-rw-r--r--platform/www/inc/preload.php5
-rw-r--r--platform/www/inc/preload.php.dist17
-rw-r--r--platform/www/inc/search.php518
-rw-r--r--platform/www/inc/template.php1895
-rw-r--r--platform/www/inc/toolbar.php277
-rw-r--r--platform/www/inc/utf8.php284
287 files changed, 57328 insertions, 0 deletions
diff --git a/platform/www/inc/.htaccess b/platform/www/inc/.htaccess
new file mode 100644
index 0000000..6ba7d91
--- /dev/null
+++ b/platform/www/inc/.htaccess
@@ -0,0 +1,8 @@
+## no access to the inc directory
+<IfModule mod_authz_core.c>
+ Require all denied
+</IfModule>
+<IfModule !mod_authz_core.c>
+ Order allow,deny
+ Deny from all
+</IfModule>
diff --git a/platform/www/inc/Action/AbstractAclAction.php b/platform/www/inc/Action/AbstractAclAction.php
new file mode 100644
index 0000000..871edb0
--- /dev/null
+++ b/platform/www/inc/Action/AbstractAclAction.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAclRequiredException;
+
+/**
+ * Class AbstractAclAction
+ *
+ * An action that requires the ACL subsystem to be enabled (eg. useacl=1)
+ *
+ * @package dokuwiki\Action
+ */
+abstract class AbstractAclAction extends AbstractAction {
+
+ /** @inheritdoc */
+ public function checkPreconditions() {
+ parent::checkPreconditions();
+ global $conf;
+ global $auth;
+ if(!$conf['useacl']) throw new ActionAclRequiredException();
+ if(!$auth) throw new ActionAclRequiredException();
+ }
+
+}
diff --git a/platform/www/inc/Action/AbstractAction.php b/platform/www/inc/Action/AbstractAction.php
new file mode 100644
index 0000000..ea86238
--- /dev/null
+++ b/platform/www/inc/Action/AbstractAction.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionDisabledException;
+use dokuwiki\Action\Exception\ActionException;
+use dokuwiki\Action\Exception\FatalException;
+
+/**
+ * Class AbstractAction
+ *
+ * Base class for all actions
+ *
+ * @package dokuwiki\Action
+ */
+abstract class AbstractAction {
+
+ /** @var string holds the name of the action (lowercase class name, no namespace) */
+ protected $actionname;
+
+ /**
+ * AbstractAction constructor.
+ *
+ * @param string $actionname the name of this action (see getActionName() for caveats)
+ */
+ public function __construct($actionname = '') {
+ if($actionname !== '') {
+ $this->actionname = $actionname;
+ } else {
+ // http://stackoverflow.com/a/27457689/172068
+ $this->actionname = strtolower(substr(strrchr(get_class($this), '\\'), 1));
+ }
+ }
+
+ /**
+ * Return the minimum permission needed
+ *
+ * This needs to return one of the AUTH_* constants. It will be checked against
+ * the current user and page after checkPermissions() ran through. If it fails,
+ * the user will be shown the Denied action.
+ *
+ * @return int
+ */
+ abstract public function minimumPermission();
+
+ /**
+ * Check conditions are met to run this action
+ *
+ * @throws ActionException
+ * @return void
+ */
+ public function checkPreconditions() {
+ }
+
+ /**
+ * Process data
+ *
+ * This runs before any output is sent to the browser.
+ *
+ * Throw an Exception if a different action should be run after this step.
+ *
+ * @throws ActionException
+ * @return void
+ */
+ public function preProcess() {
+ }
+
+ /**
+ * Output whatever content is wanted within tpl_content();
+ *
+ * @fixme we may want to return a Ui class here
+ */
+ public function tplContent() {
+ throw new FatalException('No content for Action ' . $this->actionname);
+ }
+
+ /**
+ * Returns the name of this action
+ *
+ * This is usually the lowercased class name, but may differ for some actions.
+ * eg. the export_ modes or for the Plugin action.
+ *
+ * @return string
+ */
+ public function getActionName() {
+ return $this->actionname;
+ }
+}
diff --git a/platform/www/inc/Action/AbstractAliasAction.php b/platform/www/inc/Action/AbstractAliasAction.php
new file mode 100644
index 0000000..7240f5e
--- /dev/null
+++ b/platform/www/inc/Action/AbstractAliasAction.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\FatalException;
+
+/**
+ * Class AbstractAliasAction
+ *
+ * An action that is an alias for another action. Skips the minimumPermission check
+ *
+ * Be sure to implement preProcess() and throw an ActionAbort exception
+ * with the proper action.
+ *
+ * @package dokuwiki\Action
+ */
+abstract class AbstractAliasAction extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_NONE;
+ }
+
+ public function preProcess() {
+ throw new FatalException('Alias Actions need to implement preProcess to load the aliased action');
+ }
+
+}
diff --git a/platform/www/inc/Action/AbstractUserAction.php b/platform/www/inc/Action/AbstractUserAction.php
new file mode 100644
index 0000000..b4e3f1a
--- /dev/null
+++ b/platform/www/inc/Action/AbstractUserAction.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionUserRequiredException;
+
+/**
+ * Class AbstractUserAction
+ *
+ * An action that requires a logged in user
+ *
+ * @package dokuwiki\Action
+ */
+abstract class AbstractUserAction extends AbstractAclAction {
+
+ /** @inheritdoc */
+ public function checkPreconditions() {
+ parent::checkPreconditions();
+ global $INPUT;
+ if(!$INPUT->server->str('REMOTE_USER')) {
+ throw new ActionUserRequiredException();
+ }
+ }
+
+}
diff --git a/platform/www/inc/Action/Admin.php b/platform/www/inc/Action/Admin.php
new file mode 100644
index 0000000..1c9afd6
--- /dev/null
+++ b/platform/www/inc/Action/Admin.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionException;
+
+/**
+ * Class Admin
+ *
+ * Action to show the admin interface or admin plugins
+ *
+ * @package dokuwiki\Action
+ */
+class Admin extends AbstractUserAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_READ; // let in check later
+ }
+
+ public function checkPreconditions() {
+ parent::checkPreconditions();
+ }
+
+ public function preProcess() {
+ global $INPUT;
+ global $INFO;
+
+ // retrieve admin plugin name from $_REQUEST['page']
+ if(($page = $INPUT->str('page', '', true)) != '') {
+ /** @var $plugin \dokuwiki\Extension\AdminPlugin */
+ if($plugin = plugin_getRequestAdminPlugin()) { // FIXME this method does also permission checking
+ if(!$plugin->isAccessibleByCurrentUser()) {
+ throw new ActionException('denied');
+ }
+ $plugin->handle();
+ }
+ }
+ }
+
+ public function tplContent() {
+ tpl_admin();
+ }
+
+}
diff --git a/platform/www/inc/Action/Backlink.php b/platform/www/inc/Action/Backlink.php
new file mode 100644
index 0000000..0337917
--- /dev/null
+++ b/platform/www/inc/Action/Backlink.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Backlink
+ *
+ * Shows which pages link to the current page
+ *
+ * @package dokuwiki\Action
+ */
+class Backlink extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_NONE;
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ html_backlinks();
+ }
+
+}
diff --git a/platform/www/inc/Action/Cancel.php b/platform/www/inc/Action/Cancel.php
new file mode 100644
index 0000000..d4d8277
--- /dev/null
+++ b/platform/www/inc/Action/Cancel.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+
+/**
+ * Class Cancel
+ *
+ * Alias for show. Aborts editing
+ *
+ * @package dokuwiki\Action
+ */
+class Cancel extends AbstractAliasAction {
+
+ /** @inheritdoc */
+ public function preProcess() {
+ global $ID;
+ unlock($ID);
+
+ // continue with draftdel -> redirect -> show
+ throw new ActionAbort('draftdel');
+ }
+
+}
diff --git a/platform/www/inc/Action/Check.php b/platform/www/inc/Action/Check.php
new file mode 100644
index 0000000..36ae8e8
--- /dev/null
+++ b/platform/www/inc/Action/Check.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+
+/**
+ * Class Check
+ *
+ * Adds some debugging info before aborting to show
+ *
+ * @package dokuwiki\Action
+ */
+class Check extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_READ;
+ }
+
+ public function preProcess() {
+ check();
+ throw new ActionAbort();
+ }
+
+}
diff --git a/platform/www/inc/Action/Conflict.php b/platform/www/inc/Action/Conflict.php
new file mode 100644
index 0000000..d880b5b
--- /dev/null
+++ b/platform/www/inc/Action/Conflict.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Conflict
+ *
+ * Show the conflict resolution screen
+ *
+ * @package dokuwiki\Action
+ */
+class Conflict extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ global $INFO;
+ if($INFO['exists']) {
+ return AUTH_EDIT;
+ } else {
+ return AUTH_CREATE;
+ }
+ }
+
+ public function tplContent() {
+ global $PRE;
+ global $TEXT;
+ global $SUF;
+ global $SUM;
+
+ html_conflict(con($PRE, $TEXT, $SUF), $SUM);
+ html_diff(con($PRE, $TEXT, $SUF), false);
+ }
+
+}
diff --git a/platform/www/inc/Action/Denied.php b/platform/www/inc/Action/Denied.php
new file mode 100644
index 0000000..c8e0192
--- /dev/null
+++ b/platform/www/inc/Action/Denied.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Denied
+ *
+ * Show the access denied screen
+ *
+ * @package dokuwiki\Action
+ */
+class Denied extends AbstractAclAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_NONE;
+ }
+
+ public function tplContent() {
+ html_denied();
+ }
+
+}
diff --git a/platform/www/inc/Action/Diff.php b/platform/www/inc/Action/Diff.php
new file mode 100644
index 0000000..b14b1d0
--- /dev/null
+++ b/platform/www/inc/Action/Diff.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Diff
+ *
+ * Show the differences between two revisions
+ *
+ * @package dokuwiki\Action
+ */
+class Diff extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_READ;
+ }
+
+ /** @inheritdoc */
+ public function preProcess() {
+ global $INPUT;
+
+ // store the selected diff type in cookie
+ $difftype = $INPUT->str('difftype');
+ if(!empty($difftype)) {
+ set_doku_pref('difftype', $difftype);
+ }
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ html_diff();
+ }
+
+}
diff --git a/platform/www/inc/Action/Draft.php b/platform/www/inc/Action/Draft.php
new file mode 100644
index 0000000..caf0870
--- /dev/null
+++ b/platform/www/inc/Action/Draft.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionException;
+
+/**
+ * Class Draft
+ *
+ * Screen to see and recover a draft
+ *
+ * @package dokuwiki\Action
+ * @fixme combine with Recover?
+ */
+class Draft extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ global $INFO;
+ if($INFO['exists']) {
+ return AUTH_EDIT;
+ } else {
+ return AUTH_CREATE;
+ }
+ }
+
+ /** @inheritdoc */
+ public function checkPreconditions() {
+ parent::checkPreconditions();
+ global $INFO;
+ if(!file_exists($INFO['draft'])) throw new ActionException('edit');
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ html_draft();
+ }
+
+}
diff --git a/platform/www/inc/Action/Draftdel.php b/platform/www/inc/Action/Draftdel.php
new file mode 100644
index 0000000..756c0e8
--- /dev/null
+++ b/platform/www/inc/Action/Draftdel.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+
+/**
+ * Class Draftdel
+ *
+ * Delete a draft
+ *
+ * @package dokuwiki\Action
+ */
+class Draftdel extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_EDIT;
+ }
+
+ /**
+ * Delete an existing draft for the current page and user if any
+ *
+ * Redirects to show, afterwards.
+ *
+ * @throws ActionAbort
+ */
+ public function preProcess() {
+ global $INFO, $ID;
+ $draft = new \dokuwiki\Draft($ID, $INFO['client']);
+ if ($draft->isDraftAvailable()) {
+ $draft->deleteDraft();
+ }
+
+ throw new ActionAbort('redirect');
+ }
+
+}
diff --git a/platform/www/inc/Action/Edit.php b/platform/www/inc/Action/Edit.php
new file mode 100644
index 0000000..061c9e2
--- /dev/null
+++ b/platform/www/inc/Action/Edit.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+
+/**
+ * Class Edit
+ *
+ * Handle editing
+ *
+ * @package dokuwiki\Action
+ */
+class Edit extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ global $INFO;
+ if($INFO['exists']) {
+ return AUTH_READ; // we check again below
+ } else {
+ return AUTH_CREATE;
+ }
+ }
+
+ /**
+ * @inheritdoc falls back to 'source' if page not writable
+ */
+ public function checkPreconditions() {
+ parent::checkPreconditions();
+ global $INFO;
+
+ // no edit permission? view source
+ if($INFO['exists'] && !$INFO['writable']) {
+ throw new ActionAbort('source');
+ }
+ }
+
+ /** @inheritdoc */
+ public function preProcess() {
+ global $ID;
+ global $INFO;
+
+ global $TEXT;
+ global $RANGE;
+ global $PRE;
+ global $SUF;
+ global $REV;
+ global $SUM;
+ global $lang;
+ global $DATE;
+
+ if(!isset($TEXT)) {
+ if($INFO['exists']) {
+ if($RANGE) {
+ list($PRE, $TEXT, $SUF) = rawWikiSlices($RANGE, $ID, $REV);
+ } else {
+ $TEXT = rawWiki($ID, $REV);
+ }
+ } else {
+ $TEXT = pageTemplate($ID);
+ }
+ }
+
+ //set summary default
+ if(!$SUM) {
+ if($REV) {
+ $SUM = sprintf($lang['restored'], dformat($REV));
+ } elseif(!$INFO['exists']) {
+ $SUM = $lang['created'];
+ }
+ }
+
+ // Use the date of the newest revision, not of the revision we edit
+ // This is used for conflict detection
+ if(!$DATE) $DATE = @filemtime(wikiFN($ID));
+
+ //check if locked by anyone - if not lock for my self
+ $lockedby = checklock($ID);
+ if($lockedby) {
+ throw new ActionAbort('locked');
+ };
+ lock($ID);
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ html_edit();
+ }
+
+}
diff --git a/platform/www/inc/Action/Exception/ActionAbort.php b/platform/www/inc/Action/Exception/ActionAbort.php
new file mode 100644
index 0000000..9c188bb
--- /dev/null
+++ b/platform/www/inc/Action/Exception/ActionAbort.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace dokuwiki\Action\Exception;
+
+/**
+ * Class ActionAbort
+ *
+ * Strictly speaking not an Exception but an expected execution path. Used to
+ * signal when one action is done and another should take over.
+ *
+ * If you want to signal the same but under some error condition use ActionException
+ * or one of it's decendants.
+ *
+ * The message will NOT be shown to the enduser
+ *
+ * @package dokuwiki\Action\Exception
+ */
+class ActionAbort extends ActionException {
+
+}
diff --git a/platform/www/inc/Action/Exception/ActionAclRequiredException.php b/platform/www/inc/Action/Exception/ActionAclRequiredException.php
new file mode 100644
index 0000000..64a2c61
--- /dev/null
+++ b/platform/www/inc/Action/Exception/ActionAclRequiredException.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace dokuwiki\Action\Exception;
+
+/**
+ * Class ActionAclRequiredException
+ *
+ * Thrown by AbstractACLAction when an action requires that the ACL subsystem is
+ * enabled but it isn't. You should not use it
+ *
+ * The message will NOT be shown to the enduser
+ *
+ * @package dokuwiki\Action\Exception
+ */
+class ActionAclRequiredException extends ActionException {
+
+}
diff --git a/platform/www/inc/Action/Exception/ActionDisabledException.php b/platform/www/inc/Action/Exception/ActionDisabledException.php
new file mode 100644
index 0000000..40a0c7d
--- /dev/null
+++ b/platform/www/inc/Action/Exception/ActionDisabledException.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace dokuwiki\Action\Exception;
+
+/**
+ * Class ActionDisabledException
+ *
+ * Thrown when the requested action has been disabled. Eg. through the 'disableactions'
+ * config setting. You should probably not use it.
+ *
+ * The message will NOT be shown to the enduser, but a generic information will be shown.
+ *
+ * @package dokuwiki\Action\Exception
+ */
+class ActionDisabledException extends ActionException {
+
+}
diff --git a/platform/www/inc/Action/Exception/ActionException.php b/platform/www/inc/Action/Exception/ActionException.php
new file mode 100644
index 0000000..381584c
--- /dev/null
+++ b/platform/www/inc/Action/Exception/ActionException.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace dokuwiki\Action\Exception;
+
+/**
+ * Class ActionException
+ *
+ * This exception and its subclasses signal that the current action should be
+ * aborted and a different action should be used instead. The new action can
+ * be given as parameter in the constructor. Defaults to 'show'
+ *
+ * The message will NOT be shown to the enduser
+ *
+ * @package dokuwiki\Action\Exception
+ */
+class ActionException extends \Exception {
+
+ /** @var string the new action */
+ protected $newaction;
+
+ /** @var bool should the exception's message be shown to the user? */
+ protected $displayToUser = false;
+
+ /**
+ * ActionException constructor.
+ *
+ * When no new action is given 'show' is assumed. For requests that originated in a POST,
+ * a 'redirect' is used which will cause a redirect to the 'show' action.
+ *
+ * @param string|null $newaction the action that should be used next
+ * @param string $message optional message, will not be shown except for some dub classes
+ */
+ public function __construct($newaction = null, $message = '') {
+ global $INPUT;
+ parent::__construct($message);
+ if(is_null($newaction)) {
+ if(strtolower($INPUT->server->str('REQUEST_METHOD')) == 'post') {
+ $newaction = 'redirect';
+ } else {
+ $newaction = 'show';
+ }
+ }
+
+ $this->newaction = $newaction;
+ }
+
+ /**
+ * Returns the action to use next
+ *
+ * @return string
+ */
+ public function getNewAction() {
+ return $this->newaction;
+ }
+
+ /**
+ * Should this Exception's message be shown to the user?
+ *
+ * @param null|bool $set when null is given, the current setting is not changed
+ * @return bool
+ */
+ public function displayToUser($set = null) {
+ if(!is_null($set)) $this->displayToUser = $set;
+ return $set;
+ }
+}
diff --git a/platform/www/inc/Action/Exception/ActionUserRequiredException.php b/platform/www/inc/Action/Exception/ActionUserRequiredException.php
new file mode 100644
index 0000000..aab06cc
--- /dev/null
+++ b/platform/www/inc/Action/Exception/ActionUserRequiredException.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace dokuwiki\Action\Exception;
+
+/**
+ * Class ActionUserRequiredException
+ *
+ * Thrown by AbstractUserAction when an action requires that a user is logged
+ * in but it isn't. You should not use it.
+ *
+ * The message will NOT be shown to the enduser
+ *
+ * @package dokuwiki\Action\Exception
+ */
+class ActionUserRequiredException extends ActionException {
+
+}
diff --git a/platform/www/inc/Action/Exception/FatalException.php b/platform/www/inc/Action/Exception/FatalException.php
new file mode 100644
index 0000000..42e30cc
--- /dev/null
+++ b/platform/www/inc/Action/Exception/FatalException.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace dokuwiki\Action\Exception;
+
+/**
+ * Class FatalException
+ *
+ * A fatal exception during handling the action
+ *
+ * Will abort all handling and display some info to the user. The HTTP status code
+ * can be defined.
+ *
+ * @package dokuwiki\Action\Exception
+ */
+class FatalException extends \Exception {
+ /**
+ * FatalException constructor.
+ *
+ * @param string $message the message to send
+ * @param int $status the HTTP status to send
+ * @param null|\Exception $previous previous exception
+ */
+ public function __construct($message = 'A fatal error occured', $status = 500, $previous = null) {
+ parent::__construct($message, $status, $previous);
+ }
+}
diff --git a/platform/www/inc/Action/Exception/NoActionException.php b/platform/www/inc/Action/Exception/NoActionException.php
new file mode 100644
index 0000000..1c4e4d0
--- /dev/null
+++ b/platform/www/inc/Action/Exception/NoActionException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace dokuwiki\Action\Exception;
+
+/**
+ * Class NoActionException
+ *
+ * Thrown in the ActionRouter when a wanted action can not be found. Triggers
+ * the unknown action event
+ *
+ * @package dokuwiki\Action\Exception
+ */
+class NoActionException extends \Exception {
+
+}
diff --git a/platform/www/inc/Action/Export.php b/platform/www/inc/Action/Export.php
new file mode 100644
index 0000000..6b46b27
--- /dev/null
+++ b/platform/www/inc/Action/Export.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Extension\Event;
+
+/**
+ * Class Export
+ *
+ * Handle exporting by calling the appropriate renderer
+ *
+ * @package dokuwiki\Action
+ */
+class Export extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_READ;
+ }
+
+ /**
+ * Export a wiki page for various formats
+ *
+ * Triggers ACTION_EXPORT_POSTPROCESS
+ *
+ * Event data:
+ * data['id'] -- page id
+ * data['mode'] -- requested export mode
+ * data['headers'] -- export headers
+ * data['output'] -- export output
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Michael Klier <chi@chimeric.de>
+ * @inheritdoc
+ */
+ public function preProcess() {
+ global $ID;
+ global $REV;
+ global $conf;
+ global $lang;
+
+ $pre = '';
+ $post = '';
+ $headers = array();
+
+ // search engines: never cache exported docs! (Google only currently)
+ $headers['X-Robots-Tag'] = 'noindex';
+
+ $mode = substr($this->actionname, 7);
+ switch($mode) {
+ case 'raw':
+ $headers['Content-Type'] = 'text/plain; charset=utf-8';
+ $headers['Content-Disposition'] = 'attachment; filename=' . noNS($ID) . '.txt';
+ $output = rawWiki($ID, $REV);
+ break;
+ case 'xhtml':
+ $pre .= '<!DOCTYPE html>' . DOKU_LF;
+ $pre .= '<html lang="' . $conf['lang'] . '" dir="' . $lang['direction'] . '">' . DOKU_LF;
+ $pre .= '<head>' . DOKU_LF;
+ $pre .= ' <meta charset="utf-8" />' . DOKU_LF; // FIXME improve wrapper
+ $pre .= ' <title>' . $ID . '</title>' . DOKU_LF;
+
+ // get metaheaders
+ ob_start();
+ tpl_metaheaders();
+ $pre .= ob_get_clean();
+
+ $pre .= '</head>' . DOKU_LF;
+ $pre .= '<body>' . DOKU_LF;
+ $pre .= '<div class="dokuwiki export">' . DOKU_LF;
+
+ // get toc
+ $pre .= tpl_toc(true);
+
+ $headers['Content-Type'] = 'text/html; charset=utf-8';
+ $output = p_wiki_xhtml($ID, $REV, false);
+
+ $post .= '</div>' . DOKU_LF;
+ $post .= '</body>' . DOKU_LF;
+ $post .= '</html>' . DOKU_LF;
+ break;
+ case 'xhtmlbody':
+ $headers['Content-Type'] = 'text/html; charset=utf-8';
+ $output = p_wiki_xhtml($ID, $REV, false);
+ break;
+ default:
+ $output = p_cached_output(wikiFN($ID, $REV), $mode, $ID);
+ $headers = p_get_metadata($ID, "format $mode");
+ break;
+ }
+
+ // prepare event data
+ $data = array();
+ $data['id'] = $ID;
+ $data['mode'] = $mode;
+ $data['headers'] = $headers;
+ $data['output'] =& $output;
+
+ Event::createAndTrigger('ACTION_EXPORT_POSTPROCESS', $data);
+
+ if(!empty($data['output'])) {
+ if(is_array($data['headers'])) foreach($data['headers'] as $key => $val) {
+ header("$key: $val");
+ }
+ print $pre . $data['output'] . $post;
+ exit;
+ }
+
+ throw new ActionAbort();
+ }
+
+}
diff --git a/platform/www/inc/Action/Index.php b/platform/www/inc/Action/Index.php
new file mode 100644
index 0000000..c87a3f8
--- /dev/null
+++ b/platform/www/inc/Action/Index.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Index
+ *
+ * Show the human readable sitemap. Do not confuse with Sitemap
+ *
+ * @package dokuwiki\Action
+ */
+class Index extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_NONE;
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ global $IDX;
+ html_index($IDX);
+ }
+
+}
diff --git a/platform/www/inc/Action/Locked.php b/platform/www/inc/Action/Locked.php
new file mode 100644
index 0000000..41866e3
--- /dev/null
+++ b/platform/www/inc/Action/Locked.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Locked
+ *
+ * Show a locked screen when a page is locked
+ *
+ * @package dokuwiki\Action
+ */
+class Locked extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_READ;
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ html_locked();
+ html_edit();
+ }
+
+}
diff --git a/platform/www/inc/Action/Login.php b/platform/www/inc/Action/Login.php
new file mode 100644
index 0000000..7f903ff
--- /dev/null
+++ b/platform/www/inc/Action/Login.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionException;
+
+/**
+ * Class Login
+ *
+ * The login form. Actual logins are handled in inc/auth.php
+ *
+ * @package dokuwiki\Action
+ */
+class Login extends AbstractAclAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_NONE;
+ }
+
+ /** @inheritdoc */
+ public function checkPreconditions() {
+ global $INPUT;
+ parent::checkPreconditions();
+ if($INPUT->server->has('REMOTE_USER')) {
+ // nothing to do
+ throw new ActionException();
+ }
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ html_login();
+ }
+
+}
diff --git a/platform/www/inc/Action/Logout.php b/platform/www/inc/Action/Logout.php
new file mode 100644
index 0000000..28e8fee
--- /dev/null
+++ b/platform/www/inc/Action/Logout.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionDisabledException;
+use dokuwiki\Action\Exception\ActionException;
+
+/**
+ * Class Logout
+ *
+ * Log out a user
+ *
+ * @package dokuwiki\Action
+ */
+class Logout extends AbstractUserAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_NONE;
+ }
+
+ /** @inheritdoc */
+ public function checkPreconditions() {
+ parent::checkPreconditions();
+
+ /** @var \dokuwiki\Extension\AuthPlugin $auth */
+ global $auth;
+ if(!$auth->canDo('logout')) throw new ActionDisabledException();
+ }
+
+ /** @inheritdoc */
+ public function preProcess() {
+ global $ID;
+ global $INPUT;
+
+ // when logging out during an edit session, unlock the page
+ $lockedby = checklock($ID);
+ if($lockedby == $INPUT->server->str('REMOTE_USER')) {
+ unlock($ID);
+ }
+
+ // do the logout stuff and redirect to login
+ auth_logoff();
+ send_redirect(wl($ID, array('do' => 'login'), true, '&'));
+
+ // should never be reached
+ throw new ActionException('login');
+ }
+
+}
diff --git a/platform/www/inc/Action/Media.php b/platform/www/inc/Action/Media.php
new file mode 100644
index 0000000..77a2a6f
--- /dev/null
+++ b/platform/www/inc/Action/Media.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Media
+ *
+ * The full screen media manager
+ *
+ * @package dokuwiki\Action
+ */
+class Media extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_READ;
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ tpl_media();
+ }
+
+}
diff --git a/platform/www/inc/Action/Plugin.php b/platform/www/inc/Action/Plugin.php
new file mode 100644
index 0000000..43964cf
--- /dev/null
+++ b/platform/www/inc/Action/Plugin.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Plugin
+ *
+ * Used to run action plugins
+ *
+ * @package dokuwiki\Action
+ */
+class Plugin extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_NONE;
+ }
+
+ /**
+ * Outputs nothing but a warning unless an action plugin overwrites it
+ *
+ * @inheritdoc
+ * @triggers TPL_ACT_UNKNOWN
+ */
+ public function tplContent() {
+ $evt = new \dokuwiki\Extension\Event('TPL_ACT_UNKNOWN', $this->actionname);
+ if($evt->advise_before()) {
+ msg('Failed to handle action: ' . hsc($this->actionname), -1);
+ }
+ $evt->advise_after();
+ }
+}
diff --git a/platform/www/inc/Action/Preview.php b/platform/www/inc/Action/Preview.php
new file mode 100644
index 0000000..7a5aa48
--- /dev/null
+++ b/platform/www/inc/Action/Preview.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Preview
+ *
+ * preview during editing
+ *
+ * @package dokuwiki\Action
+ */
+class Preview extends Edit {
+
+ /** @inheritdoc */
+ public function preProcess() {
+ header('X-XSS-Protection: 0');
+ $this->savedraft();
+ parent::preProcess();
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ global $TEXT;
+ html_edit();
+ html_show($TEXT);
+ }
+
+ /**
+ * Saves a draft on preview
+ */
+ protected function savedraft() {
+ global $ID, $INFO;
+ $draft = new \dokuwiki\Draft($ID, $INFO['client']);
+ if (!$draft->saveDraft()) {
+ $errors = $draft->getErrors();
+ foreach ($errors as $error) {
+ msg(hsc($error), -1);
+ }
+ }
+ }
+
+}
diff --git a/platform/www/inc/Action/Profile.php b/platform/www/inc/Action/Profile.php
new file mode 100644
index 0000000..654a238
--- /dev/null
+++ b/platform/www/inc/Action/Profile.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Action\Exception\ActionDisabledException;
+
+/**
+ * Class Profile
+ *
+ * Handle the profile form
+ *
+ * @package dokuwiki\Action
+ */
+class Profile extends AbstractUserAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_NONE;
+ }
+
+ /** @inheritdoc */
+ public function checkPreconditions() {
+ parent::checkPreconditions();
+
+ /** @var \dokuwiki\Extension\AuthPlugin $auth */
+ global $auth;
+ if(!$auth->canDo('Profile')) throw new ActionDisabledException();
+ }
+
+ /** @inheritdoc */
+ public function preProcess() {
+ global $lang;
+ if(updateprofile()) {
+ msg($lang['profchanged'], 1);
+ throw new ActionAbort('show');
+ }
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ html_updateprofile();
+ }
+
+}
diff --git a/platform/www/inc/Action/ProfileDelete.php b/platform/www/inc/Action/ProfileDelete.php
new file mode 100644
index 0000000..89c58ed
--- /dev/null
+++ b/platform/www/inc/Action/ProfileDelete.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Action\Exception\ActionDisabledException;
+
+/**
+ * Class ProfileDelete
+ *
+ * Delete a user account
+ *
+ * @package dokuwiki\Action
+ */
+class ProfileDelete extends AbstractUserAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_NONE;
+ }
+
+ /** @inheritdoc */
+ public function checkPreconditions() {
+ parent::checkPreconditions();
+
+ /** @var \dokuwiki\Extension\AuthPlugin $auth */
+ global $auth;
+ if(!$auth->canDo('delUser')) throw new ActionDisabledException();
+ }
+
+ /** @inheritdoc */
+ public function preProcess() {
+ global $lang;
+ if(auth_deleteprofile()) {
+ msg($lang['profdeleted'], 1);
+ throw new ActionAbort('show');
+ } else {
+ throw new ActionAbort('profile');
+ }
+ }
+
+}
diff --git a/platform/www/inc/Action/Recent.php b/platform/www/inc/Action/Recent.php
new file mode 100644
index 0000000..9273d52
--- /dev/null
+++ b/platform/www/inc/Action/Recent.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Recent
+ *
+ * The recent changes view
+ *
+ * @package dokuwiki\Action
+ */
+class Recent extends AbstractAction {
+
+ /** @var string what type of changes to show */
+ protected $showType = 'both';
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_NONE;
+ }
+
+ /** @inheritdoc */
+ public function preProcess() {
+ global $INPUT;
+ $show_changes = $INPUT->str('show_changes');
+ if(!empty($show_changes)) {
+ set_doku_pref('show_changes', $show_changes);
+ $this->showType = $show_changes;
+ } else {
+ $this->showType = get_doku_pref('show_changes', 'both');
+ }
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ global $INPUT;
+ html_recent((int) $INPUT->extract('first')->int('first'), $this->showType);
+ }
+
+}
diff --git a/platform/www/inc/Action/Recover.php b/platform/www/inc/Action/Recover.php
new file mode 100644
index 0000000..7966396
--- /dev/null
+++ b/platform/www/inc/Action/Recover.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+
+/**
+ * Class Recover
+ *
+ * Recover a draft
+ *
+ * @package dokuwiki\Action
+ */
+class Recover extends AbstractAliasAction {
+
+ /** @inheritdoc */
+ public function preProcess() {
+ throw new ActionAbort('edit');
+ }
+
+}
diff --git a/platform/www/inc/Action/Redirect.php b/platform/www/inc/Action/Redirect.php
new file mode 100644
index 0000000..dca911a
--- /dev/null
+++ b/platform/www/inc/Action/Redirect.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Extension\Event;
+
+/**
+ * Class Redirect
+ *
+ * Used to redirect to the current page with the last edited section as a target if found
+ *
+ * @package dokuwiki\Action
+ */
+class Redirect extends AbstractAliasAction {
+
+ /**
+ * Redirect to the show action, trying to jump to the previously edited section
+ *
+ * @triggers ACTION_SHOW_REDIRECT
+ * @throws ActionAbort
+ */
+ public function preProcess() {
+ global $PRE;
+ global $TEXT;
+ global $INPUT;
+ global $ID;
+ global $ACT;
+
+ $opts = array(
+ 'id' => $ID,
+ 'preact' => $ACT
+ );
+ //get section name when coming from section edit
+ if($INPUT->has('hid')) {
+ // Use explicitly transmitted header id
+ $opts['fragment'] = $INPUT->str('hid');
+ } else if($PRE && preg_match('/^\s*==+([^=\n]+)/', $TEXT, $match)) {
+ // Fallback to old mechanism
+ $check = false; //Byref
+ $opts['fragment'] = sectionID($match[0], $check);
+ }
+
+ // execute the redirect
+ Event::createAndTrigger('ACTION_SHOW_REDIRECT', $opts, array($this, 'redirect'));
+
+ // should never be reached
+ throw new ActionAbort('show');
+ }
+
+ /**
+ * Execute the redirect
+ *
+ * Default action for ACTION_SHOW_REDIRECT
+ *
+ * @param array $opts id and fragment for the redirect and the preact
+ */
+ public function redirect($opts) {
+ $go = wl($opts['id'], '', true, '&');
+ if(isset($opts['fragment'])) $go .= '#' . $opts['fragment'];
+
+ //show it
+ send_redirect($go);
+ }
+}
diff --git a/platform/www/inc/Action/Register.php b/platform/www/inc/Action/Register.php
new file mode 100644
index 0000000..7d21bff
--- /dev/null
+++ b/platform/www/inc/Action/Register.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Action\Exception\ActionDisabledException;
+
+/**
+ * Class Register
+ *
+ * Self registering a new user
+ *
+ * @package dokuwiki\Action
+ */
+class Register extends AbstractAclAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_NONE;
+ }
+
+ /** @inheritdoc */
+ public function checkPreconditions() {
+ parent::checkPreconditions();
+
+ /** @var \dokuwiki\Extension\AuthPlugin $auth */
+ global $auth;
+ global $conf;
+ if(isset($conf['openregister']) && !$conf['openregister']) throw new ActionDisabledException();
+ if(!$auth->canDo('addUser')) throw new ActionDisabledException();
+ }
+
+ /** @inheritdoc */
+ public function preProcess() {
+ if(register()) { // FIXME could be moved from auth to here
+ throw new ActionAbort('login');
+ }
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ html_register();
+ }
+
+}
diff --git a/platform/www/inc/Action/Resendpwd.php b/platform/www/inc/Action/Resendpwd.php
new file mode 100644
index 0000000..dfa4a99
--- /dev/null
+++ b/platform/www/inc/Action/Resendpwd.php
@@ -0,0 +1,177 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Action\Exception\ActionDisabledException;
+
+/**
+ * Class Resendpwd
+ *
+ * Handle password recovery
+ *
+ * @package dokuwiki\Action
+ */
+class Resendpwd extends AbstractAclAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_NONE;
+ }
+
+ /** @inheritdoc */
+ public function checkPreconditions() {
+ parent::checkPreconditions();
+
+ /** @var \dokuwiki\Extension\AuthPlugin $auth */
+ global $auth;
+ global $conf;
+ if(isset($conf['resendpasswd']) && !$conf['resendpasswd']) throw new ActionDisabledException(); //legacy option
+ if(!$auth->canDo('modPass')) throw new ActionDisabledException();
+ }
+
+ /** @inheritdoc */
+ public function preProcess() {
+ if($this->resendpwd()) {
+ throw new ActionAbort('login');
+ }
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ html_resendpwd();
+ }
+
+ /**
+ * Send a new password
+ *
+ * This function handles both phases of the password reset:
+ *
+ * - handling the first request of password reset
+ * - validating the password reset auth token
+ *
+ * @author Benoit Chesneau <benoit@bchesneau.info>
+ * @author Chris Smith <chris@jalakai.co.uk>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @fixme this should be split up into multiple methods
+ * @return bool true on success, false on any error
+ */
+ protected function resendpwd() {
+ global $lang;
+ global $conf;
+ /* @var \dokuwiki\Extension\AuthPlugin $auth */
+ global $auth;
+ global $INPUT;
+
+ if(!actionOK('resendpwd')) {
+ msg($lang['resendna'], -1);
+ return false;
+ }
+
+ $token = preg_replace('/[^a-f0-9]+/', '', $INPUT->str('pwauth'));
+
+ if($token) {
+ // we're in token phase - get user info from token
+
+ $tfile = $conf['cachedir'] . '/' . $token[0] . '/' . $token . '.pwauth';
+ if(!file_exists($tfile)) {
+ msg($lang['resendpwdbadauth'], -1);
+ $INPUT->remove('pwauth');
+ return false;
+ }
+ // token is only valid for 3 days
+ if((time() - filemtime($tfile)) > (3 * 60 * 60 * 24)) {
+ msg($lang['resendpwdbadauth'], -1);
+ $INPUT->remove('pwauth');
+ @unlink($tfile);
+ return false;
+ }
+
+ $user = io_readfile($tfile);
+ $userinfo = $auth->getUserData($user, $requireGroups = false);
+ if(!$userinfo['mail']) {
+ msg($lang['resendpwdnouser'], -1);
+ return false;
+ }
+
+ if(!$conf['autopasswd']) { // we let the user choose a password
+ $pass = $INPUT->str('pass');
+
+ // password given correctly?
+ if(!$pass) return false;
+ if($pass != $INPUT->str('passchk')) {
+ msg($lang['regbadpass'], -1);
+ return false;
+ }
+
+ // change it
+ if(!$auth->triggerUserMod('modify', array($user, array('pass' => $pass)))) {
+ msg($lang['proffail'], -1);
+ return false;
+ }
+
+ } else { // autogenerate the password and send by mail
+
+ $pass = auth_pwgen($user);
+ if(!$auth->triggerUserMod('modify', array($user, array('pass' => $pass)))) {
+ msg($lang['proffail'], -1);
+ return false;
+ }
+
+ if(auth_sendPassword($user, $pass)) {
+ msg($lang['resendpwdsuccess'], 1);
+ } else {
+ msg($lang['regmailfail'], -1);
+ }
+ }
+
+ @unlink($tfile);
+ return true;
+
+ } else {
+ // we're in request phase
+
+ if(!$INPUT->post->bool('save')) return false;
+
+ if(!$INPUT->post->str('login')) {
+ msg($lang['resendpwdmissing'], -1);
+ return false;
+ } else {
+ $user = trim($auth->cleanUser($INPUT->post->str('login')));
+ }
+
+ $userinfo = $auth->getUserData($user, $requireGroups = false);
+ if(!$userinfo['mail']) {
+ msg($lang['resendpwdnouser'], -1);
+ return false;
+ }
+
+ // generate auth token
+ $token = md5(auth_randombytes(16)); // random secret
+ $tfile = $conf['cachedir'] . '/' . $token[0] . '/' . $token . '.pwauth';
+ $url = wl('', array('do' => 'resendpwd', 'pwauth' => $token), true, '&');
+
+ io_saveFile($tfile, $user);
+
+ $text = rawLocale('pwconfirm');
+ $trep = array(
+ 'FULLNAME' => $userinfo['name'],
+ 'LOGIN' => $user,
+ 'CONFIRM' => $url
+ );
+
+ $mail = new \Mailer();
+ $mail->to($userinfo['name'] . ' <' . $userinfo['mail'] . '>');
+ $mail->subject($lang['regpwmail']);
+ $mail->setBody($text, $trep);
+ if($mail->send()) {
+ msg($lang['resendpwdconfirm'], 1);
+ } else {
+ msg($lang['regmailfail'], -1);
+ }
+ return true;
+ }
+ // never reached
+ }
+
+}
diff --git a/platform/www/inc/Action/Revert.php b/platform/www/inc/Action/Revert.php
new file mode 100644
index 0000000..07c322c
--- /dev/null
+++ b/platform/www/inc/Action/Revert.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Action\Exception\ActionException;
+
+/**
+ * Class Revert
+ *
+ * Quick revert to an old revision
+ *
+ * @package dokuwiki\Action
+ */
+class Revert extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_EDIT;
+ }
+
+ /**
+ *
+ * @inheritdoc
+ * @throws ActionAbort
+ * @throws ActionException
+ * @todo check for writability of the current page ($INFO might do it wrong and check the attic version)
+ */
+ public function preProcess() {
+ if(!checkSecurityToken()) throw new ActionException();
+
+ global $ID;
+ global $REV;
+ global $lang;
+
+ // when no revision is given, delete current one
+ // FIXME this feature is not exposed in the GUI currently
+ $text = '';
+ $sum = $lang['deleted'];
+ if($REV) {
+ $text = rawWiki($ID, $REV);
+ if(!$text) throw new ActionException(); //something went wrong
+ $sum = sprintf($lang['restored'], dformat($REV));
+ }
+
+ // spam check
+ if(checkwordblock($text)) {
+ msg($lang['wordblock'], -1);
+ throw new ActionException('edit');
+ }
+
+ saveWikiText($ID, $text, $sum, false);
+ msg($sum, 1);
+ $REV = '';
+
+ // continue with draftdel -> redirect -> show
+ throw new ActionAbort('draftdel');
+ }
+
+}
diff --git a/platform/www/inc/Action/Revisions.php b/platform/www/inc/Action/Revisions.php
new file mode 100644
index 0000000..b8db531
--- /dev/null
+++ b/platform/www/inc/Action/Revisions.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Revisions
+ *
+ * Show the list of old revisions of the current page
+ *
+ * @package dokuwiki\Action
+ */
+class Revisions extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_READ;
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ global $INPUT;
+ html_revisions($INPUT->int('first'));
+ }
+}
diff --git a/platform/www/inc/Action/Save.php b/platform/www/inc/Action/Save.php
new file mode 100644
index 0000000..0b24729
--- /dev/null
+++ b/platform/www/inc/Action/Save.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Action\Exception\ActionException;
+
+/**
+ * Class Save
+ *
+ * Save at the end of an edit session
+ *
+ * @package dokuwiki\Action
+ */
+class Save extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ global $INFO;
+ if($INFO['exists']) {
+ return AUTH_EDIT;
+ } else {
+ return AUTH_CREATE;
+ }
+ }
+
+ /** @inheritdoc */
+ public function preProcess() {
+ if(!checkSecurityToken()) throw new ActionException('preview');
+
+ global $ID;
+ global $DATE;
+ global $PRE;
+ global $TEXT;
+ global $SUF;
+ global $SUM;
+ global $lang;
+ global $INFO;
+ global $INPUT;
+
+ //spam check
+ if(checkwordblock()) {
+ msg($lang['wordblock'], -1);
+ throw new ActionException('edit');
+ }
+ //conflict check
+ if($DATE != 0 && $INFO['meta']['date']['modified'] > $DATE) {
+ throw new ActionException('conflict');
+ }
+
+ //save it
+ saveWikiText($ID, con($PRE, $TEXT, $SUF, true), $SUM, $INPUT->bool('minor')); //use pretty mode for con
+ //unlock it
+ unlock($ID);
+
+ // continue with draftdel -> redirect -> show
+ throw new ActionAbort('draftdel');
+ }
+
+}
diff --git a/platform/www/inc/Action/Search.php b/platform/www/inc/Action/Search.php
new file mode 100644
index 0000000..88bd0ba
--- /dev/null
+++ b/platform/www/inc/Action/Search.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+
+/**
+ * Class Search
+ *
+ * Search for pages and content
+ *
+ * @package dokuwiki\Action
+ */
+class Search extends AbstractAction {
+
+ protected $pageLookupResults = array();
+ protected $fullTextResults = array();
+ protected $highlight = array();
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_NONE;
+ }
+
+ /**
+ * we only search if a search word was given
+ *
+ * @inheritdoc
+ */
+ public function checkPreconditions() {
+ parent::checkPreconditions();
+ }
+
+ public function preProcess()
+ {
+ global $QUERY, $ID, $conf, $INPUT;
+ $s = cleanID($QUERY);
+
+ if ($ID !== $conf['start'] && !$INPUT->has('q')) {
+ parse_str($INPUT->server->str('QUERY_STRING'), $urlParts);
+ $urlParts['q'] = $urlParts['id'];
+ unset($urlParts['id']);
+ $url = wl($ID, $urlParts, true, '&');
+ send_redirect($url);
+ }
+
+ if ($s === '') throw new ActionAbort();
+ $this->adjustGlobalQuery();
+ }
+
+ /** @inheritdoc */
+ public function tplContent()
+ {
+ $this->execute();
+
+ $search = new \dokuwiki\Ui\Search($this->pageLookupResults, $this->fullTextResults, $this->highlight);
+ $search->show();
+ }
+
+
+ /**
+ * run the search
+ */
+ protected function execute()
+ {
+ global $INPUT, $QUERY;
+ $after = $INPUT->str('min');
+ $before = $INPUT->str('max');
+ $this->pageLookupResults = ft_pageLookup($QUERY, true, useHeading('navigation'), $after, $before);
+ $this->fullTextResults = ft_pageSearch($QUERY, $highlight, $INPUT->str('srt'), $after, $before);
+ $this->highlight = $highlight;
+ }
+
+ /**
+ * Adjust the global query accordingly to the config search_nslimit and search_fragment
+ *
+ * This will only do something if the search didn't originate from the form on the searchpage itself
+ */
+ protected function adjustGlobalQuery()
+ {
+ global $conf, $INPUT, $QUERY, $ID;
+
+ if ($INPUT->bool('sf')) {
+ return;
+ }
+
+ $Indexer = idx_get_indexer();
+ $parsedQuery = ft_queryParser($Indexer, $QUERY);
+
+ if (empty($parsedQuery['ns']) && empty($parsedQuery['notns'])) {
+ if ($conf['search_nslimit'] > 0) {
+ if (getNS($ID) !== false) {
+ $nsParts = explode(':', getNS($ID));
+ $ns = implode(':', array_slice($nsParts, 0, $conf['search_nslimit']));
+ $QUERY .= " @$ns";
+ }
+ }
+ }
+
+ if ($conf['search_fragment'] !== 'exact') {
+ if (empty(array_diff($parsedQuery['words'], $parsedQuery['and']))) {
+ if (strpos($QUERY, '*') === false) {
+ $queryParts = explode(' ', $QUERY);
+ $queryParts = array_map(function ($part) {
+ if (strpos($part, '@') === 0) {
+ return $part;
+ }
+ if (strpos($part, 'ns:') === 0) {
+ return $part;
+ }
+ if (strpos($part, '^') === 0) {
+ return $part;
+ }
+ if (strpos($part, '-ns:') === 0) {
+ return $part;
+ }
+
+ global $conf;
+
+ if ($conf['search_fragment'] === 'starts_with') {
+ return $part . '*';
+ }
+ if ($conf['search_fragment'] === 'ends_with') {
+ return '*' . $part;
+ }
+
+ return '*' . $part . '*';
+
+ }, $queryParts);
+ $QUERY = implode(' ', $queryParts);
+ }
+ }
+ }
+ }
+}
diff --git a/platform/www/inc/Action/Show.php b/platform/www/inc/Action/Show.php
new file mode 100644
index 0000000..a5cb534
--- /dev/null
+++ b/platform/www/inc/Action/Show.php
@@ -0,0 +1,36 @@
+<?php
+/**
+ * Created by IntelliJ IDEA.
+ * User: andi
+ * Date: 2/10/17
+ * Time: 4:32 PM
+ */
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Show
+ *
+ * The default action of showing a page
+ *
+ * @package dokuwiki\Action
+ */
+class Show extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_READ;
+ }
+
+ /** @inheritdoc */
+ public function preProcess() {
+ global $ID;
+ unlock($ID);
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ html_show();
+ }
+
+}
diff --git a/platform/www/inc/Action/Sitemap.php b/platform/www/inc/Action/Sitemap.php
new file mode 100644
index 0000000..370bcf0
--- /dev/null
+++ b/platform/www/inc/Action/Sitemap.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\FatalException;
+use dokuwiki\Sitemap\Mapper;
+
+/**
+ * Class Sitemap
+ *
+ * Generate an XML sitemap for search engines. Do not confuse with Index
+ *
+ * @package dokuwiki\Action
+ */
+class Sitemap extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_NONE;
+ }
+
+ /**
+ * Handle sitemap delivery
+ *
+ * @author Michael Hamann <michael@content-space.de>
+ * @throws FatalException
+ * @inheritdoc
+ */
+ public function preProcess() {
+ global $conf;
+
+ if($conf['sitemap'] < 1 || !is_numeric($conf['sitemap'])) {
+ throw new FatalException('Sitemap generation is disabled', 404);
+ }
+
+ $sitemap = Mapper::getFilePath();
+ if(Mapper::sitemapIsCompressed()) {
+ $mime = 'application/x-gzip';
+ } else {
+ $mime = 'application/xml; charset=utf-8';
+ }
+
+ // Check if sitemap file exists, otherwise create it
+ if(!is_readable($sitemap)) {
+ Mapper::generate();
+ }
+
+ if(is_readable($sitemap)) {
+ // Send headers
+ header('Content-Type: ' . $mime);
+ header('Content-Disposition: attachment; filename=' . \dokuwiki\Utf8\PhpString::basename($sitemap));
+
+ http_conditionalRequest(filemtime($sitemap));
+
+ // Send file
+ //use x-sendfile header to pass the delivery to compatible webservers
+ http_sendfile($sitemap);
+
+ readfile($sitemap);
+ exit;
+ }
+
+ throw new FatalException('Could not read the sitemap file - bad permissions?');
+ }
+
+}
diff --git a/platform/www/inc/Action/Source.php b/platform/www/inc/Action/Source.php
new file mode 100644
index 0000000..9b03fe9
--- /dev/null
+++ b/platform/www/inc/Action/Source.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Source
+ *
+ * Show the source of a page
+ *
+ * @package dokuwiki\Action
+ */
+class Source extends AbstractAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_READ;
+ }
+
+ /** @inheritdoc */
+ public function preProcess() {
+ global $TEXT;
+ global $INFO;
+ global $ID;
+ global $REV;
+
+ if($INFO['exists']) {
+ $TEXT = rawWiki($ID, $REV);
+ }
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ html_edit();
+ }
+
+}
diff --git a/platform/www/inc/Action/Subscribe.php b/platform/www/inc/Action/Subscribe.php
new file mode 100644
index 0000000..a129a86
--- /dev/null
+++ b/platform/www/inc/Action/Subscribe.php
@@ -0,0 +1,168 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Action\Exception\ActionDisabledException;
+use dokuwiki\Subscriptions\SubscriberManager;
+use dokuwiki\Extension\Event;
+
+/**
+ * Class Subscribe
+ *
+ * E-Mail subscription handling
+ *
+ * @package dokuwiki\Action
+ */
+class Subscribe extends AbstractUserAction {
+
+ /** @inheritdoc */
+ public function minimumPermission() {
+ return AUTH_READ;
+ }
+
+ /** @inheritdoc */
+ public function checkPreconditions() {
+ parent::checkPreconditions();
+
+ global $conf;
+ if(isset($conf['subscribers']) && !$conf['subscribers']) throw new ActionDisabledException();
+ }
+
+ /** @inheritdoc */
+ public function preProcess() {
+ try {
+ $this->handleSubscribeData();
+ } catch(ActionAbort $e) {
+ throw $e;
+ } catch(\Exception $e) {
+ msg($e->getMessage(), -1);
+ }
+ }
+
+ /** @inheritdoc */
+ public function tplContent() {
+ tpl_subscribe();
+ }
+
+ /**
+ * Handle page 'subscribe'
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ * @throws \Exception if (un)subscribing fails
+ * @throws ActionAbort when (un)subscribing worked
+ */
+ protected function handleSubscribeData() {
+ global $lang;
+ global $INFO;
+ global $INPUT;
+
+ // get and preprocess data.
+ $params = array();
+ foreach(array('target', 'style', 'action') as $param) {
+ if($INPUT->has("sub_$param")) {
+ $params[$param] = $INPUT->str("sub_$param");
+ }
+ }
+
+ // any action given? if not just return and show the subscription page
+ if(empty($params['action']) || !checkSecurityToken()) return;
+
+ // Handle POST data, may throw exception.
+ Event::createAndTrigger('ACTION_HANDLE_SUBSCRIBE', $params, array($this, 'handlePostData'));
+
+ $target = $params['target'];
+ $style = $params['style'];
+ $action = $params['action'];
+
+ // Perform action.
+ $subManager = new SubscriberManager();
+ if($action === 'unsubscribe') {
+ $ok = $subManager->remove($target, $INPUT->server->str('REMOTE_USER'), $style);
+ } else {
+ $ok = $subManager->add($target, $INPUT->server->str('REMOTE_USER'), $style);
+ }
+
+ if($ok) {
+ msg(
+ sprintf(
+ $lang["subscr_{$action}_success"], hsc($INFO['userinfo']['name']),
+ prettyprint_id($target)
+ ), 1
+ );
+ throw new ActionAbort('redirect');
+ }
+
+ throw new \Exception(
+ sprintf(
+ $lang["subscr_{$action}_error"],
+ hsc($INFO['userinfo']['name']),
+ prettyprint_id($target)
+ )
+ );
+ }
+
+ /**
+ * Validate POST data
+ *
+ * Validates POST data for a subscribe or unsubscribe request. This is the
+ * default action for the event ACTION_HANDLE_SUBSCRIBE.
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ *
+ * @param array &$params the parameters: target, style and action
+ * @throws \Exception
+ */
+ public function handlePostData(&$params) {
+ global $INFO;
+ global $lang;
+ global $INPUT;
+
+ // Get and validate parameters.
+ if(!isset($params['target'])) {
+ throw new \Exception('no subscription target given');
+ }
+ $target = $params['target'];
+ $valid_styles = array('every', 'digest');
+ if(substr($target, -1, 1) === ':') {
+ // Allow “list” subscribe style since the target is a namespace.
+ $valid_styles[] = 'list';
+ }
+ $style = valid_input_set(
+ 'style', $valid_styles, $params,
+ 'invalid subscription style given'
+ );
+ $action = valid_input_set(
+ 'action', array('subscribe', 'unsubscribe'),
+ $params, 'invalid subscription action given'
+ );
+
+ // Check other conditions.
+ if($action === 'subscribe') {
+ if($INFO['userinfo']['mail'] === '') {
+ throw new \Exception($lang['subscr_subscribe_noaddress']);
+ }
+ } elseif($action === 'unsubscribe') {
+ $is = false;
+ foreach($INFO['subscribed'] as $subscr) {
+ if($subscr['target'] === $target) {
+ $is = true;
+ }
+ }
+ if($is === false) {
+ throw new \Exception(
+ sprintf(
+ $lang['subscr_not_subscribed'],
+ $INPUT->server->str('REMOTE_USER'),
+ prettyprint_id($target)
+ )
+ );
+ }
+ // subscription_set deletes a subscription if style = null.
+ $style = null;
+ }
+
+ $params = compact('target', 'style', 'action');
+ }
+
+}
diff --git a/platform/www/inc/ActionRouter.php b/platform/www/inc/ActionRouter.php
new file mode 100644
index 0000000..7d8a72a
--- /dev/null
+++ b/platform/www/inc/ActionRouter.php
@@ -0,0 +1,228 @@
+<?php
+
+namespace dokuwiki;
+
+use dokuwiki\Action\AbstractAction;
+use dokuwiki\Action\Exception\ActionDisabledException;
+use dokuwiki\Action\Exception\ActionException;
+use dokuwiki\Action\Exception\FatalException;
+use dokuwiki\Action\Exception\NoActionException;
+use dokuwiki\Action\Plugin;
+
+/**
+ * Class ActionRouter
+ * @package dokuwiki
+ */
+class ActionRouter {
+
+ /** @var AbstractAction */
+ protected $action;
+
+ /** @var ActionRouter */
+ protected static $instance = null;
+
+ /** @var int transition counter */
+ protected $transitions = 0;
+
+ /** maximum loop */
+ const MAX_TRANSITIONS = 5;
+
+ /** @var string[] the actions disabled in the configuration */
+ protected $disabled;
+
+ /**
+ * ActionRouter constructor. Singleton, thus protected!
+ *
+ * Sets up the correct action based on the $ACT global. Writes back
+ * the selected action to $ACT
+ */
+ protected function __construct() {
+ global $ACT;
+ global $conf;
+
+ $this->disabled = explode(',', $conf['disableactions']);
+ $this->disabled = array_map('trim', $this->disabled);
+ $this->transitions = 0;
+
+ $ACT = act_clean($ACT);
+ $this->setupAction($ACT);
+ $ACT = $this->action->getActionName();
+ }
+
+ /**
+ * Get the singleton instance
+ *
+ * @param bool $reinit
+ * @return ActionRouter
+ */
+ public static function getInstance($reinit = false) {
+ if((self::$instance === null) || $reinit) {
+ self::$instance = new ActionRouter();
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Setup the given action
+ *
+ * Instantiates the right class, runs permission checks and pre-processing and
+ * sets $action
+ *
+ * @param string $actionname this is passed as a reference to $ACT, for plugin backward compatibility
+ * @triggers ACTION_ACT_PREPROCESS
+ */
+ protected function setupAction(&$actionname) {
+ $presetup = $actionname;
+
+ try {
+ // give plugins an opportunity to process the actionname
+ $evt = new Extension\Event('ACTION_ACT_PREPROCESS', $actionname);
+ if ($evt->advise_before()) {
+ $this->action = $this->loadAction($actionname);
+ $this->checkAction($this->action);
+ $this->action->preProcess();
+ } else {
+ // event said the action should be kept, assume action plugin will handle it later
+ $this->action = new Plugin($actionname);
+ }
+ $evt->advise_after();
+
+ } catch(ActionException $e) {
+ // we should have gotten a new action
+ $actionname = $e->getNewAction();
+
+ // this one should trigger a user message
+ if(is_a($e, ActionDisabledException::class)) {
+ msg('Action disabled: ' . hsc($presetup), -1);
+ }
+
+ // some actions may request the display of a message
+ if($e->displayToUser()) {
+ msg(hsc($e->getMessage()), -1);
+ }
+
+ // do setup for new action
+ $this->transitionAction($presetup, $actionname);
+
+ } catch(NoActionException $e) {
+ msg('Action unknown: ' . hsc($actionname), -1);
+ $actionname = 'show';
+ $this->transitionAction($presetup, $actionname);
+ } catch(\Exception $e) {
+ $this->handleFatalException($e);
+ }
+ }
+
+ /**
+ * Transitions from one action to another
+ *
+ * Basically just calls setupAction() again but does some checks before.
+ *
+ * @param string $from current action name
+ * @param string $to new action name
+ * @param null|ActionException $e any previous exception that caused the transition
+ */
+ protected function transitionAction($from, $to, $e = null) {
+ $this->transitions++;
+
+ // no infinite recursion
+ if($from == $to) {
+ $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
+ }
+
+ // larger loops will be caught here
+ if($this->transitions >= self::MAX_TRANSITIONS) {
+ $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
+ }
+
+ // do the recursion
+ $this->setupAction($to);
+ }
+
+ /**
+ * Aborts all processing with a message
+ *
+ * When a FataException instanc is passed, the code is treated as Status code
+ *
+ * @param \Exception|FatalException $e
+ * @throws FatalException during unit testing
+ */
+ protected function handleFatalException(\Exception $e) {
+ if(is_a($e, FatalException::class)) {
+ http_status($e->getCode());
+ } else {
+ http_status(500);
+ }
+ if(defined('DOKU_UNITTEST')) {
+ throw $e;
+ }
+ $msg = 'Something unforeseen has happened: ' . $e->getMessage();
+ nice_die(hsc($msg));
+ }
+
+ /**
+ * Load the given action
+ *
+ * This translates the given name to a class name by uppercasing the first letter.
+ * Underscores translate to camelcase names. For actions with underscores, the different
+ * parts are removed beginning from the end until a matching class is found. The instatiated
+ * Action will always have the full original action set as Name
+ *
+ * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export'
+ *
+ * @param $actionname
+ * @return AbstractAction
+ * @throws NoActionException
+ */
+ public function loadAction($actionname) {
+ $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else?
+ $parts = explode('_', $actionname);
+ while(!empty($parts)) {
+ $load = join('_', $parts);
+ $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_'));
+ if(class_exists($class)) {
+ return new $class($actionname);
+ }
+ array_pop($parts);
+ }
+
+ throw new NoActionException();
+ }
+
+ /**
+ * Execute all the checks to see if this action can be executed
+ *
+ * @param AbstractAction $action
+ * @throws ActionDisabledException
+ * @throws ActionException
+ */
+ public function checkAction(AbstractAction $action) {
+ global $INFO;
+ global $ID;
+
+ if(in_array($action->getActionName(), $this->disabled)) {
+ throw new ActionDisabledException();
+ }
+
+ $action->checkPreconditions();
+
+ if(isset($INFO)) {
+ $perm = $INFO['perm'];
+ } else {
+ $perm = auth_quickaclcheck($ID);
+ }
+
+ if($perm < $action->minimumPermission()) {
+ throw new ActionException('denied');
+ }
+ }
+
+ /**
+ * Returns the action handling the current request
+ *
+ * @return AbstractAction
+ */
+ public function getAction() {
+ return $this->action;
+ }
+}
diff --git a/platform/www/inc/Ajax.php b/platform/www/inc/Ajax.php
new file mode 100644
index 0000000..386d653
--- /dev/null
+++ b/platform/www/inc/Ajax.php
@@ -0,0 +1,438 @@
+<?php
+
+namespace dokuwiki;
+
+/**
+ * Manage all builtin AJAX calls
+ *
+ * @todo The calls should be refactored out to their own proper classes
+ * @package dokuwiki
+ */
+class Ajax {
+
+ /**
+ * Execute the given call
+ *
+ * @param string $call name of the ajax call
+ */
+ public function __construct($call) {
+ $callfn = 'call' . ucfirst($call);
+ if(method_exists($this, $callfn)) {
+ $this->$callfn();
+ } else {
+ $evt = new Extension\Event('AJAX_CALL_UNKNOWN', $call);
+ if($evt->advise_before()) {
+ print "AJAX call '" . hsc($call) . "' unknown!\n";
+ } else {
+ $evt->advise_after();
+ unset($evt);
+ }
+ }
+ }
+
+ /**
+ * Searches for matching pagenames
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function callQsearch() {
+ global $lang;
+ global $INPUT;
+
+ $maxnumbersuggestions = 50;
+
+ $query = $INPUT->post->str('q');
+ if(empty($query)) $query = $INPUT->get->str('q');
+ if(empty($query)) return;
+
+ $query = urldecode($query);
+
+ $data = ft_pageLookup($query, true, useHeading('navigation'));
+
+ if(!count($data)) return;
+
+ print '<strong>' . $lang['quickhits'] . '</strong>';
+ print '<ul>';
+ $counter = 0;
+ foreach($data as $id => $title) {
+ if(useHeading('navigation')) {
+ $name = $title;
+ } else {
+ $ns = getNS($id);
+ if($ns) {
+ $name = noNS($id) . ' (' . $ns . ')';
+ } else {
+ $name = $id;
+ }
+ }
+ echo '<li>' . html_wikilink(':' . $id, $name) . '</li>';
+
+ $counter++;
+ if($counter > $maxnumbersuggestions) {
+ echo '<li>...</li>';
+ break;
+ }
+ }
+ print '</ul>';
+ }
+
+ /**
+ * Support OpenSearch suggestions
+ *
+ * @link http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.0
+ * @author Mike Frysinger <vapier@gentoo.org>
+ */
+ protected function callSuggestions() {
+ global $INPUT;
+
+ $query = cleanID($INPUT->post->str('q'));
+ if(empty($query)) $query = cleanID($INPUT->get->str('q'));
+ if(empty($query)) return;
+
+ $data = ft_pageLookup($query);
+ if(!count($data)) return;
+ $data = array_keys($data);
+
+ // limit results to 15 hits
+ $data = array_slice($data, 0, 15);
+ $data = array_map('trim', $data);
+ $data = array_map('noNS', $data);
+ $data = array_unique($data);
+ sort($data);
+
+ /* now construct a json */
+ $suggestions = array(
+ $query, // the original query
+ $data, // some suggestions
+ array(), // no description
+ array() // no urls
+ );
+
+ header('Content-Type: application/x-suggestions+json');
+ print json_encode($suggestions);
+ }
+
+ /**
+ * Refresh a page lock and save draft
+ *
+ * Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function callLock() {
+ global $ID;
+ global $INFO;
+ global $INPUT;
+
+ $ID = cleanID($INPUT->post->str('id'));
+ if(empty($ID)) return;
+
+ $INFO = pageinfo();
+
+ $response = [
+ 'errors' => [],
+ 'lock' => '0',
+ 'draft' => '',
+ ];
+ if(!$INFO['writable']) {
+ $response['errors'][] = 'Permission to write this page has been denied.';
+ echo json_encode($response);
+ return;
+ }
+
+ if(!checklock($ID)) {
+ lock($ID);
+ $response['lock'] = '1';
+ }
+
+ $draft = new Draft($ID, $INFO['client']);
+ if ($draft->saveDraft()) {
+ $response['draft'] = $draft->getDraftMessage();
+ } else {
+ $response['errors'] = array_merge($response['errors'], $draft->getErrors());
+ }
+ echo json_encode($response);
+ }
+
+ /**
+ * Delete a draft
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function callDraftdel() {
+ global $INPUT;
+ $id = cleanID($INPUT->str('id'));
+ if(empty($id)) return;
+
+ $client = $_SERVER['REMOTE_USER'];
+ if(!$client) $client = clientIP(true);
+
+ $cname = getCacheName($client . $id, '.draft');
+ @unlink($cname);
+ }
+
+ /**
+ * Return subnamespaces for the Mediamanager
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function callMedians() {
+ global $conf;
+ global $INPUT;
+
+ // wanted namespace
+ $ns = cleanID($INPUT->post->str('ns'));
+ $dir = utf8_encodeFN(str_replace(':', '/', $ns));
+
+ $lvl = count(explode(':', $ns));
+
+ $data = array();
+ search($data, $conf['mediadir'], 'search_index', array('nofiles' => true), $dir);
+ foreach(array_keys($data) as $item) {
+ $data[$item]['level'] = $lvl + 1;
+ }
+ echo html_buildlist($data, 'idx', 'media_nstree_item', 'media_nstree_li');
+ }
+
+ /**
+ * Return list of files for the Mediamanager
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function callMedialist() {
+ global $NS;
+ global $INPUT;
+
+ $NS = cleanID($INPUT->post->str('ns'));
+ $sort = $INPUT->post->bool('recent') ? 'date' : 'natural';
+ if($INPUT->post->str('do') == 'media') {
+ tpl_mediaFileList();
+ } else {
+ tpl_mediaContent(true, $sort);
+ }
+ }
+
+ /**
+ * Return the content of the right column
+ * (image details) for the Mediamanager
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ */
+ protected function callMediadetails() {
+ global $IMG, $JUMPTO, $REV, $fullscreen, $INPUT;
+ $fullscreen = true;
+ require_once(DOKU_INC . 'lib/exe/mediamanager.php');
+
+ $image = '';
+ if($INPUT->has('image')) $image = cleanID($INPUT->str('image'));
+ if(isset($IMG)) $image = $IMG;
+ if(isset($JUMPTO)) $image = $JUMPTO;
+ $rev = false;
+ if(isset($REV) && !$JUMPTO) $rev = $REV;
+
+ html_msgarea();
+ tpl_mediaFileDetails($image, $rev);
+ }
+
+ /**
+ * Returns image diff representation for mediamanager
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ */
+ protected function callMediadiff() {
+ global $NS;
+ global $INPUT;
+
+ $image = '';
+ if($INPUT->has('image')) $image = cleanID($INPUT->str('image'));
+ $NS = getNS($image);
+ $auth = auth_quickaclcheck("$NS:*");
+ media_diff($image, $NS, $auth, true);
+ }
+
+ /**
+ * Manages file uploads
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ */
+ protected function callMediaupload() {
+ global $NS, $MSG, $INPUT;
+
+ $id = '';
+ if(isset($_FILES['qqfile']['tmp_name'])) {
+ $id = $INPUT->post->str('mediaid', $_FILES['qqfile']['name']);
+ } elseif($INPUT->get->has('qqfile')) {
+ $id = $INPUT->get->str('qqfile');
+ }
+
+ $id = cleanID($id);
+
+ $NS = $INPUT->str('ns');
+ $ns = $NS . ':' . getNS($id);
+
+ $AUTH = auth_quickaclcheck("$ns:*");
+ if($AUTH >= AUTH_UPLOAD) {
+ io_createNamespace("$ns:xxx", 'media');
+ }
+
+ if(isset($_FILES['qqfile']['error']) && $_FILES['qqfile']['error']) unset($_FILES['qqfile']);
+
+ $res = false;
+ if(isset($_FILES['qqfile']['tmp_name'])) $res = media_upload($NS, $AUTH, $_FILES['qqfile']);
+ if($INPUT->get->has('qqfile')) $res = media_upload_xhr($NS, $AUTH);
+
+ if($res) {
+ $result = array(
+ 'success' => true,
+ 'link' => media_managerURL(array('ns' => $ns, 'image' => $NS . ':' . $id), '&'),
+ 'id' => $NS . ':' . $id,
+ 'ns' => $NS
+ );
+ } else {
+ $error = '';
+ if(isset($MSG)) {
+ foreach($MSG as $msg) {
+ $error .= $msg['msg'];
+ }
+ }
+ $result = array(
+ 'error' => $error,
+ 'ns' => $NS
+ );
+ }
+
+ header('Content-Type: application/json');
+ echo json_encode($result);
+ }
+
+ /**
+ * Return sub index for index view
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function callIndex() {
+ global $conf;
+ global $INPUT;
+
+ // wanted namespace
+ $ns = cleanID($INPUT->post->str('idx'));
+ $dir = utf8_encodeFN(str_replace(':', '/', $ns));
+
+ $lvl = count(explode(':', $ns));
+
+ $data = array();
+ search($data, $conf['datadir'], 'search_index', array('ns' => $ns), $dir);
+ foreach(array_keys($data) as $item) {
+ $data[$item]['level'] = $lvl + 1;
+ }
+ echo html_buildlist($data, 'idx', 'html_list_index', 'html_li_index');
+ }
+
+ /**
+ * List matching namespaces and pages for the link wizard
+ *
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+ protected function callLinkwiz() {
+ global $conf;
+ global $lang;
+ global $INPUT;
+
+ $q = ltrim(trim($INPUT->post->str('q')), ':');
+ $id = noNS($q);
+ $ns = getNS($q);
+
+ $ns = cleanID($ns);
+ $id = cleanID($id);
+
+ $nsd = utf8_encodeFN(str_replace(':', '/', $ns));
+
+ $data = array();
+ if($q !== '' && $ns === '') {
+
+ // use index to lookup matching pages
+ $pages = ft_pageLookup($id, true);
+
+ // result contains matches in pages and namespaces
+ // we now extract the matching namespaces to show
+ // them seperately
+ $dirs = array();
+
+ foreach($pages as $pid => $title) {
+ if(strpos(noNS($pid), $id) === false) {
+ // match was in the namespace
+ $dirs[getNS($pid)] = 1; // assoc array avoids dupes
+ } else {
+ // it is a matching page, add it to the result
+ $data[] = array(
+ 'id' => $pid,
+ 'title' => $title,
+ 'type' => 'f',
+ );
+ }
+ unset($pages[$pid]);
+ }
+ foreach($dirs as $dir => $junk) {
+ $data[] = array(
+ 'id' => $dir,
+ 'type' => 'd',
+ );
+ }
+
+ } else {
+
+ $opts = array(
+ 'depth' => 1,
+ 'listfiles' => true,
+ 'listdirs' => true,
+ 'pagesonly' => true,
+ 'firsthead' => true,
+ 'sneakyacl' => $conf['sneaky_index'],
+ );
+ if($id) $opts['filematch'] = '^.*\/' . $id;
+ if($id) $opts['dirmatch'] = '^.*\/' . $id;
+ search($data, $conf['datadir'], 'search_universal', $opts, $nsd);
+
+ // add back to upper
+ if($ns) {
+ array_unshift(
+ $data, array(
+ 'id' => getNS($ns),
+ 'type' => 'u',
+ )
+ );
+ }
+ }
+
+ // fixme sort results in a useful way ?
+
+ if(!count($data)) {
+ echo $lang['nothingfound'];
+ exit;
+ }
+
+ // output the found data
+ $even = 1;
+ foreach($data as $item) {
+ $even *= -1; //zebra
+
+ if(($item['type'] == 'd' || $item['type'] == 'u') && $item['id'] !== '') $item['id'] .= ':';
+ $link = wl($item['id']);
+
+ echo '<div class="' . (($even > 0) ? 'even' : 'odd') . ' type_' . $item['type'] . '">';
+
+ if($item['type'] == 'u') {
+ $name = $lang['upperns'];
+ } else {
+ $name = hsc($item['id']);
+ }
+
+ echo '<a href="' . $link . '" title="' . hsc($item['id']) . '" class="wikilink1">' . $name . '</a>';
+
+ if(!blank($item['title'])) {
+ echo '<span>' . hsc($item['title']) . '</span>';
+ }
+ echo '</div>';
+ }
+
+ }
+
+}
diff --git a/platform/www/inc/Cache/Cache.php b/platform/www/inc/Cache/Cache.php
new file mode 100644
index 0000000..af82e6b
--- /dev/null
+++ b/platform/www/inc/Cache/Cache.php
@@ -0,0 +1,240 @@
+<?php
+
+namespace dokuwiki\Cache;
+
+use dokuwiki\Debug\PropertyDeprecationHelper;
+use dokuwiki\Extension\Event;
+
+/**
+ * Generic handling of caching
+ */
+class Cache
+{
+ use PropertyDeprecationHelper;
+
+ public $key = ''; // primary identifier for this item
+ public $ext = ''; // file ext for cache data, secondary identifier for this item
+ public $cache = ''; // cache file name
+ public $depends = array(); // array containing cache dependency information,
+ // used by makeDefaultCacheDecision to determine cache validity
+
+ // phpcs:disable
+ /**
+ * @deprecated since 2019-02-02 use the respective getters instead!
+ */
+ protected $_event = ''; // event to be triggered during useCache
+ protected $_time;
+ protected $_nocache = false; // if set to true, cache will not be used or stored
+ // phpcs:enable
+
+ /**
+ * @param string $key primary identifier
+ * @param string $ext file extension
+ */
+ public function __construct($key, $ext)
+ {
+ $this->key = $key;
+ $this->ext = $ext;
+ $this->cache = getCacheName($key, $ext);
+
+ /**
+ * @deprecated since 2019-02-02 use the respective getters instead!
+ */
+ $this->deprecatePublicProperty('_event');
+ $this->deprecatePublicProperty('_time');
+ $this->deprecatePublicProperty('_nocache');
+ }
+
+ public function getTime()
+ {
+ return $this->_time;
+ }
+
+ public function getEvent()
+ {
+ return $this->_event;
+ }
+
+ public function setEvent($event)
+ {
+ $this->_event = $event;
+ }
+
+ /**
+ * public method to determine whether the cache can be used
+ *
+ * to assist in centralisation of event triggering and calculation of cache statistics,
+ * don't override this function override makeDefaultCacheDecision()
+ *
+ * @param array $depends array of cache dependencies, support dependecies:
+ * 'age' => max age of the cache in seconds
+ * 'files' => cache must be younger than mtime of each file
+ * (nb. dependency passes if file doesn't exist)
+ *
+ * @return bool true if cache can be used, false otherwise
+ */
+ public function useCache($depends = array())
+ {
+ $this->depends = $depends;
+ $this->addDependencies();
+
+ if ($this->getEvent()) {
+ return $this->stats(
+ Event::createAndTrigger(
+ $this->getEvent(),
+ $this,
+ array($this, 'makeDefaultCacheDecision')
+ )
+ );
+ }
+
+ return $this->stats($this->makeDefaultCacheDecision());
+ }
+
+ /**
+ * internal method containing cache use decision logic
+ *
+ * this function processes the following keys in the depends array
+ * purge - force a purge on any non empty value
+ * age - expire cache if older than age (seconds)
+ * files - expire cache if any file in this array was updated more recently than the cache
+ *
+ * Note that this function needs to be public as it is used as callback for the event handler
+ *
+ * can be overridden
+ *
+ * @internal This method may only be called by the event handler! Call \dokuwiki\Cache\Cache::useCache instead!
+ *
+ * @return bool see useCache()
+ */
+ public function makeDefaultCacheDecision()
+ {
+ if ($this->_nocache) {
+ return false;
+ } // caching turned off
+ if (!empty($this->depends['purge'])) {
+ return false;
+ } // purge requested?
+ if (!($this->_time = @filemtime($this->cache))) {
+ return false;
+ } // cache exists?
+
+ // cache too old?
+ if (!empty($this->depends['age']) && ((time() - $this->_time) > $this->depends['age'])) {
+ return false;
+ }
+
+ if (!empty($this->depends['files'])) {
+ foreach ($this->depends['files'] as $file) {
+ if ($this->_time <= @filemtime($file)) {
+ return false;
+ } // cache older than files it depends on?
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * add dependencies to the depends array
+ *
+ * this method should only add dependencies,
+ * it should not remove any existing dependencies and
+ * it should only overwrite a dependency when the new value is more stringent than the old
+ */
+ protected function addDependencies()
+ {
+ global $INPUT;
+ if ($INPUT->has('purge')) {
+ $this->depends['purge'] = true;
+ } // purge requested
+ }
+
+ /**
+ * retrieve the cached data
+ *
+ * @param bool $clean true to clean line endings, false to leave line endings alone
+ * @return string cache contents
+ */
+ public function retrieveCache($clean = true)
+ {
+ return io_readFile($this->cache, $clean);
+ }
+
+ /**
+ * cache $data
+ *
+ * @param string $data the data to be cached
+ * @return bool true on success, false otherwise
+ */
+ public function storeCache($data)
+ {
+ if ($this->_nocache) {
+ return false;
+ }
+
+ return io_saveFile($this->cache, $data);
+ }
+
+ /**
+ * remove any cached data associated with this cache instance
+ */
+ public function removeCache()
+ {
+ @unlink($this->cache);
+ }
+
+ /**
+ * Record cache hits statistics.
+ * (Only when debugging allowed, to reduce overhead.)
+ *
+ * @param bool $success result of this cache use attempt
+ * @return bool pass-thru $success value
+ */
+ protected function stats($success)
+ {
+ global $conf;
+ static $stats = null;
+ static $file;
+
+ if (!$conf['allowdebug']) {
+ return $success;
+ }
+
+ if (is_null($stats)) {
+ $file = $conf['cachedir'] . '/cache_stats.txt';
+ $lines = explode("\n", io_readFile($file));
+
+ foreach ($lines as $line) {
+ $i = strpos($line, ',');
+ $stats[substr($line, 0, $i)] = $line;
+ }
+ }
+
+ if (isset($stats[$this->ext])) {
+ list($ext, $count, $hits) = explode(',', $stats[$this->ext]);
+ } else {
+ $ext = $this->ext;
+ $count = 0;
+ $hits = 0;
+ }
+
+ $count++;
+ if ($success) {
+ $hits++;
+ }
+ $stats[$this->ext] = "$ext,$count,$hits";
+
+ io_saveFile($file, join("\n", $stats));
+
+ return $success;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isNoCache()
+ {
+ return $this->_nocache;
+ }
+}
diff --git a/platform/www/inc/Cache/CacheInstructions.php b/platform/www/inc/Cache/CacheInstructions.php
new file mode 100644
index 0000000..acd02ab
--- /dev/null
+++ b/platform/www/inc/Cache/CacheInstructions.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace dokuwiki\Cache;
+
+/**
+ * Caching of parser instructions
+ */
+class CacheInstructions extends \dokuwiki\Cache\CacheParser
+{
+
+ /**
+ * @param string $id page id
+ * @param string $file source file for cache
+ */
+ public function __construct($id, $file)
+ {
+ parent::__construct($id, $file, 'i');
+ }
+
+ /**
+ * retrieve the cached data
+ *
+ * @param bool $clean true to clean line endings, false to leave line endings alone
+ * @return array cache contents
+ */
+ public function retrieveCache($clean = true)
+ {
+ $contents = io_readFile($this->cache, false);
+ return !empty($contents) ? unserialize($contents) : array();
+ }
+
+ /**
+ * cache $instructions
+ *
+ * @param array $instructions the instruction to be cached
+ * @return bool true on success, false otherwise
+ */
+ public function storeCache($instructions)
+ {
+ if ($this->_nocache) {
+ return false;
+ }
+
+ return io_saveFile($this->cache, serialize($instructions));
+ }
+}
diff --git a/platform/www/inc/Cache/CacheParser.php b/platform/www/inc/Cache/CacheParser.php
new file mode 100644
index 0000000..ed476f4
--- /dev/null
+++ b/platform/www/inc/Cache/CacheParser.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace dokuwiki\Cache;
+
+/**
+ * Parser caching
+ */
+class CacheParser extends Cache
+{
+
+ public $file = ''; // source file for cache
+ public $mode = ''; // input mode (represents the processing the input file will undergo)
+ public $page = '';
+
+ /**
+ *
+ * @param string $id page id
+ * @param string $file source file for cache
+ * @param string $mode input mode
+ */
+ public function __construct($id, $file, $mode)
+ {
+ if ($id) {
+ $this->page = $id;
+ }
+ $this->file = $file;
+ $this->mode = $mode;
+
+ $this->setEvent('PARSER_CACHE_USE');
+ parent::__construct($file . $_SERVER['HTTP_HOST'] . $_SERVER['SERVER_PORT'], '.' . $mode);
+ }
+
+ /**
+ * method contains cache use decision logic
+ *
+ * @return bool see useCache()
+ */
+ public function makeDefaultCacheDecision()
+ {
+
+ if (!file_exists($this->file)) {
+ return false;
+ } // source exists?
+ return parent::makeDefaultCacheDecision();
+ }
+
+ protected function addDependencies()
+ {
+
+ // parser cache file dependencies ...
+ $files = array(
+ $this->file, // ... source
+ DOKU_INC . 'inc/parser/Parser.php', // ... parser
+ DOKU_INC . 'inc/parser/handler.php', // ... handler
+ );
+ $files = array_merge($files, getConfigFiles('main')); // ... wiki settings
+
+ $this->depends['files'] = !empty($this->depends['files']) ?
+ array_merge($files, $this->depends['files']) :
+ $files;
+ parent::addDependencies();
+ }
+
+}
diff --git a/platform/www/inc/Cache/CacheRenderer.php b/platform/www/inc/Cache/CacheRenderer.php
new file mode 100644
index 0000000..e8a28c3
--- /dev/null
+++ b/platform/www/inc/Cache/CacheRenderer.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace dokuwiki\Cache;
+
+/**
+ * Caching of data of renderer
+ */
+class CacheRenderer extends CacheParser
+{
+
+ /**
+ * method contains cache use decision logic
+ *
+ * @return bool see useCache()
+ */
+ public function makeDefaultCacheDecision()
+ {
+ global $conf;
+
+ if (!parent::makeDefaultCacheDecision()) {
+ return false;
+ }
+
+ if (!isset($this->page)) {
+ return true;
+ }
+
+ // meta cache older than file it depends on?
+ if ($this->_time < @filemtime(metaFN($this->page, '.meta'))) {
+ return false;
+ }
+
+ // check current link existence is consistent with cache version
+ // first check the purgefile
+ // - if the cache is more recent than the purgefile we know no links can have been updated
+ if ($this->_time >= @filemtime($conf['cachedir'] . '/purgefile')) {
+ return true;
+ }
+
+ // for wiki pages, check metadata dependencies
+ $metadata = p_get_metadata($this->page);
+
+ if (!isset($metadata['relation']['references']) ||
+ empty($metadata['relation']['references'])) {
+ return true;
+ }
+
+ foreach ($metadata['relation']['references'] as $id => $exists) {
+ if ($exists != page_exists($id, '', false)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ protected function addDependencies()
+ {
+ global $conf;
+
+ // default renderer cache file 'age' is dependent on 'cachetime' setting, two special values:
+ // -1 : do not cache (should not be overridden)
+ // 0 : cache never expires (can be overridden) - no need to set depends['age']
+ if ($conf['cachetime'] == -1) {
+ $this->_nocache = true;
+ return;
+ } elseif ($conf['cachetime'] > 0) {
+ $this->depends['age'] = isset($this->depends['age']) ?
+ min($this->depends['age'], $conf['cachetime']) : $conf['cachetime'];
+ }
+
+ // renderer cache file dependencies ...
+ $files = array(
+ DOKU_INC . 'inc/parser/' . $this->mode . '.php', // ... the renderer
+ );
+
+ // page implies metadata and possibly some other dependencies
+ if (isset($this->page)) {
+
+ // for xhtml this will render the metadata if needed
+ $valid = p_get_metadata($this->page, 'date valid');
+ if (!empty($valid['age'])) {
+ $this->depends['age'] = isset($this->depends['age']) ?
+ min($this->depends['age'], $valid['age']) : $valid['age'];
+ }
+ }
+
+ $this->depends['files'] = !empty($this->depends['files']) ?
+ array_merge($files, $this->depends['files']) :
+ $files;
+
+ parent::addDependencies();
+ }
+}
diff --git a/platform/www/inc/ChangeLog/ChangeLog.php b/platform/www/inc/ChangeLog/ChangeLog.php
new file mode 100644
index 0000000..16b5cc2
--- /dev/null
+++ b/platform/www/inc/ChangeLog/ChangeLog.php
@@ -0,0 +1,666 @@
+<?php
+
+namespace dokuwiki\ChangeLog;
+
+/**
+ * methods for handling of changelog of pages or media files
+ */
+abstract class ChangeLog
+{
+
+ /** @var string */
+ protected $id;
+ /** @var int */
+ protected $chunk_size;
+ /** @var array */
+ protected $cache;
+
+ /**
+ * Constructor
+ *
+ * @param string $id page id
+ * @param int $chunk_size maximum block size read from file
+ */
+ public function __construct($id, $chunk_size = 8192)
+ {
+ global $cache_revinfo;
+
+ $this->cache =& $cache_revinfo;
+ if (!isset($this->cache[$id])) {
+ $this->cache[$id] = array();
+ }
+
+ $this->id = $id;
+ $this->setChunkSize($chunk_size);
+
+ }
+
+ /**
+ * Set chunk size for file reading
+ * Chunk size zero let read whole file at once
+ *
+ * @param int $chunk_size maximum block size read from file
+ */
+ public function setChunkSize($chunk_size)
+ {
+ if (!is_numeric($chunk_size)) $chunk_size = 0;
+
+ $this->chunk_size = (int)max($chunk_size, 0);
+ }
+
+ /**
+ * Returns path to changelog
+ *
+ * @return string path to file
+ */
+ abstract protected function getChangelogFilename();
+
+ /**
+ * Returns path to current page/media
+ *
+ * @return string path to file
+ */
+ abstract protected function getFilename();
+
+ /**
+ * Get the changelog information for a specific page id and revision (timestamp)
+ *
+ * Adjacent changelog lines are optimistically parsed and cached to speed up
+ * consecutive calls to getRevisionInfo. For large changelog files, only the chunk
+ * containing the requested changelog line is read.
+ *
+ * @param int $rev revision timestamp
+ * @return bool|array false or array with entries:
+ * - date: unix timestamp
+ * - ip: IPv4 address (127.0.0.1)
+ * - type: log line type
+ * - id: page id
+ * - user: user name
+ * - sum: edit summary (or action reason)
+ * - extra: extra data (varies by line type)
+ *
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ */
+ public function getRevisionInfo($rev)
+ {
+ $rev = max($rev, 0);
+
+ // check if it's already in the memory cache
+ if (isset($this->cache[$this->id]) && isset($this->cache[$this->id][$rev])) {
+ return $this->cache[$this->id][$rev];
+ }
+
+ //read lines from changelog
+ list($fp, $lines) = $this->readloglines($rev);
+ if ($fp) {
+ fclose($fp);
+ }
+ if (empty($lines)) return false;
+
+ // parse and cache changelog lines
+ foreach ($lines as $value) {
+ $tmp = parseChangelogLine($value);
+ if ($tmp !== false) {
+ $this->cache[$this->id][$tmp['date']] = $tmp;
+ }
+ }
+ if (!isset($this->cache[$this->id][$rev])) {
+ return false;
+ }
+ return $this->cache[$this->id][$rev];
+ }
+
+ /**
+ * Return a list of page revisions numbers
+ *
+ * Does not guarantee that the revision exists in the attic,
+ * only that a line with the date exists in the changelog.
+ * By default the current revision is skipped.
+ *
+ * The current revision is automatically skipped when the page exists.
+ * See $INFO['meta']['last_change'] for the current revision.
+ * A negative $first let read the current revision too.
+ *
+ * For efficiency, the log lines are parsed and cached for later
+ * calls to getRevisionInfo. Large changelog files are read
+ * backwards in chunks until the requested number of changelog
+ * lines are recieved.
+ *
+ * @param int $first skip the first n changelog lines
+ * @param int $num number of revisions to return
+ * @return array with the revision timestamps
+ *
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ */
+ public function getRevisions($first, $num)
+ {
+ $revs = array();
+ $lines = array();
+ $count = 0;
+
+ $num = max($num, 0);
+ if ($num == 0) {
+ return $revs;
+ }
+
+ if ($first < 0) {
+ $first = 0;
+ } else {
+ if (file_exists($this->getFilename())) {
+ // skip current revision if the page exists
+ $first = max($first + 1, 0);
+ }
+ }
+
+ $file = $this->getChangelogFilename();
+
+ if (!file_exists($file)) {
+ return $revs;
+ }
+ if (filesize($file) < $this->chunk_size || $this->chunk_size == 0) {
+ // read whole file
+ $lines = file($file);
+ if ($lines === false) {
+ return $revs;
+ }
+ } else {
+ // read chunks backwards
+ $fp = fopen($file, 'rb'); // "file pointer"
+ if ($fp === false) {
+ return $revs;
+ }
+ fseek($fp, 0, SEEK_END);
+ $tail = ftell($fp);
+
+ // chunk backwards
+ $finger = max($tail - $this->chunk_size, 0);
+ while ($count < $num + $first) {
+ $nl = $this->getNewlinepointer($fp, $finger);
+
+ // was the chunk big enough? if not, take another bite
+ if ($nl > 0 && $tail <= $nl) {
+ $finger = max($finger - $this->chunk_size, 0);
+ continue;
+ } else {
+ $finger = $nl;
+ }
+
+ // read chunk
+ $chunk = '';
+ $read_size = max($tail - $finger, 0); // found chunk size
+ $got = 0;
+ while ($got < $read_size && !feof($fp)) {
+ $tmp = @fread($fp, max(min($this->chunk_size, $read_size - $got), 0));
+ if ($tmp === false) {
+ break;
+ } //error state
+ $got += strlen($tmp);
+ $chunk .= $tmp;
+ }
+ $tmp = explode("\n", $chunk);
+ array_pop($tmp); // remove trailing newline
+
+ // combine with previous chunk
+ $count += count($tmp);
+ $lines = array_merge($tmp, $lines);
+
+ // next chunk
+ if ($finger == 0) {
+ break;
+ } else { // already read all the lines
+ $tail = $finger;
+ $finger = max($tail - $this->chunk_size, 0);
+ }
+ }
+ fclose($fp);
+ }
+
+ // skip parsing extra lines
+ $num = max(min(count($lines) - $first, $num), 0);
+ if ($first > 0 && $num > 0) {
+ $lines = array_slice($lines, max(count($lines) - $first - $num, 0), $num);
+ } else {
+ if ($first > 0 && $num == 0) {
+ $lines = array_slice($lines, 0, max(count($lines) - $first, 0));
+ } elseif ($first == 0 && $num > 0) {
+ $lines = array_slice($lines, max(count($lines) - $num, 0));
+ }
+ }
+
+ // handle lines in reverse order
+ for ($i = count($lines) - 1; $i >= 0; $i--) {
+ $tmp = parseChangelogLine($lines[$i]);
+ if ($tmp !== false) {
+ $this->cache[$this->id][$tmp['date']] = $tmp;
+ $revs[] = $tmp['date'];
+ }
+ }
+
+ return $revs;
+ }
+
+ /**
+ * Get the nth revision left or right handside for a specific page id and revision (timestamp)
+ *
+ * For large changelog files, only the chunk containing the
+ * reference revision $rev is read and sometimes a next chunck.
+ *
+ * Adjacent changelog lines are optimistically parsed and cached to speed up
+ * consecutive calls to getRevisionInfo.
+ *
+ * @param int $rev revision timestamp used as startdate (doesn't need to be revisionnumber)
+ * @param int $direction give position of returned revision with respect to $rev; positive=next, negative=prev
+ * @return bool|int
+ * timestamp of the requested revision
+ * otherwise false
+ */
+ public function getRelativeRevision($rev, $direction)
+ {
+ $rev = max($rev, 0);
+ $direction = (int)$direction;
+
+ //no direction given or last rev, so no follow-up
+ if (!$direction || ($direction > 0 && $this->isCurrentRevision($rev))) {
+ return false;
+ }
+
+ //get lines from changelog
+ list($fp, $lines, $head, $tail, $eof) = $this->readloglines($rev);
+ if (empty($lines)) return false;
+
+ // look for revisions later/earlier then $rev, when founded count till the wanted revision is reached
+ // also parse and cache changelog lines for getRevisionInfo().
+ $revcounter = 0;
+ $relativerev = false;
+ $checkotherchunck = true; //always runs once
+ while (!$relativerev && $checkotherchunck) {
+ $tmp = array();
+ //parse in normal or reverse order
+ $count = count($lines);
+ if ($direction > 0) {
+ $start = 0;
+ $step = 1;
+ } else {
+ $start = $count - 1;
+ $step = -1;
+ }
+ for ($i = $start; $i >= 0 && $i < $count; $i = $i + $step) {
+ $tmp = parseChangelogLine($lines[$i]);
+ if ($tmp !== false) {
+ $this->cache[$this->id][$tmp['date']] = $tmp;
+ //look for revs older/earlier then reference $rev and select $direction-th one
+ if (($direction > 0 && $tmp['date'] > $rev) || ($direction < 0 && $tmp['date'] < $rev)) {
+ $revcounter++;
+ if ($revcounter == abs($direction)) {
+ $relativerev = $tmp['date'];
+ }
+ }
+ }
+ }
+
+ //true when $rev is found, but not the wanted follow-up.
+ $checkotherchunck = $fp
+ && ($tmp['date'] == $rev || ($revcounter > 0 && !$relativerev))
+ && !(($tail == $eof && $direction > 0) || ($head == 0 && $direction < 0));
+
+ if ($checkotherchunck) {
+ list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, $direction);
+
+ if (empty($lines)) break;
+ }
+ }
+ if ($fp) {
+ fclose($fp);
+ }
+
+ return $relativerev;
+ }
+
+ /**
+ * Returns revisions around rev1 and rev2
+ * When available it returns $max entries for each revision
+ *
+ * @param int $rev1 oldest revision timestamp
+ * @param int $rev2 newest revision timestamp (0 looks up last revision)
+ * @param int $max maximum number of revisions returned
+ * @return array with two arrays with revisions surrounding rev1 respectively rev2
+ */
+ public function getRevisionsAround($rev1, $rev2, $max = 50)
+ {
+ $max = floor(abs($max) / 2) * 2 + 1;
+ $rev1 = max($rev1, 0);
+ $rev2 = max($rev2, 0);
+
+ if ($rev2) {
+ if ($rev2 < $rev1) {
+ $rev = $rev2;
+ $rev2 = $rev1;
+ $rev1 = $rev;
+ }
+ } else {
+ //empty right side means a removed page. Look up last revision.
+ $revs = $this->getRevisions(-1, 1);
+ $rev2 = $revs[0];
+ }
+ //collect revisions around rev2
+ list($revs2, $allrevs, $fp, $lines, $head, $tail) = $this->retrieveRevisionsAround($rev2, $max);
+
+ if (empty($revs2)) return array(array(), array());
+
+ //collect revisions around rev1
+ $index = array_search($rev1, $allrevs);
+ if ($index === false) {
+ //no overlapping revisions
+ list($revs1, , , , ,) = $this->retrieveRevisionsAround($rev1, $max);
+ if (empty($revs1)) $revs1 = array();
+ } else {
+ //revisions overlaps, reuse revisions around rev2
+ $revs1 = $allrevs;
+ while ($head > 0) {
+ for ($i = count($lines) - 1; $i >= 0; $i--) {
+ $tmp = parseChangelogLine($lines[$i]);
+ if ($tmp !== false) {
+ $this->cache[$this->id][$tmp['date']] = $tmp;
+ $revs1[] = $tmp['date'];
+ $index++;
+
+ if ($index > floor($max / 2)) break 2;
+ }
+ }
+
+ list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, -1);
+ }
+ sort($revs1);
+ //return wanted selection
+ $revs1 = array_slice($revs1, max($index - floor($max / 2), 0), $max);
+ }
+
+ return array(array_reverse($revs1), array_reverse($revs2));
+ }
+
+
+ /**
+ * Checks if the ID has old revisons
+ * @return boolean
+ */
+ public function hasRevisions() {
+ $file = $this->getChangelogFilename();
+ return file_exists($file);
+ }
+
+ /**
+ * Returns lines from changelog.
+ * If file larger than $chuncksize, only chunck is read that could contain $rev.
+ *
+ * @param int $rev revision timestamp
+ * @return array|false
+ * if success returns array(fp, array(changeloglines), $head, $tail, $eof)
+ * where fp only defined for chuck reading, needs closing.
+ * otherwise false
+ */
+ protected function readloglines($rev)
+ {
+ $file = $this->getChangelogFilename();
+
+ if (!file_exists($file)) {
+ return false;
+ }
+
+ $fp = null;
+ $head = 0;
+ $tail = 0;
+ $eof = 0;
+
+ if (filesize($file) < $this->chunk_size || $this->chunk_size == 0) {
+ // read whole file
+ $lines = file($file);
+ if ($lines === false) {
+ return false;
+ }
+ } else {
+ // read by chunk
+ $fp = fopen($file, 'rb'); // "file pointer"
+ if ($fp === false) {
+ return false;
+ }
+ $head = 0;
+ fseek($fp, 0, SEEK_END);
+ $eof = ftell($fp);
+ $tail = $eof;
+
+ // find chunk
+ while ($tail - $head > $this->chunk_size) {
+ $finger = $head + floor(($tail - $head) / 2.0);
+ $finger = $this->getNewlinepointer($fp, $finger);
+ $tmp = fgets($fp);
+ if ($finger == $head || $finger == $tail) {
+ break;
+ }
+ $tmp = parseChangelogLine($tmp);
+ $finger_rev = $tmp['date'];
+
+ if ($finger_rev > $rev) {
+ $tail = $finger;
+ } else {
+ $head = $finger;
+ }
+ }
+
+ if ($tail - $head < 1) {
+ // cound not find chunk, assume requested rev is missing
+ fclose($fp);
+ return false;
+ }
+
+ $lines = $this->readChunk($fp, $head, $tail);
+ }
+ return array(
+ $fp,
+ $lines,
+ $head,
+ $tail,
+ $eof,
+ );
+ }
+
+ /**
+ * Read chunk and return array with lines of given chunck.
+ * Has no check if $head and $tail are really at a new line
+ *
+ * @param resource $fp resource filepointer
+ * @param int $head start point chunck
+ * @param int $tail end point chunck
+ * @return array lines read from chunck
+ */
+ protected function readChunk($fp, $head, $tail)
+ {
+ $chunk = '';
+ $chunk_size = max($tail - $head, 0); // found chunk size
+ $got = 0;
+ fseek($fp, $head);
+ while ($got < $chunk_size && !feof($fp)) {
+ $tmp = @fread($fp, max(min($this->chunk_size, $chunk_size - $got), 0));
+ if ($tmp === false) { //error state
+ break;
+ }
+ $got += strlen($tmp);
+ $chunk .= $tmp;
+ }
+ $lines = explode("\n", $chunk);
+ array_pop($lines); // remove trailing newline
+ return $lines;
+ }
+
+ /**
+ * Set pointer to first new line after $finger and return its position
+ *
+ * @param resource $fp filepointer
+ * @param int $finger a pointer
+ * @return int pointer
+ */
+ protected function getNewlinepointer($fp, $finger)
+ {
+ fseek($fp, $finger);
+ $nl = $finger;
+ if ($finger > 0) {
+ fgets($fp); // slip the finger forward to a new line
+ $nl = ftell($fp);
+ }
+ return $nl;
+ }
+
+ /**
+ * Check whether given revision is the current page
+ *
+ * @param int $rev timestamp of current page
+ * @return bool true if $rev is current revision, otherwise false
+ */
+ public function isCurrentRevision($rev)
+ {
+ return $rev == @filemtime($this->getFilename());
+ }
+
+ /**
+ * Return an existing revision for a specific date which is
+ * the current one or younger or equal then the date
+ *
+ * @param number $date_at timestamp
+ * @return string revision ('' for current)
+ */
+ public function getLastRevisionAt($date_at)
+ {
+ //requested date_at(timestamp) younger or equal then modified_time($this->id) => load current
+ if (file_exists($this->getFilename()) && $date_at >= @filemtime($this->getFilename())) {
+ return '';
+ } else {
+ if ($rev = $this->getRelativeRevision($date_at + 1, -1)) { //+1 to get also the requested date revision
+ return $rev;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Returns the next lines of the changelog of the chunck before head or after tail
+ *
+ * @param resource $fp filepointer
+ * @param int $head position head of last chunk
+ * @param int $tail position tail of last chunk
+ * @param int $direction positive forward, negative backward
+ * @return array with entries:
+ * - $lines: changelog lines of readed chunk
+ * - $head: head of chunk
+ * - $tail: tail of chunk
+ */
+ protected function readAdjacentChunk($fp, $head, $tail, $direction)
+ {
+ if (!$fp) return array(array(), $head, $tail);
+
+ if ($direction > 0) {
+ //read forward
+ $head = $tail;
+ $tail = $head + floor($this->chunk_size * (2 / 3));
+ $tail = $this->getNewlinepointer($fp, $tail);
+ } else {
+ //read backward
+ $tail = $head;
+ $head = max($tail - $this->chunk_size, 0);
+ while (true) {
+ $nl = $this->getNewlinepointer($fp, $head);
+ // was the chunk big enough? if not, take another bite
+ if ($nl > 0 && $tail <= $nl) {
+ $head = max($head - $this->chunk_size, 0);
+ } else {
+ $head = $nl;
+ break;
+ }
+ }
+ }
+
+ //load next chunck
+ $lines = $this->readChunk($fp, $head, $tail);
+ return array($lines, $head, $tail);
+ }
+
+ /**
+ * Collect the $max revisions near to the timestamp $rev
+ *
+ * @param int $rev revision timestamp
+ * @param int $max maximum number of revisions to be returned
+ * @return bool|array
+ * return array with entries:
+ * - $requestedrevs: array of with $max revision timestamps
+ * - $revs: all parsed revision timestamps
+ * - $fp: filepointer only defined for chuck reading, needs closing.
+ * - $lines: non-parsed changelog lines before the parsed revisions
+ * - $head: position of first readed changelogline
+ * - $lasttail: position of end of last readed changelogline
+ * otherwise false
+ */
+ protected function retrieveRevisionsAround($rev, $max)
+ {
+ //get lines from changelog
+ list($fp, $lines, $starthead, $starttail, /* $eof */) = $this->readloglines($rev);
+ if (empty($lines)) return false;
+
+ //parse chunk containing $rev, and read forward more chunks until $max/2 is reached
+ $head = $starthead;
+ $tail = $starttail;
+ $revs = array();
+ $aftercount = $beforecount = 0;
+ while (count($lines) > 0) {
+ foreach ($lines as $line) {
+ $tmp = parseChangelogLine($line);
+ if ($tmp !== false) {
+ $this->cache[$this->id][$tmp['date']] = $tmp;
+ $revs[] = $tmp['date'];
+ if ($tmp['date'] >= $rev) {
+ //count revs after reference $rev
+ $aftercount++;
+ if ($aftercount == 1) $beforecount = count($revs);
+ }
+ //enough revs after reference $rev?
+ if ($aftercount > floor($max / 2)) break 2;
+ }
+ }
+ //retrieve next chunk
+ list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, 1);
+ }
+ if ($aftercount == 0) return false;
+
+ $lasttail = $tail;
+
+ //read additional chuncks backward until $max/2 is reached and total number of revs is equal to $max
+ $lines = array();
+ $i = 0;
+ if ($aftercount > 0) {
+ $head = $starthead;
+ $tail = $starttail;
+ while ($head > 0) {
+ list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, -1);
+
+ for ($i = count($lines) - 1; $i >= 0; $i--) {
+ $tmp = parseChangelogLine($lines[$i]);
+ if ($tmp !== false) {
+ $this->cache[$this->id][$tmp['date']] = $tmp;
+ $revs[] = $tmp['date'];
+ $beforecount++;
+ //enough revs before reference $rev?
+ if ($beforecount > max(floor($max / 2), $max - $aftercount)) break 2;
+ }
+ }
+ }
+ }
+ sort($revs);
+
+ //keep only non-parsed lines
+ $lines = array_slice($lines, 0, $i);
+ //trunk desired selection
+ $requestedrevs = array_slice($revs, -$max, $max);
+
+ return array($requestedrevs, $revs, $fp, $lines, $head, $lasttail);
+ }
+}
diff --git a/platform/www/inc/ChangeLog/MediaChangeLog.php b/platform/www/inc/ChangeLog/MediaChangeLog.php
new file mode 100644
index 0000000..0d7d8d3
--- /dev/null
+++ b/platform/www/inc/ChangeLog/MediaChangeLog.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace dokuwiki\ChangeLog;
+
+/**
+ * handles changelog of a media file
+ */
+class MediaChangeLog extends ChangeLog
+{
+
+ /**
+ * Returns path to changelog
+ *
+ * @return string path to file
+ */
+ protected function getChangelogFilename()
+ {
+ return mediaMetaFN($this->id, '.changes');
+ }
+
+ /**
+ * Returns path to current page/media
+ *
+ * @return string path to file
+ */
+ protected function getFilename()
+ {
+ return mediaFN($this->id);
+ }
+}
diff --git a/platform/www/inc/ChangeLog/PageChangeLog.php b/platform/www/inc/ChangeLog/PageChangeLog.php
new file mode 100644
index 0000000..f1b91de
--- /dev/null
+++ b/platform/www/inc/ChangeLog/PageChangeLog.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace dokuwiki\ChangeLog;
+
+/**
+ * handles changelog of a wiki page
+ */
+class PageChangeLog extends ChangeLog
+{
+
+ /**
+ * Returns path to changelog
+ *
+ * @return string path to file
+ */
+ protected function getChangelogFilename()
+ {
+ return metaFN($this->id, '.changes');
+ }
+
+ /**
+ * Returns path to current page/media
+ *
+ * @return string path to file
+ */
+ protected function getFilename()
+ {
+ return wikiFN($this->id);
+ }
+}
diff --git a/platform/www/inc/Debug/DebugHelper.php b/platform/www/inc/Debug/DebugHelper.php
new file mode 100644
index 0000000..09ff76b
--- /dev/null
+++ b/platform/www/inc/Debug/DebugHelper.php
@@ -0,0 +1,167 @@
+<?php
+
+
+namespace dokuwiki\Debug;
+
+use Doku_Event;
+use dokuwiki\Extension\EventHandler;
+
+class DebugHelper
+{
+ const INFO_DEPRECATION_LOG_EVENT = 'INFO_DEPRECATION_LOG';
+
+ /**
+ * Log accesses to deprecated fucntions to the debug log
+ *
+ * @param string $alternative (optional) The function or method that should be used instead
+ * @param int $callerOffset (optional) How far the deprecated method is removed from this one
+ *
+ * @triggers \dokuwiki\Debug::INFO_DEPRECATION_LOG_EVENT
+ */
+ public static function dbgDeprecatedFunction($alternative = '', $callerOffset = 1)
+ {
+ global $conf;
+ /** @var EventHandler $EVENT_HANDLER */
+ global $EVENT_HANDLER;
+ if (
+ !$conf['allowdebug'] &&
+ ($EVENT_HANDLER === null || !$EVENT_HANDLER->hasHandlerForEvent('INFO_DEPRECATION_LOG'))
+ ){
+ // avoid any work if no one cares
+ return;
+ }
+
+ $backtrace = debug_backtrace();
+ for ($i = 0; $i < $callerOffset; $i += 1) {
+ array_shift($backtrace);
+ }
+
+ list($self, $call) = $backtrace;
+
+ self::triggerDeprecationEvent(
+ $backtrace,
+ $alternative,
+ trim(
+ (!empty($self['class']) ? ($self['class'] . '::') : '') .
+ $self['function'] . '()', ':'),
+ trim(
+ (!empty($call['class']) ? ($call['class'] . '::') : '') .
+ $call['function'] . '()', ':'),
+ $call['file'],
+ $call['line']
+ );
+ }
+
+ /**
+ * This marks logs a deprecation warning for a property that should no longer be used
+ *
+ * This is usually called withing a magic getter or setter.
+ * For logging deprecated functions or methods see dbgDeprecatedFunction()
+ *
+ * @param string $class The class with the deprecated property
+ * @param string $propertyName The name of the deprecated property
+ *
+ * @triggers \dokuwiki\Debug::INFO_DEPRECATION_LOG_EVENT
+ */
+ public static function dbgDeprecatedProperty($class, $propertyName)
+ {
+ global $conf;
+ global $EVENT_HANDLER;
+ if (!$conf['allowdebug'] && !$EVENT_HANDLER->hasHandlerForEvent(self::INFO_DEPRECATION_LOG_EVENT)) {
+ // avoid any work if no one cares
+ return;
+ }
+
+ $backtrace = debug_backtrace();
+ array_shift($backtrace);
+ $call = $backtrace[1];
+ $caller = trim($call['class'] . '::' . $call['function'] . '()', ':');
+ $qualifiedName = $class . '::$' . $propertyName;
+ self::triggerDeprecationEvent(
+ $backtrace,
+ '',
+ $qualifiedName,
+ $caller,
+ $backtrace[0]['file'],
+ $backtrace[0]['line']
+ );
+ }
+
+ /**
+ * Trigger a custom deprecation event
+ *
+ * Usually dbgDeprecatedFunction() or dbgDeprecatedProperty() should be used instead.
+ * This method is intended only for those situation where they are not applicable.
+ *
+ * @param string $alternative
+ * @param string $deprecatedThing
+ * @param string $caller
+ * @param string $file
+ * @param int $line
+ * @param int $callerOffset How many lines should be removed from the beginning of the backtrace
+ */
+ public static function dbgCustomDeprecationEvent(
+ $alternative,
+ $deprecatedThing,
+ $caller,
+ $file,
+ $line,
+ $callerOffset = 1
+ ) {
+ global $conf;
+ /** @var EventHandler $EVENT_HANDLER */
+ global $EVENT_HANDLER;
+ if (!$conf['allowdebug'] && !$EVENT_HANDLER->hasHandlerForEvent(self::INFO_DEPRECATION_LOG_EVENT)) {
+ // avoid any work if no one cares
+ return;
+ }
+
+ $backtrace = array_slice(debug_backtrace(), $callerOffset);
+
+ self::triggerDeprecationEvent(
+ $backtrace,
+ $alternative,
+ $deprecatedThing,
+ $caller,
+ $file,
+ $line
+ );
+
+ }
+
+ /**
+ * @param array $backtrace
+ * @param string $alternative
+ * @param string $deprecatedThing
+ * @param string $caller
+ * @param string $file
+ * @param int $line
+ */
+ private static function triggerDeprecationEvent(
+ array $backtrace,
+ $alternative,
+ $deprecatedThing,
+ $caller,
+ $file,
+ $line
+ ) {
+ $data = [
+ 'trace' => $backtrace,
+ 'alternative' => $alternative,
+ 'called' => $deprecatedThing,
+ 'caller' => $caller,
+ 'file' => $file,
+ 'line' => $line,
+ ];
+ $event = new Doku_Event(self::INFO_DEPRECATION_LOG_EVENT, $data);
+ if ($event->advise_before()) {
+ $msg = $event->data['called'] . ' is deprecated. It was called from ';
+ $msg .= $event->data['caller'] . ' in ' . $event->data['file'] . ':' . $event->data['line'];
+ if ($event->data['alternative']) {
+ $msg .= ' ' . $event->data['alternative'] . ' should be used instead!';
+ }
+ dbglog($msg);
+ }
+ $event->advise_after();
+ }
+}
diff --git a/platform/www/inc/Debug/PropertyDeprecationHelper.php b/platform/www/inc/Debug/PropertyDeprecationHelper.php
new file mode 100644
index 0000000..6289d5b
--- /dev/null
+++ b/platform/www/inc/Debug/PropertyDeprecationHelper.php
@@ -0,0 +1,134 @@
+<?php
+/**
+ * Trait for issuing warnings on deprecated access.
+ *
+ * Adapted from https://github.com/wikimedia/mediawiki/blob/4aedefdbfd193f323097354bf581de1c93f02715/includes/debug/DeprecationHelper.php
+ *
+ */
+
+
+namespace dokuwiki\Debug;
+
+/**
+ * Use this trait in classes which have properties for which public access
+ * is deprecated. Set the list of properties in $deprecatedPublicProperties
+ * and make the properties non-public. The trait will preserve public access
+ * but issue deprecation warnings when it is needed.
+ *
+ * Example usage:
+ * class Foo {
+ * use DeprecationHelper;
+ * protected $bar;
+ * public function __construct() {
+ * $this->deprecatePublicProperty( 'bar', '1.21', __CLASS__ );
+ * }
+ * }
+ *
+ * $foo = new Foo;
+ * $foo->bar; // works but logs a warning
+ *
+ * Cannot be used with classes that have their own __get/__set methods.
+ *
+ */
+trait PropertyDeprecationHelper
+{
+
+ /**
+ * List of deprecated properties, in <property name> => <class> format
+ * where <class> is the the name of the class defining the property
+ *
+ * E.g. [ '_event' => '\dokuwiki\Cache\Cache' ]
+ * @var string[]
+ */
+ protected $deprecatedPublicProperties = [];
+
+ /**
+ * Mark a property as deprecated. Only use this for properties that used to be public and only
+ * call it in the constructor.
+ *
+ * @param string $property The name of the property.
+ * @param null $class name of the class defining the property
+ * @see DebugHelper::dbgDeprecatedProperty
+ */
+ protected function deprecatePublicProperty(
+ $property,
+ $class = null
+ ) {
+ $this->deprecatedPublicProperties[$property] = $class ?: get_class();
+ }
+
+ public function __get($name)
+ {
+ if (isset($this->deprecatedPublicProperties[$name])) {
+ $class = $this->deprecatedPublicProperties[$name];
+ DebugHelper::dbgDeprecatedProperty($class, $name);
+ return $this->$name;
+ }
+
+ $qualifiedName = get_class() . '::$' . $name;
+ if ($this->deprecationHelperGetPropertyOwner($name)) {
+ // Someone tried to access a normal non-public property. Try to behave like PHP would.
+ trigger_error("Cannot access non-public property $qualifiedName", E_USER_ERROR);
+ } else {
+ // Non-existing property. Try to behave like PHP would.
+ trigger_error("Undefined property: $qualifiedName", E_USER_NOTICE);
+ }
+ return null;
+ }
+
+ public function __set($name, $value)
+ {
+ if (isset($this->deprecatedPublicProperties[$name])) {
+ $class = $this->deprecatedPublicProperties[$name];
+ DebugHelper::dbgDeprecatedProperty($class, $name);
+ $this->$name = $value;
+ return;
+ }
+
+ $qualifiedName = get_class() . '::$' . $name;
+ if ($this->deprecationHelperGetPropertyOwner($name)) {
+ // Someone tried to access a normal non-public property. Try to behave like PHP would.
+ trigger_error("Cannot access non-public property $qualifiedName", E_USER_ERROR);
+ } else {
+ // Non-existing property. Try to behave like PHP would.
+ $this->$name = $value;
+ }
+ }
+
+ /**
+ * Like property_exists but also check for non-visible private properties and returns which
+ * class in the inheritance chain declared the property.
+ * @param string $property
+ * @return string|bool Best guess for the class in which the property is defined.
+ */
+ private function deprecationHelperGetPropertyOwner($property)
+ {
+ // Easy branch: check for protected property / private property of the current class.
+ if (property_exists($this, $property)) {
+ // The class name is not necessarily correct here but getting the correct class
+ // name would be expensive, this will work most of the time and getting it
+ // wrong is not a big deal.
+ return __CLASS__;
+ }
+ // property_exists() returns false when the property does exist but is private (and not
+ // defined by the current class, for some value of "current" that differs slightly
+ // between engines).
+ // Since PHP triggers an error on public access of non-public properties but happily
+ // allows public access to undefined properties, we need to detect this case as well.
+ // Reflection is slow so use array cast hack to check for that:
+ $obfuscatedProps = array_keys((array)$this);
+ $obfuscatedPropTail = "\0$property";
+ foreach ($obfuscatedProps as $obfuscatedProp) {
+ // private props are in the form \0<classname>\0<propname>
+ if (strpos($obfuscatedProp, $obfuscatedPropTail, 1) !== false) {
+ $classname = substr($obfuscatedProp, 1, -strlen($obfuscatedPropTail));
+ if ($classname === '*') {
+ // sanity; this shouldn't be possible as protected properties were handled earlier
+ $classname = __CLASS__;
+ }
+ return $classname;
+ }
+ }
+ return false;
+ }
+}
diff --git a/platform/www/inc/DifferenceEngine.php b/platform/www/inc/DifferenceEngine.php
new file mode 100644
index 0000000..70877a4
--- /dev/null
+++ b/platform/www/inc/DifferenceEngine.php
@@ -0,0 +1,1544 @@
+<?php
+/**
+ * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
+ *
+ * Additions by Axel Boldt for MediaWiki
+ *
+ * @copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * @license You may copy this code freely under the conditions of the GPL.
+ */
+define('USE_ASSERTS', function_exists('assert'));
+
+class _DiffOp {
+ var $type;
+ var $orig;
+ var $closing;
+
+ /**
+ * @return _DiffOp
+ */
+ function reverse() {
+ trigger_error("pure virtual", E_USER_ERROR);
+ }
+
+ function norig() {
+ return $this->orig ? count($this->orig) : 0;
+ }
+
+ function nclosing() {
+ return $this->closing ? count($this->closing) : 0;
+ }
+}
+
+class _DiffOp_Copy extends _DiffOp {
+ var $type = 'copy';
+
+ function __construct($orig, $closing = false) {
+ if (!is_array($closing))
+ $closing = $orig;
+ $this->orig = $orig;
+ $this->closing = $closing;
+ }
+
+ function reverse() {
+ return new _DiffOp_Copy($this->closing, $this->orig);
+ }
+}
+
+class _DiffOp_Delete extends _DiffOp {
+ var $type = 'delete';
+
+ function __construct($lines) {
+ $this->orig = $lines;
+ $this->closing = false;
+ }
+
+ function reverse() {
+ return new _DiffOp_Add($this->orig);
+ }
+}
+
+class _DiffOp_Add extends _DiffOp {
+ var $type = 'add';
+
+ function __construct($lines) {
+ $this->closing = $lines;
+ $this->orig = false;
+ }
+
+ function reverse() {
+ return new _DiffOp_Delete($this->closing);
+ }
+}
+
+class _DiffOp_Change extends _DiffOp {
+ var $type = 'change';
+
+ function __construct($orig, $closing) {
+ $this->orig = $orig;
+ $this->closing = $closing;
+ }
+
+ function reverse() {
+ return new _DiffOp_Change($this->closing, $this->orig);
+ }
+}
+
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ * http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * @author Geoffrey T. Dairiki
+ * @access private
+ */
+class _DiffEngine {
+
+ var $xchanged = array();
+ var $ychanged = array();
+ var $xv = array();
+ var $yv = array();
+ var $xind = array();
+ var $yind = array();
+ var $seq;
+ var $in_seq;
+ var $lcs;
+
+ /**
+ * @param array $from_lines
+ * @param array $to_lines
+ * @return _DiffOp[]
+ */
+ function diff($from_lines, $to_lines) {
+ $n_from = count($from_lines);
+ $n_to = count($to_lines);
+
+ $this->xchanged = $this->ychanged = array();
+ $this->xv = $this->yv = array();
+ $this->xind = $this->yind = array();
+ unset($this->seq);
+ unset($this->in_seq);
+ unset($this->lcs);
+
+ // Skip leading common lines.
+ for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+ if ($from_lines[$skip] != $to_lines[$skip])
+ break;
+ $this->xchanged[$skip] = $this->ychanged[$skip] = false;
+ }
+ // Skip trailing common lines.
+ $xi = $n_from;
+ $yi = $n_to;
+ for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+ if ($from_lines[$xi] != $to_lines[$yi])
+ break;
+ $this->xchanged[$xi] = $this->ychanged[$yi] = false;
+ }
+
+ // Ignore lines which do not exist in both files.
+ for ($xi = $skip; $xi < $n_from - $endskip; $xi++)
+ $xhash[$from_lines[$xi]] = 1;
+ for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+ $line = $to_lines[$yi];
+ if (($this->ychanged[$yi] = empty($xhash[$line])))
+ continue;
+ $yhash[$line] = 1;
+ $this->yv[] = $line;
+ $this->yind[] = $yi;
+ }
+ for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+ $line = $from_lines[$xi];
+ if (($this->xchanged[$xi] = empty($yhash[$line])))
+ continue;
+ $this->xv[] = $line;
+ $this->xind[] = $xi;
+ }
+
+ // Find the LCS.
+ $this->_compareseq(0, count($this->xv), 0, count($this->yv));
+
+ // Merge edits when possible
+ $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
+ $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
+
+ // Compute the edit operations.
+ $edits = array();
+ $xi = $yi = 0;
+ while ($xi < $n_from || $yi < $n_to) {
+ USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+ USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+ // Skip matching "snake".
+ $copy = array();
+ while ($xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+ $copy[] = $from_lines[$xi++];
+ ++$yi;
+ }
+ if ($copy)
+ $edits[] = new _DiffOp_Copy($copy);
+
+ // Find deletes & adds.
+ $delete = array();
+ while ($xi < $n_from && $this->xchanged[$xi])
+ $delete[] = $from_lines[$xi++];
+
+ $add = array();
+ while ($yi < $n_to && $this->ychanged[$yi])
+ $add[] = $to_lines[$yi++];
+
+ if ($delete && $add)
+ $edits[] = new _DiffOp_Change($delete, $add);
+ elseif ($delete)
+ $edits[] = new _DiffOp_Delete($delete);
+ elseif ($add)
+ $edits[] = new _DiffOp_Add($add);
+ }
+ return $edits;
+ }
+
+
+ /**
+ * Divide the Largest Common Subsequence (LCS) of the sequences
+ * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
+ * sized segments.
+ *
+ * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an
+ * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
+ * sub sequences. The first sub-sequence is contained in [X0, X1),
+ * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note
+ * that (X0, Y0) == (XOFF, YOFF) and
+ * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+ *
+ * This function assumes that the first lines of the specified portions
+ * of the two files do not match, and likewise that the last lines do not
+ * match. The caller must trim matching lines from the beginning and end
+ * of the portions it is going to specify.
+ *
+ * @param integer $xoff
+ * @param integer $xlim
+ * @param integer $yoff
+ * @param integer $ylim
+ * @param integer $nchunks
+ *
+ * @return array
+ */
+ function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) {
+ $flip = false;
+
+ if ($xlim - $xoff > $ylim - $yoff) {
+ // Things seems faster (I'm not sure I understand why)
+ // when the shortest sequence in X.
+ $flip = true;
+ list ($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
+ }
+
+ if ($flip)
+ for ($i = $ylim - 1; $i >= $yoff; $i--)
+ $ymatches[$this->xv[$i]][] = $i;
+ else
+ for ($i = $ylim - 1; $i >= $yoff; $i--)
+ $ymatches[$this->yv[$i]][] = $i;
+
+ $this->lcs = 0;
+ $this->seq[0]= $yoff - 1;
+ $this->in_seq = array();
+ $ymids[0] = array();
+
+ $numer = $xlim - $xoff + $nchunks - 1;
+ $x = $xoff;
+ for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+ if ($chunk > 0)
+ for ($i = 0; $i <= $this->lcs; $i++)
+ $ymids[$i][$chunk-1] = $this->seq[$i];
+
+ $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
+ for ( ; $x < $x1; $x++) {
+ $line = $flip ? $this->yv[$x] : $this->xv[$x];
+ if (empty($ymatches[$line]))
+ continue;
+ $matches = $ymatches[$line];
+ $switch = false;
+ foreach ($matches as $y) {
+ if ($switch && $y > $this->seq[$k-1]) {
+ USE_ASSERTS && assert($y < $this->seq[$k]);
+ // Optimization: this is a common case:
+ // next match is just replacing previous match.
+ $this->in_seq[$this->seq[$k]] = false;
+ $this->seq[$k] = $y;
+ $this->in_seq[$y] = 1;
+ }
+ else if (empty($this->in_seq[$y])) {
+ $k = $this->_lcs_pos($y);
+ USE_ASSERTS && assert($k > 0);
+ $ymids[$k] = $ymids[$k-1];
+ $switch = true;
+ }
+ }
+ }
+ }
+
+ $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+ $ymid = $ymids[$this->lcs];
+ for ($n = 0; $n < $nchunks - 1; $n++) {
+ $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+ $y1 = $ymid[$n] + 1;
+ $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+ }
+ $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+ return array($this->lcs, $seps);
+ }
+
+ function _lcs_pos($ypos) {
+ $end = $this->lcs;
+ if ($end == 0 || $ypos > $this->seq[$end]) {
+ $this->seq[++$this->lcs] = $ypos;
+ $this->in_seq[$ypos] = 1;
+ return $this->lcs;
+ }
+
+ $beg = 1;
+ while ($beg < $end) {
+ $mid = (int)(($beg + $end) / 2);
+ if ($ypos > $this->seq[$mid])
+ $beg = $mid + 1;
+ else
+ $end = $mid;
+ }
+
+ USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+ $this->in_seq[$this->seq[$end]] = false;
+ $this->seq[$end] = $ypos;
+ $this->in_seq[$ypos] = 1;
+ return $end;
+ }
+
+ /**
+ * Find LCS of two sequences.
+ *
+ * The results are recorded in the vectors $this->{x,y}changed[], by
+ * storing a 1 in the element for each line that is an insertion
+ * or deletion (ie. is not in the LCS).
+ *
+ * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
+ *
+ * Note that XLIM, YLIM are exclusive bounds.
+ * All line numbers are origin-0 and discarded lines are not counted.
+ *
+ * @param integer $xoff
+ * @param integer $xlim
+ * @param integer $yoff
+ * @param integer $ylim
+ */
+ function _compareseq($xoff, $xlim, $yoff, $ylim) {
+ // Slide down the bottom initial diagonal.
+ while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) {
+ ++$xoff;
+ ++$yoff;
+ }
+
+ // Slide up the top initial diagonal.
+ while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+ --$xlim;
+ --$ylim;
+ }
+
+ if ($xoff == $xlim || $yoff == $ylim)
+ $lcs = 0;
+ else {
+ // This is ad hoc but seems to work well.
+ //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
+ //$nchunks = max(2,min(8,(int)$nchunks));
+ $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+ list ($lcs, $seps)
+ = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
+ }
+
+ if ($lcs == 0) {
+ // X and Y sequences have no common subsequence:
+ // mark all changed.
+ while ($yoff < $ylim)
+ $this->ychanged[$this->yind[$yoff++]] = 1;
+ while ($xoff < $xlim)
+ $this->xchanged[$this->xind[$xoff++]] = 1;
+ }
+ else {
+ // Use the partitions to split this problem into subproblems.
+ reset($seps);
+ $pt1 = $seps[0];
+ while ($pt2 = next($seps)) {
+ $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+ $pt1 = $pt2;
+ }
+ }
+ }
+
+ /**
+ * Adjust inserts/deletes of identical lines to join changes
+ * as much as possible.
+ *
+ * We do something when a run of changed lines include a
+ * line at one end and has an excluded, identical line at the other.
+ * We are free to choose which identical line is included.
+ * `compareseq' usually chooses the one at the beginning,
+ * but usually it is cleaner to consider the following identical line
+ * to be the "change".
+ *
+ * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+ *
+ * @param array $lines
+ * @param array $changed
+ * @param array $other_changed
+ */
+ function _shift_boundaries($lines, &$changed, $other_changed) {
+ $i = 0;
+ $j = 0;
+
+ USE_ASSERTS && assert(count($lines) == count($changed));
+ $len = count($lines);
+ $other_len = count($other_changed);
+
+ while (1) {
+ /*
+ * Scan forwards to find beginning of another run of changes.
+ * Also keep track of the corresponding point in the other file.
+ *
+ * Throughout this code, $i and $j are adjusted together so that
+ * the first $i elements of $changed and the first $j elements
+ * of $other_changed both contain the same number of zeros
+ * (unchanged lines).
+ * Furthermore, $j is always kept so that $j == $other_len or
+ * $other_changed[$j] == false.
+ */
+ while ($j < $other_len && $other_changed[$j])
+ $j++;
+
+ while ($i < $len && ! $changed[$i]) {
+ USE_ASSERTS && assert($j < $other_len && ! $other_changed[$j]);
+ $i++;
+ $j++;
+ while ($j < $other_len && $other_changed[$j])
+ $j++;
+ }
+
+ if ($i == $len)
+ break;
+
+ $start = $i;
+
+ // Find the end of this run of changes.
+ while (++$i < $len && $changed[$i])
+ continue;
+
+ do {
+ /*
+ * Record the length of this run of changes, so that
+ * we can later determine whether the run has grown.
+ */
+ $runlength = $i - $start;
+
+ /*
+ * Move the changed region back, so long as the
+ * previous unchanged line matches the last changed one.
+ * This merges with previous changed regions.
+ */
+ while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+ $changed[--$start] = 1;
+ $changed[--$i] = false;
+ while ($start > 0 && $changed[$start - 1])
+ $start--;
+ USE_ASSERTS && assert($j > 0);
+ while ($other_changed[--$j])
+ continue;
+ USE_ASSERTS && assert($j >= 0 && !$other_changed[$j]);
+ }
+
+ /*
+ * Set CORRESPONDING to the end of the changed run, at the last
+ * point where it corresponds to a changed run in the other file.
+ * CORRESPONDING == LEN means no such point has been found.
+ */
+ $corresponding = $j < $other_len ? $i : $len;
+
+ /*
+ * Move the changed region forward, so long as the
+ * first changed line matches the following unchanged one.
+ * This merges with following changed regions.
+ * Do this second, so that if there are no merges,
+ * the changed region is moved forward as far as possible.
+ */
+ while ($i < $len && $lines[$start] == $lines[$i]) {
+ $changed[$start++] = false;
+ $changed[$i++] = 1;
+ while ($i < $len && $changed[$i])
+ $i++;
+
+ USE_ASSERTS && assert($j < $other_len && ! $other_changed[$j]);
+ $j++;
+ if ($j < $other_len && $other_changed[$j]) {
+ $corresponding = $i;
+ while ($j < $other_len && $other_changed[$j])
+ $j++;
+ }
+ }
+ } while ($runlength != $i - $start);
+
+ /*
+ * If possible, move the fully-merged run of changes
+ * back to a corresponding run in the other file.
+ */
+ while ($corresponding < $i) {
+ $changed[--$start] = 1;
+ $changed[--$i] = 0;
+ USE_ASSERTS && assert($j > 0);
+ while ($other_changed[--$j])
+ continue;
+ USE_ASSERTS && assert($j >= 0 && !$other_changed[$j]);
+ }
+ }
+ }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ */
+class Diff {
+
+ var $edits;
+
+ /**
+ * Constructor.
+ * Computes diff between sequences of strings.
+ *
+ * @param array $from_lines An array of strings.
+ * (Typically these are lines from a file.)
+ * @param array $to_lines An array of strings.
+ */
+ function __construct($from_lines, $to_lines) {
+ $eng = new _DiffEngine;
+ $this->edits = $eng->diff($from_lines, $to_lines);
+ //$this->_check($from_lines, $to_lines);
+ }
+
+ /**
+ * Compute reversed Diff.
+ *
+ * SYNOPSIS:
+ *
+ * $diff = new Diff($lines1, $lines2);
+ * $rev = $diff->reverse();
+ *
+ * @return Diff A Diff object representing the inverse of the
+ * original diff.
+ */
+ function reverse() {
+ $rev = $this;
+ $rev->edits = array();
+ foreach ($this->edits as $edit) {
+ $rev->edits[] = $edit->reverse();
+ }
+ return $rev;
+ }
+
+ /**
+ * Check for empty diff.
+ *
+ * @return bool True iff two sequences were identical.
+ */
+ function isEmpty() {
+ foreach ($this->edits as $edit) {
+ if ($edit->type != 'copy')
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Compute the length of the Longest Common Subsequence (LCS).
+ *
+ * This is mostly for diagnostic purposed.
+ *
+ * @return int The length of the LCS.
+ */
+ function lcs() {
+ $lcs = 0;
+ foreach ($this->edits as $edit) {
+ if ($edit->type == 'copy')
+ $lcs += count($edit->orig);
+ }
+ return $lcs;
+ }
+
+ /**
+ * Get the original set of lines.
+ *
+ * This reconstructs the $from_lines parameter passed to the
+ * constructor.
+ *
+ * @return array The original sequence of strings.
+ */
+ function orig() {
+ $lines = array();
+
+ foreach ($this->edits as $edit) {
+ if ($edit->orig)
+ array_splice($lines, count($lines), 0, $edit->orig);
+ }
+ return $lines;
+ }
+
+ /**
+ * Get the closing set of lines.
+ *
+ * This reconstructs the $to_lines parameter passed to the
+ * constructor.
+ *
+ * @return array The sequence of strings.
+ */
+ function closing() {
+ $lines = array();
+
+ foreach ($this->edits as $edit) {
+ if ($edit->closing)
+ array_splice($lines, count($lines), 0, $edit->closing);
+ }
+ return $lines;
+ }
+
+ /**
+ * Check a Diff for validity.
+ *
+ * This is here only for debugging purposes.
+ *
+ * @param mixed $from_lines
+ * @param mixed $to_lines
+ */
+ function _check($from_lines, $to_lines) {
+ if (serialize($from_lines) != serialize($this->orig()))
+ trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+ if (serialize($to_lines) != serialize($this->closing()))
+ trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+
+ $rev = $this->reverse();
+ if (serialize($to_lines) != serialize($rev->orig()))
+ trigger_error("Reversed original doesn't match", E_USER_ERROR);
+ if (serialize($from_lines) != serialize($rev->closing()))
+ trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+
+ $prevtype = 'none';
+ foreach ($this->edits as $edit) {
+ if ($prevtype == $edit->type)
+ trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+ $prevtype = $edit->type;
+ }
+
+ $lcs = $this->lcs();
+ trigger_error("Diff okay: LCS = $lcs", E_USER_NOTICE);
+ }
+}
+
+/**
+ * FIXME: bad name.
+ */
+class MappedDiff extends Diff {
+ /**
+ * Constructor.
+ *
+ * Computes diff between sequences of strings.
+ *
+ * This can be used to compute things like
+ * case-insensitve diffs, or diffs which ignore
+ * changes in white-space.
+ *
+ * @param string[] $from_lines An array of strings.
+ * (Typically these are lines from a file.)
+ *
+ * @param string[] $to_lines An array of strings.
+ *
+ * @param string[] $mapped_from_lines This array should
+ * have the same size number of elements as $from_lines.
+ * The elements in $mapped_from_lines and
+ * $mapped_to_lines are what is actually compared
+ * when computing the diff.
+ *
+ * @param string[] $mapped_to_lines This array should
+ * have the same number of elements as $to_lines.
+ */
+ function __construct($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) {
+
+ assert(count($from_lines) == count($mapped_from_lines));
+ assert(count($to_lines) == count($mapped_to_lines));
+
+ parent::__construct($mapped_from_lines, $mapped_to_lines);
+
+ $xi = $yi = 0;
+ $ecnt = count($this->edits);
+ for ($i = 0; $i < $ecnt; $i++) {
+ $orig = &$this->edits[$i]->orig;
+ if (is_array($orig)) {
+ $orig = array_slice($from_lines, $xi, count($orig));
+ $xi += count($orig);
+ }
+
+ $closing = &$this->edits[$i]->closing;
+ if (is_array($closing)) {
+ $closing = array_slice($to_lines, $yi, count($closing));
+ $yi += count($closing);
+ }
+ }
+ }
+}
+
+/**
+ * A class to format Diffs
+ *
+ * This class formats the diff in classic diff format.
+ * It is intended that this class be customized via inheritance,
+ * to obtain fancier outputs.
+ */
+class DiffFormatter {
+ /**
+ * Number of leading context "lines" to preserve.
+ *
+ * This should be left at zero for this class, but subclasses
+ * may want to set this to other values.
+ */
+ var $leading_context_lines = 0;
+
+ /**
+ * Number of trailing context "lines" to preserve.
+ *
+ * This should be left at zero for this class, but subclasses
+ * may want to set this to other values.
+ */
+ var $trailing_context_lines = 0;
+
+ /**
+ * Format a diff.
+ *
+ * @param Diff $diff A Diff object.
+ * @return string The formatted output.
+ */
+ function format($diff) {
+
+ $xi = $yi = 1;
+ $x0 = $y0 = 0;
+ $block = false;
+ $context = array();
+
+ $nlead = $this->leading_context_lines;
+ $ntrail = $this->trailing_context_lines;
+
+ $this->_start_diff();
+
+ foreach ($diff->edits as $edit) {
+ if ($edit->type == 'copy') {
+ if (is_array($block)) {
+ if (count($edit->orig) <= $nlead + $ntrail) {
+ $block[] = $edit;
+ }
+ else{
+ if ($ntrail) {
+ $context = array_slice($edit->orig, 0, $ntrail);
+ $block[] = new _DiffOp_Copy($context);
+ }
+ $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
+ $block = false;
+ }
+ }
+ $context = $edit->orig;
+ }
+ else {
+ if (! is_array($block)) {
+ $context = array_slice($context, count($context) - $nlead);
+ $x0 = $xi - count($context);
+ $y0 = $yi - count($context);
+ $block = array();
+ if ($context)
+ $block[] = new _DiffOp_Copy($context);
+ }
+ $block[] = $edit;
+ }
+
+ if ($edit->orig)
+ $xi += count($edit->orig);
+ if ($edit->closing)
+ $yi += count($edit->closing);
+ }
+
+ if (is_array($block))
+ $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
+
+ return $this->_end_diff();
+ }
+
+ /**
+ * @param int $xbeg
+ * @param int $xlen
+ * @param int $ybeg
+ * @param int $ylen
+ * @param array $edits
+ */
+ function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
+ $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
+ foreach ($edits as $edit) {
+ if ($edit->type == 'copy')
+ $this->_context($edit->orig);
+ elseif ($edit->type == 'add')
+ $this->_added($edit->closing);
+ elseif ($edit->type == 'delete')
+ $this->_deleted($edit->orig);
+ elseif ($edit->type == 'change')
+ $this->_changed($edit->orig, $edit->closing);
+ else
+ trigger_error("Unknown edit type", E_USER_ERROR);
+ }
+ $this->_end_block();
+ }
+
+ function _start_diff() {
+ ob_start();
+ }
+
+ function _end_diff() {
+ $val = ob_get_contents();
+ ob_end_clean();
+ return $val;
+ }
+
+ /**
+ * @param int $xbeg
+ * @param int $xlen
+ * @param int $ybeg
+ * @param int $ylen
+ * @return string
+ */
+ function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+ if ($xlen > 1)
+ $xbeg .= "," . ($xbeg + $xlen - 1);
+ if ($ylen > 1)
+ $ybeg .= "," . ($ybeg + $ylen - 1);
+
+ return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+ }
+
+ /**
+ * @param string $header
+ */
+ function _start_block($header) {
+ echo $header;
+ }
+
+ function _end_block() {
+ }
+
+ function _lines($lines, $prefix = ' ') {
+ foreach ($lines as $line)
+ echo "$prefix ".$this->_escape($line)."\n";
+ }
+
+ function _context($lines) {
+ $this->_lines($lines);
+ }
+
+ function _added($lines) {
+ $this->_lines($lines, ">");
+ }
+ function _deleted($lines) {
+ $this->_lines($lines, "<");
+ }
+
+ function _changed($orig, $closing) {
+ $this->_deleted($orig);
+ echo "---\n";
+ $this->_added($closing);
+ }
+
+ /**
+ * Escape string
+ *
+ * Override this method within other formatters if escaping required.
+ * Base class requires $str to be returned WITHOUT escaping.
+ *
+ * @param $str string Text string to escape
+ * @return string The escaped string.
+ */
+ function _escape($str){
+ return $str;
+ }
+}
+
+/**
+ * Utilityclass for styling HTML formatted diffs
+ *
+ * Depends on global var $DIFF_INLINESTYLES, if true some minimal predefined
+ * inline styles are used. Useful for HTML mails and RSS feeds
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+class HTMLDiff {
+ /**
+ * Holds the style names and basic CSS
+ */
+ static public $styles = array(
+ 'diff-addedline' => 'background-color: #ddffdd;',
+ 'diff-deletedline' => 'background-color: #ffdddd;',
+ 'diff-context' => 'background-color: #f5f5f5;',
+ 'diff-mark' => 'color: #ff0000;',
+ );
+
+ /**
+ * Return a class or style parameter
+ *
+ * @param string $classname
+ *
+ * @return string
+ */
+ static function css($classname){
+ global $DIFF_INLINESTYLES;
+
+ if($DIFF_INLINESTYLES){
+ if(!isset(self::$styles[$classname])) return '';
+ return 'style="'.self::$styles[$classname].'"';
+ }else{
+ return 'class="'.$classname.'"';
+ }
+ }
+}
+
+/**
+ * Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ *
+ */
+
+define('NBSP', "\xC2\xA0"); // utf-8 non-breaking space.
+
+class _HWLDF_WordAccumulator {
+
+ function __construct() {
+ $this->_lines = array();
+ $this->_line = '';
+ $this->_group = '';
+ $this->_tag = '';
+ }
+
+ function _flushGroup($new_tag) {
+ if ($this->_group !== '') {
+ if ($this->_tag == 'mark')
+ $this->_line .= '<strong '.HTMLDiff::css('diff-mark').'>'.$this->_escape($this->_group).'</strong>';
+ elseif ($this->_tag == 'add')
+ $this->_line .= '<span '.HTMLDiff::css('diff-addedline').'>'.$this->_escape($this->_group).'</span>';
+ elseif ($this->_tag == 'del')
+ $this->_line .= '<span '.HTMLDiff::css('diff-deletedline').'><del>'.$this->_escape($this->_group).'</del></span>';
+ else
+ $this->_line .= $this->_escape($this->_group);
+ }
+ $this->_group = '';
+ $this->_tag = $new_tag;
+ }
+
+ /**
+ * @param string $new_tag
+ */
+ function _flushLine($new_tag) {
+ $this->_flushGroup($new_tag);
+ if ($this->_line != '')
+ $this->_lines[] = $this->_line;
+ $this->_line = '';
+ }
+
+ function addWords($words, $tag = '') {
+ if ($tag != $this->_tag)
+ $this->_flushGroup($tag);
+
+ foreach ($words as $word) {
+ // new-line should only come as first char of word.
+ if ($word == '')
+ continue;
+ if ($word[0] == "\n") {
+ $this->_group .= NBSP;
+ $this->_flushLine($tag);
+ $word = substr($word, 1);
+ }
+ assert(!strstr($word, "\n"));
+ $this->_group .= $word;
+ }
+ }
+
+ function getLines() {
+ $this->_flushLine('~done');
+ return $this->_lines;
+ }
+
+ function _escape($str){
+ return hsc($str);
+ }
+}
+
+class WordLevelDiff extends MappedDiff {
+
+ function __construct($orig_lines, $closing_lines) {
+ list ($orig_words, $orig_stripped) = $this->_split($orig_lines);
+ list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+ parent::__construct($orig_words, $closing_words, $orig_stripped, $closing_stripped);
+ }
+
+ function _split($lines) {
+ if (!preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xsu',
+ implode("\n", $lines), $m)) {
+ return array(array(''), array(''));
+ }
+ return array($m[0], $m[1]);
+ }
+
+ function orig() {
+ $orig = new _HWLDF_WordAccumulator;
+
+ foreach ($this->edits as $edit) {
+ if ($edit->type == 'copy')
+ $orig->addWords($edit->orig);
+ elseif ($edit->orig)
+ $orig->addWords($edit->orig, 'mark');
+ }
+ return $orig->getLines();
+ }
+
+ function closing() {
+ $closing = new _HWLDF_WordAccumulator;
+
+ foreach ($this->edits as $edit) {
+ if ($edit->type == 'copy')
+ $closing->addWords($edit->closing);
+ elseif ($edit->closing)
+ $closing->addWords($edit->closing, 'mark');
+ }
+ return $closing->getLines();
+ }
+}
+
+class InlineWordLevelDiff extends MappedDiff {
+
+ function __construct($orig_lines, $closing_lines) {
+ list ($orig_words, $orig_stripped) = $this->_split($orig_lines);
+ list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+ parent::__construct($orig_words, $closing_words, $orig_stripped, $closing_stripped);
+ }
+
+ function _split($lines) {
+ if (!preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xsu',
+ implode("\n", $lines), $m)) {
+ return array(array(''), array(''));
+ }
+ return array($m[0], $m[1]);
+ }
+
+ function inline() {
+ $orig = new _HWLDF_WordAccumulator;
+ foreach ($this->edits as $edit) {
+ if ($edit->type == 'copy')
+ $orig->addWords($edit->closing);
+ elseif ($edit->type == 'change'){
+ $orig->addWords($edit->orig, 'del');
+ $orig->addWords($edit->closing, 'add');
+ } elseif ($edit->type == 'delete')
+ $orig->addWords($edit->orig, 'del');
+ elseif ($edit->type == 'add')
+ $orig->addWords($edit->closing, 'add');
+ elseif ($edit->orig)
+ $orig->addWords($edit->orig, 'del');
+ }
+ return $orig->getLines();
+ }
+}
+
+/**
+ * "Unified" diff formatter.
+ *
+ * This class formats the diff in classic "unified diff" format.
+ *
+ * NOTE: output is plain text and unsafe for use in HTML without escaping.
+ */
+class UnifiedDiffFormatter extends DiffFormatter {
+
+ function __construct($context_lines = 4) {
+ $this->leading_context_lines = $context_lines;
+ $this->trailing_context_lines = $context_lines;
+ }
+
+ function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+ if ($xlen != 1)
+ $xbeg .= "," . $xlen;
+ if ($ylen != 1)
+ $ybeg .= "," . $ylen;
+ return "@@ -$xbeg +$ybeg @@\n";
+ }
+
+ function _added($lines) {
+ $this->_lines($lines, "+");
+ }
+ function _deleted($lines) {
+ $this->_lines($lines, "-");
+ }
+ function _changed($orig, $final) {
+ $this->_deleted($orig);
+ $this->_added($final);
+ }
+}
+
+/**
+ * Wikipedia Table style diff formatter.
+ *
+ */
+class TableDiffFormatter extends DiffFormatter {
+ var $colspan = 2;
+
+ function __construct() {
+ $this->leading_context_lines = 2;
+ $this->trailing_context_lines = 2;
+ }
+
+ /**
+ * @param Diff $diff
+ * @return string
+ */
+ function format($diff) {
+ // Preserve whitespaces by converting some to non-breaking spaces.
+ // Do not convert all of them to allow word-wrap.
+ $val = parent::format($diff);
+ $val = str_replace(' ','&#160; ', $val);
+ $val = preg_replace('/ (?=<)|(?<=[ >]) /', '&#160;', $val);
+ return $val;
+ }
+
+ function _pre($text){
+ $text = htmlspecialchars($text);
+ return $text;
+ }
+
+ function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+ global $lang;
+ $l1 = $lang['line'].' '.$xbeg;
+ $l2 = $lang['line'].' '.$ybeg;
+ $r = '<tr><td '.HTMLDiff::css('diff-blockheader').' colspan="'.$this->colspan.'">'.$l1.":</td>\n".
+ '<td '.HTMLDiff::css('diff-blockheader').' colspan="'.$this->colspan.'">'.$l2.":</td>\n".
+ "</tr>\n";
+ return $r;
+ }
+
+ function _start_block($header) {
+ print($header);
+ }
+
+ function _end_block() {
+ }
+
+ function _lines($lines, $prefix=' ', $color="white") {
+ }
+
+ function addedLine($line,$escaped=false) {
+ if (!$escaped){
+ $line = $this->_escape($line);
+ }
+ return '<td '.HTMLDiff::css('diff-lineheader').'>+</td>'.
+ '<td '.HTMLDiff::css('diff-addedline').'>' . $line.'</td>';
+ }
+
+ function deletedLine($line,$escaped=false) {
+ if (!$escaped){
+ $line = $this->_escape($line);
+ }
+ return '<td '.HTMLDiff::css('diff-lineheader').'>-</td>'.
+ '<td '.HTMLDiff::css('diff-deletedline').'>' . $line.'</td>';
+ }
+
+ function emptyLine() {
+ return '<td colspan="'.$this->colspan.'">&#160;</td>';
+ }
+
+ function contextLine($line) {
+ return '<td '.HTMLDiff::css('diff-lineheader').'>&#160;</td>'.
+ '<td '.HTMLDiff::css('diff-context').'>'.$this->_escape($line).'</td>';
+ }
+
+ function _added($lines) {
+ $this->_addedLines($lines,false);
+ }
+
+ function _addedLines($lines,$escaped=false){
+ foreach ($lines as $line) {
+ print('<tr>' . $this->emptyLine() . $this->addedLine($line,$escaped) . "</tr>\n");
+ }
+ }
+
+ function _deleted($lines) {
+ foreach ($lines as $line) {
+ print('<tr>' . $this->deletedLine($line) . $this->emptyLine() . "</tr>\n");
+ }
+ }
+
+ function _context($lines) {
+ foreach ($lines as $line) {
+ print('<tr>' . $this->contextLine($line) . $this->contextLine($line) . "</tr>\n");
+ }
+ }
+
+ function _changed($orig, $closing) {
+ $diff = new WordLevelDiff($orig, $closing); // this escapes the diff data
+ $del = $diff->orig();
+ $add = $diff->closing();
+
+ while ($line = array_shift($del)) {
+ $aline = array_shift($add);
+ print('<tr>' . $this->deletedLine($line,true) . $this->addedLine($aline,true) . "</tr>\n");
+ }
+ $this->_addedLines($add,true); # If any leftovers
+ }
+
+ function _escape($str) {
+ return hsc($str);
+ }
+}
+
+/**
+ * Inline style diff formatter.
+ *
+ */
+class InlineDiffFormatter extends DiffFormatter {
+ var $colspan = 2;
+
+ function __construct() {
+ $this->leading_context_lines = 2;
+ $this->trailing_context_lines = 2;
+ }
+
+ /**
+ * @param Diff $diff
+ * @return string
+ */
+ function format($diff) {
+ // Preserve whitespaces by converting some to non-breaking spaces.
+ // Do not convert all of them to allow word-wrap.
+ $val = parent::format($diff);
+ $val = str_replace(' ','&#160; ', $val);
+ $val = preg_replace('/ (?=<)|(?<=[ >]) /', '&#160;', $val);
+ return $val;
+ }
+
+ function _pre($text){
+ $text = htmlspecialchars($text);
+ return $text;
+ }
+
+ function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+ global $lang;
+ if ($xlen != 1)
+ $xbeg .= "," . $xlen;
+ if ($ylen != 1)
+ $ybeg .= "," . $ylen;
+ $r = '<tr><td colspan="'.$this->colspan.'" '.HTMLDiff::css('diff-blockheader').'>@@ '.$lang['line']." -$xbeg +$ybeg @@";
+ $r .= ' <span '.HTMLDiff::css('diff-deletedline').'><del>'.$lang['deleted'].'</del></span>';
+ $r .= ' <span '.HTMLDiff::css('diff-addedline').'>'.$lang['created'].'</span>';
+ $r .= "</td></tr>\n";
+ return $r;
+ }
+
+ function _start_block($header) {
+ print($header."\n");
+ }
+
+ function _end_block() {
+ }
+
+ function _lines($lines, $prefix=' ', $color="white") {
+ }
+
+ function _added($lines) {
+ foreach ($lines as $line) {
+ print('<tr><td '.HTMLDiff::css('diff-lineheader').'>&#160;</td><td '.HTMLDiff::css('diff-addedline').'>'. $this->_escape($line) . "</td></tr>\n");
+ }
+ }
+
+ function _deleted($lines) {
+ foreach ($lines as $line) {
+ print('<tr><td '.HTMLDiff::css('diff-lineheader').'>&#160;</td><td '.HTMLDiff::css('diff-deletedline').'><del>' . $this->_escape($line) . "</del></td></tr>\n");
+ }
+ }
+
+ function _context($lines) {
+ foreach ($lines as $line) {
+ print('<tr><td '.HTMLDiff::css('diff-lineheader').'>&#160;</td><td '.HTMLDiff::css('diff-context').'>'. $this->_escape($line) ."</td></tr>\n");
+ }
+ }
+
+ function _changed($orig, $closing) {
+ $diff = new InlineWordLevelDiff($orig, $closing); // this escapes the diff data
+ $add = $diff->inline();
+
+ foreach ($add as $line)
+ print('<tr><td '.HTMLDiff::css('diff-lineheader').'>&#160;</td><td>'.$line."</td></tr>\n");
+ }
+
+ function _escape($str) {
+ return hsc($str);
+ }
+}
+
+/**
+ * A class for computing three way diffs.
+ *
+ * @author Geoffrey T. Dairiki <dairiki@dairiki.org>
+ */
+class Diff3 extends Diff {
+
+ /**
+ * Conflict counter.
+ *
+ * @var integer
+ */
+ var $_conflictingBlocks = 0;
+
+ /**
+ * Computes diff between 3 sequences of strings.
+ *
+ * @param array $orig The original lines to use.
+ * @param array $final1 The first version to compare to.
+ * @param array $final2 The second version to compare to.
+ */
+ function __construct($orig, $final1, $final2) {
+ $engine = new _DiffEngine();
+
+ $this->_edits = $this->_diff3($engine->diff($orig, $final1),
+ $engine->diff($orig, $final2));
+ }
+
+ /**
+ * Returns the merged lines
+ *
+ * @param string $label1 label for first version
+ * @param string $label2 label for second version
+ * @param string $label3 separator between versions
+ * @return array lines of the merged text
+ */
+ function mergedOutput($label1='<<<<<<<',$label2='>>>>>>>',$label3='=======') {
+ $lines = array();
+ foreach ($this->_edits as $edit) {
+ if ($edit->isConflict()) {
+ /* FIXME: this should probably be moved somewhere else. */
+ $lines = array_merge($lines,
+ array($label1),
+ $edit->final1,
+ array($label3),
+ $edit->final2,
+ array($label2));
+ $this->_conflictingBlocks++;
+ } else {
+ $lines = array_merge($lines, $edit->merged());
+ }
+ }
+
+ return $lines;
+ }
+
+ /**
+ * @access private
+ *
+ * @param array $edits1
+ * @param array $edits2
+ *
+ * @return array
+ */
+ function _diff3($edits1, $edits2) {
+ $edits = array();
+ $bb = new _Diff3_BlockBuilder();
+
+ $e1 = current($edits1);
+ $e2 = current($edits2);
+ while ($e1 || $e2) {
+ if ($e1 && $e2 && is_a($e1, '_DiffOp_copy') && is_a($e2, '_DiffOp_copy')) {
+ /* We have copy blocks from both diffs. This is the (only)
+ * time we want to emit a diff3 copy block. Flush current
+ * diff3 diff block, if any. */
+ if ($edit = $bb->finish()) {
+ $edits[] = $edit;
+ }
+
+ $ncopy = min($e1->norig(), $e2->norig());
+ assert($ncopy > 0);
+ $edits[] = new _Diff3_Op_copy(array_slice($e1->orig, 0, $ncopy));
+
+ if ($e1->norig() > $ncopy) {
+ array_splice($e1->orig, 0, $ncopy);
+ array_splice($e1->closing, 0, $ncopy);
+ } else {
+ $e1 = next($edits1);
+ }
+
+ if ($e2->norig() > $ncopy) {
+ array_splice($e2->orig, 0, $ncopy);
+ array_splice($e2->closing, 0, $ncopy);
+ } else {
+ $e2 = next($edits2);
+ }
+ } else {
+ if ($e1 && $e2) {
+ if ($e1->orig && $e2->orig) {
+ $norig = min($e1->norig(), $e2->norig());
+ $orig = array_splice($e1->orig, 0, $norig);
+ array_splice($e2->orig, 0, $norig);
+ $bb->input($orig);
+ }
+
+ if (is_a($e1, '_DiffOp_copy')) {
+ $bb->out1(array_splice($e1->closing, 0, $norig));
+ }
+
+ if (is_a($e2, '_DiffOp_copy')) {
+ $bb->out2(array_splice($e2->closing, 0, $norig));
+ }
+ }
+
+ if ($e1 && ! $e1->orig) {
+ $bb->out1($e1->closing);
+ $e1 = next($edits1);
+ }
+ if ($e2 && ! $e2->orig) {
+ $bb->out2($e2->closing);
+ $e2 = next($edits2);
+ }
+ }
+ }
+
+ if ($edit = $bb->finish()) {
+ $edits[] = $edit;
+ }
+
+ return $edits;
+ }
+}
+
+/**
+ * @author Geoffrey T. Dairiki <dairiki@dairiki.org>
+ *
+ * @access private
+ */
+class _Diff3_Op {
+
+ function __construct($orig = false, $final1 = false, $final2 = false) {
+ $this->orig = $orig ? $orig : array();
+ $this->final1 = $final1 ? $final1 : array();
+ $this->final2 = $final2 ? $final2 : array();
+ }
+
+ function merged() {
+ if (!isset($this->_merged)) {
+ if ($this->final1 === $this->final2) {
+ $this->_merged = &$this->final1;
+ } elseif ($this->final1 === $this->orig) {
+ $this->_merged = &$this->final2;
+ } elseif ($this->final2 === $this->orig) {
+ $this->_merged = &$this->final1;
+ } else {
+ $this->_merged = false;
+ }
+ }
+
+ return $this->_merged;
+ }
+
+ function isConflict() {
+ return $this->merged() === false;
+ }
+
+}
+
+/**
+ * @author Geoffrey T. Dairiki <dairiki@dairiki.org>
+ *
+ * @access private
+ */
+class _Diff3_Op_copy extends _Diff3_Op {
+
+ function __construct($lines = false) {
+ $this->orig = $lines ? $lines : array();
+ $this->final1 = &$this->orig;
+ $this->final2 = &$this->orig;
+ }
+
+ function merged() {
+ return $this->orig;
+ }
+
+ function isConflict() {
+ return false;
+ }
+}
+
+/**
+ * @author Geoffrey T. Dairiki <dairiki@dairiki.org>
+ *
+ * @access private
+ */
+class _Diff3_BlockBuilder {
+
+ function __construct() {
+ $this->_init();
+ }
+
+ function input($lines) {
+ if ($lines) {
+ $this->_append($this->orig, $lines);
+ }
+ }
+
+ function out1($lines) {
+ if ($lines) {
+ $this->_append($this->final1, $lines);
+ }
+ }
+
+ function out2($lines) {
+ if ($lines) {
+ $this->_append($this->final2, $lines);
+ }
+ }
+
+ function isEmpty() {
+ return !$this->orig && !$this->final1 && !$this->final2;
+ }
+
+ function finish() {
+ if ($this->isEmpty()) {
+ return false;
+ } else {
+ $edit = new _Diff3_Op($this->orig, $this->final1, $this->final2);
+ $this->_init();
+ return $edit;
+ }
+ }
+
+ function _init() {
+ $this->orig = $this->final1 = $this->final2 = array();
+ }
+
+ function _append(&$array, $lines) {
+ array_splice($array, sizeof($array), 0, $lines);
+ }
+}
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/inc/Draft.php b/platform/www/inc/Draft.php
new file mode 100644
index 0000000..f80016c
--- /dev/null
+++ b/platform/www/inc/Draft.php
@@ -0,0 +1,165 @@
+<?php
+
+namespace dokuwiki;
+
+/**
+ * Class Draft
+ *
+ * @package dokuwiki
+ */
+class Draft
+{
+
+ protected $errors = [];
+ protected $cname;
+ protected $id;
+ protected $client;
+
+ /**
+ * Draft constructor.
+ *
+ * @param string $ID the page id for this draft
+ * @param string $client the client identification (username or ip or similar) for this draft
+ */
+ public function __construct($ID, $client)
+ {
+ $this->id = $ID;
+ $this->client = $client;
+ $this->cname = getCacheName($client.$ID, '.draft');
+ if(file_exists($this->cname) && file_exists(wikiFN($ID))) {
+ if (filemtime($this->cname) < filemtime(wikiFN($ID))) {
+ // remove stale draft
+ $this->deleteDraft();
+ }
+ }
+ }
+
+ /**
+ * Get the filename for this draft (whether or not it exists)
+ *
+ * @return string
+ */
+ public function getDraftFilename()
+ {
+ return $this->cname;
+ }
+
+ /**
+ * Checks if this draft exists on the filesystem
+ *
+ * @return bool
+ */
+ public function isDraftAvailable()
+ {
+ return file_exists($this->cname);
+ }
+
+ /**
+ * Save a draft of a current edit session
+ *
+ * The draft will not be saved if
+ * - drafts are deactivated in the config
+ * - or the editarea is empty and there are no event handlers registered
+ * - or the event is prevented
+ *
+ * @triggers DRAFT_SAVE
+ *
+ * @return bool whether has the draft been saved
+ */
+ public function saveDraft()
+ {
+ global $INPUT, $INFO, $EVENT_HANDLER, $conf;
+ if (!$conf['usedraft']) {
+ return false;
+ }
+ if (!$INPUT->post->has('wikitext') &&
+ !$EVENT_HANDLER->hasHandlerForEvent('DRAFT_SAVE')) {
+ return false;
+ }
+ $draft = [
+ 'id' => $this->id,
+ 'prefix' => substr($INPUT->post->str('prefix'), 0, -1),
+ 'text' => $INPUT->post->str('wikitext'),
+ 'suffix' => $INPUT->post->str('suffix'),
+ 'date' => $INPUT->post->int('date'),
+ 'client' => $this->client,
+ 'cname' => $this->cname,
+ 'errors' => [],
+ ];
+ $event = new Extension\Event('DRAFT_SAVE', $draft);
+ if ($event->advise_before()) {
+ $draft['hasBeenSaved'] = io_saveFile($draft['cname'], serialize($draft));
+ if ($draft['hasBeenSaved']) {
+ $INFO['draft'] = $draft['cname'];
+ }
+ } else {
+ $draft['hasBeenSaved'] = false;
+ }
+ $event->advise_after();
+
+ $this->errors = $draft['errors'];
+
+ return $draft['hasBeenSaved'];
+ }
+
+ /**
+ * Get the text from the draft file
+ *
+ * @throws \RuntimeException if the draft file doesn't exist
+ *
+ * @return string
+ */
+ public function getDraftText()
+ {
+ if (!file_exists($this->cname)) {
+ throw new \RuntimeException(
+ "Draft for page $this->id and user $this->client doesn't exist at $this->cname."
+ );
+ }
+ $draft = unserialize(io_readFile($this->cname,false));
+ return cleanText(con($draft['prefix'],$draft['text'],$draft['suffix'],true));
+ }
+
+ /**
+ * Remove the draft from the filesystem
+ *
+ * Also sets $INFO['draft'] to null
+ */
+ public function deleteDraft()
+ {
+ global $INFO;
+ @unlink($this->cname);
+ $INFO['draft'] = null;
+ }
+
+ /**
+ * Get a formatted message stating when the draft was saved
+ *
+ * @return string
+ */
+ public function getDraftMessage()
+ {
+ global $lang;
+ return $lang['draftdate'] . ' ' . dformat(filemtime($this->cname));
+ }
+
+ /**
+ * Retrieve the errors that occured when saving the draft
+ *
+ * @return array
+ */
+ public function getErrors()
+ {
+ return $this->errors;
+ }
+
+ /**
+ * Get the timestamp when this draft was saved
+ *
+ * @return int
+ */
+ public function getDraftDate()
+ {
+ return filemtime($this->cname);
+ }
+}
diff --git a/platform/www/inc/Extension/ActionPlugin.php b/platform/www/inc/Extension/ActionPlugin.php
new file mode 100644
index 0000000..ed6d820
--- /dev/null
+++ b/platform/www/inc/Extension/ActionPlugin.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace dokuwiki\Extension;
+
+/**
+ * Action Plugin Prototype
+ *
+ * Handles action hooks within a plugin
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+abstract class ActionPlugin extends Plugin
+{
+
+ /**
+ * Registers a callback function for a given event
+ *
+ * @param \Doku_Event_Handler $controller
+ */
+ abstract public function register(\Doku_Event_Handler $controller);
+}
diff --git a/platform/www/inc/Extension/AdminPlugin.php b/platform/www/inc/Extension/AdminPlugin.php
new file mode 100644
index 0000000..7900a1e
--- /dev/null
+++ b/platform/www/inc/Extension/AdminPlugin.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace dokuwiki\Extension;
+
+/**
+ * Admin Plugin Prototype
+ *
+ * Implements an admin interface in a plugin
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+abstract class AdminPlugin extends Plugin
+{
+
+ /**
+ * Return the text that is displayed at the main admin menu
+ * (Default localized language string 'menu' is returned, override this function for setting another name)
+ *
+ * @param string $language language code
+ * @return string menu string
+ */
+ public function getMenuText($language)
+ {
+ $menutext = $this->getLang('menu');
+ if (!$menutext) {
+ $info = $this->getInfo();
+ $menutext = $info['name'] . ' ...';
+ }
+ return $menutext;
+ }
+
+ /**
+ * Return the path to the icon being displayed in the main admin menu.
+ * By default it tries to find an 'admin.svg' file in the plugin directory.
+ * (Override this function for setting another image)
+ *
+ * Important: you have to return a single path, monochrome SVG icon! It has to be
+ * under 2 Kilobytes!
+ *
+ * We recommend icons from https://materialdesignicons.com/ or to use a matching
+ * style.
+ *
+ * @return string full path to the icon file
+ */
+ public function getMenuIcon()
+ {
+ $plugin = $this->getPluginName();
+ return DOKU_PLUGIN . $plugin . '/admin.svg';
+ }
+
+ /**
+ * Determine position in list in admin window
+ * Lower values are sorted up
+ *
+ * @return int
+ */
+ public function getMenuSort()
+ {
+ return 1000;
+ }
+
+ /**
+ * Carry out required processing
+ */
+ public function handle()
+ {
+ // some plugins might not need this
+ }
+
+ /**
+ * Output html of the admin page
+ */
+ abstract public function html();
+
+ /**
+ * Checks if access should be granted to this admin plugin
+ *
+ * @return bool true if the current user may access this admin plugin
+ */
+ public function isAccessibleByCurrentUser() {
+ $data = [];
+ $data['instance'] = $this;
+ $data['hasAccess'] = false;
+
+ $event = new Event('ADMINPLUGIN_ACCESS_CHECK', $data);
+ if($event->advise_before()) {
+ if ($this->forAdminOnly()) {
+ $data['hasAccess'] = auth_isadmin();
+ } else {
+ $data['hasAccess'] = auth_ismanager();
+ }
+ }
+ $event->advise_after();
+
+ return $data['hasAccess'];
+ }
+
+ /**
+ * Return true for access only by admins (config:superuser) or false if managers are allowed as well
+ *
+ * @return bool
+ */
+ public function forAdminOnly()
+ {
+ return true;
+ }
+
+ /**
+ * Return array with ToC items. Items can be created with the html_mktocitem()
+ *
+ * @see html_mktocitem()
+ * @see tpl_toc()
+ *
+ * @return array
+ */
+ public function getTOC()
+ {
+ return array();
+ }
+
+}
+
diff --git a/platform/www/inc/Extension/AuthPlugin.php b/platform/www/inc/Extension/AuthPlugin.php
new file mode 100644
index 0000000..4b75fba
--- /dev/null
+++ b/platform/www/inc/Extension/AuthPlugin.php
@@ -0,0 +1,461 @@
+<?php
+
+namespace dokuwiki\Extension;
+
+/**
+ * Auth Plugin Prototype
+ *
+ * allows to authenticate users in a plugin
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Chris Smith <chris@jalakai.co.uk>
+ * @author Jan Schumann <js@jschumann-it.com>
+ */
+abstract class AuthPlugin extends Plugin
+{
+ public $success = true;
+
+ /**
+ * Possible things an auth backend module may be able to
+ * do. The things a backend can do need to be set to true
+ * in the constructor.
+ */
+ protected $cando = array(
+ 'addUser' => false, // can Users be created?
+ 'delUser' => false, // can Users be deleted?
+ 'modLogin' => false, // can login names be changed?
+ 'modPass' => false, // can passwords be changed?
+ 'modName' => false, // can real names be changed?
+ 'modMail' => false, // can emails be changed?
+ 'modGroups' => false, // can groups be changed?
+ 'getUsers' => false, // can a (filtered) list of users be retrieved?
+ 'getUserCount' => false, // can the number of users be retrieved?
+ 'getGroups' => false, // can a list of available groups be retrieved?
+ 'external' => false, // does the module do external auth checking?
+ 'logout' => true, // can the user logout again? (eg. not possible with HTTP auth)
+ );
+
+ /**
+ * Constructor.
+ *
+ * Carry out sanity checks to ensure the object is
+ * able to operate. Set capabilities in $this->cando
+ * array here
+ *
+ * For future compatibility, sub classes should always include a call
+ * to parent::__constructor() in their constructors!
+ *
+ * Set $this->success to false if checks fail
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+ public function __construct()
+ {
+ // the base class constructor does nothing, derived class
+ // constructors do the real work
+ }
+
+ /**
+ * Available Capabilities. [ DO NOT OVERRIDE ]
+ *
+ * For introspection/debugging
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ * @return array
+ */
+ public function getCapabilities()
+ {
+ return array_keys($this->cando);
+ }
+
+ /**
+ * Capability check. [ DO NOT OVERRIDE ]
+ *
+ * Checks the capabilities set in the $this->cando array and
+ * some pseudo capabilities (shortcutting access to multiple
+ * ones)
+ *
+ * ususal capabilities start with lowercase letter
+ * shortcut capabilities start with uppercase letter
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $cap the capability to check
+ * @return bool
+ */
+ public function canDo($cap)
+ {
+ switch ($cap) {
+ case 'Profile':
+ // can at least one of the user's properties be changed?
+ return ($this->cando['modPass'] ||
+ $this->cando['modName'] ||
+ $this->cando['modMail']);
+ break;
+ case 'UserMod':
+ // can at least anything be changed?
+ return ($this->cando['modPass'] ||
+ $this->cando['modName'] ||
+ $this->cando['modMail'] ||
+ $this->cando['modLogin'] ||
+ $this->cando['modGroups'] ||
+ $this->cando['modMail']);
+ break;
+ default:
+ // print a helping message for developers
+ if (!isset($this->cando[$cap])) {
+ msg("Check for unknown capability '$cap' - Do you use an outdated Plugin?", -1);
+ }
+ return $this->cando[$cap];
+ }
+ }
+
+ /**
+ * Trigger the AUTH_USERDATA_CHANGE event and call the modification function. [ DO NOT OVERRIDE ]
+ *
+ * You should use this function instead of calling createUser, modifyUser or
+ * deleteUsers directly. The event handlers can prevent the modification, for
+ * example for enforcing a user name schema.
+ *
+ * @author Gabriel Birke <birke@d-scribe.de>
+ * @param string $type Modification type ('create', 'modify', 'delete')
+ * @param array $params Parameters for the createUser, modifyUser or deleteUsers method.
+ * The content of this array depends on the modification type
+ * @return bool|null|int Result from the modification function or false if an event handler has canceled the action
+ */
+ public function triggerUserMod($type, $params)
+ {
+ $validTypes = array(
+ 'create' => 'createUser',
+ 'modify' => 'modifyUser',
+ 'delete' => 'deleteUsers',
+ );
+ if (empty($validTypes[$type])) {
+ return false;
+ }
+
+ $result = false;
+ $eventdata = array('type' => $type, 'params' => $params, 'modification_result' => null);
+ $evt = new Event('AUTH_USER_CHANGE', $eventdata);
+ if ($evt->advise_before(true)) {
+ $result = call_user_func_array(array($this, $validTypes[$type]), $evt->data['params']);
+ $evt->data['modification_result'] = $result;
+ }
+ $evt->advise_after();
+ unset($evt);
+ return $result;
+ }
+
+ /**
+ * Log off the current user [ OPTIONAL ]
+ *
+ * Is run in addition to the ususal logoff method. Should
+ * only be needed when trustExternal is implemented.
+ *
+ * @see auth_logoff()
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function logOff()
+ {
+ }
+
+ /**
+ * Do all authentication [ OPTIONAL ]
+ *
+ * Set $this->cando['external'] = true when implemented
+ *
+ * If this function is implemented it will be used to
+ * authenticate a user - all other DokuWiki internals
+ * will not be used for authenticating (except this
+ * function returns null, in which case, DokuWiki will
+ * still run auth_login as a fallback, which may call
+ * checkPass()). If this function is not returning null,
+ * implementing checkPass() is not needed here anymore.
+ *
+ * The function can be used to authenticate against third
+ * party cookies or Apache auth mechanisms and replaces
+ * the auth_login() function
+ *
+ * The function will be called with or without a set
+ * username. If the Username is given it was called
+ * from the login form and the given credentials might
+ * need to be checked. If no username was given it
+ * the function needs to check if the user is logged in
+ * by other means (cookie, environment).
+ *
+ * The function needs to set some globals needed by
+ * DokuWiki like auth_login() does.
+ *
+ * @see auth_login()
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $user Username
+ * @param string $pass Cleartext Password
+ * @param bool $sticky Cookie should not expire
+ * @return bool true on successful auth,
+ * null on unknown result (fallback to checkPass)
+ */
+ public function trustExternal($user, $pass, $sticky = false)
+ {
+ /* some example:
+
+ global $USERINFO;
+ global $conf;
+ $sticky ? $sticky = true : $sticky = false; //sanity check
+
+ // do the checking here
+
+ // set the globals if authed
+ $USERINFO['name'] = 'FIXME';
+ $USERINFO['mail'] = 'FIXME';
+ $USERINFO['grps'] = array('FIXME');
+ $_SERVER['REMOTE_USER'] = $user;
+ $_SESSION[DOKU_COOKIE]['auth']['user'] = $user;
+ $_SESSION[DOKU_COOKIE]['auth']['pass'] = $pass;
+ $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
+ return true;
+
+ */
+ }
+
+ /**
+ * Check user+password [ MUST BE OVERRIDDEN ]
+ *
+ * Checks if the given user exists and the given
+ * plaintext password is correct
+ *
+ * May be ommited if trustExternal is used.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $user the user name
+ * @param string $pass the clear text password
+ * @return bool
+ */
+ public function checkPass($user, $pass)
+ {
+ msg("no valid authorisation system in use", -1);
+ return false;
+ }
+
+ /**
+ * Return user info [ MUST BE OVERRIDDEN ]
+ *
+ * Returns info about the given user needs to contain
+ * at least these fields:
+ *
+ * name string full name of the user
+ * mail string email address of the user
+ * grps array list of groups the user is in
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $user the user name
+ * @param bool $requireGroups whether or not the returned data must include groups
+ * @return false|array containing user data or false
+ */
+ public function getUserData($user, $requireGroups = true)
+ {
+ if (!$this->cando['external']) msg("no valid authorisation system in use", -1);
+ return false;
+ }
+
+ /**
+ * Create a new User [implement only where required/possible]
+ *
+ * Returns false if the user already exists, null when an error
+ * occurred and true if everything went well.
+ *
+ * The new user HAS TO be added to the default group by this
+ * function!
+ *
+ * Set addUser capability when implemented
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $user
+ * @param string $pass
+ * @param string $name
+ * @param string $mail
+ * @param null|array $grps
+ * @return bool|null
+ */
+ public function createUser($user, $pass, $name, $mail, $grps = null)
+ {
+ msg("authorisation method does not allow creation of new users", -1);
+ return null;
+ }
+
+ /**
+ * Modify user data [implement only where required/possible]
+ *
+ * Set the mod* capabilities according to the implemented features
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ * @param string $user nick of the user to be changed
+ * @param array $changes array of field/value pairs to be changed (password will be clear text)
+ * @return bool
+ */
+ public function modifyUser($user, $changes)
+ {
+ msg("authorisation method does not allow modifying of user data", -1);
+ return false;
+ }
+
+ /**
+ * Delete one or more users [implement only where required/possible]
+ *
+ * Set delUser capability when implemented
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ * @param array $users
+ * @return int number of users deleted
+ */
+ public function deleteUsers($users)
+ {
+ msg("authorisation method does not allow deleting of users", -1);
+ return 0;
+ }
+
+ /**
+ * Return a count of the number of user which meet $filter criteria
+ * [should be implemented whenever retrieveUsers is implemented]
+ *
+ * Set getUserCount capability when implemented
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ * @param array $filter array of field/pattern pairs, empty array for no filter
+ * @return int
+ */
+ public function getUserCount($filter = array())
+ {
+ msg("authorisation method does not provide user counts", -1);
+ return 0;
+ }
+
+ /**
+ * Bulk retrieval of user data [implement only where required/possible]
+ *
+ * Set getUsers capability when implemented
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ * @param int $start index of first user to be returned
+ * @param int $limit max number of users to be returned, 0 for unlimited
+ * @param array $filter array of field/pattern pairs, null for no filter
+ * @return array list of userinfo (refer getUserData for internal userinfo details)
+ */
+ public function retrieveUsers($start = 0, $limit = 0, $filter = null)
+ {
+ msg("authorisation method does not support mass retrieval of user data", -1);
+ return array();
+ }
+
+ /**
+ * Define a group [implement only where required/possible]
+ *
+ * Set addGroup capability when implemented
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ * @param string $group
+ * @return bool
+ */
+ public function addGroup($group)
+ {
+ msg("authorisation method does not support independent group creation", -1);
+ return false;
+ }
+
+ /**
+ * Retrieve groups [implement only where required/possible]
+ *
+ * Set getGroups capability when implemented
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ * @param int $start
+ * @param int $limit
+ * @return array
+ */
+ public function retrieveGroups($start = 0, $limit = 0)
+ {
+ msg("authorisation method does not support group list retrieval", -1);
+ return array();
+ }
+
+ /**
+ * Return case sensitivity of the backend [OPTIONAL]
+ *
+ * When your backend is caseinsensitive (eg. you can login with USER and
+ * user) then you need to overwrite this method and return false
+ *
+ * @return bool
+ */
+ public function isCaseSensitive()
+ {
+ return true;
+ }
+
+ /**
+ * Sanitize a given username [OPTIONAL]
+ *
+ * This function is applied to any user name that is given to
+ * the backend and should also be applied to any user name within
+ * the backend before returning it somewhere.
+ *
+ * This should be used to enforce username restrictions.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $user username
+ * @return string the cleaned username
+ */
+ public function cleanUser($user)
+ {
+ return $user;
+ }
+
+ /**
+ * Sanitize a given groupname [OPTIONAL]
+ *
+ * This function is applied to any groupname that is given to
+ * the backend and should also be applied to any groupname within
+ * the backend before returning it somewhere.
+ *
+ * This should be used to enforce groupname restrictions.
+ *
+ * Groupnames are to be passed without a leading '@' here.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $group groupname
+ * @return string the cleaned groupname
+ */
+ public function cleanGroup($group)
+ {
+ return $group;
+ }
+
+ /**
+ * Check Session Cache validity [implement only where required/possible]
+ *
+ * DokuWiki caches user info in the user's session for the timespan defined
+ * in $conf['auth_security_timeout'].
+ *
+ * This makes sure slow authentication backends do not slow down DokuWiki.
+ * This also means that changes to the user database will not be reflected
+ * on currently logged in users.
+ *
+ * To accommodate for this, the user manager plugin will touch a reference
+ * file whenever a change is submitted. This function compares the filetime
+ * of this reference file with the time stored in the session.
+ *
+ * This reference file mechanism does not reflect changes done directly in
+ * the backend's database through other means than the user manager plugin.
+ *
+ * Fast backends might want to return always false, to force rechecks on
+ * each page load. Others might want to use their own checking here. If
+ * unsure, do not override.
+ *
+ * @param string $user - The username
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @return bool
+ */
+ public function useSessionCache($user)
+ {
+ global $conf;
+ return ($_SESSION[DOKU_COOKIE]['auth']['time'] >= @filemtime($conf['cachedir'] . '/sessionpurge'));
+ }
+}
diff --git a/platform/www/inc/Extension/CLIPlugin.php b/platform/www/inc/Extension/CLIPlugin.php
new file mode 100644
index 0000000..8637ccf
--- /dev/null
+++ b/platform/www/inc/Extension/CLIPlugin.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace dokuwiki\Extension;
+
+/**
+ * CLI plugin prototype
+ *
+ * Provides DokuWiki plugin functionality on top of php-cli
+ */
+abstract class CLIPlugin extends \splitbrain\phpcli\CLI implements PluginInterface
+{
+ use PluginTrait;
+}
diff --git a/platform/www/inc/Extension/Event.php b/platform/www/inc/Extension/Event.php
new file mode 100644
index 0000000..32f346c
--- /dev/null
+++ b/platform/www/inc/Extension/Event.php
@@ -0,0 +1,197 @@
+<?php
+// phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
+
+namespace dokuwiki\Extension;
+
+/**
+ * The Action plugin event
+ */
+class Event
+{
+ /** @var string READONLY event name, objects must register against this name to see the event */
+ public $name = '';
+ /** @var mixed|null READWRITE data relevant to the event, no standardised format, refer to event docs */
+ public $data = null;
+ /**
+ * @var mixed|null READWRITE the results of the event action, only relevant in "_AFTER" advise
+ * event handlers may modify this if they are preventing the default action
+ * to provide the after event handlers with event results
+ */
+ public $result = null;
+ /** @var bool READONLY if true, event handlers can prevent the events default action */
+ public $canPreventDefault = true;
+
+ /** @var bool whether or not to carry out the default action associated with the event */
+ protected $runDefault = true;
+ /** @var bool whether or not to continue propagating the event to other handlers */
+ protected $mayContinue = true;
+
+ /**
+ * event constructor
+ *
+ * @param string $name
+ * @param mixed $data
+ */
+ public function __construct($name, &$data)
+ {
+
+ $this->name = $name;
+ $this->data =& $data;
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->name;
+ }
+
+ /**
+ * advise all registered BEFORE handlers of this event
+ *
+ * if these methods are used by functions outside of this object, they must
+ * properly handle correct processing of any default action and issue an
+ * advise_after() signal. e.g.
+ * $evt = new dokuwiki\Plugin\Doku_Event(name, data);
+ * if ($evt->advise_before(canPreventDefault) {
+ * // default action code block
+ * }
+ * $evt->advise_after();
+ * unset($evt);
+ *
+ * @param bool $enablePreventDefault
+ * @return bool results of processing the event, usually $this->runDefault
+ */
+ public function advise_before($enablePreventDefault = true)
+ {
+ global $EVENT_HANDLER;
+
+ $this->canPreventDefault = $enablePreventDefault;
+ if ($EVENT_HANDLER !== null) {
+ $EVENT_HANDLER->process_event($this, 'BEFORE');
+ } else {
+ dbglog($this->name . ':BEFORE event triggered before event system was initialized');
+ }
+
+ return (!$enablePreventDefault || $this->runDefault);
+ }
+
+ /**
+ * advise all registered AFTER handlers of this event
+ *
+ * @param bool $enablePreventDefault
+ * @see advise_before() for details
+ */
+ public function advise_after()
+ {
+ global $EVENT_HANDLER;
+
+ $this->mayContinue = true;
+
+ if ($EVENT_HANDLER !== null) {
+ $EVENT_HANDLER->process_event($this, 'AFTER');
+ } else {
+ dbglog($this->name . ':AFTER event triggered before event system was initialized');
+ }
+ }
+
+ /**
+ * trigger
+ *
+ * - advise all registered (<event>_BEFORE) handlers that this event is about to take place
+ * - carry out the default action using $this->data based on $enablePrevent and
+ * $this->_default, all of which may have been modified by the event handlers.
+ * - advise all registered (<event>_AFTER) handlers that the event has taken place
+ *
+ * @param null|callable $action
+ * @param bool $enablePrevent
+ * @return mixed $event->results
+ * the value set by any <event>_before or <event> handlers if the default action is prevented
+ * or the results of the default action (as modified by <event>_after handlers)
+ * or NULL no action took place and no handler modified the value
+ */
+ public function trigger($action = null, $enablePrevent = true)
+ {
+
+ if (!is_callable($action)) {
+ $enablePrevent = false;
+ if ($action !== null) {
+ trigger_error(
+ 'The default action of ' . $this .
+ ' is not null but also not callable. Maybe the method is not public?',
+ E_USER_WARNING
+ );
+ }
+ }
+
+ if ($this->advise_before($enablePrevent) && is_callable($action)) {
+ $this->result = call_user_func_array($action, [&$this->data]);
+ }
+
+ $this->advise_after();
+
+ return $this->result;
+ }
+
+ /**
+ * stopPropagation
+ *
+ * stop any further processing of the event by event handlers
+ * this function does not prevent the default action taking place
+ */
+ public function stopPropagation()
+ {
+ $this->mayContinue = false;
+ }
+
+ /**
+ * may the event propagate to the next handler?
+ *
+ * @return bool
+ */
+ public function mayPropagate()
+ {
+ return $this->mayContinue;
+ }
+
+ /**
+ * preventDefault
+ *
+ * prevent the default action taking place
+ */
+ public function preventDefault()
+ {
+ $this->runDefault = false;
+ }
+
+ /**
+ * should the default action be executed?
+ *
+ * @return bool
+ */
+ public function mayRunDefault()
+ {
+ return $this->runDefault;
+ }
+
+ /**
+ * Convenience method to trigger an event
+ *
+ * Creates, triggers and destroys an event in one go
+ *
+ * @param string $name name for the event
+ * @param mixed $data event data
+ * @param callable $action (optional, default=NULL) default action, a php callback function
+ * @param bool $canPreventDefault (optional, default=true) can hooks prevent the default action
+ *
+ * @return mixed the event results value after all event processing is complete
+ * by default this is the return value of the default action however
+ * it can be set or modified by event handler hooks
+ */
+ static public function createAndTrigger($name, &$data, $action = null, $canPreventDefault = true)
+ {
+ $evt = new Event($name, $data);
+ return $evt->trigger($action, $canPreventDefault);
+ }
+}
diff --git a/platform/www/inc/Extension/EventHandler.php b/platform/www/inc/Extension/EventHandler.php
new file mode 100644
index 0000000..7bed0fe
--- /dev/null
+++ b/platform/www/inc/Extension/EventHandler.php
@@ -0,0 +1,108 @@
+<?php
+// phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
+
+namespace dokuwiki\Extension;
+
+/**
+ * Controls the registration and execution of all events,
+ */
+class EventHandler
+{
+
+ // public properties: none
+
+ // private properties
+ protected $hooks = array(); // array of events and their registered handlers
+
+ /**
+ * event_handler
+ *
+ * constructor, loads all action plugins and calls their register() method giving them
+ * an opportunity to register any hooks they require
+ */
+ public function __construct()
+ {
+
+ // load action plugins
+ /** @var ActionPlugin $plugin */
+ $plugin = null;
+ $pluginlist = plugin_list('action');
+
+ foreach ($pluginlist as $plugin_name) {
+ $plugin = plugin_load('action', $plugin_name);
+
+ if ($plugin !== null) $plugin->register($this);
+ }
+ }
+
+ /**
+ * register_hook
+ *
+ * register a hook for an event
+ *
+ * @param string $event string name used by the event, (incl '_before' or '_after' for triggers)
+ * @param string $advise
+ * @param object $obj object in whose scope method is to be executed,
+ * if NULL, method is assumed to be a globally available function
+ * @param string $method event handler function
+ * @param mixed $param data passed to the event handler
+ * @param int $seq sequence number for ordering hook execution (ascending)
+ */
+ public function register_hook($event, $advise, $obj, $method, $param = null, $seq = 0)
+ {
+ $seq = (int)$seq;
+ $doSort = !isset($this->hooks[$event . '_' . $advise][$seq]);
+ $this->hooks[$event . '_' . $advise][$seq][] = array($obj, $method, $param);
+
+ if ($doSort) {
+ ksort($this->hooks[$event . '_' . $advise]);
+ }
+ }
+
+ /**
+ * process the before/after event
+ *
+ * @param Event $event
+ * @param string $advise BEFORE or AFTER
+ */
+ public function process_event($event, $advise = '')
+ {
+
+ $evt_name = $event->name . ($advise ? '_' . $advise : '_BEFORE');
+
+ if (!empty($this->hooks[$evt_name])) {
+ foreach ($this->hooks[$evt_name] as $sequenced_hooks) {
+ foreach ($sequenced_hooks as $hook) {
+ list($obj, $method, $param) = $hook;
+
+ if ($obj === null) {
+ $method($event, $param);
+ } else {
+ $obj->$method($event, $param);
+ }
+
+ if (!$event->mayPropagate()) return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Check if an event has any registered handlers
+ *
+ * When $advise is empty, both BEFORE and AFTER events will be considered,
+ * otherwise only the given advisory is checked
+ *
+ * @param string $name Name of the event
+ * @param string $advise BEFORE, AFTER or empty
+ * @return bool
+ */
+ public function hasHandlerForEvent($name, $advise = '')
+ {
+ if ($advise) {
+ return isset($this->hooks[$name . '_' . $advise]);
+ }
+
+ return isset($this->hooks[$name . '_BEFORE']) || isset($this->hooks[$name . '_AFTER']);
+ }
+}
diff --git a/platform/www/inc/Extension/Plugin.php b/platform/www/inc/Extension/Plugin.php
new file mode 100644
index 0000000..03637fe
--- /dev/null
+++ b/platform/www/inc/Extension/Plugin.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace dokuwiki\Extension;
+
+/**
+ * DokuWiki Base Plugin
+ *
+ * Most plugin types inherit from this class
+ */
+abstract class Plugin implements PluginInterface
+{
+ use PluginTrait;
+}
diff --git a/platform/www/inc/Extension/PluginController.php b/platform/www/inc/Extension/PluginController.php
new file mode 100644
index 0000000..638fd39
--- /dev/null
+++ b/platform/www/inc/Extension/PluginController.php
@@ -0,0 +1,393 @@
+<?php
+
+namespace dokuwiki\Extension;
+
+/**
+ * Class to encapsulate access to dokuwiki plugins
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+class PluginController
+{
+ /** @var array the types of plugins DokuWiki supports */
+ const PLUGIN_TYPES = ['auth', 'admin', 'syntax', 'action', 'renderer', 'helper', 'remote', 'cli'];
+
+ protected $listByType = [];
+ /** @var array all installed plugins and their enabled state [plugin=>enabled] */
+ protected $masterList = [];
+ protected $pluginCascade = ['default' => [], 'local' => [], 'protected' => []];
+ protected $lastLocalConfigFile = '';
+
+ /**
+ * Populates the master list of plugins
+ */
+ public function __construct()
+ {
+ $this->loadConfig();
+ $this->populateMasterList();
+ }
+
+ /**
+ * Returns a list of available plugins of given type
+ *
+ * @param $type string, plugin_type name;
+ * the type of plugin to return,
+ * use empty string for all types
+ * @param $all bool;
+ * false to only return enabled plugins,
+ * true to return both enabled and disabled plugins
+ *
+ * @return array of
+ * - plugin names when $type = ''
+ * - or plugin component names when a $type is given
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function getList($type = '', $all = false)
+ {
+
+ // request the complete list
+ if (!$type) {
+ return $all ? array_keys($this->masterList) : array_keys(array_filter($this->masterList));
+ }
+
+ if (!isset($this->listByType[$type]['enabled'])) {
+ $this->listByType[$type]['enabled'] = $this->getListByType($type, true);
+ }
+ if ($all && !isset($this->listByType[$type]['disabled'])) {
+ $this->listByType[$type]['disabled'] = $this->getListByType($type, false);
+ }
+
+ return $all
+ ? array_merge($this->listByType[$type]['enabled'], $this->listByType[$type]['disabled'])
+ : $this->listByType[$type]['enabled'];
+ }
+
+ /**
+ * Loads the given plugin and creates an object of it
+ *
+ * @param $type string type of plugin to load
+ * @param $name string name of the plugin to load
+ * @param $new bool true to return a new instance of the plugin, false to use an already loaded instance
+ * @param $disabled bool true to load even disabled plugins
+ * @return PluginInterface|null the plugin object or null on failure
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ */
+ public function load($type, $name, $new = false, $disabled = false)
+ {
+
+ //we keep all loaded plugins available in global scope for reuse
+ global $DOKU_PLUGINS;
+
+ list($plugin, /* $component */) = $this->splitName($name);
+
+ // check if disabled
+ if (!$disabled && !$this->isEnabled($plugin)) {
+ return null;
+ }
+
+ $class = $type . '_plugin_' . $name;
+
+ //plugin already loaded?
+ if (!empty($DOKU_PLUGINS[$type][$name])) {
+ if ($new || !$DOKU_PLUGINS[$type][$name]->isSingleton()) {
+ return class_exists($class, true) ? new $class : null;
+ }
+
+ return $DOKU_PLUGINS[$type][$name];
+ }
+
+ //construct class and instantiate
+ if (!class_exists($class, true)) {
+
+ # the plugin might be in the wrong directory
+ $inf = confToHash(DOKU_PLUGIN . "$plugin/plugin.info.txt");
+ if ($inf['base'] && $inf['base'] != $plugin) {
+ msg(
+ sprintf(
+ "Plugin installed incorrectly. Rename plugin directory '%s' to '%s'.",
+ hsc($plugin),
+ hsc(
+ $inf['base']
+ )
+ ), -1
+ );
+ } elseif (preg_match('/^' . DOKU_PLUGIN_NAME_REGEX . '$/', $plugin) !== 1) {
+ msg(
+ sprintf(
+ "Plugin name '%s' is not a valid plugin name, only the characters a-z and 0-9 are allowed. " .
+ 'Maybe the plugin has been installed in the wrong directory?', hsc($plugin)
+ ), -1
+ );
+ }
+ return null;
+ }
+
+ $DOKU_PLUGINS[$type][$name] = new $class;
+ return $DOKU_PLUGINS[$type][$name];
+ }
+
+ /**
+ * Whether plugin is disabled
+ *
+ * @param string $plugin name of plugin
+ * @return bool true disabled, false enabled
+ * @deprecated in favor of the more sensible isEnabled where the return value matches the enabled state
+ */
+ public function isDisabled($plugin)
+ {
+ dbg_deprecated('isEnabled()');
+ return !$this->isEnabled($plugin);
+ }
+
+ /**
+ * Check whether plugin is disabled
+ *
+ * @param string $plugin name of plugin
+ * @return bool true enabled, false disabled
+ */
+ public function isEnabled($plugin)
+ {
+ return !empty($this->masterList[$plugin]);
+ }
+
+ /**
+ * Disable the plugin
+ *
+ * @param string $plugin name of plugin
+ * @return bool true saving succeed, false saving failed
+ */
+ public function disable($plugin)
+ {
+ if (array_key_exists($plugin, $this->pluginCascade['protected'])) return false;
+ $this->masterList[$plugin] = 0;
+ return $this->saveList();
+ }
+
+ /**
+ * Enable the plugin
+ *
+ * @param string $plugin name of plugin
+ * @return bool true saving succeed, false saving failed
+ */
+ public function enable($plugin)
+ {
+ if (array_key_exists($plugin, $this->pluginCascade['protected'])) return false;
+ $this->masterList[$plugin] = 1;
+ return $this->saveList();
+ }
+
+ /**
+ * Returns cascade of the config files
+ *
+ * @return array with arrays of plugin configs
+ */
+ public function getCascade()
+ {
+ return $this->pluginCascade;
+ }
+
+ /**
+ * Read all installed plugins and their current enabled state
+ */
+ protected function populateMasterList()
+ {
+ if ($dh = @opendir(DOKU_PLUGIN)) {
+ $all_plugins = array();
+ while (false !== ($plugin = readdir($dh))) {
+ if ($plugin[0] === '.') continue; // skip hidden entries
+ if (is_file(DOKU_PLUGIN . $plugin)) continue; // skip files, we're only interested in directories
+
+ if (array_key_exists($plugin, $this->masterList) && $this->masterList[$plugin] == 0) {
+ $all_plugins[$plugin] = 0;
+
+ } elseif (array_key_exists($plugin, $this->masterList) && $this->masterList[$plugin] == 1) {
+ $all_plugins[$plugin] = 1;
+ } else {
+ $all_plugins[$plugin] = 1;
+ }
+ }
+ $this->masterList = $all_plugins;
+ if (!file_exists($this->lastLocalConfigFile)) {
+ $this->saveList(true);
+ }
+ }
+ }
+
+ /**
+ * Includes the plugin config $files
+ * and returns the entries of the $plugins array set in these files
+ *
+ * @param array $files list of files to include, latter overrides previous
+ * @return array with entries of the $plugins arrays of the included files
+ */
+ protected function checkRequire($files)
+ {
+ $plugins = array();
+ foreach ($files as $file) {
+ if (file_exists($file)) {
+ include_once($file);
+ }
+ }
+ return $plugins;
+ }
+
+ /**
+ * Save the current list of plugins
+ *
+ * @param bool $forceSave ;
+ * false to save only when config changed
+ * true to always save
+ * @return bool true saving succeed, false saving failed
+ */
+ protected function saveList($forceSave = false)
+ {
+ global $conf;
+
+ if (empty($this->masterList)) return false;
+
+ // Rebuild list of local settings
+ $local_plugins = $this->rebuildLocal();
+ if ($local_plugins != $this->pluginCascade['local'] || $forceSave) {
+ $file = $this->lastLocalConfigFile;
+ $out = "<?php\n/*\n * Local plugin enable/disable settings\n" .
+ " * Auto-generated through plugin/extension manager\n *\n" .
+ " * NOTE: Plugins will not be added to this file unless there " .
+ "is a need to override a default setting. Plugins are\n" .
+ " * enabled by default.\n */\n";
+ foreach ($local_plugins as $plugin => $value) {
+ $out .= "\$plugins['$plugin'] = $value;\n";
+ }
+ // backup current file (remove any existing backup)
+ if (file_exists($file)) {
+ $backup = $file . '.bak';
+ if (file_exists($backup)) @unlink($backup);
+ if (!@copy($file, $backup)) return false;
+ if (!empty($conf['fperm'])) chmod($backup, $conf['fperm']);
+ }
+ //check if can open for writing, else restore
+ return io_saveFile($file, $out);
+ }
+ return false;
+ }
+
+ /**
+ * Rebuild the set of local plugins
+ *
+ * @return array array of plugins to be saved in end($config_cascade['plugins']['local'])
+ */
+ protected function rebuildLocal()
+ {
+ //assign to local variable to avoid overwriting
+ $backup = $this->masterList;
+ //Can't do anything about protected one so rule them out completely
+ $local_default = array_diff_key($backup, $this->pluginCascade['protected']);
+ //Diff between local+default and default
+ //gives us the ones we need to check and save
+ $diffed_ones = array_diff_key($local_default, $this->pluginCascade['default']);
+ //The ones which we are sure of (list of 0s not in default)
+ $sure_plugins = array_filter($diffed_ones, array($this, 'negate'));
+ //the ones in need of diff
+ $conflicts = array_diff_key($local_default, $diffed_ones);
+ //The final list
+ return array_merge($sure_plugins, array_diff_assoc($conflicts, $this->pluginCascade['default']));
+ }
+
+ /**
+ * Build the list of plugins and cascade
+ *
+ */
+ protected function loadConfig()
+ {
+ global $config_cascade;
+ foreach (array('default', 'protected') as $type) {
+ if (array_key_exists($type, $config_cascade['plugins'])) {
+ $this->pluginCascade[$type] = $this->checkRequire($config_cascade['plugins'][$type]);
+ }
+ }
+ $local = $config_cascade['plugins']['local'];
+ $this->lastLocalConfigFile = array_pop($local);
+ $this->pluginCascade['local'] = $this->checkRequire(array($this->lastLocalConfigFile));
+ if (is_array($local)) {
+ $this->pluginCascade['default'] = array_merge(
+ $this->pluginCascade['default'],
+ $this->checkRequire($local)
+ );
+ }
+ $this->masterList = array_merge(
+ $this->pluginCascade['default'],
+ $this->pluginCascade['local'],
+ $this->pluginCascade['protected']
+ );
+ }
+
+ /**
+ * Returns a list of available plugin components of given type
+ *
+ * @param string $type plugin_type name; the type of plugin to return,
+ * @param bool $enabled true to return enabled plugins,
+ * false to return disabled plugins
+ * @return array of plugin components of requested type
+ */
+ protected function getListByType($type, $enabled)
+ {
+ $master_list = $enabled
+ ? array_keys(array_filter($this->masterList))
+ : array_keys(array_filter($this->masterList, array($this, 'negate')));
+ $plugins = array();
+
+ foreach ($master_list as $plugin) {
+
+ if (file_exists(DOKU_PLUGIN . "$plugin/$type.php")) {
+ $plugins[] = $plugin;
+ continue;
+ }
+
+ $typedir = DOKU_PLUGIN . "$plugin/$type/";
+ if (is_dir($typedir)) {
+ if ($dp = opendir($typedir)) {
+ while (false !== ($component = readdir($dp))) {
+ if (strpos($component, '.') === 0 || strtolower(substr($component, -4)) !== '.php') continue;
+ if (is_file($typedir . $component)) {
+ $plugins[] = $plugin . '_' . substr($component, 0, -4);
+ }
+ }
+ closedir($dp);
+ }
+ }
+
+ }//foreach
+
+ return $plugins;
+ }
+
+ /**
+ * Split name in a plugin name and a component name
+ *
+ * @param string $name
+ * @return array with
+ * - plugin name
+ * - and component name when available, otherwise empty string
+ */
+ protected function splitName($name)
+ {
+ if (!isset($this->masterList[$name])) {
+ return explode('_', $name, 2);
+ }
+
+ return array($name, '');
+ }
+
+ /**
+ * Returns inverse boolean value of the input
+ *
+ * @param mixed $input
+ * @return bool inversed boolean value of input
+ */
+ protected function negate($input)
+ {
+ return !(bool)$input;
+ }
+}
diff --git a/platform/www/inc/Extension/PluginInterface.php b/platform/www/inc/Extension/PluginInterface.php
new file mode 100644
index 0000000..f2dbe86
--- /dev/null
+++ b/platform/www/inc/Extension/PluginInterface.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace dokuwiki\Extension;
+
+/**
+ * DokuWiki Plugin Interface
+ *
+ * Defines the public contract all DokuWiki plugins will adhere to. The actual code
+ * to do so is defined in dokuwiki\Extension\PluginTrait
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+interface PluginInterface
+{
+ /**
+ * General Info
+ *
+ * Needs to return a associative array with the following values:
+ *
+ * base - the plugin's base name (eg. the directory it needs to be installed in)
+ * author - Author of the plugin
+ * email - Email address to contact the author
+ * date - Last modified date of the plugin in YYYY-MM-DD format
+ * name - Name of the plugin
+ * desc - Short description of the plugin (Text only)
+ * url - Website with more information on the plugin (eg. syntax description)
+ */
+ public function getInfo();
+
+ /**
+ * The type of the plugin inferred from the class name
+ *
+ * @return string plugin type
+ */
+ public function getPluginType();
+
+ /**
+ * The name of the plugin inferred from the class name
+ *
+ * @return string plugin name
+ */
+ public function getPluginName();
+
+ /**
+ * The component part of the plugin inferred from the class name
+ *
+ * @return string component name
+ */
+ public function getPluginComponent();
+
+ /**
+ * Access plugin language strings
+ *
+ * to try to minimise unnecessary loading of the strings when the plugin doesn't require them
+ * e.g. when info plugin is querying plugins for information about themselves.
+ *
+ * @param string $id id of the string to be retrieved
+ * @return string in appropriate language or english if not available
+ */
+ public function getLang($id);
+
+ /**
+ * retrieve a language dependent file and pass to xhtml renderer for display
+ * plugin equivalent of p_locale_xhtml()
+ *
+ * @param string $id id of language dependent wiki page
+ * @return string parsed contents of the wiki page in xhtml format
+ */
+ public function locale_xhtml($id);
+
+ /**
+ * Prepends appropriate path for a language dependent filename
+ * plugin equivalent of localFN()
+ *
+ * @param string $id id of localization file
+ * @param string $ext The file extension (usually txt)
+ * @return string wiki text
+ */
+ public function localFN($id, $ext = 'txt');
+
+ /**
+ * Reads all the plugins language dependent strings into $this->lang
+ * this function is automatically called by getLang()
+ *
+ * @todo this could be made protected and be moved to the trait only
+ */
+ public function setupLocale();
+
+ /**
+ * use this function to access plugin configuration variables
+ *
+ * @param string $setting the setting to access
+ * @param mixed $notset what to return if the setting is not available
+ * @return mixed
+ */
+ public function getConf($setting, $notset = false);
+
+ /**
+ * merges the plugin's default settings with any local settings
+ * this function is automatically called through getConf()
+ *
+ * @todo this could be made protected and be moved to the trait only
+ */
+ public function loadConfig();
+
+ /**
+ * Loads a given helper plugin (if enabled)
+ *
+ * @author Esther Brunner <wikidesign@gmail.com>
+ *
+ * @param string $name name of plugin to load
+ * @param bool $msg if a message should be displayed in case the plugin is not available
+ * @return PluginInterface|null helper plugin object
+ */
+ public function loadHelper($name, $msg = true);
+
+ /**
+ * email
+ * standardised function to generate an email link according to obfuscation settings
+ *
+ * @param string $email
+ * @param string $name
+ * @param string $class
+ * @param string $more
+ * @return string html
+ */
+ public function email($email, $name = '', $class = '', $more = '');
+
+ /**
+ * external_link
+ * standardised function to generate an external link according to conf settings
+ *
+ * @param string $link
+ * @param string $title
+ * @param string $class
+ * @param string $target
+ * @param string $more
+ * @return string
+ */
+ public function external_link($link, $title = '', $class = '', $target = '', $more = '');
+
+ /**
+ * output text string through the parser, allows dokuwiki markup to be used
+ * very ineffecient for small pieces of data - try not to use
+ *
+ * @param string $text wiki markup to parse
+ * @param string $format output format
+ * @return null|string
+ */
+ public function render_text($text, $format = 'xhtml');
+
+ /**
+ * Allow the plugin to prevent DokuWiki from reusing an instance
+ *
+ * @return bool false if the plugin has to be instantiated
+ */
+ public function isSingleton();
+}
+
+
+
diff --git a/platform/www/inc/Extension/PluginTrait.php b/platform/www/inc/Extension/PluginTrait.php
new file mode 100644
index 0000000..f1db0f5
--- /dev/null
+++ b/platform/www/inc/Extension/PluginTrait.php
@@ -0,0 +1,256 @@
+<?php
+
+namespace dokuwiki\Extension;
+
+/**
+ * Provides standard DokuWiki plugin behaviour
+ */
+trait PluginTrait
+{
+
+ protected $localised = false; // set to true by setupLocale() after loading language dependent strings
+ protected $lang = array(); // array to hold language dependent strings, best accessed via ->getLang()
+ protected $configloaded = false; // set to true by loadConfig() after loading plugin configuration variables
+ protected $conf = array(); // array to hold plugin settings, best accessed via ->getConf()
+
+ /**
+ * @see PluginInterface::getInfo()
+ */
+ public function getInfo()
+ {
+ $parts = explode('_', get_class($this));
+ $info = DOKU_PLUGIN . '/' . $parts[2] . '/plugin.info.txt';
+ if (file_exists($info)) return confToHash($info);
+
+ msg(
+ 'getInfo() not implemented in ' . get_class($this) . ' and ' . $info . ' not found.<br />' .
+ 'Verify you\'re running the latest version of the plugin. If the problem persists, send a ' .
+ 'bug report to the author of the ' . $parts[2] . ' plugin.', -1
+ );
+ return array(
+ 'date' => '0000-00-00',
+ 'name' => $parts[2] . ' plugin',
+ );
+ }
+
+ /**
+ * @see PluginInterface::isSingleton()
+ */
+ public function isSingleton()
+ {
+ return true;
+ }
+
+ /**
+ * @see PluginInterface::loadHelper()
+ */
+ public function loadHelper($name, $msg = true)
+ {
+ $obj = plugin_load('helper', $name);
+ if (is_null($obj) && $msg) msg("Helper plugin $name is not available or invalid.", -1);
+ return $obj;
+ }
+
+ // region introspection methods
+
+ /**
+ * @see PluginInterface::getPluginType()
+ */
+ public function getPluginType()
+ {
+ list($t) = explode('_', get_class($this), 2);
+ return $t;
+ }
+
+ /**
+ * @see PluginInterface::getPluginName()
+ */
+ public function getPluginName()
+ {
+ list(/* $t */, /* $p */, $n) = explode('_', get_class($this), 4);
+ return $n;
+ }
+
+ /**
+ * @see PluginInterface::getPluginComponent()
+ */
+ public function getPluginComponent()
+ {
+ list(/* $t */, /* $p */, /* $n */, $c) = explode('_', get_class($this), 4);
+ return (isset($c) ? $c : '');
+ }
+
+ // endregion
+ // region localization methods
+
+ /**
+ * @see PluginInterface::getLang()
+ */
+ public function getLang($id)
+ {
+ if (!$this->localised) $this->setupLocale();
+
+ return (isset($this->lang[$id]) ? $this->lang[$id] : '');
+ }
+
+ /**
+ * @see PluginInterface::locale_xhtml()
+ */
+ public function locale_xhtml($id)
+ {
+ return p_cached_output($this->localFN($id));
+ }
+
+ /**
+ * @see PluginInterface::localFN()
+ */
+ public function localFN($id, $ext = 'txt')
+ {
+ global $conf;
+ $plugin = $this->getPluginName();
+ $file = DOKU_CONF . 'plugin_lang/' . $plugin . '/' . $conf['lang'] . '/' . $id . '.' . $ext;
+ if (!file_exists($file)) {
+ $file = DOKU_PLUGIN . $plugin . '/lang/' . $conf['lang'] . '/' . $id . '.' . $ext;
+ if (!file_exists($file)) {
+ //fall back to english
+ $file = DOKU_PLUGIN . $plugin . '/lang/en/' . $id . '.' . $ext;
+ }
+ }
+ return $file;
+ }
+
+ /**
+ * @see PluginInterface::setupLocale()
+ */
+ public function setupLocale()
+ {
+ if ($this->localised) return;
+
+ global $conf, $config_cascade; // definitely don't invoke "global $lang"
+ $path = DOKU_PLUGIN . $this->getPluginName() . '/lang/';
+
+ $lang = array();
+
+ // don't include once, in case several plugin components require the same language file
+ @include($path . 'en/lang.php');
+ foreach ($config_cascade['lang']['plugin'] as $config_file) {
+ if (file_exists($config_file . $this->getPluginName() . '/en/lang.php')) {
+ include($config_file . $this->getPluginName() . '/en/lang.php');
+ }
+ }
+
+ if ($conf['lang'] != 'en') {
+ @include($path . $conf['lang'] . '/lang.php');
+ foreach ($config_cascade['lang']['plugin'] as $config_file) {
+ if (file_exists($config_file . $this->getPluginName() . '/' . $conf['lang'] . '/lang.php')) {
+ include($config_file . $this->getPluginName() . '/' . $conf['lang'] . '/lang.php');
+ }
+ }
+ }
+
+ $this->lang = $lang;
+ $this->localised = true;
+ }
+
+ // endregion
+ // region configuration methods
+
+ /**
+ * @see PluginInterface::getConf()
+ */
+ public function getConf($setting, $notset = false)
+ {
+
+ if (!$this->configloaded) {
+ $this->loadConfig();
+ }
+
+ if (isset($this->conf[$setting])) {
+ return $this->conf[$setting];
+ } else {
+ return $notset;
+ }
+ }
+
+ /**
+ * @see PluginInterface::loadConfig()
+ */
+ public function loadConfig()
+ {
+ global $conf;
+
+ $defaults = $this->readDefaultSettings();
+ $plugin = $this->getPluginName();
+
+ foreach ($defaults as $key => $value) {
+ if (isset($conf['plugin'][$plugin][$key])) continue;
+ $conf['plugin'][$plugin][$key] = $value;
+ }
+
+ $this->configloaded = true;
+ $this->conf =& $conf['plugin'][$plugin];
+ }
+
+ /**
+ * read the plugin's default configuration settings from conf/default.php
+ * this function is automatically called through getConf()
+ *
+ * @return array setting => value
+ */
+ protected function readDefaultSettings()
+ {
+
+ $path = DOKU_PLUGIN . $this->getPluginName() . '/conf/';
+ $conf = array();
+
+ if (file_exists($path . 'default.php')) {
+ include($path . 'default.php');
+ }
+
+ return $conf;
+ }
+
+ // endregion
+ // region output methods
+
+ /**
+ * @see PluginInterface::email()
+ */
+ public function email($email, $name = '', $class = '', $more = '')
+ {
+ if (!$email) return $name;
+ $email = obfuscate($email);
+ if (!$name) $name = $email;
+ $class = "class='" . ($class ? $class : 'mail') . "'";
+ return "<a href='mailto:$email' $class title='$email' $more>$name</a>";
+ }
+
+ /**
+ * @see PluginInterface::external_link()
+ */
+ public function external_link($link, $title = '', $class = '', $target = '', $more = '')
+ {
+ global $conf;
+
+ $link = htmlentities($link);
+ if (!$title) $title = $link;
+ if (!$target) $target = $conf['target']['extern'];
+ if ($conf['relnofollow']) $more .= ' rel="nofollow"';
+
+ if ($class) $class = " class='$class'";
+ if ($target) $target = " target='$target'";
+ if ($more) $more = " " . trim($more);
+
+ return "<a href='$link'$class$target$more>$title</a>";
+ }
+
+ /**
+ * @see PluginInterface::render_text()
+ */
+ public function render_text($text, $format = 'xhtml')
+ {
+ return p_render($format, p_get_instructions($text), $info);
+ }
+
+ // endregion
+}
diff --git a/platform/www/inc/Extension/RemotePlugin.php b/platform/www/inc/Extension/RemotePlugin.php
new file mode 100644
index 0000000..33bca98
--- /dev/null
+++ b/platform/www/inc/Extension/RemotePlugin.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace dokuwiki\Extension;
+
+use dokuwiki\Remote\Api;
+use ReflectionException;
+use ReflectionMethod;
+
+/**
+ * Remote Plugin prototype
+ *
+ * Add functionality to the remote API in a plugin
+ */
+abstract class RemotePlugin extends Plugin
+{
+
+ private $api;
+
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ $this->api = new Api();
+ }
+
+ /**
+ * Get all available methods with remote access.
+ *
+ * By default it exports all public methods of a remote plugin. Methods beginning
+ * with an underscore are skipped.
+ *
+ * @return array Information about all provided methods. {@see dokuwiki\Remote\RemoteAPI}.
+ * @throws ReflectionException
+ */
+ public function _getMethods()
+ {
+ $result = array();
+
+ $reflection = new \ReflectionClass($this);
+ foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
+ // skip parent methods, only methods further down are exported
+ $declaredin = $method->getDeclaringClass()->name;
+ if ($declaredin === 'dokuwiki\Extension\Plugin' || $declaredin === 'dokuwiki\Extension\RemotePlugin') {
+ continue;
+ }
+ $method_name = $method->name;
+ if (strpos($method_name, '_') === 0) {
+ continue;
+ }
+
+ // strip asterisks
+ $doc = $method->getDocComment();
+ $doc = preg_replace(
+ array('/^[ \t]*\/\*+[ \t]*/m', '/[ \t]*\*+[ \t]*/m', '/\*+\/\s*$/m', '/\s*\/\s*$/m'),
+ array('', '', '', ''),
+ $doc
+ );
+
+ // prepare data
+ $data = array();
+ $data['name'] = $method_name;
+ $data['public'] = 0;
+ $data['doc'] = $doc;
+ $data['args'] = array();
+
+ // get parameter type from doc block type hint
+ foreach ($method->getParameters() as $parameter) {
+ $name = $parameter->name;
+ $type = 'string'; // we default to string
+ if (preg_match('/^@param[ \t]+([\w|\[\]]+)[ \t]\$' . $name . '/m', $doc, $m)) {
+ $type = $this->cleanTypeHint($m[1]);
+ }
+ $data['args'][] = $type;
+ }
+
+ // get return type from doc block type hint
+ if (preg_match('/^@return[ \t]+([\w|\[\]]+)/m', $doc, $m)) {
+ $data['return'] = $this->cleanTypeHint($m[1]);
+ } else {
+ $data['return'] = 'string';
+ }
+
+ // add to result
+ $result[$method_name] = $data;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Matches the given type hint against the valid options for the remote API
+ *
+ * @param string $hint
+ * @return string
+ */
+ protected function cleanTypeHint($hint)
+ {
+ $types = explode('|', $hint);
+ foreach ($types as $t) {
+ if (substr($t, -2) === '[]') {
+ return 'array';
+ }
+ if ($t === 'boolean') {
+ return 'bool';
+ }
+ if (in_array($t, array('array', 'string', 'int', 'double', 'bool', 'null', 'date', 'file'))) {
+ return $t;
+ }
+ }
+ return 'string';
+ }
+
+ /**
+ * @return Api
+ */
+ protected function getApi()
+ {
+ return $this->api;
+ }
+
+}
diff --git a/platform/www/inc/Extension/SyntaxPlugin.php b/platform/www/inc/Extension/SyntaxPlugin.php
new file mode 100644
index 0000000..ea8f51b
--- /dev/null
+++ b/platform/www/inc/Extension/SyntaxPlugin.php
@@ -0,0 +1,132 @@
+<?php
+
+namespace dokuwiki\Extension;
+
+use Doku_Handler;
+use Doku_Renderer;
+
+/**
+ * Syntax Plugin Prototype
+ *
+ * All DokuWiki plugins to extend the parser/rendering mechanism
+ * need to inherit from this class
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+abstract class SyntaxPlugin extends \dokuwiki\Parsing\ParserMode\Plugin
+{
+ use PluginTrait;
+
+ protected $allowedModesSetup = false;
+
+ /**
+ * Syntax Type
+ *
+ * Needs to return one of the mode types defined in $PARSER_MODES in Parser.php
+ *
+ * @return string
+ */
+ abstract public function getType();
+
+ /**
+ * Allowed Mode Types
+ *
+ * Defines the mode types for other dokuwiki markup that maybe nested within the
+ * plugin's own markup. Needs to return an array of one or more of the mode types
+ * defined in $PARSER_MODES in Parser.php
+ *
+ * @return array
+ */
+ public function getAllowedTypes()
+ {
+ return array();
+ }
+
+ /**
+ * Paragraph Type
+ *
+ * Defines how this syntax is handled regarding paragraphs. This is important
+ * for correct XHTML nesting. Should return one of the following:
+ *
+ * 'normal' - The plugin can be used inside paragraphs
+ * 'block' - Open paragraphs need to be closed before plugin output
+ * 'stack' - Special case. Plugin wraps other paragraphs.
+ *
+ * @see Doku_Handler_Block
+ *
+ * @return string
+ */
+ public function getPType()
+ {
+ return 'normal';
+ }
+
+ /**
+ * Handler to prepare matched data for the rendering process
+ *
+ * This function can only pass data to render() via its return value - render()
+ * may be not be run during the object's current life.
+ *
+ * Usually you should only need the $match param.
+ *
+ * @param string $match The text matched by the patterns
+ * @param int $state The lexer state for the match
+ * @param int $pos The character position of the matched text
+ * @param Doku_Handler $handler The Doku_Handler object
+ * @return bool|array Return an array with all data you want to use in render, false don't add an instruction
+ */
+ abstract public function handle($match, $state, $pos, Doku_Handler $handler);
+
+ /**
+ * Handles the actual output creation.
+ *
+ * The function must not assume any other of the classes methods have been run
+ * during the object's current life. The only reliable data it receives are its
+ * parameters.
+ *
+ * The function should always check for the given output format and return false
+ * when a format isn't supported.
+ *
+ * $renderer contains a reference to the renderer object which is
+ * currently handling the rendering. You need to use it for writing
+ * the output. How this is done depends on the renderer used (specified
+ * by $format
+ *
+ * The contents of the $data array depends on what the handler() function above
+ * created
+ *
+ * @param string $format output format being rendered
+ * @param Doku_Renderer $renderer the current renderer object
+ * @param array $data data created by handler()
+ * @return boolean rendered correctly? (however, returned value is not used at the moment)
+ */
+ abstract public function render($format, Doku_Renderer $renderer, $data);
+
+ /**
+ * There should be no need to override this function
+ *
+ * @param string $mode
+ * @return bool
+ */
+ public function accepts($mode)
+ {
+
+ if (!$this->allowedModesSetup) {
+ global $PARSER_MODES;
+
+ $allowedModeTypes = $this->getAllowedTypes();
+ foreach ($allowedModeTypes as $mt) {
+ $this->allowedModes = array_merge($this->allowedModes, $PARSER_MODES[$mt]);
+ }
+
+ $idx = array_search(substr(get_class($this), 7), (array)$this->allowedModes);
+ if ($idx !== false) {
+ unset($this->allowedModes[$idx]);
+ }
+ $this->allowedModesSetup = true;
+ }
+
+ return parent::accepts($mode);
+ }
+}
diff --git a/platform/www/inc/FeedParser.php b/platform/www/inc/FeedParser.php
new file mode 100644
index 0000000..8f71b96
--- /dev/null
+++ b/platform/www/inc/FeedParser.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ * We override some methods of the original SimplePie class here
+ */
+class FeedParser extends SimplePie {
+
+ /**
+ * Constructor. Set some defaults
+ */
+ public function __construct(){
+ parent::__construct();
+ $this->enable_cache(false);
+ $this->set_file_class(\dokuwiki\FeedParserFile::class);
+ }
+
+ /**
+ * Backward compatibility for older plugins
+ *
+ * phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
+ * @param string $url
+ */
+ public function feed_url($url){
+ $this->set_feed_url($url);
+ }
+}
+
+
diff --git a/platform/www/inc/FeedParserFile.php b/platform/www/inc/FeedParserFile.php
new file mode 100644
index 0000000..be3417e
--- /dev/null
+++ b/platform/www/inc/FeedParserFile.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace dokuwiki;
+
+use dokuwiki\HTTP\DokuHTTPClient;
+
+/**
+ * Fetch an URL using our own HTTPClient
+ *
+ * Replaces SimplePie's own class
+ */
+class FeedParserFile extends \SimplePie_File
+{
+ protected $http;
+ /** @noinspection PhpMissingParentConstructorInspection */
+
+ /**
+ * Inititializes the HTTPClient
+ *
+ * We ignore all given parameters - they are set in DokuHTTPClient
+ *
+ * @inheritdoc
+ */
+ public function __construct(
+ $url,
+ $timeout = 10,
+ $redirects = 5,
+ $headers = null,
+ $useragent = null,
+ $force_fsockopen = false,
+ $curl_options = array()
+ ) {
+ $this->http = new DokuHTTPClient();
+ $this->success = $this->http->sendRequest($url);
+
+ $this->headers = $this->http->resp_headers;
+ $this->body = $this->http->resp_body;
+ $this->error = $this->http->error;
+
+ $this->method = SIMPLEPIE_FILE_SOURCE_REMOTE | SIMPLEPIE_FILE_SOURCE_FSOCKOPEN;
+
+ return $this->success;
+ }
+
+ /** @inheritdoc */
+ public function headers()
+ {
+ return $this->headers;
+ }
+
+ /** @inheritdoc */
+ public function body()
+ {
+ return $this->body;
+ }
+
+ /** @inheritdoc */
+ public function close()
+ {
+ return true;
+ }
+}
diff --git a/platform/www/inc/Form/ButtonElement.php b/platform/www/inc/Form/ButtonElement.php
new file mode 100644
index 0000000..4f585f0
--- /dev/null
+++ b/platform/www/inc/Form/ButtonElement.php
@@ -0,0 +1,34 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class ButtonElement
+ *
+ * Represents a simple button
+ *
+ * @package dokuwiki\Form
+ */
+class ButtonElement extends Element {
+
+ /** @var string HTML content */
+ protected $content = '';
+
+ /**
+ * @param string $name
+ * @param string $content HTML content of the button. You have to escape it yourself.
+ */
+ public function __construct($name, $content = '') {
+ parent::__construct('button', array('name' => $name, 'value' => 1));
+ $this->content = $content;
+ }
+
+ /**
+ * The HTML representation of this element
+ *
+ * @return string
+ */
+ public function toHTML() {
+ return '<button ' . buildAttributes($this->attrs(), true) . '>'.$this->content.'</button>';
+ }
+
+}
diff --git a/platform/www/inc/Form/CheckableElement.php b/platform/www/inc/Form/CheckableElement.php
new file mode 100644
index 0000000..27d5c2e
--- /dev/null
+++ b/platform/www/inc/Form/CheckableElement.php
@@ -0,0 +1,62 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class CheckableElement
+ *
+ * For Radio- and Checkboxes
+ *
+ * @package dokuwiki\Form
+ */
+class CheckableElement extends InputElement {
+
+ /**
+ * @param string $type The type of this element
+ * @param string $name The name of this form element
+ * @param string $label The label text for this element
+ */
+ public function __construct($type, $name, $label) {
+ parent::__construct($type, $name, $label);
+ // default value is 1
+ $this->attr('value', 1);
+ }
+
+ /**
+ * Handles the useInput flag and sets the checked attribute accordingly
+ */
+ protected function prefillInput() {
+ global $INPUT;
+ list($name, $key) = $this->getInputName();
+ $myvalue = $this->val();
+
+ if(!$INPUT->has($name)) return;
+
+ if($key === null) {
+ // no key - single value
+ $value = $INPUT->str($name);
+ if($value == $myvalue) {
+ $this->attr('checked', 'checked');
+ } else {
+ $this->rmattr('checked');
+ }
+ } else {
+ // we have an array, there might be several values in it
+ $input = $INPUT->arr($name);
+ if(isset($input[$key])) {
+ $this->rmattr('checked');
+
+ // values seem to be in another sub array
+ if(is_array($input[$key])) {
+ $input = $input[$key];
+ }
+
+ foreach($input as $value) {
+ if($value == $myvalue) {
+ $this->attr('checked', 'checked');
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/platform/www/inc/Form/DropdownElement.php b/platform/www/inc/Form/DropdownElement.php
new file mode 100644
index 0000000..51f4751
--- /dev/null
+++ b/platform/www/inc/Form/DropdownElement.php
@@ -0,0 +1,198 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class DropdownElement
+ *
+ * Represents a HTML select. Please note that this does not support multiple selected options!
+ *
+ * @package dokuwiki\Form
+ */
+class DropdownElement extends InputElement {
+
+ /** @var array OptGroup[] */
+ protected $optGroups = array();
+
+ /**
+ * @param string $name The name of this form element
+ * @param array $options The available options
+ * @param string $label The label text for this element (will be autoescaped)
+ */
+ public function __construct($name, $options, $label = '') {
+ parent::__construct('dropdown', $name, $label);
+ $this->rmattr('type');
+ $this->optGroups[''] = new OptGroup(null, $options);
+ $this->val('');
+ }
+
+ /**
+ * Add an `<optgroup>` and respective options
+ *
+ * @param string $label
+ * @param array $options
+ * @return OptGroup a reference to the added optgroup
+ * @throws \Exception
+ */
+ public function addOptGroup($label, $options) {
+ if (empty($label)) {
+ throw new \InvalidArgumentException(hsc('<optgroup> must have a label!'));
+ }
+ $this->optGroups[$label] = new OptGroup($label, $options);
+ return end($this->optGroups);
+ }
+
+ /**
+ * Set or get the optgroups of an Dropdown-Element.
+ *
+ * optgroups have to be given as associative array
+ * * the key being the label of the group
+ * * the value being an array of options as defined in @see OptGroup::options()
+ *
+ * @param null|array $optGroups
+ * @return OptGroup[]|DropdownElement
+ */
+ public function optGroups($optGroups = null) {
+ if($optGroups === null) {
+ return $this->optGroups;
+ }
+ if (!is_array($optGroups)) {
+ throw new \InvalidArgumentException(hsc('Argument must be an associative array of label => [options]!'));
+ }
+ $this->optGroups = array();
+ foreach ($optGroups as $label => $options) {
+ $this->addOptGroup($label, $options);
+ }
+ return $this;
+ }
+
+ /**
+ * Get or set the options of the Dropdown
+ *
+ * Options can be given as associative array (value => label) or as an
+ * indexd array (label = value) or as an array of arrays. In the latter
+ * case an element has to look as follows:
+ * option-value => array (
+ * 'label' => option-label,
+ * 'attrs' => array (
+ * attr-key => attr-value, ...
+ * )
+ * )
+ *
+ * @param null|array $options
+ * @return $this|array
+ */
+ public function options($options = null) {
+ if ($options === null) {
+ return $this->optGroups['']->options();
+ }
+ $this->optGroups[''] = new OptGroup(null, $options);
+ return $this;
+ }
+
+ /**
+ * Gets or sets an attribute
+ *
+ * When no $value is given, the current content of the attribute is returned.
+ * An empty string is returned for unset attributes.
+ *
+ * When a $value is given, the content is set to that value and the Element
+ * itself is returned for easy chaining
+ *
+ * @param string $name Name of the attribute to access
+ * @param null|string $value New value to set
+ * @return string|$this
+ */
+ public function attr($name, $value = null) {
+ if(strtolower($name) == 'multiple') {
+ throw new \InvalidArgumentException(
+ 'Sorry, the dropdown element does not support the "multiple" attribute'
+ );
+ }
+ return parent::attr($name, $value);
+ }
+
+ /**
+ * Get or set the current value
+ *
+ * When setting a value that is not defined in the options, the value is ignored
+ * and the first option's value is selected instead
+ *
+ * @param null|string $value The value to set
+ * @return $this|string
+ */
+ public function val($value = null) {
+ if($value === null) return $this->value;
+
+ $value_exists = $this->setValueInOptGroups($value);
+
+ if($value_exists) {
+ $this->value = $value;
+ } else {
+ // unknown value set, select first option instead
+ $this->value = $this->getFirstOption();
+ $this->setValueInOptGroups($this->value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns the first options as it will be rendered in HTML
+ *
+ * @return string
+ */
+ protected function getFirstOption() {
+ $options = $this->options();
+ if (!empty($options)) {
+ $keys = array_keys($options);
+ return (string) array_shift($keys);
+ }
+ foreach ($this->optGroups as $optGroup) {
+ $options = $optGroup->options();
+ if (!empty($options)) {
+ $keys = array_keys($options);
+ return (string) array_shift($keys);
+ }
+ }
+ }
+
+ /**
+ * Set the value in the OptGroups, including the optgroup for the options without optgroup.
+ *
+ * @param string $value
+ * @return bool
+ */
+ protected function setValueInOptGroups($value) {
+ $value_exists = false;
+ /** @var OptGroup $optGroup */
+ foreach ($this->optGroups as $optGroup) {
+ $value_exists = $optGroup->storeValue($value) || $value_exists;
+ if ($value_exists) {
+ $value = null;
+ }
+ }
+ return $value_exists;
+ }
+
+ /**
+ * Create the HTML for the select it self
+ *
+ * @return string
+ */
+ protected function mainElementHTML() {
+ if($this->useInput) $this->prefillInput();
+
+ $html = '<select ' . buildAttributes($this->attrs()) . '>';
+ $html = array_reduce(
+ $this->optGroups,
+ function ($html, OptGroup $optGroup) {
+ return $html . $optGroup->toHTML();
+ },
+ $html
+ );
+ $html .= '</select>';
+
+ return $html;
+ }
+
+}
diff --git a/platform/www/inc/Form/Element.php b/platform/www/inc/Form/Element.php
new file mode 100644
index 0000000..a357882
--- /dev/null
+++ b/platform/www/inc/Form/Element.php
@@ -0,0 +1,151 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class Element
+ *
+ * The basic building block of a form
+ *
+ * @package dokuwiki\Form
+ */
+abstract class Element {
+
+ /**
+ * @var array the attributes of this element
+ */
+ protected $attributes = array();
+
+ /**
+ * @var string The type of this element
+ */
+ protected $type;
+
+ /**
+ * @param string $type The type of this element
+ * @param array $attributes
+ */
+ public function __construct($type, $attributes = array()) {
+ $this->type = $type;
+ $this->attributes = $attributes;
+ }
+
+ /**
+ * Type of this element
+ *
+ * @return string
+ */
+ public function getType() {
+ return $this->type;
+ }
+
+ /**
+ * Gets or sets an attribute
+ *
+ * When no $value is given, the current content of the attribute is returned.
+ * An empty string is returned for unset attributes.
+ *
+ * When a $value is given, the content is set to that value and the Element
+ * itself is returned for easy chaining
+ *
+ * @param string $name Name of the attribute to access
+ * @param null|string $value New value to set
+ * @return string|$this
+ */
+ public function attr($name, $value = null) {
+ // set
+ if($value !== null) {
+ $this->attributes[$name] = $value;
+ return $this;
+ }
+
+ // get
+ if(isset($this->attributes[$name])) {
+ return $this->attributes[$name];
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Removes the given attribute if it exists
+ *
+ * @param string $name
+ * @return $this
+ */
+ public function rmattr($name) {
+ if(isset($this->attributes[$name])) {
+ unset($this->attributes[$name]);
+ }
+ return $this;
+ }
+
+ /**
+ * Gets or adds a all given attributes at once
+ *
+ * @param array|null $attributes
+ * @return array|$this
+ */
+ public function attrs($attributes = null) {
+ // set
+ if($attributes) {
+ foreach((array) $attributes as $key => $val) {
+ $this->attr($key, $val);
+ }
+ return $this;
+ }
+ // get
+ return $this->attributes;
+ }
+
+ /**
+ * Adds a class to the class attribute
+ *
+ * This is the preferred method of setting the element's class
+ *
+ * @param string $class the new class to add
+ * @return $this
+ */
+ public function addClass($class) {
+ $classes = explode(' ', $this->attr('class'));
+ $classes[] = $class;
+ $classes = array_unique($classes);
+ $classes = array_filter($classes);
+ $this->attr('class', join(' ', $classes));
+ return $this;
+ }
+
+ /**
+ * Get or set the element's ID
+ *
+ * This is the preferred way of setting the element's ID
+ *
+ * @param null|string $id
+ * @return string|$this
+ */
+ public function id($id = null) {
+ if(strpos($id, '__') === false) {
+ throw new \InvalidArgumentException('IDs in DokuWiki have to contain two subsequent underscores');
+ }
+
+ return $this->attr('id', $id);
+ }
+
+ /**
+ * Get or set the element's value
+ *
+ * This is the preferred way of setting the element's value
+ *
+ * @param null|string $value
+ * @return string|$this
+ */
+ public function val($value = null) {
+ return $this->attr('value', $value);
+ }
+
+ /**
+ * The HTML representation of this element
+ *
+ * @return string
+ */
+ abstract public function toHTML();
+}
diff --git a/platform/www/inc/Form/FieldsetCloseElement.php b/platform/www/inc/Form/FieldsetCloseElement.php
new file mode 100644
index 0000000..8f26717
--- /dev/null
+++ b/platform/www/inc/Form/FieldsetCloseElement.php
@@ -0,0 +1,30 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class FieldsetCloseElement
+ *
+ * Closes an open Fieldset
+ *
+ * @package dokuwiki\Form
+ */
+class FieldsetCloseElement extends TagCloseElement {
+
+ /**
+ * @param array $attributes
+ */
+ public function __construct($attributes = array()) {
+ parent::__construct('', $attributes);
+ $this->type = 'fieldsetclose';
+ }
+
+
+ /**
+ * The HTML representation of this element
+ *
+ * @return string
+ */
+ public function toHTML() {
+ return '</fieldset>';
+ }
+}
diff --git a/platform/www/inc/Form/FieldsetOpenElement.php b/platform/www/inc/Form/FieldsetOpenElement.php
new file mode 100644
index 0000000..a7de461
--- /dev/null
+++ b/platform/www/inc/Form/FieldsetOpenElement.php
@@ -0,0 +1,36 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class FieldsetOpenElement
+ *
+ * Opens a Fieldset with an optional legend
+ *
+ * @package dokuwiki\Form
+ */
+class FieldsetOpenElement extends TagOpenElement {
+
+ /**
+ * @param string $legend
+ * @param array $attributes
+ */
+ public function __construct($legend='', $attributes = array()) {
+ // this is a bit messy and we just do it for the nicer class hierarchy
+ // the parent would expect the tag in $value but we're storing the
+ // legend there, so we have to set the type manually
+ parent::__construct($legend, $attributes);
+ $this->type = 'fieldsetopen';
+ }
+
+ /**
+ * The HTML representation of this element
+ *
+ * @return string
+ */
+ public function toHTML() {
+ $html = '<fieldset '.buildAttributes($this->attrs()).'>';
+ $legend = $this->val();
+ if($legend) $html .= DOKU_LF.'<legend>'.hsc($legend).'</legend>';
+ return $html;
+ }
+}
diff --git a/platform/www/inc/Form/Form.php b/platform/www/inc/Form/Form.php
new file mode 100644
index 0000000..c741a69
--- /dev/null
+++ b/platform/www/inc/Form/Form.php
@@ -0,0 +1,462 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class Form
+ *
+ * Represents the whole Form. This is what you work on, and add Elements to
+ *
+ * @package dokuwiki\Form
+ */
+class Form extends Element {
+
+ /**
+ * @var array name value pairs for hidden values
+ */
+ protected $hidden = array();
+
+ /**
+ * @var Element[] the elements of the form
+ */
+ protected $elements = array();
+
+ /**
+ * Creates a new, empty form with some default attributes
+ *
+ * @param array $attributes
+ * @param bool $unsafe if true, then the security token is ommited
+ */
+ public function __construct($attributes = array(), $unsafe = false) {
+ global $ID;
+
+ parent::__construct('form', $attributes);
+
+ // use the current URL as default action
+ if(!$this->attr('action')) {
+ $get = $_GET;
+ if(isset($get['id'])) unset($get['id']);
+ $self = wl($ID, $get, false, '&'); //attributes are escaped later
+ $this->attr('action', $self);
+ }
+
+ // post is default
+ if(!$this->attr('method')) {
+ $this->attr('method', 'post');
+ }
+
+ // we like UTF-8
+ if(!$this->attr('accept-charset')) {
+ $this->attr('accept-charset', 'utf-8');
+ }
+
+ // add the security token by default
+ if (!$unsafe) {
+ $this->setHiddenField('sectok', getSecurityToken());
+ }
+
+ // identify this as a new form based form in HTML
+ $this->addClass('doku_form');
+ }
+
+ /**
+ * Sets a hidden field
+ *
+ * @param string $name
+ * @param string $value
+ * @return $this
+ */
+ public function setHiddenField($name, $value) {
+ $this->hidden[$name] = $value;
+ return $this;
+ }
+
+ #region element query function
+
+ /**
+ * Returns the numbers of elements in the form
+ *
+ * @return int
+ */
+ public function elementCount() {
+ return count($this->elements);
+ }
+
+ /**
+ * Get the position of the element in the form or false if it is not in the form
+ *
+ * Warning: This function may return Boolean FALSE, but may also return a non-Boolean value which evaluates
+ * to FALSE. Please read the section on Booleans for more information. Use the === operator for testing the
+ * return value of this function.
+ *
+ * @param Element $element
+ *
+ * @return false|int
+ */
+ public function getElementPosition(Element $element)
+ {
+ return array_search($element, $this->elements, true);
+ }
+
+ /**
+ * Returns a reference to the element at a position.
+ * A position out-of-bounds will return either the
+ * first (underflow) or last (overflow) element.
+ *
+ * @param int $pos
+ * @return Element
+ */
+ public function getElementAt($pos) {
+ if($pos < 0) $pos = count($this->elements) + $pos;
+ if($pos < 0) $pos = 0;
+ if($pos >= count($this->elements)) $pos = count($this->elements) - 1;
+ return $this->elements[$pos];
+ }
+
+ /**
+ * Gets the position of the first of a type of element
+ *
+ * @param string $type Element type to look for.
+ * @param int $offset search from this position onward
+ * @return false|int position of element if found, otherwise false
+ */
+ public function findPositionByType($type, $offset = 0) {
+ $len = $this->elementCount();
+ for($pos = $offset; $pos < $len; $pos++) {
+ if($this->elements[$pos]->getType() == $type) {
+ return $pos;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets the position of the first element matching the attribute
+ *
+ * @param string $name Name of the attribute
+ * @param string $value Value the attribute should have
+ * @param int $offset search from this position onward
+ * @return false|int position of element if found, otherwise false
+ */
+ public function findPositionByAttribute($name, $value, $offset = 0) {
+ $len = $this->elementCount();
+ for($pos = $offset; $pos < $len; $pos++) {
+ if($this->elements[$pos]->attr($name) == $value) {
+ return $pos;
+ }
+ }
+ return false;
+ }
+
+ #endregion
+
+ #region Element positioning functions
+
+ /**
+ * Adds or inserts an element to the form
+ *
+ * @param Element $element
+ * @param int $pos 0-based position in the form, -1 for at the end
+ * @return Element
+ */
+ public function addElement(Element $element, $pos = -1) {
+ if(is_a($element, '\dokuwiki\Form\Form')) throw new \InvalidArgumentException(
+ 'You can\'t add a form to a form'
+ );
+ if($pos < 0) {
+ $this->elements[] = $element;
+ } else {
+ array_splice($this->elements, $pos, 0, array($element));
+ }
+ return $element;
+ }
+
+ /**
+ * Replaces an existing element with a new one
+ *
+ * @param Element $element the new element
+ * @param int $pos 0-based position of the element to replace
+ */
+ public function replaceElement(Element $element, $pos) {
+ if(is_a($element, '\dokuwiki\Form\Form')) throw new \InvalidArgumentException(
+ 'You can\'t add a form to a form'
+ );
+ array_splice($this->elements, $pos, 1, array($element));
+ }
+
+ /**
+ * Remove an element from the form completely
+ *
+ * @param int $pos 0-based position of the element to remove
+ */
+ public function removeElement($pos) {
+ array_splice($this->elements, $pos, 1);
+ }
+
+ #endregion
+
+ #region Element adding functions
+
+ /**
+ * Adds a text input field
+ *
+ * @param string $name
+ * @param string $label
+ * @param int $pos
+ * @return InputElement
+ */
+ public function addTextInput($name, $label = '', $pos = -1) {
+ return $this->addElement(new InputElement('text', $name, $label), $pos);
+ }
+
+ /**
+ * Adds a password input field
+ *
+ * @param string $name
+ * @param string $label
+ * @param int $pos
+ * @return InputElement
+ */
+ public function addPasswordInput($name, $label = '', $pos = -1) {
+ return $this->addElement(new InputElement('password', $name, $label), $pos);
+ }
+
+ /**
+ * Adds a radio button field
+ *
+ * @param string $name
+ * @param string $label
+ * @param int $pos
+ * @return CheckableElement
+ */
+ public function addRadioButton($name, $label = '', $pos = -1) {
+ return $this->addElement(new CheckableElement('radio', $name, $label), $pos);
+ }
+
+ /**
+ * Adds a checkbox field
+ *
+ * @param string $name
+ * @param string $label
+ * @param int $pos
+ * @return CheckableElement
+ */
+ public function addCheckbox($name, $label = '', $pos = -1) {
+ return $this->addElement(new CheckableElement('checkbox', $name, $label), $pos);
+ }
+
+ /**
+ * Adds a dropdown field
+ *
+ * @param string $name
+ * @param array $options
+ * @param string $label
+ * @param int $pos
+ * @return DropdownElement
+ */
+ public function addDropdown($name, $options, $label = '', $pos = -1) {
+ return $this->addElement(new DropdownElement($name, $options, $label), $pos);
+ }
+
+ /**
+ * Adds a textarea field
+ *
+ * @param string $name
+ * @param string $label
+ * @param int $pos
+ * @return TextareaElement
+ */
+ public function addTextarea($name, $label = '', $pos = -1) {
+ return $this->addElement(new TextareaElement($name, $label), $pos);
+ }
+
+ /**
+ * Adds a simple button, escapes the content for you
+ *
+ * @param string $name
+ * @param string $content
+ * @param int $pos
+ * @return Element
+ */
+ public function addButton($name, $content, $pos = -1) {
+ return $this->addElement(new ButtonElement($name, hsc($content)), $pos);
+ }
+
+ /**
+ * Adds a simple button, allows HTML for content
+ *
+ * @param string $name
+ * @param string $html
+ * @param int $pos
+ * @return Element
+ */
+ public function addButtonHTML($name, $html, $pos = -1) {
+ return $this->addElement(new ButtonElement($name, $html), $pos);
+ }
+
+ /**
+ * Adds a label referencing another input element, escapes the label for you
+ *
+ * @param string $label
+ * @param string $for
+ * @param int $pos
+ * @return Element
+ */
+ public function addLabel($label, $for='', $pos = -1) {
+ return $this->addLabelHTML(hsc($label), $for, $pos);
+ }
+
+ /**
+ * Adds a label referencing another input element, allows HTML for content
+ *
+ * @param string $content
+ * @param string|Element $for
+ * @param int $pos
+ * @return Element
+ */
+ public function addLabelHTML($content, $for='', $pos = -1) {
+ $element = new LabelElement(hsc($content));
+
+ if(is_a($for, '\dokuwiki\Form\Element')) {
+ /** @var Element $for */
+ $for = $for->id();
+ }
+ $for = (string) $for;
+ if($for !== '') {
+ $element->attr('for', $for);
+ }
+
+ return $this->addElement($element, $pos);
+ }
+
+ /**
+ * Add fixed HTML to the form
+ *
+ * @param string $html
+ * @param int $pos
+ * @return HTMLElement
+ */
+ public function addHTML($html, $pos = -1) {
+ return $this->addElement(new HTMLElement($html), $pos);
+ }
+
+ /**
+ * Add a closed HTML tag to the form
+ *
+ * @param string $tag
+ * @param int $pos
+ * @return TagElement
+ */
+ public function addTag($tag, $pos = -1) {
+ return $this->addElement(new TagElement($tag), $pos);
+ }
+
+ /**
+ * Add an open HTML tag to the form
+ *
+ * Be sure to close it again!
+ *
+ * @param string $tag
+ * @param int $pos
+ * @return TagOpenElement
+ */
+ public function addTagOpen($tag, $pos = -1) {
+ return $this->addElement(new TagOpenElement($tag), $pos);
+ }
+
+ /**
+ * Add a closing HTML tag to the form
+ *
+ * Be sure it had been opened before
+ *
+ * @param string $tag
+ * @param int $pos
+ * @return TagCloseElement
+ */
+ public function addTagClose($tag, $pos = -1) {
+ return $this->addElement(new TagCloseElement($tag), $pos);
+ }
+
+ /**
+ * Open a Fieldset
+ *
+ * @param string $legend
+ * @param int $pos
+ * @return FieldsetOpenElement
+ */
+ public function addFieldsetOpen($legend = '', $pos = -1) {
+ return $this->addElement(new FieldsetOpenElement($legend), $pos);
+ }
+
+ /**
+ * Close a fieldset
+ *
+ * @param int $pos
+ * @return TagCloseElement
+ */
+ public function addFieldsetClose($pos = -1) {
+ return $this->addElement(new FieldsetCloseElement(), $pos);
+ }
+
+ #endregion
+
+ /**
+ * Adjust the elements so that fieldset open and closes are matching
+ */
+ protected function balanceFieldsets() {
+ $lastclose = 0;
+ $isopen = false;
+ $len = count($this->elements);
+
+ for($pos = 0; $pos < $len; $pos++) {
+ $type = $this->elements[$pos]->getType();
+ if($type == 'fieldsetopen') {
+ if($isopen) {
+ //close previous fieldset
+ $this->addFieldsetClose($pos);
+ $lastclose = $pos + 1;
+ $pos++;
+ $len++;
+ }
+ $isopen = true;
+ } else if($type == 'fieldsetclose') {
+ if(!$isopen) {
+ // make sure there was a fieldsetopen
+ // either right after the last close or at the begining
+ $this->addFieldsetOpen('', $lastclose);
+ $len++;
+ $pos++;
+ }
+ $lastclose = $pos;
+ $isopen = false;
+ }
+ }
+
+ // close open fieldset at the end
+ if($isopen) {
+ $this->addFieldsetClose();
+ }
+ }
+
+ /**
+ * The HTML representation of the whole form
+ *
+ * @return string
+ */
+ public function toHTML() {
+ $this->balanceFieldsets();
+
+ $html = '<form ' . buildAttributes($this->attrs()) . '>';
+
+ foreach($this->hidden as $name => $value) {
+ $html .= '<input type="hidden" name="' . $name . '" value="' . formText($value) . '" />';
+ }
+
+ foreach($this->elements as $element) {
+ $html .= $element->toHTML();
+ }
+
+ $html .= '</form>';
+
+ return $html;
+ }
+}
diff --git a/platform/www/inc/Form/HTMLElement.php b/platform/www/inc/Form/HTMLElement.php
new file mode 100644
index 0000000..591cf47
--- /dev/null
+++ b/platform/www/inc/Form/HTMLElement.php
@@ -0,0 +1,29 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class HTMLElement
+ *
+ * Holds arbitrary HTML that is added as is to the Form
+ *
+ * @package dokuwiki\Form
+ */
+class HTMLElement extends ValueElement {
+
+
+ /**
+ * @param string $html
+ */
+ public function __construct($html) {
+ parent::__construct('html', $html);
+ }
+
+ /**
+ * The HTML representation of this element
+ *
+ * @return string
+ */
+ public function toHTML() {
+ return $this->val();
+ }
+}
diff --git a/platform/www/inc/Form/InputElement.php b/platform/www/inc/Form/InputElement.php
new file mode 100644
index 0000000..0242b61
--- /dev/null
+++ b/platform/www/inc/Form/InputElement.php
@@ -0,0 +1,159 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class InputElement
+ *
+ * Base class for all input elements. Uses a wrapping label when label
+ * text is given.
+ *
+ * @todo figure out how to make wrapping or related label configurable
+ * @package dokuwiki\Form
+ */
+class InputElement extends Element {
+ /**
+ * @var LabelElement
+ */
+ protected $label = null;
+
+ /**
+ * @var bool if the element should reflect posted values
+ */
+ protected $useInput = true;
+
+ /**
+ * @param string $type The type of this element
+ * @param string $name The name of this form element
+ * @param string $label The label text for this element (will be autoescaped)
+ */
+ public function __construct($type, $name, $label = '') {
+ parent::__construct($type, array('name' => $name));
+ $this->attr('name', $name);
+ $this->attr('type', $type);
+ if($label) $this->label = new LabelElement($label);
+ }
+
+ /**
+ * Returns the label element if there's one set
+ *
+ * @return LabelElement|null
+ */
+ public function getLabel() {
+ return $this->label;
+ }
+
+ /**
+ * Should the user sent input be used to initialize the input field
+ *
+ * The default is true. Any set values will be overwritten by the INPUT
+ * provided values.
+ *
+ * @param bool $useinput
+ * @return $this
+ */
+ public function useInput($useinput) {
+ $this->useInput = (bool) $useinput;
+ return $this;
+ }
+
+ /**
+ * Get or set the element's ID
+ *
+ * @param null|string $id
+ * @return string|$this
+ */
+ public function id($id = null) {
+ if($this->label) $this->label->attr('for', $id);
+ return parent::id($id);
+ }
+
+ /**
+ * Adds a class to the class attribute
+ *
+ * This is the preferred method of setting the element's class
+ *
+ * @param string $class the new class to add
+ * @return $this
+ */
+ public function addClass($class) {
+ if($this->label) $this->label->addClass($class);
+ return parent::addClass($class);
+ }
+
+ /**
+ * Figures out how to access the value for this field from INPUT data
+ *
+ * The element's name could have been given as a simple string ('foo')
+ * or in array notation ('foo[bar]').
+ *
+ * Note: this function only handles one level of arrays. If your data
+ * is nested deeper, you should call useInput(false) and set the
+ * correct value yourself
+ *
+ * @return array name and array key (null if not an array)
+ */
+ protected function getInputName() {
+ $name = $this->attr('name');
+ parse_str("$name=1", $parsed);
+
+ $name = array_keys($parsed);
+ $name = array_shift($name);
+
+ if(is_array($parsed[$name])) {
+ $key = array_keys($parsed[$name]);
+ $key = array_shift($key);
+ } else {
+ $key = null;
+ }
+
+ return array($name, $key);
+ }
+
+ /**
+ * Handles the useInput flag and set the value attribute accordingly
+ */
+ protected function prefillInput() {
+ global $INPUT;
+
+ list($name, $key) = $this->getInputName();
+ if(!$INPUT->has($name)) return;
+
+ if($key === null) {
+ $value = $INPUT->str($name);
+ } else {
+ $value = $INPUT->arr($name);
+ if(isset($value[$key])) {
+ $value = $value[$key];
+ } else {
+ $value = '';
+ }
+ }
+ $this->val($value);
+ }
+
+ /**
+ * The HTML representation of this element
+ *
+ * @return string
+ */
+ protected function mainElementHTML() {
+ if($this->useInput) $this->prefillInput();
+ return '<input ' . buildAttributes($this->attrs()) . ' />';
+ }
+
+ /**
+ * The HTML representation of this element wrapped in a label
+ *
+ * @return string
+ */
+ public function toHTML() {
+ if($this->label) {
+ return '<label ' . buildAttributes($this->label->attrs()) . '>' . DOKU_LF .
+ '<span>' . hsc($this->label->val()) . '</span>' . DOKU_LF .
+ $this->mainElementHTML() . DOKU_LF .
+ '</label>';
+ } else {
+ return $this->mainElementHTML();
+ }
+ }
+}
diff --git a/platform/www/inc/Form/LabelElement.php b/platform/www/inc/Form/LabelElement.php
new file mode 100644
index 0000000..9c8d542
--- /dev/null
+++ b/platform/www/inc/Form/LabelElement.php
@@ -0,0 +1,27 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class Label
+ * @package dokuwiki\Form
+ */
+class LabelElement extends ValueElement {
+
+ /**
+ * Creates a new Label
+ *
+ * @param string $label This is is raw HTML and will not be escaped
+ */
+ public function __construct($label) {
+ parent::__construct('label', $label);
+ }
+
+ /**
+ * The HTML representation of this element
+ *
+ * @return string
+ */
+ public function toHTML() {
+ return '<label ' . buildAttributes($this->attrs()) . '>' . $this->val() . '</label>';
+ }
+}
diff --git a/platform/www/inc/Form/LegacyForm.php b/platform/www/inc/Form/LegacyForm.php
new file mode 100644
index 0000000..b30c8df
--- /dev/null
+++ b/platform/www/inc/Form/LegacyForm.php
@@ -0,0 +1,181 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class LegacyForm
+ *
+ * Provides a compatibility layer to the old Doku_Form API
+ *
+ * This can be used to work with the modern API on forms provided by old events for
+ * example. When you start new forms, just use Form\Form
+ *
+ * @package dokuwiki\Form
+ */
+class LegacyForm extends Form {
+
+ /**
+ * Creates a new modern form from an old legacy Doku_Form
+ *
+ * @param \Doku_Form $oldform
+ */
+ public function __construct(\Doku_Form $oldform) {
+ parent::__construct($oldform->params);
+
+ $this->hidden = $oldform->_hidden;
+
+ foreach($oldform->_content as $element) {
+ list($ctl, $attr) = $this->parseLegacyAttr($element);
+
+ if(is_array($element)) {
+ switch($ctl['elem']) {
+ case 'wikitext':
+ $this->addTextarea('wikitext')
+ ->attrs($attr)
+ ->id('wiki__text')
+ ->val($ctl['text'])
+ ->addClass($ctl['class']);
+ break;
+ case 'textfield':
+ $this->addTextInput($ctl['name'], $ctl['text'])
+ ->attrs($attr)
+ ->id($ctl['id'])
+ ->addClass($ctl['class']);
+ break;
+ case 'passwordfield':
+ $this->addPasswordInput($ctl['name'], $ctl['text'])
+ ->attrs($attr)
+ ->id($ctl['id'])
+ ->addClass($ctl['class']);
+ break;
+ case 'checkboxfield':
+ $this->addCheckbox($ctl['name'], $ctl['text'])
+ ->attrs($attr)
+ ->id($ctl['id'])
+ ->addClass($ctl['class']);
+ break;
+ case 'radiofield':
+ $this->addRadioButton($ctl['name'], $ctl['text'])
+ ->attrs($attr)
+ ->id($ctl['id'])
+ ->addClass($ctl['class']);
+ break;
+ case 'tag':
+ $this->addTag($ctl['tag'])
+ ->attrs($attr)
+ ->attr('name', $ctl['name'])
+ ->id($ctl['id'])
+ ->addClass($ctl['class']);
+ break;
+ case 'opentag':
+ $this->addTagOpen($ctl['tag'])
+ ->attrs($attr)
+ ->attr('name', $ctl['name'])
+ ->id($ctl['id'])
+ ->addClass($ctl['class']);
+ break;
+ case 'closetag':
+ $this->addTagClose($ctl['tag']);
+ break;
+ case 'openfieldset':
+ $this->addFieldsetOpen($ctl['legend'])
+ ->attrs($attr)
+ ->attr('name', $ctl['name'])
+ ->id($ctl['id'])
+ ->addClass($ctl['class']);
+ break;
+ case 'closefieldset':
+ $this->addFieldsetClose();
+ break;
+ case 'button':
+ case 'field':
+ case 'fieldright':
+ case 'filefield':
+ case 'menufield':
+ case 'listboxfield':
+ throw new \UnexpectedValueException('Unsupported legacy field ' . $ctl['elem']);
+ break;
+ default:
+ throw new \UnexpectedValueException('Unknown legacy field ' . $ctl['elem']);
+
+ }
+ } else {
+ $this->addHTML($element);
+ }
+ }
+
+ }
+
+ /**
+ * Parses out what is the elements attributes and what is control info
+ *
+ * @param array $legacy
+ * @return array
+ */
+ protected function parseLegacyAttr($legacy) {
+ $attributes = array();
+ $control = array();
+
+ foreach($legacy as $key => $val) {
+ if($key[0] == '_') {
+ $control[substr($key, 1)] = $val;
+ } elseif($key == 'name') {
+ $control[$key] = $val;
+ } elseif($key == 'id') {
+ $control[$key] = $val;
+ } else {
+ $attributes[$key] = $val;
+ }
+ }
+
+ return array($control, $attributes);
+ }
+
+ /**
+ * Translates our types to the legacy types
+ *
+ * @param string $type
+ * @return string
+ */
+ protected function legacyType($type) {
+ static $types = array(
+ 'text' => 'textfield',
+ 'password' => 'passwordfield',
+ 'checkbox' => 'checkboxfield',
+ 'radio' => 'radiofield',
+ 'tagopen' => 'opentag',
+ 'tagclose' => 'closetag',
+ 'fieldsetopen' => 'openfieldset',
+ 'fieldsetclose' => 'closefieldset',
+ );
+ if(isset($types[$type])) return $types[$type];
+ return $type;
+ }
+
+ /**
+ * Creates an old legacy form from this modern form's data
+ *
+ * @return \Doku_Form
+ */
+ public function toLegacy() {
+ $this->balanceFieldsets();
+
+ $legacy = new \Doku_Form($this->attrs());
+ $legacy->_hidden = $this->hidden;
+ foreach($this->elements as $element) {
+ if(is_a($element, 'dokuwiki\Form\HTMLElement')) {
+ $legacy->_content[] = $element->toHTML();
+ } elseif(is_a($element, 'dokuwiki\Form\InputElement')) {
+ /** @var InputElement $element */
+ $data = $element->attrs();
+ $data['_elem'] = $this->legacyType($element->getType());
+ $label = $element->getLabel();
+ if($label) {
+ $data['_class'] = $label->attr('class');
+ }
+ $legacy->_content[] = $data;
+ }
+ }
+
+ return $legacy;
+ }
+}
diff --git a/platform/www/inc/Form/OptGroup.php b/platform/www/inc/Form/OptGroup.php
new file mode 100644
index 0000000..40149b1
--- /dev/null
+++ b/platform/www/inc/Form/OptGroup.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace dokuwiki\Form;
+
+
+class OptGroup extends Element {
+ protected $options = array();
+ protected $value;
+
+ /**
+ * @param string $label The label text for this element (will be autoescaped)
+ * @param array $options The available options
+ */
+ public function __construct($label, $options) {
+ parent::__construct('optGroup', array('label' => $label));
+ $this->options($options);
+ }
+
+ /**
+ * Store the given value so it can be used during rendering
+ *
+ * This is intended to be only called from within @see DropdownElement::val()
+ *
+ * @param string $value
+ * @return bool true if an option with the given value exists, false otherwise
+ */
+ public function storeValue($value) {
+ $this->value = $value;
+ return isset($this->options[$value]);
+ }
+
+ /**
+ * Get or set the options of the optgroup
+ *
+ * Options can be given as associative array (value => label) or as an
+ * indexd array (label = value) or as an array of arrays. In the latter
+ * case an element has to look as follows:
+ * option-value => array (
+ * 'label' => option-label,
+ * 'attrs' => array (
+ * attr-key => attr-value, ...
+ * )
+ * )
+ *
+ * @param null|array $options
+ * @return $this|array
+ */
+ public function options($options = null) {
+ if($options === null) return $this->options;
+ if(!is_array($options)) throw new \InvalidArgumentException('Options have to be an array');
+ $this->options = array();
+ foreach($options as $key => $val) {
+ if (is_array($val)) {
+ if (!key_exists('label', $val)) throw new \InvalidArgumentException(
+ 'If option is given as array, it has to have a "label"-key!'
+ );
+ if (key_exists('attrs', $val) && is_array($val['attrs']) && key_exists('selected', $val['attrs'])) {
+ throw new \InvalidArgumentException(
+ 'Please use function "DropdownElement::val()" to set the selected option'
+ );
+ }
+ $this->options[$key] = $val;
+ } elseif(is_int($key)) {
+ $this->options[$val] = array('label' => (string) $val);
+ } else {
+ $this->options[$key] = array('label' => (string) $val);
+ }
+ }
+ return $this;
+ }
+
+
+ /**
+ * The HTML representation of this element
+ *
+ * @return string
+ */
+ public function toHTML() {
+ if ($this->attributes['label'] === null) {
+ return $this->renderOptions();
+ }
+ $html = '<optgroup '. buildAttributes($this->attrs()) . '>';
+ $html .= $this->renderOptions();
+ $html .= '</optgroup>';
+ return $html;
+ }
+
+
+ /**
+ * @return string
+ */
+ protected function renderOptions() {
+ $html = '';
+ foreach($this->options as $key => $val) {
+ $selected = ((string)$key === (string)$this->value) ? ' selected="selected"' : '';
+ $attrs = '';
+ if (!empty($val['attrs']) && is_array($val['attrs'])) {
+ $attrs = buildAttributes($val['attrs']);
+ }
+ $html .= '<option' . $selected . ' value="' . hsc($key) . '" '.$attrs.'>';
+ $html .= hsc($val['label']);
+ $html .= '</option>';
+ }
+ return $html;
+ }
+}
diff --git a/platform/www/inc/Form/TagCloseElement.php b/platform/www/inc/Form/TagCloseElement.php
new file mode 100644
index 0000000..b6bf753
--- /dev/null
+++ b/platform/www/inc/Form/TagCloseElement.php
@@ -0,0 +1,88 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class TagCloseElement
+ *
+ * Creates an HTML close tag. You have to make sure it has been opened
+ * before or this will produce invalid HTML
+ *
+ * @package dokuwiki\Form
+ */
+class TagCloseElement extends ValueElement {
+
+ /**
+ * @param string $tag
+ * @param array $attributes
+ */
+ public function __construct($tag, $attributes = array()) {
+ parent::__construct('tagclose', $tag, $attributes);
+ }
+
+ /**
+ * do not call this
+ *
+ * @param string $class
+ * @return void
+ * @throws \BadMethodCallException
+ */
+ public function addClass($class) {
+ throw new \BadMethodCallException('You can\t add classes to closing tag');
+ }
+
+ /**
+ * do not call this
+ *
+ * @param null|string $id
+ * @return string
+ * @throws \BadMethodCallException
+ */
+ public function id($id = null) {
+ if ($id === null) {
+ return '';
+ } else {
+ throw new \BadMethodCallException('You can\t add ID to closing tag');
+ }
+ }
+
+ /**
+ * do not call this
+ *
+ * @param string $name
+ * @param null|string $value
+ * @return string
+ * @throws \BadMethodCallException
+ */
+ public function attr($name, $value = null) {
+ if ($value === null) {
+ return '';
+ } else {
+ throw new \BadMethodCallException('You can\t add attributes to closing tag');
+ }
+ }
+
+ /**
+ * do not call this
+ *
+ * @param array|null $attributes
+ * @return array
+ * @throws \BadMethodCallException
+ */
+ public function attrs($attributes = null) {
+ if ($attributes === null) {
+ return array();
+ } else {
+ throw new \BadMethodCallException('You can\t add attributes to closing tag');
+ }
+ }
+
+ /**
+ * The HTML representation of this element
+ *
+ * @return string
+ */
+ public function toHTML() {
+ return '</'.$this->val().'>';
+ }
+
+}
diff --git a/platform/www/inc/Form/TagElement.php b/platform/www/inc/Form/TagElement.php
new file mode 100644
index 0000000..ea5144c
--- /dev/null
+++ b/platform/www/inc/Form/TagElement.php
@@ -0,0 +1,29 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class TagElement
+ *
+ * Creates a self closing HTML tag
+ *
+ * @package dokuwiki\Form
+ */
+class TagElement extends ValueElement {
+
+ /**
+ * @param string $tag
+ * @param array $attributes
+ */
+ public function __construct($tag, $attributes = array()) {
+ parent::__construct('tag', $tag, $attributes);
+ }
+
+ /**
+ * The HTML representation of this element
+ *
+ * @return string
+ */
+ public function toHTML() {
+ return '<'.$this->val().' '.buildAttributes($this->attrs()).' />';
+ }
+}
diff --git a/platform/www/inc/Form/TagOpenElement.php b/platform/www/inc/Form/TagOpenElement.php
new file mode 100644
index 0000000..0afe97b
--- /dev/null
+++ b/platform/www/inc/Form/TagOpenElement.php
@@ -0,0 +1,30 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class TagOpenElement
+ *
+ * Creates an open HTML tag. You have to make sure you close it
+ * again or this will produce invalid HTML
+ *
+ * @package dokuwiki\Form
+ */
+class TagOpenElement extends ValueElement {
+
+ /**
+ * @param string $tag
+ * @param array $attributes
+ */
+ public function __construct($tag, $attributes = array()) {
+ parent::__construct('tagopen', $tag, $attributes);
+ }
+
+ /**
+ * The HTML representation of this element
+ *
+ * @return string
+ */
+ public function toHTML() {
+ return '<'.$this->val().' '.buildAttributes($this->attrs()).'>';
+ }
+}
diff --git a/platform/www/inc/Form/TextareaElement.php b/platform/www/inc/Form/TextareaElement.php
new file mode 100644
index 0000000..92741ee
--- /dev/null
+++ b/platform/www/inc/Form/TextareaElement.php
@@ -0,0 +1,51 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class TextareaElement
+ * @package dokuwiki\Form
+ */
+class TextareaElement extends InputElement {
+
+ /**
+ * @var string the actual text within the area
+ */
+ protected $text;
+
+ /**
+ * @param string $name The name of this form element
+ * @param string $label The label text for this element
+ */
+ public function __construct($name, $label) {
+ parent::__construct('textarea', $name, $label);
+ $this->attr('dir', 'auto');
+ }
+
+ /**
+ * Get or set the element's value
+ *
+ * This is the preferred way of setting the element's value
+ *
+ * @param null|string $value
+ * @return string|$this
+ */
+ public function val($value = null) {
+ if($value !== null) {
+ $this->text = cleanText($value);
+ return $this;
+ }
+ return $this->text;
+ }
+
+ /**
+ * The HTML representation of this element
+ *
+ * @return string
+ */
+ protected function mainElementHTML() {
+ if($this->useInput) $this->prefillInput();
+ return '<textarea ' . buildAttributes($this->attrs()) . '>' .
+ formText($this->val()) . '</textarea>';
+ }
+
+}
diff --git a/platform/www/inc/Form/ValueElement.php b/platform/www/inc/Form/ValueElement.php
new file mode 100644
index 0000000..88db167
--- /dev/null
+++ b/platform/www/inc/Form/ValueElement.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace dokuwiki\Form;
+
+/**
+ * Class ValueElement
+ *
+ * Just like an Element but it's value is not part of its attributes
+ *
+ * What the value is (tag name, content, etc) is defined by the actual implementations
+ *
+ * @package dokuwiki\Form
+ */
+abstract class ValueElement extends Element {
+
+ /**
+ * @var string holds the element's value
+ */
+ protected $value = '';
+
+ /**
+ * @param string $type
+ * @param string $value
+ * @param array $attributes
+ */
+ public function __construct($type, $value, $attributes = array()) {
+ parent::__construct($type, $attributes);
+ $this->val($value);
+ }
+
+ /**
+ * Get or set the element's value
+ *
+ * @param null|string $value
+ * @return string|$this
+ */
+ public function val($value = null) {
+ if($value !== null) {
+ $this->value = $value;
+ return $this;
+ }
+ return $this->value;
+ }
+
+}
diff --git a/platform/www/inc/HTTP/DokuHTTPClient.php b/platform/www/inc/HTTP/DokuHTTPClient.php
new file mode 100644
index 0000000..b1db7e3
--- /dev/null
+++ b/platform/www/inc/HTTP/DokuHTTPClient.php
@@ -0,0 +1,77 @@
+<?php
+
+
+namespace dokuwiki\HTTP;
+
+
+
+/**
+ * Adds DokuWiki specific configs to the HTTP client
+ *
+ * @author Andreas Goetz <cpuidle@gmx.de>
+ */
+class DokuHTTPClient extends HTTPClient {
+
+ /**
+ * Constructor.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function __construct(){
+ global $conf;
+
+ // call parent constructor
+ parent::__construct();
+
+ // set some values from the config
+ $this->proxy_host = $conf['proxy']['host'];
+ $this->proxy_port = $conf['proxy']['port'];
+ $this->proxy_user = $conf['proxy']['user'];
+ $this->proxy_pass = conf_decodeString($conf['proxy']['pass']);
+ $this->proxy_ssl = $conf['proxy']['ssl'];
+ $this->proxy_except = $conf['proxy']['except'];
+
+ // allow enabling debugging via URL parameter (if debugging allowed)
+ if($conf['allowdebug']) {
+ if(
+ isset($_REQUEST['httpdebug']) ||
+ (
+ isset($_SERVER['HTTP_REFERER']) &&
+ strpos($_SERVER['HTTP_REFERER'], 'httpdebug') !== false
+ )
+ ) {
+ $this->debug = true;
+ }
+ }
+ }
+
+
+ /**
+ * Wraps an event around the parent function
+ *
+ * @triggers HTTPCLIENT_REQUEST_SEND
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ /**
+ * @param string $url
+ * @param string|array $data the post data either as array or raw data
+ * @param string $method
+ * @return bool
+ */
+ public function sendRequest($url,$data='',$method='GET'){
+ $httpdata = array('url' => $url,
+ 'data' => $data,
+ 'method' => $method);
+ $evt = new \Doku_Event('HTTPCLIENT_REQUEST_SEND',$httpdata);
+ if($evt->advise_before()){
+ $url = $httpdata['url'];
+ $data = $httpdata['data'];
+ $method = $httpdata['method'];
+ }
+ $evt->advise_after();
+ unset($evt);
+ return parent::sendRequest($url,$data,$method);
+ }
+
+}
+
diff --git a/platform/www/inc/HTTP/HTTPClient.php b/platform/www/inc/HTTP/HTTPClient.php
new file mode 100644
index 0000000..4aaf471
--- /dev/null
+++ b/platform/www/inc/HTTP/HTTPClient.php
@@ -0,0 +1,885 @@
+<?php
+
+namespace dokuwiki\HTTP;
+
+define('HTTP_NL',"\r\n");
+
+
+/**
+ * This class implements a basic HTTP client
+ *
+ * It supports POST and GET, Proxy usage, basic authentication,
+ * handles cookies and referers. It is based upon the httpclient
+ * function from the VideoDB project.
+ *
+ * @link http://www.splitbrain.org/go/videodb
+ * @author Andreas Goetz <cpuidle@gmx.de>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Tobias Sarnowski <sarnowski@new-thoughts.org>
+ */
+class HTTPClient {
+ //set these if you like
+ public $agent; // User agent
+ public $http; // HTTP version defaults to 1.0
+ public $timeout; // read timeout (seconds)
+ public $cookies;
+ public $referer;
+ public $max_redirect;
+ public $max_bodysize;
+ public $max_bodysize_abort = true; // if set, abort if the response body is bigger than max_bodysize
+ public $header_regexp; // if set this RE must match against the headers, else abort
+ public $headers;
+ public $debug;
+ public $start = 0.0; // for timings
+ public $keep_alive = true; // keep alive rocks
+
+ // don't set these, read on error
+ public $error;
+ public $redirect_count;
+
+ // read these after a successful request
+ public $status;
+ public $resp_body;
+ public $resp_headers;
+
+ // set these to do basic authentication
+ public $user;
+ public $pass;
+
+ // set these if you need to use a proxy
+ public $proxy_host;
+ public $proxy_port;
+ public $proxy_user;
+ public $proxy_pass;
+ public $proxy_ssl; //boolean set to true if your proxy needs SSL
+ public $proxy_except; // regexp of URLs to exclude from proxy
+
+ // list of kept alive connections
+ protected static $connections = array();
+
+ // what we use as boundary on multipart/form-data posts
+ protected $boundary = '---DokuWikiHTTPClient--4523452351';
+
+ /**
+ * Constructor.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function __construct(){
+ $this->agent = 'Mozilla/4.0 (compatible; DokuWiki HTTP Client; '.PHP_OS.')';
+ $this->timeout = 15;
+ $this->cookies = array();
+ $this->referer = '';
+ $this->max_redirect = 3;
+ $this->redirect_count = 0;
+ $this->status = 0;
+ $this->headers = array();
+ $this->http = '1.0';
+ $this->debug = false;
+ $this->max_bodysize = 0;
+ $this->header_regexp= '';
+ if(extension_loaded('zlib')) $this->headers['Accept-encoding'] = 'gzip';
+ $this->headers['Accept'] = 'text/xml,application/xml,application/xhtml+xml,'.
+ 'text/html,text/plain,image/png,image/jpeg,image/gif,*/*';
+ $this->headers['Accept-Language'] = 'en-us';
+ }
+
+
+ /**
+ * Simple function to do a GET request
+ *
+ * Returns the wanted page or false on an error;
+ *
+ * @param string $url The URL to fetch
+ * @param bool $sloppy304 Return body on 304 not modified
+ * @return false|string response body, false on error
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function get($url,$sloppy304=false){
+ if(!$this->sendRequest($url)) return false;
+ if($this->status == 304 && $sloppy304) return $this->resp_body;
+ if($this->status < 200 || $this->status > 206) return false;
+ return $this->resp_body;
+ }
+
+ /**
+ * Simple function to do a GET request with given parameters
+ *
+ * Returns the wanted page or false on an error.
+ *
+ * This is a convenience wrapper around get(). The given parameters
+ * will be correctly encoded and added to the given base URL.
+ *
+ * @param string $url The URL to fetch
+ * @param array $data Associative array of parameters
+ * @param bool $sloppy304 Return body on 304 not modified
+ * @return false|string response body, false on error
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function dget($url,$data,$sloppy304=false){
+ if(strpos($url,'?')){
+ $url .= '&';
+ }else{
+ $url .= '?';
+ }
+ $url .= $this->postEncode($data);
+ return $this->get($url,$sloppy304);
+ }
+
+ /**
+ * Simple function to do a POST request
+ *
+ * Returns the resulting page or false on an error;
+ *
+ * @param string $url The URL to fetch
+ * @param array $data Associative array of parameters
+ * @return false|string response body, false on error
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function post($url,$data){
+ if(!$this->sendRequest($url,$data,'POST')) return false;
+ if($this->status < 200 || $this->status > 206) return false;
+ return $this->resp_body;
+ }
+
+ /**
+ * Send an HTTP request
+ *
+ * This method handles the whole HTTP communication. It respects set proxy settings,
+ * builds the request headers, follows redirects and parses the response.
+ *
+ * Post data should be passed as associative array. When passed as string it will be
+ * sent as is. You will need to setup your own Content-Type header then.
+ *
+ * @param string $url - the complete URL
+ * @param mixed $data - the post data either as array or raw data
+ * @param string $method - HTTP Method usually GET or POST.
+ * @return bool - true on success
+ *
+ * @author Andreas Goetz <cpuidle@gmx.de>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function sendRequest($url,$data='',$method='GET'){
+ $this->start = $this->time();
+ $this->error = '';
+ $this->status = 0;
+ $this->resp_body = '';
+ $this->resp_headers = array();
+
+ // don't accept gzip if truncated bodies might occur
+ if($this->max_bodysize &&
+ !$this->max_bodysize_abort &&
+ $this->headers['Accept-encoding'] == 'gzip'){
+ unset($this->headers['Accept-encoding']);
+ }
+
+ // parse URL into bits
+ $uri = parse_url($url);
+ $server = $uri['host'];
+ $path = $uri['path'];
+ if(empty($path)) $path = '/';
+ if(!empty($uri['query'])) $path .= '?'.$uri['query'];
+ if(!empty($uri['port'])) $port = $uri['port'];
+ if(isset($uri['user'])) $this->user = $uri['user'];
+ if(isset($uri['pass'])) $this->pass = $uri['pass'];
+
+ // proxy setup
+ if($this->useProxyForUrl($url)){
+ $request_url = $url;
+ $server = $this->proxy_host;
+ $port = $this->proxy_port;
+ if (empty($port)) $port = 8080;
+ $use_tls = $this->proxy_ssl;
+ }else{
+ $request_url = $path;
+ if (!isset($port)) $port = ($uri['scheme'] == 'https') ? 443 : 80;
+ $use_tls = ($uri['scheme'] == 'https');
+ }
+
+ // add SSL stream prefix if needed - needs SSL support in PHP
+ if($use_tls) {
+ if(!in_array('ssl', stream_get_transports())) {
+ $this->status = -200;
+ $this->error = 'This PHP version does not support SSL - cannot connect to server';
+ }
+ $server = 'ssl://'.$server;
+ }
+
+ // prepare headers
+ $headers = $this->headers;
+ $headers['Host'] = $uri['host'];
+ if(!empty($uri['port'])) $headers['Host'].= ':'.$uri['port'];
+ $headers['User-Agent'] = $this->agent;
+ $headers['Referer'] = $this->referer;
+
+ if($method == 'POST'){
+ if(is_array($data)){
+ if (empty($headers['Content-Type'])) {
+ $headers['Content-Type'] = null;
+ }
+ switch ($headers['Content-Type']) {
+ case 'multipart/form-data':
+ $headers['Content-Type'] = 'multipart/form-data; boundary=' . $this->boundary;
+ $data = $this->postMultipartEncode($data);
+ break;
+ default:
+ $headers['Content-Type'] = 'application/x-www-form-urlencoded';
+ $data = $this->postEncode($data);
+ }
+ }
+ }elseif($method == 'GET'){
+ $data = ''; //no data allowed on GET requests
+ }
+
+ $contentlength = strlen($data);
+ if($contentlength) {
+ $headers['Content-Length'] = $contentlength;
+ }
+
+ if($this->user) {
+ $headers['Authorization'] = 'Basic '.base64_encode($this->user.':'.$this->pass);
+ }
+ if($this->proxy_user) {
+ $headers['Proxy-Authorization'] = 'Basic '.base64_encode($this->proxy_user.':'.$this->proxy_pass);
+ }
+
+ // already connected?
+ $connectionId = $this->uniqueConnectionId($server,$port);
+ $this->debug('connection pool', self::$connections);
+ $socket = null;
+ if (isset(self::$connections[$connectionId])) {
+ $this->debug('reusing connection', $connectionId);
+ $socket = self::$connections[$connectionId];
+ }
+ if (is_null($socket) || feof($socket)) {
+ $this->debug('opening connection', $connectionId);
+ // open socket
+ $socket = @fsockopen($server,$port,$errno, $errstr, $this->timeout);
+ if (!$socket){
+ $this->status = -100;
+ $this->error = "Could not connect to $server:$port\n$errstr ($errno)";
+ return false;
+ }
+
+ // try establish a CONNECT tunnel for SSL
+ try {
+ if($this->ssltunnel($socket, $request_url)){
+ // no keep alive for tunnels
+ $this->keep_alive = false;
+ // tunnel is authed already
+ if(isset($headers['Proxy-Authentication'])) unset($headers['Proxy-Authentication']);
+ }
+ } catch (HTTPClientException $e) {
+ $this->status = $e->getCode();
+ $this->error = $e->getMessage();
+ fclose($socket);
+ return false;
+ }
+
+ // keep alive?
+ if ($this->keep_alive) {
+ self::$connections[$connectionId] = $socket;
+ } else {
+ unset(self::$connections[$connectionId]);
+ }
+ }
+
+ if ($this->keep_alive && !$this->useProxyForUrl($request_url)) {
+ // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive
+ // connection token to a proxy server. We still do keep the connection the
+ // proxy alive (well except for CONNECT tunnels)
+ $headers['Connection'] = 'Keep-Alive';
+ } else {
+ $headers['Connection'] = 'Close';
+ }
+
+ try {
+ //set non-blocking
+ stream_set_blocking($socket, 0);
+
+ // build request
+ $request = "$method $request_url HTTP/".$this->http.HTTP_NL;
+ $request .= $this->buildHeaders($headers);
+ $request .= $this->getCookies();
+ $request .= HTTP_NL;
+ $request .= $data;
+
+ $this->debug('request',$request);
+ $this->sendData($socket, $request, 'request');
+
+ // read headers from socket
+ $r_headers = '';
+ do{
+ $r_line = $this->readLine($socket, 'headers');
+ $r_headers .= $r_line;
+ }while($r_line != "\r\n" && $r_line != "\n");
+
+ $this->debug('response headers',$r_headers);
+
+ // check if expected body size exceeds allowance
+ if($this->max_bodysize && preg_match('/\r?\nContent-Length:\s*(\d+)\r?\n/i',$r_headers,$match)){
+ if($match[1] > $this->max_bodysize){
+ if ($this->max_bodysize_abort)
+ throw new HTTPClientException('Reported content length exceeds allowed response size');
+ else
+ $this->error = 'Reported content length exceeds allowed response size';
+ }
+ }
+
+ // get Status
+ if (!preg_match('/^HTTP\/(\d\.\d)\s*(\d+).*?\n/s', $r_headers, $m))
+ throw new HTTPClientException('Server returned bad answer '.$r_headers);
+
+ $this->status = $m[2];
+
+ // handle headers and cookies
+ $this->resp_headers = $this->parseHeaders($r_headers);
+ if(isset($this->resp_headers['set-cookie'])){
+ foreach ((array) $this->resp_headers['set-cookie'] as $cookie){
+ list($cookie) = explode(';',$cookie,2);
+ list($key,$val) = explode('=',$cookie,2);
+ $key = trim($key);
+ if($val == 'deleted'){
+ if(isset($this->cookies[$key])){
+ unset($this->cookies[$key]);
+ }
+ }elseif($key){
+ $this->cookies[$key] = $val;
+ }
+ }
+ }
+
+ $this->debug('Object headers',$this->resp_headers);
+
+ // check server status code to follow redirect
+ if($this->status == 301 || $this->status == 302 ){
+ if (empty($this->resp_headers['location'])){
+ throw new HTTPClientException('Redirect but no Location Header found');
+ }elseif($this->redirect_count == $this->max_redirect){
+ throw new HTTPClientException('Maximum number of redirects exceeded');
+ }else{
+ // close the connection because we don't handle content retrieval here
+ // that's the easiest way to clean up the connection
+ fclose($socket);
+ unset(self::$connections[$connectionId]);
+
+ $this->redirect_count++;
+ $this->referer = $url;
+ // handle non-RFC-compliant relative redirects
+ if (!preg_match('/^http/i', $this->resp_headers['location'])){
+ if($this->resp_headers['location'][0] != '/'){
+ $this->resp_headers['location'] = $uri['scheme'].'://'.$uri['host'].':'.$uri['port'].
+ dirname($uri['path']).'/'.$this->resp_headers['location'];
+ }else{
+ $this->resp_headers['location'] = $uri['scheme'].'://'.$uri['host'].':'.$uri['port'].
+ $this->resp_headers['location'];
+ }
+ }
+ // perform redirected request, always via GET (required by RFC)
+ return $this->sendRequest($this->resp_headers['location'],array(),'GET');
+ }
+ }
+
+ // check if headers are as expected
+ if($this->header_regexp && !preg_match($this->header_regexp,$r_headers))
+ throw new HTTPClientException('The received headers did not match the given regexp');
+
+ //read body (with chunked encoding if needed)
+ $r_body = '';
+ if(
+ (
+ isset($this->resp_headers['transfer-encoding']) &&
+ $this->resp_headers['transfer-encoding'] == 'chunked'
+ ) || (
+ isset($this->resp_headers['transfer-coding']) &&
+ $this->resp_headers['transfer-coding'] == 'chunked'
+ )
+ ) {
+ $abort = false;
+ do {
+ $chunk_size = '';
+ while (preg_match('/^[a-zA-Z0-9]?$/',$byte=$this->readData($socket,1,'chunk'))){
+ // read chunksize until \r
+ $chunk_size .= $byte;
+ if (strlen($chunk_size) > 128) // set an abritrary limit on the size of chunks
+ throw new HTTPClientException('Allowed response size exceeded');
+ }
+ $this->readLine($socket, 'chunk'); // readtrailing \n
+ $chunk_size = hexdec($chunk_size);
+
+ if($this->max_bodysize && $chunk_size+strlen($r_body) > $this->max_bodysize){
+ if ($this->max_bodysize_abort)
+ throw new HTTPClientException('Allowed response size exceeded');
+ $this->error = 'Allowed response size exceeded';
+ $chunk_size = $this->max_bodysize - strlen($r_body);
+ $abort = true;
+ }
+
+ if ($chunk_size > 0) {
+ $r_body .= $this->readData($socket, $chunk_size, 'chunk');
+ $this->readData($socket, 2, 'chunk'); // read trailing \r\n
+ }
+ } while ($chunk_size && !$abort);
+ }elseif(isset($this->resp_headers['content-length']) && !isset($this->resp_headers['transfer-encoding'])){
+ /* RFC 2616
+ * If a message is received with both a Transfer-Encoding header field and a Content-Length
+ * header field, the latter MUST be ignored.
+ */
+
+ // read up to the content-length or max_bodysize
+ // for keep alive we need to read the whole message to clean up the socket for the next read
+ if(
+ !$this->keep_alive &&
+ $this->max_bodysize &&
+ $this->max_bodysize < $this->resp_headers['content-length']
+ ) {
+ $length = $this->max_bodysize + 1;
+ }else{
+ $length = $this->resp_headers['content-length'];
+ }
+
+ $r_body = $this->readData($socket, $length, 'response (content-length limited)', true);
+ }elseif( !isset($this->resp_headers['transfer-encoding']) && $this->max_bodysize && !$this->keep_alive){
+ $r_body = $this->readData($socket, $this->max_bodysize+1, 'response (content-length limited)', true);
+ } elseif ((int)$this->status === 204) {
+ // request has no content
+ } else{
+ // read entire socket
+ while (!feof($socket)) {
+ $r_body .= $this->readData($socket, 4096, 'response (unlimited)', true);
+ }
+ }
+
+ // recheck body size, we might have read max_bodysize+1 or even the whole body, so we abort late here
+ if($this->max_bodysize){
+ if(strlen($r_body) > $this->max_bodysize){
+ if ($this->max_bodysize_abort) {
+ throw new HTTPClientException('Allowed response size exceeded');
+ } else {
+ $this->error = 'Allowed response size exceeded';
+ }
+ }
+ }
+
+ } catch (HTTPClientException $err) {
+ $this->error = $err->getMessage();
+ if ($err->getCode())
+ $this->status = $err->getCode();
+ unset(self::$connections[$connectionId]);
+ fclose($socket);
+ return false;
+ }
+
+ if (!$this->keep_alive ||
+ (isset($this->resp_headers['connection']) && $this->resp_headers['connection'] == 'Close')) {
+ // close socket
+ fclose($socket);
+ unset(self::$connections[$connectionId]);
+ }
+
+ // decode gzip if needed
+ if(isset($this->resp_headers['content-encoding']) &&
+ $this->resp_headers['content-encoding'] == 'gzip' &&
+ strlen($r_body) > 10 && substr($r_body,0,3)=="\x1f\x8b\x08"){
+ $this->resp_body = @gzinflate(substr($r_body, 10));
+ if($this->resp_body === false){
+ $this->error = 'Failed to decompress gzip encoded content';
+ $this->resp_body = $r_body;
+ }
+ }else{
+ $this->resp_body = $r_body;
+ }
+
+ $this->debug('response body',$this->resp_body);
+ $this->redirect_count = 0;
+ return true;
+ }
+
+ /**
+ * Tries to establish a CONNECT tunnel via Proxy
+ *
+ * Protocol, Servername and Port will be stripped from the request URL when a successful CONNECT happened
+ *
+ * @param resource &$socket
+ * @param string &$requesturl
+ * @throws HTTPClientException when a tunnel is needed but could not be established
+ * @return bool true if a tunnel was established
+ */
+ protected function ssltunnel(&$socket, &$requesturl){
+ if(!$this->useProxyForUrl($requesturl)) return false;
+ $requestinfo = parse_url($requesturl);
+ if($requestinfo['scheme'] != 'https') return false;
+ if(!$requestinfo['port']) $requestinfo['port'] = 443;
+
+ // build request
+ $request = "CONNECT {$requestinfo['host']}:{$requestinfo['port']} HTTP/1.0".HTTP_NL;
+ $request .= "Host: {$requestinfo['host']}".HTTP_NL;
+ if($this->proxy_user) {
+ $request .= 'Proxy-Authorization: Basic '.base64_encode($this->proxy_user.':'.$this->proxy_pass).HTTP_NL;
+ }
+ $request .= HTTP_NL;
+
+ $this->debug('SSL Tunnel CONNECT',$request);
+ $this->sendData($socket, $request, 'SSL Tunnel CONNECT');
+
+ // read headers from socket
+ $r_headers = '';
+ do{
+ $r_line = $this->readLine($socket, 'headers');
+ $r_headers .= $r_line;
+ }while($r_line != "\r\n" && $r_line != "\n");
+
+ $this->debug('SSL Tunnel Response',$r_headers);
+ if(preg_match('/^HTTP\/1\.[01] 200/i',$r_headers)){
+ // set correct peer name for verification (enabled since PHP 5.6)
+ stream_context_set_option($socket, 'ssl', 'peer_name', $requestinfo['host']);
+
+ // SSLv3 is broken, use only TLS connections.
+ // @link https://bugs.php.net/69195
+ if (PHP_VERSION_ID >= 50600 && PHP_VERSION_ID <= 50606) {
+ $cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT;
+ } else {
+ // actually means neither SSLv2 nor SSLv3
+ $cryptoMethod = STREAM_CRYPTO_METHOD_SSLv23_CLIENT;
+ }
+
+ if (@stream_socket_enable_crypto($socket, true, $cryptoMethod)) {
+ $requesturl = $requestinfo['path'].
+ (!empty($requestinfo['query'])?'?'.$requestinfo['query']:'');
+ return true;
+ }
+
+ throw new HTTPClientException(
+ 'Failed to set up crypto for secure connection to '.$requestinfo['host'], -151
+ );
+ }
+
+ throw new HTTPClientException('Failed to establish secure proxy connection', -150);
+ }
+
+ /**
+ * Safely write data to a socket
+ *
+ * @param resource $socket An open socket handle
+ * @param string $data The data to write
+ * @param string $message Description of what is being read
+ * @throws HTTPClientException
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ protected function sendData($socket, $data, $message) {
+ // send request
+ $towrite = strlen($data);
+ $written = 0;
+ while($written < $towrite){
+ // check timeout
+ $time_used = $this->time() - $this->start;
+ if($time_used > $this->timeout)
+ throw new HTTPClientException(sprintf('Timeout while sending %s (%.3fs)',$message, $time_used), -100);
+ if(feof($socket))
+ throw new HTTPClientException("Socket disconnected while writing $message");
+
+ // select parameters
+ $sel_r = null;
+ $sel_w = array($socket);
+ $sel_e = null;
+ // wait for stream ready or timeout (1sec)
+ if(@stream_select($sel_r,$sel_w,$sel_e,1) === false){
+ usleep(1000);
+ continue;
+ }
+
+ // write to stream
+ $nbytes = fwrite($socket, substr($data,$written,4096));
+ if($nbytes === false)
+ throw new HTTPClientException("Failed writing to socket while sending $message", -100);
+ $written += $nbytes;
+ }
+ }
+
+ /**
+ * Safely read data from a socket
+ *
+ * Reads up to a given number of bytes or throws an exception if the
+ * response times out or ends prematurely.
+ *
+ * @param resource $socket An open socket handle in non-blocking mode
+ * @param int $nbytes Number of bytes to read
+ * @param string $message Description of what is being read
+ * @param bool $ignore_eof End-of-file is not an error if this is set
+ * @throws HTTPClientException
+ * @return string
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ protected function readData($socket, $nbytes, $message, $ignore_eof = false) {
+ $r_data = '';
+ // Does not return immediately so timeout and eof can be checked
+ if ($nbytes < 0) $nbytes = 0;
+ $to_read = $nbytes;
+ do {
+ $time_used = $this->time() - $this->start;
+ if ($time_used > $this->timeout)
+ throw new HTTPClientException(
+ sprintf('Timeout while reading %s after %d bytes (%.3fs)', $message,
+ strlen($r_data), $time_used), -100);
+ if(feof($socket)) {
+ if(!$ignore_eof)
+ throw new HTTPClientException("Premature End of File (socket) while reading $message");
+ break;
+ }
+
+ if ($to_read > 0) {
+ // select parameters
+ $sel_r = array($socket);
+ $sel_w = null;
+ $sel_e = null;
+ // wait for stream ready or timeout (1sec)
+ if(@stream_select($sel_r,$sel_w,$sel_e,1) === false){
+ usleep(1000);
+ continue;
+ }
+
+ $bytes = fread($socket, $to_read);
+ if($bytes === false)
+ throw new HTTPClientException("Failed reading from socket while reading $message", -100);
+ $r_data .= $bytes;
+ $to_read -= strlen($bytes);
+ }
+ } while ($to_read > 0 && strlen($r_data) < $nbytes);
+ return $r_data;
+ }
+
+ /**
+ * Safely read a \n-terminated line from a socket
+ *
+ * Always returns a complete line, including the terminating \n.
+ *
+ * @param resource $socket An open socket handle in non-blocking mode
+ * @param string $message Description of what is being read
+ * @throws HTTPClientException
+ * @return string
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ protected function readLine($socket, $message) {
+ $r_data = '';
+ do {
+ $time_used = $this->time() - $this->start;
+ if ($time_used > $this->timeout)
+ throw new HTTPClientException(
+ sprintf('Timeout while reading %s (%.3fs) >%s<', $message, $time_used, $r_data),
+ -100);
+ if(feof($socket))
+ throw new HTTPClientException("Premature End of File (socket) while reading $message");
+
+ // select parameters
+ $sel_r = array($socket);
+ $sel_w = null;
+ $sel_e = null;
+ // wait for stream ready or timeout (1sec)
+ if(@stream_select($sel_r,$sel_w,$sel_e,1) === false){
+ usleep(1000);
+ continue;
+ }
+
+ $r_data = fgets($socket, 1024);
+ } while (!preg_match('/\n$/',$r_data));
+ return $r_data;
+ }
+
+ /**
+ * print debug info
+ *
+ * Uses _debug_text or _debug_html depending on the SAPI name
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $info
+ * @param mixed $var
+ */
+ protected function debug($info,$var=null){
+ if(!$this->debug) return;
+ if(php_sapi_name() == 'cli'){
+ $this->debugText($info, $var);
+ }else{
+ $this->debugHtml($info, $var);
+ }
+ }
+
+ /**
+ * print debug info as HTML
+ *
+ * @param string $info
+ * @param mixed $var
+ */
+ protected function debugHtml($info, $var=null){
+ print '<b>'.$info.'</b> '.($this->time() - $this->start).'s<br />';
+ if(!is_null($var)){
+ ob_start();
+ print_r($var);
+ $content = htmlspecialchars(ob_get_contents());
+ ob_end_clean();
+ print '<pre>'.$content.'</pre>';
+ }
+ }
+
+ /**
+ * prints debug info as plain text
+ *
+ * @param string $info
+ * @param mixed $var
+ */
+ protected function debugText($info, $var=null){
+ print '*'.$info.'* '.($this->time() - $this->start)."s\n";
+ if(!is_null($var)) print_r($var);
+ print "\n-----------------------------------------------\n";
+ }
+
+ /**
+ * Return current timestamp in microsecond resolution
+ *
+ * @return float
+ */
+ protected static function time(){
+ list($usec, $sec) = explode(" ", microtime());
+ return ((float)$usec + (float)$sec);
+ }
+
+ /**
+ * convert given header string to Header array
+ *
+ * All Keys are lowercased.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $string
+ * @return array
+ */
+ protected function parseHeaders($string){
+ $headers = array();
+ $lines = explode("\n",$string);
+ array_shift($lines); //skip first line (status)
+ foreach($lines as $line){
+ @list($key, $val) = explode(':',$line,2);
+ $key = trim($key);
+ $val = trim($val);
+ $key = strtolower($key);
+ if(!$key) continue;
+ if(isset($headers[$key])){
+ if(is_array($headers[$key])){
+ $headers[$key][] = $val;
+ }else{
+ $headers[$key] = array($headers[$key],$val);
+ }
+ }else{
+ $headers[$key] = $val;
+ }
+ }
+ return $headers;
+ }
+
+ /**
+ * convert given header array to header string
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $headers
+ * @return string
+ */
+ protected function buildHeaders($headers){
+ $string = '';
+ foreach($headers as $key => $value){
+ if($value === '') continue;
+ $string .= $key.': '.$value.HTTP_NL;
+ }
+ return $string;
+ }
+
+ /**
+ * get cookies as http header string
+ *
+ * @author Andreas Goetz <cpuidle@gmx.de>
+ *
+ * @return string
+ */
+ protected function getCookies(){
+ $headers = '';
+ foreach ($this->cookies as $key => $val){
+ $headers .= "$key=$val; ";
+ }
+ $headers = substr($headers, 0, -2);
+ if ($headers) $headers = "Cookie: $headers".HTTP_NL;
+ return $headers;
+ }
+
+ /**
+ * Encode data for posting
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $data
+ * @return string
+ */
+ protected function postEncode($data){
+ return http_build_query($data,'','&');
+ }
+
+ /**
+ * Encode data for posting using multipart encoding
+ *
+ * @fixme use of urlencode might be wrong here
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $data
+ * @return string
+ */
+ protected function postMultipartEncode($data){
+ $boundary = '--'.$this->boundary;
+ $out = '';
+ foreach($data as $key => $val){
+ $out .= $boundary.HTTP_NL;
+ if(!is_array($val)){
+ $out .= 'Content-Disposition: form-data; name="'.urlencode($key).'"'.HTTP_NL;
+ $out .= HTTP_NL; // end of headers
+ $out .= $val;
+ $out .= HTTP_NL;
+ }else{
+ $out .= 'Content-Disposition: form-data; name="'.urlencode($key).'"';
+ if($val['filename']) $out .= '; filename="'.urlencode($val['filename']).'"';
+ $out .= HTTP_NL;
+ if($val['mimetype']) $out .= 'Content-Type: '.$val['mimetype'].HTTP_NL;
+ $out .= HTTP_NL; // end of headers
+ $out .= $val['body'];
+ $out .= HTTP_NL;
+ }
+ }
+ $out .= "$boundary--".HTTP_NL;
+ return $out;
+ }
+
+ /**
+ * Generates a unique identifier for a connection.
+ *
+ * @param string $server
+ * @param string $port
+ * @return string unique identifier
+ */
+ protected function uniqueConnectionId($server, $port) {
+ return "$server:$port";
+ }
+
+ /**
+ * Should the Proxy be used for the given URL?
+ *
+ * Checks the exceptions
+ *
+ * @param string $url
+ * @return bool
+ */
+ protected function useProxyForUrl($url) {
+ return $this->proxy_host && (!$this->proxy_except || !preg_match('/' . $this->proxy_except . '/i', $url));
+ }
+}
diff --git a/platform/www/inc/HTTP/HTTPClientException.php b/platform/www/inc/HTTP/HTTPClientException.php
new file mode 100644
index 0000000..5b8f4ee
--- /dev/null
+++ b/platform/www/inc/HTTP/HTTPClientException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace dokuwiki\HTTP;
+
+use Exception;
+
+class HTTPClientException extends Exception
+{
+
+}
diff --git a/platform/www/inc/IXR_Library.php b/platform/www/inc/IXR_Library.php
new file mode 100644
index 0000000..bb1655f
--- /dev/null
+++ b/platform/www/inc/IXR_Library.php
@@ -0,0 +1,1135 @@
+<?php
+
+use dokuwiki\HTTP\DokuHTTPClient;
+
+/**
+ * IXR - The Incutio XML-RPC Library
+ *
+ * Copyright (c) 2010, Incutio Ltd.
+ * 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 of Incutio Ltd. 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 HOLDER 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.
+ *
+ * @package IXR
+ * @since 1.5
+ *
+ * @copyright Incutio Ltd 2010 (http://www.incutio.com)
+ * @version 1.7.4 7th September 2010
+ * @author Simon Willison
+ * @link http://scripts.incutio.com/xmlrpc/ Site/manual
+ *
+ * Modified for DokuWiki
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+class IXR_Value {
+
+ /** @var IXR_Value[]|IXR_Date|IXR_Base64|int|bool|double|string */
+ var $data;
+ /** @var string */
+ var $type;
+
+ /**
+ * @param mixed $data
+ * @param bool $type
+ */
+ function __construct($data, $type = false) {
+ $this->data = $data;
+ if(!$type) {
+ $type = $this->calculateType();
+ }
+ $this->type = $type;
+ if($type == 'struct') {
+ // Turn all the values in the array in to new IXR_Value objects
+ foreach($this->data as $key => $value) {
+ $this->data[$key] = new IXR_Value($value);
+ }
+ }
+ if($type == 'array') {
+ for($i = 0, $j = count($this->data); $i < $j; $i++) {
+ $this->data[$i] = new IXR_Value($this->data[$i]);
+ }
+ }
+ }
+
+ /**
+ * @return string
+ */
+ function calculateType() {
+ if($this->data === true || $this->data === false) {
+ return 'boolean';
+ }
+ if(is_integer($this->data)) {
+ return 'int';
+ }
+ if(is_double($this->data)) {
+ return 'double';
+ }
+
+ // Deal with IXR object types base64 and date
+ if(is_object($this->data) && is_a($this->data, 'IXR_Date')) {
+ return 'date';
+ }
+ if(is_object($this->data) && is_a($this->data, 'IXR_Base64')) {
+ return 'base64';
+ }
+
+ // If it is a normal PHP object convert it in to a struct
+ if(is_object($this->data)) {
+ $this->data = get_object_vars($this->data);
+ return 'struct';
+ }
+ if(!is_array($this->data)) {
+ return 'string';
+ }
+
+ // We have an array - is it an array or a struct?
+ if($this->isStruct($this->data)) {
+ return 'struct';
+ } else {
+ return 'array';
+ }
+ }
+
+ /**
+ * @return bool|string
+ */
+ function getXml() {
+ // Return XML for this value
+ switch($this->type) {
+ case 'boolean':
+ return '<boolean>' . (($this->data) ? '1' : '0') . '</boolean>';
+ break;
+ case 'int':
+ return '<int>' . $this->data . '</int>';
+ break;
+ case 'double':
+ return '<double>' . $this->data . '</double>';
+ break;
+ case 'string':
+ return '<string>' . htmlspecialchars($this->data) . '</string>';
+ break;
+ case 'array':
+ $return = '<array><data>' . "\n";
+ foreach($this->data as $item) {
+ $return .= ' <value>' . $item->getXml() . "</value>\n";
+ }
+ $return .= '</data></array>';
+ return $return;
+ break;
+ case 'struct':
+ $return = '<struct>' . "\n";
+ foreach($this->data as $name => $value) {
+ $return .= " <member><name>$name</name><value>";
+ $return .= $value->getXml() . "</value></member>\n";
+ }
+ $return .= '</struct>';
+ return $return;
+ break;
+ case 'date':
+ case 'base64':
+ return $this->data->getXml();
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Checks whether or not the supplied array is a struct or not
+ *
+ * @param array $array
+ * @return boolean
+ */
+ function isStruct($array) {
+ $expected = 0;
+ foreach($array as $key => $value) {
+ if((string) $key != (string) $expected) {
+ return true;
+ }
+ $expected++;
+ }
+ return false;
+ }
+}
+
+/**
+ * IXR_MESSAGE
+ *
+ * @package IXR
+ * @since 1.5
+ *
+ */
+class IXR_Message {
+ var $message;
+ var $messageType; // methodCall / methodResponse / fault
+ var $faultCode;
+ var $faultString;
+ var $methodName;
+ var $params;
+
+ // Current variable stacks
+ var $_arraystructs = array(); // The stack used to keep track of the current array/struct
+ var $_arraystructstypes = array(); // Stack keeping track of if things are structs or array
+ var $_currentStructName = array(); // A stack as well
+ var $_param;
+ var $_value;
+ var $_currentTag;
+ var $_currentTagContents;
+ var $_lastseen;
+ // The XML parser
+ var $_parser;
+
+ /**
+ * @param string $message
+ */
+ function __construct($message) {
+ $this->message =& $message;
+ }
+
+ /**
+ * @return bool
+ */
+ function parse() {
+ // first remove the XML declaration
+ // merged from WP #10698 - this method avoids the RAM usage of preg_replace on very large messages
+ $header = preg_replace('/<\?xml.*?\?' . '>/', '', substr($this->message, 0, 100), 1);
+ $this->message = substr_replace($this->message, $header, 0, 100);
+
+ // workaround for a bug in PHP/libxml2, see http://bugs.php.net/bug.php?id=45996
+ $this->message = str_replace('&lt;', '&#60;', $this->message);
+ $this->message = str_replace('&gt;', '&#62;', $this->message);
+ $this->message = str_replace('&amp;', '&#38;', $this->message);
+ $this->message = str_replace('&apos;', '&#39;', $this->message);
+ $this->message = str_replace('&quot;', '&#34;', $this->message);
+ $this->message = str_replace("\x0b", ' ', $this->message); //vertical tab
+ if(trim($this->message) == '') {
+ return false;
+ }
+ $this->_parser = xml_parser_create();
+ // Set XML parser to take the case of tags in to account
+ xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false);
+ // Set XML parser callback functions
+ xml_set_object($this->_parser, $this);
+ xml_set_element_handler($this->_parser, 'tag_open', 'tag_close');
+ xml_set_character_data_handler($this->_parser, 'cdata');
+ $chunk_size = 262144; // 256Kb, parse in chunks to avoid the RAM usage on very large messages
+ $final = false;
+ do {
+ if(strlen($this->message) <= $chunk_size) {
+ $final = true;
+ }
+ $part = substr($this->message, 0, $chunk_size);
+ $this->message = substr($this->message, $chunk_size);
+ if(!xml_parse($this->_parser, $part, $final)) {
+ return false;
+ }
+ if($final) {
+ break;
+ }
+ } while(true);
+ xml_parser_free($this->_parser);
+
+ // Grab the error messages, if any
+ if($this->messageType == 'fault') {
+ $this->faultCode = $this->params[0]['faultCode'];
+ $this->faultString = $this->params[0]['faultString'];
+ }
+ return true;
+ }
+
+ /**
+ * @param $parser
+ * @param string $tag
+ * @param $attr
+ */
+ function tag_open($parser, $tag, $attr) {
+ $this->_currentTagContents = '';
+ $this->_currentTag = $tag;
+
+ switch($tag) {
+ case 'methodCall':
+ case 'methodResponse':
+ case 'fault':
+ $this->messageType = $tag;
+ break;
+ /* Deal with stacks of arrays and structs */
+ case 'data': // data is to all intents and purposes more interesting than array
+ $this->_arraystructstypes[] = 'array';
+ $this->_arraystructs[] = array();
+ break;
+ case 'struct':
+ $this->_arraystructstypes[] = 'struct';
+ $this->_arraystructs[] = array();
+ break;
+ }
+ $this->_lastseen = $tag;
+ }
+
+ /**
+ * @param $parser
+ * @param string $cdata
+ */
+ function cdata($parser, $cdata) {
+ $this->_currentTagContents .= $cdata;
+ }
+
+ /**
+ * @param $parser
+ * @param $tag
+ */
+ function tag_close($parser, $tag) {
+ $value = null;
+ $valueFlag = false;
+ switch($tag) {
+ case 'int':
+ case 'i4':
+ $value = (int) trim($this->_currentTagContents);
+ $valueFlag = true;
+ break;
+ case 'double':
+ $value = (double) trim($this->_currentTagContents);
+ $valueFlag = true;
+ break;
+ case 'string':
+ $value = (string) $this->_currentTagContents;
+ $valueFlag = true;
+ break;
+ case 'dateTime.iso8601':
+ $value = new IXR_Date(trim($this->_currentTagContents));
+ $valueFlag = true;
+ break;
+ case 'value':
+ // "If no type is indicated, the type is string."
+ if($this->_lastseen == 'value') {
+ $value = (string) $this->_currentTagContents;
+ $valueFlag = true;
+ }
+ break;
+ case 'boolean':
+ $value = (boolean) trim($this->_currentTagContents);
+ $valueFlag = true;
+ break;
+ case 'base64':
+ $value = base64_decode($this->_currentTagContents);
+ $valueFlag = true;
+ break;
+ /* Deal with stacks of arrays and structs */
+ case 'data':
+ case 'struct':
+ $value = array_pop($this->_arraystructs);
+ array_pop($this->_arraystructstypes);
+ $valueFlag = true;
+ break;
+ case 'member':
+ array_pop($this->_currentStructName);
+ break;
+ case 'name':
+ $this->_currentStructName[] = trim($this->_currentTagContents);
+ break;
+ case 'methodName':
+ $this->methodName = trim($this->_currentTagContents);
+ break;
+ }
+
+ if($valueFlag) {
+ if(count($this->_arraystructs) > 0) {
+ // Add value to struct or array
+ if($this->_arraystructstypes[count($this->_arraystructstypes) - 1] == 'struct') {
+ // Add to struct
+ $this->_arraystructs[count($this->_arraystructs) - 1][$this->_currentStructName[count($this->_currentStructName) - 1]] = $value;
+ } else {
+ // Add to array
+ $this->_arraystructs[count($this->_arraystructs) - 1][] = $value;
+ }
+ } else {
+ // Just add as a parameter
+ $this->params[] = $value;
+ }
+ }
+ $this->_currentTagContents = '';
+ $this->_lastseen = $tag;
+ }
+}
+
+/**
+ * IXR_Server
+ *
+ * @package IXR
+ * @since 1.5
+ */
+class IXR_Server {
+ var $data;
+ /** @var array */
+ var $callbacks = array();
+ var $message;
+ /** @var array */
+ var $capabilities;
+
+ /**
+ * @param array|bool $callbacks
+ * @param bool $data
+ * @param bool $wait
+ */
+ function __construct($callbacks = false, $data = false, $wait = false) {
+ $this->setCapabilities();
+ if($callbacks) {
+ $this->callbacks = $callbacks;
+ }
+ $this->setCallbacks();
+
+ if(!$wait) {
+ $this->serve($data);
+ }
+ }
+
+ /**
+ * @param bool|string $data
+ */
+ function serve($data = false) {
+ if(!$data) {
+
+ $postData = trim(http_get_raw_post_data());
+ if(!$postData) {
+ header('Content-Type: text/plain'); // merged from WP #9093
+ die('XML-RPC server accepts POST requests only.');
+ }
+ $data = $postData;
+ }
+ $this->message = new IXR_Message($data);
+ if(!$this->message->parse()) {
+ $this->error(-32700, 'parse error. not well formed');
+ }
+ if($this->message->messageType != 'methodCall') {
+ $this->error(-32600, 'server error. invalid xml-rpc. not conforming to spec. Request must be a methodCall');
+ }
+ $result = $this->call($this->message->methodName, $this->message->params);
+
+ // Is the result an error?
+ if(is_a($result, 'IXR_Error')) {
+ $this->error($result);
+ }
+
+ // Encode the result
+ $r = new IXR_Value($result);
+ $resultxml = $r->getXml();
+
+ // Create the XML
+ $xml = <<<EOD
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ $resultxml
+ </value>
+ </param>
+ </params>
+</methodResponse>
+
+EOD;
+ // Send it
+ $this->output($xml);
+ }
+
+ /**
+ * @param string $methodname
+ * @param array $args
+ * @return IXR_Error|mixed
+ */
+ function call($methodname, $args) {
+ if(!$this->hasMethod($methodname)) {
+ return new IXR_Error(-32601, 'server error. requested method ' . $methodname . ' does not exist.');
+ }
+ $method = $this->callbacks[$methodname];
+
+ // Perform the callback and send the response
+
+ # Removed for DokuWiki to have a more consistent interface
+ # if (count($args) == 1) {
+ # // If only one parameter just send that instead of the whole array
+ # $args = $args[0];
+ # }
+
+ # Adjusted for DokuWiki to use call_user_func_array
+
+ // args need to be an array
+ $args = (array) $args;
+
+ // Are we dealing with a function or a method?
+ if(is_string($method) && substr($method, 0, 5) == 'this:') {
+ // It's a class method - check it exists
+ $method = substr($method, 5);
+ if(!method_exists($this, $method)) {
+ return new IXR_Error(-32601, 'server error. requested class method "' . $method . '" does not exist.');
+ }
+ // Call the method
+ #$result = $this->$method($args);
+ $result = call_user_func_array(array(&$this, $method), $args);
+ } elseif(substr($method, 0, 7) == 'plugin:') {
+ list($pluginname, $callback) = explode(':', substr($method, 7), 2);
+ if(!plugin_isdisabled($pluginname)) {
+ $plugin = plugin_load('action', $pluginname);
+ return call_user_func_array(array($plugin, $callback), $args);
+ } else {
+ return new IXR_Error(-99999, 'server error');
+ }
+ } else {
+ // It's a function - does it exist?
+ if(is_array($method)) {
+ if(!is_callable(array($method[0], $method[1]))) {
+ return new IXR_Error(-32601, 'server error. requested object method "' . $method[1] . '" does not exist.');
+ }
+ } else if(!function_exists($method)) {
+ return new IXR_Error(-32601, 'server error. requested function "' . $method . '" does not exist.');
+ }
+
+ // Call the function
+ $result = call_user_func($method, $args);
+ }
+ return $result;
+ }
+
+ /**
+ * @param int $error
+ * @param string|bool $message
+ */
+ function error($error, $message = false) {
+ // Accepts either an error object or an error code and message
+ if($message && !is_object($error)) {
+ $error = new IXR_Error($error, $message);
+ }
+ $this->output($error->getXml());
+ }
+
+ /**
+ * @param string $xml
+ */
+ function output($xml) {
+ header('Content-Type: text/xml; charset=utf-8');
+ echo '<?xml version="1.0"?>', "\n", $xml;
+ exit;
+ }
+
+ /**
+ * @param string $method
+ * @return bool
+ */
+ function hasMethod($method) {
+ return in_array($method, array_keys($this->callbacks));
+ }
+
+ function setCapabilities() {
+ // Initialises capabilities array
+ $this->capabilities = array(
+ 'xmlrpc' => array(
+ 'specUrl' => 'http://www.xmlrpc.com/spec',
+ 'specVersion' => 1
+ ),
+ 'faults_interop' => array(
+ 'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
+ 'specVersion' => 20010516
+ ),
+ 'system.multicall' => array(
+ 'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
+ 'specVersion' => 1
+ ),
+ );
+ }
+
+ /**
+ * @return mixed
+ */
+ function getCapabilities() {
+ return $this->capabilities;
+ }
+
+ function setCallbacks() {
+ $this->callbacks['system.getCapabilities'] = 'this:getCapabilities';
+ $this->callbacks['system.listMethods'] = 'this:listMethods';
+ $this->callbacks['system.multicall'] = 'this:multiCall';
+ }
+
+ /**
+ * @return array
+ */
+ function listMethods() {
+ // Returns a list of methods - uses array_reverse to ensure user defined
+ // methods are listed before server defined methods
+ return array_reverse(array_keys($this->callbacks));
+ }
+
+ /**
+ * @param array $methodcalls
+ * @return array
+ */
+ function multiCall($methodcalls) {
+ // See http://www.xmlrpc.com/discuss/msgReader$1208
+ $return = array();
+ foreach($methodcalls as $call) {
+ $method = $call['methodName'];
+ $params = $call['params'];
+ if($method == 'system.multicall') {
+ $result = new IXR_Error(-32800, 'Recursive calls to system.multicall are forbidden');
+ } else {
+ $result = $this->call($method, $params);
+ }
+ if(is_a($result, 'IXR_Error')) {
+ $return[] = array(
+ 'faultCode' => $result->code,
+ 'faultString' => $result->message
+ );
+ } else {
+ $return[] = array($result);
+ }
+ }
+ return $return;
+ }
+}
+
+/**
+ * IXR_Request
+ *
+ * @package IXR
+ * @since 1.5
+ */
+class IXR_Request {
+ /** @var string */
+ var $method;
+ /** @var array */
+ var $args;
+ /** @var string */
+ var $xml;
+
+ /**
+ * @param string $method
+ * @param array $args
+ */
+ function __construct($method, $args) {
+ $this->method = $method;
+ $this->args = $args;
+ $this->xml = <<<EOD
+<?xml version="1.0"?>
+<methodCall>
+<methodName>{$this->method}</methodName>
+<params>
+
+EOD;
+ foreach($this->args as $arg) {
+ $this->xml .= '<param><value>';
+ $v = new IXR_Value($arg);
+ $this->xml .= $v->getXml();
+ $this->xml .= "</value></param>\n";
+ }
+ $this->xml .= '</params></methodCall>';
+ }
+
+ /**
+ * @return int
+ */
+ function getLength() {
+ return strlen($this->xml);
+ }
+
+ /**
+ * @return string
+ */
+ function getXml() {
+ return $this->xml;
+ }
+}
+
+/**
+ * IXR_Client
+ *
+ * @package IXR
+ * @since 1.5
+ *
+ * Changed for DokuWiki to use DokuHTTPClient
+ *
+ * This should be compatible to the original class, but uses DokuWiki's
+ * HTTP client library which will respect proxy settings
+ *
+ * Because the XMLRPC client is not used in DokuWiki currently this is completely
+ * untested
+ */
+class IXR_Client extends DokuHTTPClient {
+ var $posturl = '';
+ /** @var IXR_Message|bool */
+ var $message = false;
+
+ // Storage place for an error message
+ /** @var IXR_Error|bool */
+ var $xmlerror = false;
+
+ /**
+ * @param string $server
+ * @param string|bool $path
+ * @param int $port
+ * @param int $timeout
+ */
+ function __construct($server, $path = false, $port = 80, $timeout = 15) {
+ parent::__construct();
+ if(!$path) {
+ // Assume we have been given a URL instead
+ $this->posturl = $server;
+ } else {
+ $this->posturl = 'http://' . $server . ':' . $port . $path;
+ }
+ $this->timeout = $timeout;
+ }
+
+ /**
+ * parameters: method and arguments
+ * @return bool success or error
+ */
+ function query() {
+ $args = func_get_args();
+ $method = array_shift($args);
+ $request = new IXR_Request($method, $args);
+ $xml = $request->getXml();
+
+ $this->headers['Content-Type'] = 'text/xml';
+ if(!$this->sendRequest($this->posturl, $xml, 'POST')) {
+ $this->xmlerror = new IXR_Error(-32300, 'transport error - ' . $this->error);
+ return false;
+ }
+
+ // Check HTTP Response code
+ if($this->status < 200 || $this->status > 206) {
+ $this->xmlerror = new IXR_Error(-32300, 'transport error - HTTP status ' . $this->status);
+ return false;
+ }
+
+ // Now parse what we've got back
+ $this->message = new IXR_Message($this->resp_body);
+ if(!$this->message->parse()) {
+ // XML error
+ $this->xmlerror = new IXR_Error(-32700, 'parse error. not well formed');
+ return false;
+ }
+
+ // Is the message a fault?
+ if($this->message->messageType == 'fault') {
+ $this->xmlerror = new IXR_Error($this->message->faultCode, $this->message->faultString);
+ return false;
+ }
+
+ // Message must be OK
+ return true;
+ }
+
+ /**
+ * @return mixed
+ */
+ function getResponse() {
+ // methodResponses can only have one param - return that
+ return $this->message->params[0];
+ }
+
+ /**
+ * @return bool
+ */
+ function isError() {
+ return (is_object($this->xmlerror));
+ }
+
+ /**
+ * @return int
+ */
+ function getErrorCode() {
+ return $this->xmlerror->code;
+ }
+
+ /**
+ * @return string
+ */
+ function getErrorMessage() {
+ return $this->xmlerror->message;
+ }
+}
+
+/**
+ * IXR_Error
+ *
+ * @package IXR
+ * @since 1.5
+ */
+class IXR_Error {
+ var $code;
+ var $message;
+
+ /**
+ * @param int $code
+ * @param string $message
+ */
+ function __construct($code, $message) {
+ $this->code = $code;
+ $this->message = htmlspecialchars($message);
+ }
+
+ /**
+ * @return string
+ */
+ function getXml() {
+ $xml = <<<EOD
+<methodResponse>
+ <fault>
+ <value>
+ <struct>
+ <member>
+ <name>faultCode</name>
+ <value><int>{$this->code}</int></value>
+ </member>
+ <member>
+ <name>faultString</name>
+ <value><string>{$this->message}</string></value>
+ </member>
+ </struct>
+ </value>
+ </fault>
+</methodResponse>
+
+EOD;
+ return $xml;
+ }
+}
+
+/**
+ * IXR_Date
+ *
+ * @package IXR
+ * @since 1.5
+ */
+class IXR_Date {
+
+ const XMLRPC_ISO8601 = "Ymd\TH:i:sO" ;
+ /** @var DateTime */
+ protected $date;
+
+ /**
+ * @param int|string $time
+ */
+ public function __construct($time) {
+ // $time can be a PHP timestamp or an ISO one
+ if(is_numeric($time)) {
+ $this->parseTimestamp($time);
+ } else {
+ $this->parseIso($time);
+ }
+ }
+
+ /**
+ * Parse unix timestamp
+ *
+ * @param int $timestamp
+ */
+ protected function parseTimestamp($timestamp) {
+ $this->date = new DateTime('@' . $timestamp);
+ }
+
+ /**
+ * Parses less or more complete iso dates and much more, if no timezone given assumes UTC
+ *
+ * @param string $iso
+ */
+ protected function parseIso($iso) {
+ $this->date = new DateTime($iso, new DateTimeZone("UTC"));
+ }
+
+ /**
+ * Returns date in ISO 8601 format
+ *
+ * @return string
+ */
+ public function getIso() {
+ return $this->date->format(self::XMLRPC_ISO8601);
+ }
+
+ /**
+ * Returns date in valid xml
+ *
+ * @return string
+ */
+ public function getXml() {
+ return '<dateTime.iso8601>' . $this->getIso() . '</dateTime.iso8601>';
+ }
+
+ /**
+ * Returns Unix timestamp
+ *
+ * @return int
+ */
+ function getTimestamp() {
+ return $this->date->getTimestamp();
+ }
+}
+
+/**
+ * IXR_Base64
+ *
+ * @package IXR
+ * @since 1.5
+ */
+class IXR_Base64 {
+ var $data;
+
+ /**
+ * @param string $data
+ */
+ function __construct($data) {
+ $this->data = $data;
+ }
+
+ /**
+ * @return string
+ */
+ function getXml() {
+ return '<base64>' . base64_encode($this->data) . '</base64>';
+ }
+}
+
+/**
+ * IXR_IntrospectionServer
+ *
+ * @package IXR
+ * @since 1.5
+ */
+class IXR_IntrospectionServer extends IXR_Server {
+ /** @var array[] */
+ var $signatures;
+ /** @var string[] */
+ var $help;
+
+ /**
+ * Constructor
+ */
+ function __construct() {
+ $this->setCallbacks();
+ $this->setCapabilities();
+ $this->capabilities['introspection'] = array(
+ 'specUrl' => 'http://xmlrpc.usefulinc.com/doc/reserved.html',
+ 'specVersion' => 1
+ );
+ $this->addCallback(
+ 'system.methodSignature',
+ 'this:methodSignature',
+ array('array', 'string'),
+ 'Returns an array describing the return type and required parameters of a method'
+ );
+ $this->addCallback(
+ 'system.getCapabilities',
+ 'this:getCapabilities',
+ array('struct'),
+ 'Returns a struct describing the XML-RPC specifications supported by this server'
+ );
+ $this->addCallback(
+ 'system.listMethods',
+ 'this:listMethods',
+ array('array'),
+ 'Returns an array of available methods on this server'
+ );
+ $this->addCallback(
+ 'system.methodHelp',
+ 'this:methodHelp',
+ array('string', 'string'),
+ 'Returns a documentation string for the specified method'
+ );
+ }
+
+ /**
+ * @param string $method
+ * @param string $callback
+ * @param string[] $args
+ * @param string $help
+ */
+ function addCallback($method, $callback, $args, $help) {
+ $this->callbacks[$method] = $callback;
+ $this->signatures[$method] = $args;
+ $this->help[$method] = $help;
+ }
+
+ /**
+ * @param string $methodname
+ * @param array $args
+ * @return IXR_Error|mixed
+ */
+ function call($methodname, $args) {
+ // Make sure it's in an array
+ if($args && !is_array($args)) {
+ $args = array($args);
+ }
+
+ // Over-rides default call method, adds signature check
+ if(!$this->hasMethod($methodname)) {
+ return new IXR_Error(-32601, 'server error. requested method "' . $this->message->methodName . '" not specified.');
+ }
+ $method = $this->callbacks[$methodname];
+ $signature = $this->signatures[$methodname];
+ $returnType = array_shift($signature);
+ // Check the number of arguments. Check only, if the minimum count of parameters is specified. More parameters are possible.
+ // This is a hack to allow optional parameters...
+ if(count($args) < count($signature)) {
+ // print 'Num of args: '.count($args).' Num in signature: '.count($signature);
+ return new IXR_Error(-32602, 'server error. wrong number of method parameters');
+ }
+
+ // Check the argument types
+ $ok = true;
+ $argsbackup = $args;
+ for($i = 0, $j = count($args); $i < $j; $i++) {
+ $arg = array_shift($args);
+ $type = array_shift($signature);
+ switch($type) {
+ case 'int':
+ case 'i4':
+ if(is_array($arg) || !is_int($arg)) {
+ $ok = false;
+ }
+ break;
+ case 'base64':
+ case 'string':
+ if(!is_string($arg)) {
+ $ok = false;
+ }
+ break;
+ case 'boolean':
+ if($arg !== false && $arg !== true) {
+ $ok = false;
+ }
+ break;
+ case 'float':
+ case 'double':
+ if(!is_float($arg)) {
+ $ok = false;
+ }
+ break;
+ case 'date':
+ case 'dateTime.iso8601':
+ if(!is_a($arg, 'IXR_Date')) {
+ $ok = false;
+ }
+ break;
+ }
+ if(!$ok) {
+ return new IXR_Error(-32602, 'server error. invalid method parameters');
+ }
+ }
+ // It passed the test - run the "real" method call
+ return parent::call($methodname, $argsbackup);
+ }
+
+ /**
+ * @param string $method
+ * @return array|IXR_Error
+ */
+ function methodSignature($method) {
+ if(!$this->hasMethod($method)) {
+ return new IXR_Error(-32601, 'server error. requested method "' . $method . '" not specified.');
+ }
+ // We should be returning an array of types
+ $types = $this->signatures[$method];
+ $return = array();
+ foreach($types as $type) {
+ switch($type) {
+ case 'string':
+ $return[] = 'string';
+ break;
+ case 'int':
+ case 'i4':
+ $return[] = 42;
+ break;
+ case 'double':
+ $return[] = 3.1415;
+ break;
+ case 'dateTime.iso8601':
+ $return[] = new IXR_Date(time());
+ break;
+ case 'boolean':
+ $return[] = true;
+ break;
+ case 'base64':
+ $return[] = new IXR_Base64('base64');
+ break;
+ case 'array':
+ $return[] = array('array');
+ break;
+ case 'struct':
+ $return[] = array('struct' => 'struct');
+ break;
+ }
+ }
+ return $return;
+ }
+
+ /**
+ * @param string $method
+ * @return mixed
+ */
+ function methodHelp($method) {
+ return $this->help[$method];
+ }
+}
+
+/**
+ * IXR_ClientMulticall
+ *
+ * @package IXR
+ * @since 1.5
+ */
+class IXR_ClientMulticall extends IXR_Client {
+
+ /** @var array[] */
+ var $calls = array();
+
+ /**
+ * @param string $server
+ * @param string|bool $path
+ * @param int $port
+ */
+ function __construct($server, $path = false, $port = 80) {
+ parent::__construct($server, $path, $port);
+ //$this->useragent = 'The Incutio XML-RPC PHP Library (multicall client)';
+ }
+
+ /**
+ * Add a call
+ */
+ function addCall() {
+ $args = func_get_args();
+ $methodName = array_shift($args);
+ $struct = array(
+ 'methodName' => $methodName,
+ 'params' => $args
+ );
+ $this->calls[] = $struct;
+ }
+
+ /**
+ * @return bool
+ */
+ function query() {
+ // Prepare multicall, then call the parent::query() method
+ return parent::query('system.multicall', $this->calls);
+ }
+}
+
diff --git a/platform/www/inc/Input/Get.php b/platform/www/inc/Input/Get.php
new file mode 100644
index 0000000..99ab265
--- /dev/null
+++ b/platform/www/inc/Input/Get.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace dokuwiki\Input;
+
+/**
+ * Internal class used for $_GET access in dokuwiki\Input\Input class
+ */
+class Get extends Input
+{
+ /** @noinspection PhpMissingParentConstructorInspection
+ * Initialize the $access array, remove subclass members
+ */
+ public function __construct()
+ {
+ $this->access = &$_GET;
+ }
+
+ /**
+ * Sets a parameter in $_GET and $_REQUEST
+ *
+ * @param string $name Parameter name
+ * @param mixed $value Value to set
+ */
+ public function set($name, $value)
+ {
+ parent::set($name, $value);
+ $_REQUEST[$name] = $value;
+ }
+}
diff --git a/platform/www/inc/Input/Input.php b/platform/www/inc/Input/Input.php
new file mode 100644
index 0000000..3d2426b
--- /dev/null
+++ b/platform/www/inc/Input/Input.php
@@ -0,0 +1,287 @@
+<?php
+
+namespace dokuwiki\Input;
+
+/**
+ * Encapsulates access to the $_REQUEST array, making sure used parameters are initialized and
+ * have the correct type.
+ *
+ * All function access the $_REQUEST array by default, if you want to access $_POST or $_GET
+ * explicitly use the $post and $get members.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+class Input
+{
+
+ /** @var Post Access $_POST parameters */
+ public $post;
+ /** @var Get Access $_GET parameters */
+ public $get;
+ /** @var Server Access $_SERVER parameters */
+ public $server;
+
+ protected $access;
+
+ /**
+ * @var Callable
+ */
+ protected $filter;
+
+ /**
+ * Intilizes the dokuwiki\Input\Input class and it subcomponents
+ */
+ public function __construct()
+ {
+ $this->access = &$_REQUEST;
+ $this->post = new Post();
+ $this->get = new Get();
+ $this->server = new Server();
+ }
+
+ /**
+ * Apply the set filter to the given value
+ *
+ * @param string $data
+ * @return string
+ */
+ protected function applyfilter($data)
+ {
+ if (!$this->filter) return $data;
+ return call_user_func($this->filter, $data);
+ }
+
+ /**
+ * Return a filtered copy of the input object
+ *
+ * Expects a callable that accepts one string parameter and returns a filtered string
+ *
+ * @param Callable|string $filter
+ * @return Input
+ */
+ public function filter($filter = 'stripctl')
+ {
+ $this->filter = $filter;
+ $clone = clone $this;
+ $this->filter = '';
+ return $clone;
+ }
+
+ /**
+ * Check if a parameter was set
+ *
+ * Basically a wrapper around isset. When called on the $post and $get subclasses,
+ * the parameter is set to $_POST or $_GET and to $_REQUEST
+ *
+ * @see isset
+ * @param string $name Parameter name
+ * @return bool
+ */
+ public function has($name)
+ {
+ return isset($this->access[$name]);
+ }
+
+ /**
+ * Remove a parameter from the superglobals
+ *
+ * Basically a wrapper around unset. When NOT called on the $post and $get subclasses,
+ * the parameter will also be removed from $_POST or $_GET
+ *
+ * @see isset
+ * @param string $name Parameter name
+ */
+ public function remove($name)
+ {
+ if (isset($this->access[$name])) {
+ unset($this->access[$name]);
+ }
+ // also remove from sub classes
+ if (isset($this->post) && isset($_POST[$name])) {
+ unset($_POST[$name]);
+ }
+ if (isset($this->get) && isset($_GET[$name])) {
+ unset($_GET[$name]);
+ }
+ }
+
+ /**
+ * Access a request parameter without any type conversion
+ *
+ * @param string $name Parameter name
+ * @param mixed $default Default to return if parameter isn't set
+ * @param bool $nonempty Return $default if parameter is set but empty()
+ * @return mixed
+ */
+ public function param($name, $default = null, $nonempty = false)
+ {
+ if (!isset($this->access[$name])) return $default;
+ $value = $this->applyfilter($this->access[$name]);
+ if ($nonempty && empty($value)) return $default;
+ return $value;
+ }
+
+ /**
+ * Sets a parameter
+ *
+ * @param string $name Parameter name
+ * @param mixed $value Value to set
+ */
+ public function set($name, $value)
+ {
+ $this->access[$name] = $value;
+ }
+
+ /**
+ * Get a reference to a request parameter
+ *
+ * This avoids copying data in memory, when the parameter is not set it will be created
+ * and intialized with the given $default value before a reference is returned
+ *
+ * @param string $name Parameter name
+ * @param mixed $default If parameter is not set, initialize with this value
+ * @param bool $nonempty Init with $default if parameter is set but empty()
+ * @return mixed (reference)
+ */
+ public function &ref($name, $default = '', $nonempty = false)
+ {
+ if (!isset($this->access[$name]) || ($nonempty && empty($this->access[$name]))) {
+ $this->set($name, $default);
+ }
+
+ return $this->access[$name];
+ }
+
+ /**
+ * Access a request parameter as int
+ *
+ * @param string $name Parameter name
+ * @param int $default Default to return if parameter isn't set or is an array
+ * @param bool $nonempty Return $default if parameter is set but empty()
+ * @return int
+ */
+ public function int($name, $default = 0, $nonempty = false)
+ {
+ if (!isset($this->access[$name])) return $default;
+ if (is_array($this->access[$name])) return $default;
+ $value = $this->applyfilter($this->access[$name]);
+ if ($value === '') return $default;
+ if ($nonempty && empty($value)) return $default;
+
+ return (int)$value;
+ }
+
+ /**
+ * Access a request parameter as string
+ *
+ * @param string $name Parameter name
+ * @param string $default Default to return if parameter isn't set or is an array
+ * @param bool $nonempty Return $default if parameter is set but empty()
+ * @return string
+ */
+ public function str($name, $default = '', $nonempty = false)
+ {
+ if (!isset($this->access[$name])) return $default;
+ if (is_array($this->access[$name])) return $default;
+ $value = $this->applyfilter($this->access[$name]);
+ if ($nonempty && empty($value)) return $default;
+
+ return (string)$value;
+ }
+
+ /**
+ * Access a request parameter and make sure it is has a valid value
+ *
+ * Please note that comparisons to the valid values are not done typesafe (request vars
+ * are always strings) however the function will return the correct type from the $valids
+ * array when an match was found.
+ *
+ * @param string $name Parameter name
+ * @param array $valids Array of valid values
+ * @param mixed $default Default to return if parameter isn't set or not valid
+ * @return null|mixed
+ */
+ public function valid($name, $valids, $default = null)
+ {
+ if (!isset($this->access[$name])) return $default;
+ if (is_array($this->access[$name])) return $default; // we don't allow arrays
+ $value = $this->applyfilter($this->access[$name]);
+ $found = array_search($value, $valids);
+ if ($found !== false) return $valids[$found]; // return the valid value for type safety
+ return $default;
+ }
+
+ /**
+ * Access a request parameter as bool
+ *
+ * Note: $nonempty is here for interface consistency and makes not much sense for booleans
+ *
+ * @param string $name Parameter name
+ * @param mixed $default Default to return if parameter isn't set
+ * @param bool $nonempty Return $default if parameter is set but empty()
+ * @return bool
+ */
+ public function bool($name, $default = false, $nonempty = false)
+ {
+ if (!isset($this->access[$name])) return $default;
+ if (is_array($this->access[$name])) return $default;
+ $value = $this->applyfilter($this->access[$name]);
+ if ($value === '') return $default;
+ if ($nonempty && empty($value)) return $default;
+
+ return (bool)$value;
+ }
+
+ /**
+ * Access a request parameter as array
+ *
+ * @param string $name Parameter name
+ * @param mixed $default Default to return if parameter isn't set
+ * @param bool $nonempty Return $default if parameter is set but empty()
+ * @return array
+ */
+ public function arr($name, $default = array(), $nonempty = false)
+ {
+ if (!isset($this->access[$name])) return $default;
+ if (!is_array($this->access[$name])) return $default;
+ if ($nonempty && empty($this->access[$name])) return $default;
+
+ return (array)$this->access[$name];
+ }
+
+ /**
+ * Create a simple key from an array key
+ *
+ * This is useful to access keys where the information is given as an array key or as a single array value.
+ * For example when the information was submitted as the name of a submit button.
+ *
+ * This function directly changes the access array.
+ *
+ * Eg. $_REQUEST['do']['save']='Speichern' becomes $_REQUEST['do'] = 'save'
+ *
+ * This function returns the $INPUT object itself for easy chaining
+ *
+ * @param string $name
+ * @return Input
+ */
+ public function extract($name)
+ {
+ if (!isset($this->access[$name])) return $this;
+ if (!is_array($this->access[$name])) return $this;
+ $keys = array_keys($this->access[$name]);
+ if (!$keys) {
+ // this was an empty array
+ $this->remove($name);
+ return $this;
+ }
+ // get the first key
+ $value = array_shift($keys);
+ if ($value === 0) {
+ // we had a numeric array, assume the value is not in the key
+ $value = array_shift($this->access[$name]);
+ }
+
+ $this->set($name, $value);
+ return $this;
+ }
+}
diff --git a/platform/www/inc/Input/Post.php b/platform/www/inc/Input/Post.php
new file mode 100644
index 0000000..137cd72
--- /dev/null
+++ b/platform/www/inc/Input/Post.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace dokuwiki\Input;
+
+/**
+ * Internal class used for $_POST access in dokuwiki\Input\Input class
+ */
+class Post extends Input
+{
+
+ /** @noinspection PhpMissingParentConstructorInspection
+ * Initialize the $access array, remove subclass members
+ */
+ public function __construct()
+ {
+ $this->access = &$_POST;
+ }
+
+ /**
+ * Sets a parameter in $_POST and $_REQUEST
+ *
+ * @param string $name Parameter name
+ * @param mixed $value Value to set
+ */
+ public function set($name, $value)
+ {
+ parent::set($name, $value);
+ $_REQUEST[$name] = $value;
+ }
+}
diff --git a/platform/www/inc/Input/Server.php b/platform/www/inc/Input/Server.php
new file mode 100644
index 0000000..60964fd
--- /dev/null
+++ b/platform/www/inc/Input/Server.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace dokuwiki\Input;
+
+/**
+ * Internal class used for $_SERVER access in dokuwiki\Input\Input class
+ */
+class Server extends Input
+{
+
+ /** @noinspection PhpMissingParentConstructorInspection
+ * Initialize the $access array, remove subclass members
+ */
+ public function __construct()
+ {
+ $this->access = &$_SERVER;
+ }
+
+}
diff --git a/platform/www/inc/JpegMeta.php b/platform/www/inc/JpegMeta.php
new file mode 100644
index 0000000..9ed1e2d
--- /dev/null
+++ b/platform/www/inc/JpegMeta.php
@@ -0,0 +1,3188 @@
+<?php
+/**
+ * JPEG metadata reader/writer
+ *
+ * @license BSD <http://www.opensource.org/licenses/bsd-license.php>
+ * @link http://github.com/sd/jpeg-php
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Hakan Sandell <hakan.sandell@mydata.se>
+ * @todo Add support for Maker Notes, Extend for GIF and PNG metadata
+ */
+
+// Original copyright notice:
+//
+// Copyright (c) 2003 Sebastian Delmont <sdelmont@zonageek.com>
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. 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.
+// 3. Neither the name of the author 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
+// HOLDER 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 JpegMeta {
+ var $_fileName;
+ var $_fp = null;
+ var $_fpout = null;
+ var $_type = 'unknown';
+
+ var $_markers;
+ var $_info;
+
+
+ /**
+ * Constructor
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ *
+ * @param $fileName
+ */
+ function __construct($fileName) {
+
+ $this->_fileName = $fileName;
+
+ $this->_fp = null;
+ $this->_type = 'unknown';
+
+ unset($this->_info);
+ unset($this->_markers);
+ }
+
+ /**
+ * Returns all gathered info as multidim array
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ */
+ function & getRawInfo() {
+ $this->_parseAll();
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ return $this->_info;
+ }
+
+ /**
+ * Returns basic image info
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ */
+ function & getBasicInfo() {
+ $this->_parseAll();
+
+ $info = array();
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ $info['Name'] = $this->_info['file']['Name'];
+ if (isset($this->_info['file']['Url'])) {
+ $info['Url'] = $this->_info['file']['Url'];
+ $info['NiceSize'] = "???KB";
+ } else {
+ $info['Size'] = $this->_info['file']['Size'];
+ $info['NiceSize'] = $this->_info['file']['NiceSize'];
+ }
+
+ if (@isset($this->_info['sof']['Format'])) {
+ $info['Format'] = $this->_info['sof']['Format'] . " JPEG";
+ } else {
+ $info['Format'] = $this->_info['sof']['Format'] . " JPEG";
+ }
+
+ if (@isset($this->_info['sof']['ColorChannels'])) {
+ $info['ColorMode'] = ($this->_info['sof']['ColorChannels'] > 1) ? "Color" : "B&W";
+ }
+
+ $info['Width'] = $this->getWidth();
+ $info['Height'] = $this->getHeight();
+ $info['DimStr'] = $this->getDimStr();
+
+ $dates = $this->getDates();
+
+ $info['DateTime'] = $dates['EarliestTime'];
+ $info['DateTimeStr'] = $dates['EarliestTimeStr'];
+
+ $info['HasThumbnail'] = $this->hasThumbnail();
+
+ return $info;
+ }
+
+
+ /**
+ * Convinience function to access nearly all available Data
+ * through one function
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array|string $fields field name or array with field names
+ * @return bool|string
+ */
+ function getField($fields) {
+ if(!is_array($fields)) $fields = array($fields);
+ $info = false;
+ foreach($fields as $field){
+ if(strtolower(substr($field,0,5)) == 'iptc.'){
+ $info = $this->getIPTCField(substr($field,5));
+ }elseif(strtolower(substr($field,0,5)) == 'exif.'){
+ $info = $this->getExifField(substr($field,5));
+ }elseif(strtolower(substr($field,0,4)) == 'xmp.'){
+ $info = $this->getXmpField(substr($field,4));
+ }elseif(strtolower(substr($field,0,5)) == 'file.'){
+ $info = $this->getFileField(substr($field,5));
+ }elseif(strtolower(substr($field,0,5)) == 'date.'){
+ $info = $this->getDateField(substr($field,5));
+ }elseif(strtolower($field) == 'simple.camera'){
+ $info = $this->getCamera();
+ }elseif(strtolower($field) == 'simple.raw'){
+ return $this->getRawInfo();
+ }elseif(strtolower($field) == 'simple.title'){
+ $info = $this->getTitle();
+ }elseif(strtolower($field) == 'simple.shutterspeed'){
+ $info = $this->getShutterSpeed();
+ }else{
+ $info = $this->getExifField($field);
+ }
+ if($info != false) break;
+ }
+
+ if($info === false) $info = '';
+ if(is_array($info)){
+ if(isset($info['val'])){
+ $info = $info['val'];
+ }else{
+ $info = join(', ',$info);
+ }
+ }
+ return trim($info);
+ }
+
+ /**
+ * Convinience function to set nearly all available Data
+ * through one function
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $field field name
+ * @param string $value
+ * @return bool success or fail
+ */
+ function setField($field, $value) {
+ if(strtolower(substr($field,0,5)) == 'iptc.'){
+ return $this->setIPTCField(substr($field,5),$value);
+ }elseif(strtolower(substr($field,0,5)) == 'exif.'){
+ return $this->setExifField(substr($field,5),$value);
+ }else{
+ return $this->setExifField($field,$value);
+ }
+ }
+
+ /**
+ * Convinience function to delete nearly all available Data
+ * through one function
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $field field name
+ * @return bool
+ */
+ function deleteField($field) {
+ if(strtolower(substr($field,0,5)) == 'iptc.'){
+ return $this->deleteIPTCField(substr($field,5));
+ }elseif(strtolower(substr($field,0,5)) == 'exif.'){
+ return $this->deleteExifField(substr($field,5));
+ }else{
+ return $this->deleteExifField($field);
+ }
+ }
+
+ /**
+ * Return a date field
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $field
+ * @return false|string
+ */
+ function getDateField($field) {
+ if (!isset($this->_info['dates'])) {
+ $this->_info['dates'] = $this->getDates();
+ }
+
+ if (isset($this->_info['dates'][$field])) {
+ return $this->_info['dates'][$field];
+ }
+
+ return false;
+ }
+
+ /**
+ * Return a file info field
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $field field name
+ * @return false|string
+ */
+ function getFileField($field) {
+ if (!isset($this->_info['file'])) {
+ $this->_parseFileInfo();
+ }
+
+ if (isset($this->_info['file'][$field])) {
+ return $this->_info['file'][$field];
+ }
+
+ return false;
+ }
+
+ /**
+ * Return the camera info (Maker and Model)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @todo handle makernotes
+ *
+ * @return false|string
+ */
+ function getCamera(){
+ $make = $this->getField(array('Exif.Make','Exif.TIFFMake'));
+ $model = $this->getField(array('Exif.Model','Exif.TIFFModel'));
+ $cam = trim("$make $model");
+ if(empty($cam)) return false;
+ return $cam;
+ }
+
+ /**
+ * Return shutter speed as a ratio
+ *
+ * @author Joe Lapp <joe.lapp@pobox.com>
+ *
+ * @return string
+ */
+ function getShutterSpeed() {
+ if (!isset($this->_info['exif'])) {
+ $this->_parseMarkerExif();
+ }
+ if(!isset($this->_info['exif']['ExposureTime'])){
+ return '';
+ }
+
+ $field = $this->_info['exif']['ExposureTime'];
+ if($field['den'] == 1) return $field['num'];
+ return $field['num'].'/'.$field['den'];
+ }
+
+ /**
+ * Return an EXIF field
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ *
+ * @param string $field field name
+ * @return false|string
+ */
+ function getExifField($field) {
+ if (!isset($this->_info['exif'])) {
+ $this->_parseMarkerExif();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if (isset($this->_info['exif'][$field])) {
+ return $this->_info['exif'][$field];
+ }
+
+ return false;
+ }
+
+ /**
+ * Return an XMP field
+ *
+ * @author Hakan Sandell <hakan.sandell@mydata.se>
+ *
+ * @param string $field field name
+ * @return false|string
+ */
+ function getXmpField($field) {
+ if (!isset($this->_info['xmp'])) {
+ $this->_parseMarkerXmp();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if (isset($this->_info['xmp'][$field])) {
+ return $this->_info['xmp'][$field];
+ }
+
+ return false;
+ }
+
+ /**
+ * Return an Adobe Field
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ *
+ * @param string $field field name
+ * @return false|string
+ */
+ function getAdobeField($field) {
+ if (!isset($this->_info['adobe'])) {
+ $this->_parseMarkerAdobe();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if (isset($this->_info['adobe'][$field])) {
+ return $this->_info['adobe'][$field];
+ }
+
+ return false;
+ }
+
+ /**
+ * Return an IPTC field
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ *
+ * @param string $field field name
+ * @return false|string
+ */
+ function getIPTCField($field) {
+ if (!isset($this->_info['iptc'])) {
+ $this->_parseMarkerAdobe();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if (isset($this->_info['iptc'][$field])) {
+ return $this->_info['iptc'][$field];
+ }
+
+ return false;
+ }
+
+ /**
+ * Set an EXIF field
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ * @author Joe Lapp <joe.lapp@pobox.com>
+ *
+ * @param string $field field name
+ * @param string $value
+ * @return bool
+ */
+ function setExifField($field, $value) {
+ if (!isset($this->_info['exif'])) {
+ $this->_parseMarkerExif();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if ($this->_info['exif'] == false) {
+ $this->_info['exif'] = array();
+ }
+
+ // make sure datetimes are in correct format
+ if(strlen($field) >= 8 && strtolower(substr($field, 0, 8)) == 'datetime') {
+ if(strlen($value) < 8 || $value[4] != ':' || $value[7] != ':') {
+ $value = date('Y:m:d H:i:s', strtotime($value));
+ }
+ }
+
+ $this->_info['exif'][$field] = $value;
+
+ return true;
+ }
+
+ /**
+ * Set an Adobe Field
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ *
+ * @param string $field field name
+ * @param string $value
+ * @return bool
+ */
+ function setAdobeField($field, $value) {
+ if (!isset($this->_info['adobe'])) {
+ $this->_parseMarkerAdobe();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if ($this->_info['adobe'] == false) {
+ $this->_info['adobe'] = array();
+ }
+
+ $this->_info['adobe'][$field] = $value;
+
+ return true;
+ }
+
+ /**
+ * Calculates the multiplier needed to resize the image to the given
+ * dimensions
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param int $maxwidth
+ * @param int $maxheight
+ * @return float|int
+ */
+ function getResizeRatio($maxwidth,$maxheight=0){
+ if(!$maxheight) $maxheight = $maxwidth;
+
+ $w = $this->getField('File.Width');
+ $h = $this->getField('File.Height');
+
+ $ratio = 1;
+ if($w >= $h){
+ if($w >= $maxwidth){
+ $ratio = $maxwidth/$w;
+ }elseif($h > $maxheight){
+ $ratio = $maxheight/$h;
+ }
+ }else{
+ if($h >= $maxheight){
+ $ratio = $maxheight/$h;
+ }elseif($w > $maxwidth){
+ $ratio = $maxwidth/$w;
+ }
+ }
+ return $ratio;
+ }
+
+
+ /**
+ * Set an IPTC field
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ *
+ * @param string $field field name
+ * @param string $value
+ * @return bool
+ */
+ function setIPTCField($field, $value) {
+ if (!isset($this->_info['iptc'])) {
+ $this->_parseMarkerAdobe();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if ($this->_info['iptc'] == false) {
+ $this->_info['iptc'] = array();
+ }
+
+ $this->_info['iptc'][$field] = $value;
+
+ return true;
+ }
+
+ /**
+ * Delete an EXIF field
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ *
+ * @param string $field field name
+ * @return bool
+ */
+ function deleteExifField($field) {
+ if (!isset($this->_info['exif'])) {
+ $this->_parseMarkerAdobe();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if ($this->_info['exif'] != false) {
+ unset($this->_info['exif'][$field]);
+ }
+
+ return true;
+ }
+
+ /**
+ * Delete an Adobe field
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ *
+ * @param string $field field name
+ * @return bool
+ */
+ function deleteAdobeField($field) {
+ if (!isset($this->_info['adobe'])) {
+ $this->_parseMarkerAdobe();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if ($this->_info['adobe'] != false) {
+ unset($this->_info['adobe'][$field]);
+ }
+
+ return true;
+ }
+
+ /**
+ * Delete an IPTC field
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ *
+ * @param string $field field name
+ * @return bool
+ */
+ function deleteIPTCField($field) {
+ if (!isset($this->_info['iptc'])) {
+ $this->_parseMarkerAdobe();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if ($this->_info['iptc'] != false) {
+ unset($this->_info['iptc'][$field]);
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the image's title, tries various fields
+ *
+ * @param int $max maximum number chars (keeps words)
+ * @return false|string
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ function getTitle($max=80){
+ // try various fields
+ $cap = $this->getField(array('Iptc.Headline',
+ 'Iptc.Caption',
+ 'Xmp.dc:title',
+ 'Exif.UserComment',
+ 'Exif.TIFFUserComment',
+ 'Exif.TIFFImageDescription',
+ 'File.Name'));
+ if (empty($cap)) return false;
+
+ if(!$max) return $cap;
+ // Shorten to 80 chars (keeping words)
+ $new = preg_replace('/\n.+$/','',wordwrap($cap, $max));
+ if($new != $cap) $new .= '...';
+
+ return $new;
+ }
+
+ /**
+ * Gather various date fields
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ *
+ * @return array|bool
+ */
+ function getDates() {
+ $this->_parseAll();
+ if ($this->_markers == null) {
+ if (@isset($this->_info['file']['UnixTime'])) {
+ $dates = array();
+ $dates['FileModified'] = $this->_info['file']['UnixTime'];
+ $dates['Time'] = $this->_info['file']['UnixTime'];
+ $dates['TimeSource'] = 'FileModified';
+ $dates['TimeStr'] = date("Y-m-d H:i:s", $this->_info['file']['UnixTime']);
+ $dates['EarliestTime'] = $this->_info['file']['UnixTime'];
+ $dates['EarliestTimeSource'] = 'FileModified';
+ $dates['EarliestTimeStr'] = date("Y-m-d H:i:s", $this->_info['file']['UnixTime']);
+ $dates['LatestTime'] = $this->_info['file']['UnixTime'];
+ $dates['LatestTimeSource'] = 'FileModified';
+ $dates['LatestTimeStr'] = date("Y-m-d H:i:s", $this->_info['file']['UnixTime']);
+ return $dates;
+ }
+ return false;
+ }
+
+ $dates = array();
+
+ $latestTime = 0;
+ $latestTimeSource = "";
+ $earliestTime = time();
+ $earliestTimeSource = "";
+
+ if (@isset($this->_info['exif']['DateTime'])) {
+ $dates['ExifDateTime'] = $this->_info['exif']['DateTime'];
+
+ $aux = $this->_info['exif']['DateTime'];
+ $aux[4] = "-";
+ $aux[7] = "-";
+ $t = strtotime($aux);
+
+ if ($t && $t > $latestTime) {
+ $latestTime = $t;
+ $latestTimeSource = "ExifDateTime";
+ }
+
+ if ($t && $t < $earliestTime) {
+ $earliestTime = $t;
+ $earliestTimeSource = "ExifDateTime";
+ }
+ }
+
+ if (@isset($this->_info['exif']['DateTimeOriginal'])) {
+ $dates['ExifDateTimeOriginal'] = $this->_info['exif']['DateTime'];
+
+ $aux = $this->_info['exif']['DateTimeOriginal'];
+ $aux[4] = "-";
+ $aux[7] = "-";
+ $t = strtotime($aux);
+
+ if ($t && $t > $latestTime) {
+ $latestTime = $t;
+ $latestTimeSource = "ExifDateTimeOriginal";
+ }
+
+ if ($t && $t < $earliestTime) {
+ $earliestTime = $t;
+ $earliestTimeSource = "ExifDateTimeOriginal";
+ }
+ }
+
+ if (@isset($this->_info['exif']['DateTimeDigitized'])) {
+ $dates['ExifDateTimeDigitized'] = $this->_info['exif']['DateTime'];
+
+ $aux = $this->_info['exif']['DateTimeDigitized'];
+ $aux[4] = "-";
+ $aux[7] = "-";
+ $t = strtotime($aux);
+
+ if ($t && $t > $latestTime) {
+ $latestTime = $t;
+ $latestTimeSource = "ExifDateTimeDigitized";
+ }
+
+ if ($t && $t < $earliestTime) {
+ $earliestTime = $t;
+ $earliestTimeSource = "ExifDateTimeDigitized";
+ }
+ }
+
+ if (@isset($this->_info['iptc']['DateCreated'])) {
+ $dates['IPTCDateCreated'] = $this->_info['iptc']['DateCreated'];
+
+ $aux = $this->_info['iptc']['DateCreated'];
+ $aux = substr($aux, 0, 4) . "-" . substr($aux, 4, 2) . "-" . substr($aux, 6, 2);
+ $t = strtotime($aux);
+
+ if ($t && $t > $latestTime) {
+ $latestTime = $t;
+ $latestTimeSource = "IPTCDateCreated";
+ }
+
+ if ($t && $t < $earliestTime) {
+ $earliestTime = $t;
+ $earliestTimeSource = "IPTCDateCreated";
+ }
+ }
+
+ if (@isset($this->_info['file']['UnixTime'])) {
+ $dates['FileModified'] = $this->_info['file']['UnixTime'];
+
+ $t = $this->_info['file']['UnixTime'];
+
+ if ($t && $t > $latestTime) {
+ $latestTime = $t;
+ $latestTimeSource = "FileModified";
+ }
+
+ if ($t && $t < $earliestTime) {
+ $earliestTime = $t;
+ $earliestTimeSource = "FileModified";
+ }
+ }
+
+ $dates['Time'] = $earliestTime;
+ $dates['TimeSource'] = $earliestTimeSource;
+ $dates['TimeStr'] = date("Y-m-d H:i:s", $earliestTime);
+ $dates['EarliestTime'] = $earliestTime;
+ $dates['EarliestTimeSource'] = $earliestTimeSource;
+ $dates['EarliestTimeStr'] = date("Y-m-d H:i:s", $earliestTime);
+ $dates['LatestTime'] = $latestTime;
+ $dates['LatestTimeSource'] = $latestTimeSource;
+ $dates['LatestTimeStr'] = date("Y-m-d H:i:s", $latestTime);
+
+ return $dates;
+ }
+
+ /**
+ * Get the image width, tries various fields
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ *
+ * @return false|string
+ */
+ function getWidth() {
+ if (!isset($this->_info['sof'])) {
+ $this->_parseMarkerSOF();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if (isset($this->_info['sof']['ImageWidth'])) {
+ return $this->_info['sof']['ImageWidth'];
+ }
+
+ if (!isset($this->_info['exif'])) {
+ $this->_parseMarkerExif();
+ }
+
+ if (isset($this->_info['exif']['PixelXDimension'])) {
+ return $this->_info['exif']['PixelXDimension'];
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the image height, tries various fields
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ *
+ * @return false|string
+ */
+ function getHeight() {
+ if (!isset($this->_info['sof'])) {
+ $this->_parseMarkerSOF();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if (isset($this->_info['sof']['ImageHeight'])) {
+ return $this->_info['sof']['ImageHeight'];
+ }
+
+ if (!isset($this->_info['exif'])) {
+ $this->_parseMarkerExif();
+ }
+
+ if (isset($this->_info['exif']['PixelYDimension'])) {
+ return $this->_info['exif']['PixelYDimension'];
+ }
+
+ return false;
+ }
+
+ /**
+ * Get an dimension string for use in img tag
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ *
+ * @return false|string
+ */
+ function getDimStr() {
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ $w = $this->getWidth();
+ $h = $this->getHeight();
+
+ return "width='" . $w . "' height='" . $h . "'";
+ }
+
+ /**
+ * Checks for an embedded thumbnail
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ *
+ * @param string $which possible values: 'any', 'exif' or 'adobe'
+ * @return false|string
+ */
+ function hasThumbnail($which = 'any') {
+ if (($which == 'any') || ($which == 'exif')) {
+ if (!isset($this->_info['exif'])) {
+ $this->_parseMarkerExif();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if (isset($this->_info['exif']) && is_array($this->_info['exif'])) {
+ if (isset($this->_info['exif']['JFIFThumbnail'])) {
+ return 'exif';
+ }
+ }
+ }
+
+ if ($which == 'adobe') {
+ if (!isset($this->_info['adobe'])) {
+ $this->_parseMarkerAdobe();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if (isset($this->_info['adobe']) && is_array($this->_info['adobe'])) {
+ if (isset($this->_info['adobe']['ThumbnailData'])) {
+ return 'exif';
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Send embedded thumbnail to browser
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ *
+ * @param string $which possible values: 'any', 'exif' or 'adobe'
+ * @return bool
+ */
+ function sendThumbnail($which = 'any') {
+ $data = null;
+
+ if (($which == 'any') || ($which == 'exif')) {
+ if (!isset($this->_info['exif'])) {
+ $this->_parseMarkerExif();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if (isset($this->_info['exif']) && is_array($this->_info['exif'])) {
+ if (isset($this->_info['exif']['JFIFThumbnail'])) {
+ $data =& $this->_info['exif']['JFIFThumbnail'];
+ }
+ }
+ }
+
+ if (($which == 'adobe') || ($data == null)){
+ if (!isset($this->_info['adobe'])) {
+ $this->_parseMarkerAdobe();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if (isset($this->_info['adobe']) && is_array($this->_info['adobe'])) {
+ if (isset($this->_info['adobe']['ThumbnailData'])) {
+ $data =& $this->_info['adobe']['ThumbnailData'];
+ }
+ }
+ }
+
+ if ($data != null) {
+ header("Content-type: image/jpeg");
+ echo $data;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Save changed Metadata
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $fileName file name or empty string for a random name
+ * @return bool
+ */
+ function save($fileName = "") {
+ if ($fileName == "") {
+ $tmpName = tempnam(dirname($this->_fileName),'_metatemp_');
+ $this->_writeJPEG($tmpName);
+ if (file_exists($tmpName)) {
+ return io_rename($tmpName, $this->_fileName);
+ }
+ } else {
+ return $this->_writeJPEG($fileName);
+ }
+ return false;
+ }
+
+ /*************************************************************/
+ /* PRIVATE FUNCTIONS (Internal Use Only!) */
+ /*************************************************************/
+
+ /*************************************************************/
+ function _dispose($fileName = "") {
+ $this->_fileName = $fileName;
+
+ $this->_fp = null;
+ $this->_type = 'unknown';
+
+ unset($this->_markers);
+ unset($this->_info);
+ }
+
+ /*************************************************************/
+ function _readJPEG() {
+ unset($this->_markers);
+ //unset($this->_info);
+ $this->_markers = array();
+ //$this->_info = array();
+
+ $this->_fp = @fopen($this->_fileName, 'rb');
+ if ($this->_fp) {
+ if (file_exists($this->_fileName)) {
+ $this->_type = 'file';
+ }
+ else {
+ $this->_type = 'url';
+ }
+ } else {
+ $this->_fp = null;
+ return false; // ERROR: Can't open file
+ }
+
+ // Check for the JPEG signature
+ $c1 = ord(fgetc($this->_fp));
+ $c2 = ord(fgetc($this->_fp));
+
+ if ($c1 != 0xFF || $c2 != 0xD8) { // (0xFF + SOI)
+ $this->_markers = null;
+ return false; // ERROR: File is not a JPEG
+ }
+
+ $count = 0;
+
+ $done = false;
+ $ok = true;
+
+ while (!$done) {
+ $capture = false;
+
+ // First, skip any non 0xFF bytes
+ $discarded = 0;
+ $c = ord(fgetc($this->_fp));
+ while (!feof($this->_fp) && ($c != 0xFF)) {
+ $discarded++;
+ $c = ord(fgetc($this->_fp));
+ }
+ // Then skip all 0xFF until the marker byte
+ do {
+ $marker = ord(fgetc($this->_fp));
+ } while (!feof($this->_fp) && ($marker == 0xFF));
+
+ if (feof($this->_fp)) {
+ return false; // ERROR: Unexpected EOF
+ }
+ if ($discarded != 0) {
+ return false; // ERROR: Extraneous data
+ }
+
+ $length = ord(fgetc($this->_fp)) * 256 + ord(fgetc($this->_fp));
+ if (feof($this->_fp)) {
+ return false; // ERROR: Unexpected EOF
+ }
+ if ($length < 2) {
+ return false; // ERROR: Extraneous data
+ }
+ $length = $length - 2; // The length we got counts itself
+
+ switch ($marker) {
+ case 0xC0: // SOF0
+ case 0xC1: // SOF1
+ case 0xC2: // SOF2
+ case 0xC9: // SOF9
+ case 0xE0: // APP0: JFIF data
+ case 0xE1: // APP1: EXIF or XMP data
+ case 0xED: // APP13: IPTC / Photoshop data
+ $capture = true;
+ break;
+ case 0xDA: // SOS: Start of scan... the image itself and the last block on the file
+ $capture = false;
+ $length = -1; // This field has no length... it includes all data until EOF
+ $done = true;
+ break;
+ default:
+ $capture = true;//false;
+ break;
+ }
+
+ $this->_markers[$count] = array();
+ $this->_markers[$count]['marker'] = $marker;
+ $this->_markers[$count]['length'] = $length;
+
+ if ($capture) {
+ if ($length)
+ $this->_markers[$count]['data'] = fread($this->_fp, $length);
+ else
+ $this->_markers[$count]['data'] = "";
+ }
+ elseif (!$done) {
+ $result = @fseek($this->_fp, $length, SEEK_CUR);
+ // fseek doesn't seem to like HTTP 'files', but fgetc has no problem
+ if (!($result === 0)) {
+ for ($i = 0; $i < $length; $i++) {
+ fgetc($this->_fp);
+ }
+ }
+ }
+ $count++;
+ }
+
+ if ($this->_fp) {
+ fclose($this->_fp);
+ $this->_fp = null;
+ }
+
+ return $ok;
+ }
+
+ /*************************************************************/
+ function _parseAll() {
+ if (!isset($this->_info['file'])) {
+ $this->_parseFileInfo();
+ }
+ if (!isset($this->_markers)) {
+ $this->_readJPEG();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ if (!isset($this->_info['jfif'])) {
+ $this->_parseMarkerJFIF();
+ }
+ if (!isset($this->_info['jpeg'])) {
+ $this->_parseMarkerSOF();
+ }
+ if (!isset($this->_info['exif'])) {
+ $this->_parseMarkerExif();
+ }
+ if (!isset($this->_info['xmp'])) {
+ $this->_parseMarkerXmp();
+ }
+ if (!isset($this->_info['adobe'])) {
+ $this->_parseMarkerAdobe();
+ }
+ }
+
+ /*************************************************************/
+
+ /**
+ * @param string $outputName
+ *
+ * @return bool
+ */
+ function _writeJPEG($outputName) {
+ $this->_parseAll();
+
+ $wroteEXIF = false;
+ $wroteAdobe = false;
+
+ $this->_fp = @fopen($this->_fileName, 'r');
+ if ($this->_fp) {
+ if (file_exists($this->_fileName)) {
+ $this->_type = 'file';
+ }
+ else {
+ $this->_type = 'url';
+ }
+ } else {
+ $this->_fp = null;
+ return false; // ERROR: Can't open file
+ }
+
+ $this->_fpout = fopen($outputName, 'wb');
+ if (!$this->_fpout) {
+ $this->_fpout = null;
+ fclose($this->_fp);
+ $this->_fp = null;
+ return false; // ERROR: Can't open output file
+ }
+
+ // Check for the JPEG signature
+ $c1 = ord(fgetc($this->_fp));
+ $c2 = ord(fgetc($this->_fp));
+
+ if ($c1 != 0xFF || $c2 != 0xD8) { // (0xFF + SOI)
+ return false; // ERROR: File is not a JPEG
+ }
+
+ fputs($this->_fpout, chr(0xFF), 1);
+ fputs($this->_fpout, chr(0xD8), 1); // (0xFF + SOI)
+
+ $count = 0;
+
+ $done = false;
+ $ok = true;
+
+ while (!$done) {
+ // First, skip any non 0xFF bytes
+ $discarded = 0;
+ $c = ord(fgetc($this->_fp));
+ while (!feof($this->_fp) && ($c != 0xFF)) {
+ $discarded++;
+ $c = ord(fgetc($this->_fp));
+ }
+ // Then skip all 0xFF until the marker byte
+ do {
+ $marker = ord(fgetc($this->_fp));
+ } while (!feof($this->_fp) && ($marker == 0xFF));
+
+ if (feof($this->_fp)) {
+ $ok = false;
+ break; // ERROR: Unexpected EOF
+ }
+ if ($discarded != 0) {
+ $ok = false;
+ break; // ERROR: Extraneous data
+ }
+
+ $length = ord(fgetc($this->_fp)) * 256 + ord(fgetc($this->_fp));
+ if (feof($this->_fp)) {
+ $ok = false;
+ break; // ERROR: Unexpected EOF
+ }
+ if ($length < 2) {
+ $ok = false;
+ break; // ERROR: Extraneous data
+ }
+ $length = $length - 2; // The length we got counts itself
+
+ unset($data);
+ if ($marker == 0xE1) { // APP1: EXIF data
+ $data =& $this->_createMarkerEXIF();
+ $wroteEXIF = true;
+ }
+ elseif ($marker == 0xED) { // APP13: IPTC / Photoshop data
+ $data =& $this->_createMarkerAdobe();
+ $wroteAdobe = true;
+ }
+ elseif ($marker == 0xDA) { // SOS: Start of scan... the image itself and the last block on the file
+ $done = true;
+ }
+
+ if (!$wroteEXIF && (($marker < 0xE0) || ($marker > 0xEF))) {
+ if (isset($this->_info['exif']) && is_array($this->_info['exif'])) {
+ $exif =& $this->_createMarkerEXIF();
+ $this->_writeJPEGMarker(0xE1, strlen($exif), $exif, 0);
+ unset($exif);
+ }
+ $wroteEXIF = true;
+ }
+
+ if (!$wroteAdobe && (($marker < 0xE0) || ($marker > 0xEF))) {
+ if ((isset($this->_info['adobe']) && is_array($this->_info['adobe']))
+ || (isset($this->_info['iptc']) && is_array($this->_info['iptc']))) {
+ $adobe =& $this->_createMarkerAdobe();
+ $this->_writeJPEGMarker(0xED, strlen($adobe), $adobe, 0);
+ unset($adobe);
+ }
+ $wroteAdobe = true;
+ }
+
+ $origLength = $length;
+ if (isset($data)) {
+ $length = strlen($data);
+ }
+
+ if ($marker != -1) {
+ $this->_writeJPEGMarker($marker, $length, $data, $origLength);
+ }
+ }
+
+ if ($this->_fp) {
+ fclose($this->_fp);
+ $this->_fp = null;
+ }
+
+ if ($this->_fpout) {
+ fclose($this->_fpout);
+ $this->_fpout = null;
+ }
+
+ return $ok;
+ }
+
+ /*************************************************************/
+
+ /**
+ * @param integer $marker
+ * @param integer $length
+ * @param string $data
+ * @param integer $origLength
+ *
+ * @return bool
+ */
+ function _writeJPEGMarker($marker, $length, &$data, $origLength) {
+ if ($length <= 0) {
+ return false;
+ }
+
+ fputs($this->_fpout, chr(0xFF), 1);
+ fputs($this->_fpout, chr($marker), 1);
+ fputs($this->_fpout, chr((($length + 2) & 0x0000FF00) >> 8), 1);
+ fputs($this->_fpout, chr((($length + 2) & 0x000000FF) >> 0), 1);
+
+ if (isset($data)) {
+ // Copy the generated data
+ fputs($this->_fpout, $data, $length);
+
+ if ($origLength > 0) { // Skip the original data
+ $result = @fseek($this->_fp, $origLength, SEEK_CUR);
+ // fseek doesn't seem to like HTTP 'files', but fgetc has no problem
+ if ($result != 0) {
+ for ($i = 0; $i < $origLength; $i++) {
+ fgetc($this->_fp);
+ }
+ }
+ }
+ } else {
+ if ($marker == 0xDA) { // Copy until EOF
+ while (!feof($this->_fp)) {
+ $data = fread($this->_fp, 1024 * 16);
+ fputs($this->_fpout, $data, strlen($data));
+ }
+ } else { // Copy only $length bytes
+ $data = @fread($this->_fp, $length);
+ fputs($this->_fpout, $data, $length);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Gets basic info from the file - should work with non-JPEGs
+ *
+ * @author Sebastian Delmont <sdelmont@zonageek.com>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ function _parseFileInfo() {
+ if (file_exists($this->_fileName) && is_file($this->_fileName)) {
+ $this->_info['file'] = array();
+ $this->_info['file']['Name'] = utf8_decodeFN(\dokuwiki\Utf8\PhpString::basename($this->_fileName));
+ $this->_info['file']['Path'] = fullpath($this->_fileName);
+ $this->_info['file']['Size'] = filesize($this->_fileName);
+ if ($this->_info['file']['Size'] < 1024) {
+ $this->_info['file']['NiceSize'] = $this->_info['file']['Size'] . 'B';
+ } elseif ($this->_info['file']['Size'] < (1024 * 1024)) {
+ $this->_info['file']['NiceSize'] = round($this->_info['file']['Size'] / 1024) . 'KB';
+ } elseif ($this->_info['file']['Size'] < (1024 * 1024 * 1024)) {
+ $this->_info['file']['NiceSize'] = round($this->_info['file']['Size'] / (1024*1024)) . 'MB';
+ } else {
+ $this->_info['file']['NiceSize'] = $this->_info['file']['Size'] . 'B';
+ }
+ $this->_info['file']['UnixTime'] = filemtime($this->_fileName);
+
+ // get image size directly from file
+ $size = getimagesize($this->_fileName);
+ $this->_info['file']['Width'] = $size[0];
+ $this->_info['file']['Height'] = $size[1];
+ // set mime types and formats
+ // http://php.net/manual/en/function.getimagesize.php
+ // http://php.net/manual/en/function.image-type-to-mime-type.php
+ switch ($size[2]){
+ case 1:
+ $this->_info['file']['Mime'] = 'image/gif';
+ $this->_info['file']['Format'] = 'GIF';
+ break;
+ case 2:
+ $this->_info['file']['Mime'] = 'image/jpeg';
+ $this->_info['file']['Format'] = 'JPEG';
+ break;
+ case 3:
+ $this->_info['file']['Mime'] = 'image/png';
+ $this->_info['file']['Format'] = 'PNG';
+ break;
+ case 4:
+ $this->_info['file']['Mime'] = 'application/x-shockwave-flash';
+ $this->_info['file']['Format'] = 'SWF';
+ break;
+ case 5:
+ $this->_info['file']['Mime'] = 'image/psd';
+ $this->_info['file']['Format'] = 'PSD';
+ break;
+ case 6:
+ $this->_info['file']['Mime'] = 'image/bmp';
+ $this->_info['file']['Format'] = 'BMP';
+ break;
+ case 7:
+ $this->_info['file']['Mime'] = 'image/tiff';
+ $this->_info['file']['Format'] = 'TIFF (Intel)';
+ break;
+ case 8:
+ $this->_info['file']['Mime'] = 'image/tiff';
+ $this->_info['file']['Format'] = 'TIFF (Motorola)';
+ break;
+ case 9:
+ $this->_info['file']['Mime'] = 'application/octet-stream';
+ $this->_info['file']['Format'] = 'JPC';
+ break;
+ case 10:
+ $this->_info['file']['Mime'] = 'image/jp2';
+ $this->_info['file']['Format'] = 'JP2';
+ break;
+ case 11:
+ $this->_info['file']['Mime'] = 'application/octet-stream';
+ $this->_info['file']['Format'] = 'JPX';
+ break;
+ case 12:
+ $this->_info['file']['Mime'] = 'application/octet-stream';
+ $this->_info['file']['Format'] = 'JB2';
+ break;
+ case 13:
+ $this->_info['file']['Mime'] = 'application/x-shockwave-flash';
+ $this->_info['file']['Format'] = 'SWC';
+ break;
+ case 14:
+ $this->_info['file']['Mime'] = 'image/iff';
+ $this->_info['file']['Format'] = 'IFF';
+ break;
+ case 15:
+ $this->_info['file']['Mime'] = 'image/vnd.wap.wbmp';
+ $this->_info['file']['Format'] = 'WBMP';
+ break;
+ case 16:
+ $this->_info['file']['Mime'] = 'image/xbm';
+ $this->_info['file']['Format'] = 'XBM';
+ break;
+ default:
+ $this->_info['file']['Mime'] = 'image/unknown';
+ }
+ } else {
+ $this->_info['file'] = array();
+ $this->_info['file']['Name'] = \dokuwiki\Utf8\PhpString::basename($this->_fileName);
+ $this->_info['file']['Url'] = $this->_fileName;
+ }
+
+ return true;
+ }
+
+ /*************************************************************/
+ function _parseMarkerJFIF() {
+ if (!isset($this->_markers)) {
+ $this->_readJPEG();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ $data = null;
+ $count = count($this->_markers);
+ for ($i = 0; $i < $count; $i++) {
+ if ($this->_markers[$i]['marker'] == 0xE0) {
+ $signature = $this->_getFixedString($this->_markers[$i]['data'], 0, 4);
+ if ($signature == 'JFIF') {
+ $data =& $this->_markers[$i]['data'];
+ break;
+ }
+ }
+ }
+
+ if ($data == null) {
+ $this->_info['jfif'] = false;
+ return false;
+ }
+
+ $this->_info['jfif'] = array();
+
+ $vmaj = $this->_getByte($data, 5);
+ $vmin = $this->_getByte($data, 6);
+
+ $this->_info['jfif']['Version'] = sprintf('%d.%02d', $vmaj, $vmin);
+
+ $units = $this->_getByte($data, 7);
+ switch ($units) {
+ case 0:
+ $this->_info['jfif']['Units'] = 'pixels';
+ break;
+ case 1:
+ $this->_info['jfif']['Units'] = 'dpi';
+ break;
+ case 2:
+ $this->_info['jfif']['Units'] = 'dpcm';
+ break;
+ default:
+ $this->_info['jfif']['Units'] = 'unknown';
+ break;
+ }
+
+ $xdens = $this->_getShort($data, 8);
+ $ydens = $this->_getShort($data, 10);
+
+ $this->_info['jfif']['XDensity'] = $xdens;
+ $this->_info['jfif']['YDensity'] = $ydens;
+
+ $thumbx = $this->_getByte($data, 12);
+ $thumby = $this->_getByte($data, 13);
+
+ $this->_info['jfif']['ThumbnailWidth'] = $thumbx;
+ $this->_info['jfif']['ThumbnailHeight'] = $thumby;
+
+ return true;
+ }
+
+ /*************************************************************/
+ function _parseMarkerSOF() {
+ if (!isset($this->_markers)) {
+ $this->_readJPEG();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ $data = null;
+ $count = count($this->_markers);
+ for ($i = 0; $i < $count; $i++) {
+ switch ($this->_markers[$i]['marker']) {
+ case 0xC0: // SOF0
+ case 0xC1: // SOF1
+ case 0xC2: // SOF2
+ case 0xC9: // SOF9
+ $data =& $this->_markers[$i]['data'];
+ $marker = $this->_markers[$i]['marker'];
+ break;
+ }
+ }
+
+ if ($data == null) {
+ $this->_info['sof'] = false;
+ return false;
+ }
+
+ $pos = 0;
+ $this->_info['sof'] = array();
+
+ switch ($marker) {
+ case 0xC0: // SOF0
+ $format = 'Baseline';
+ break;
+ case 0xC1: // SOF1
+ $format = 'Progessive';
+ break;
+ case 0xC2: // SOF2
+ $format = 'Non-baseline';
+ break;
+ case 0xC9: // SOF9
+ $format = 'Arithmetic';
+ break;
+ default:
+ return false;
+ }
+
+ $this->_info['sof']['Format'] = $format;
+ $this->_info['sof']['SamplePrecision'] = $this->_getByte($data, $pos + 0);
+ $this->_info['sof']['ImageHeight'] = $this->_getShort($data, $pos + 1);
+ $this->_info['sof']['ImageWidth'] = $this->_getShort($data, $pos + 3);
+ $this->_info['sof']['ColorChannels'] = $this->_getByte($data, $pos + 5);
+
+ return true;
+ }
+
+ /**
+ * Parses the XMP data
+ *
+ * @author Hakan Sandell <hakan.sandell@mydata.se>
+ */
+ function _parseMarkerXmp() {
+ if (!isset($this->_markers)) {
+ $this->_readJPEG();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ $data = null;
+ $count = count($this->_markers);
+ for ($i = 0; $i < $count; $i++) {
+ if ($this->_markers[$i]['marker'] == 0xE1) {
+ $signature = $this->_getFixedString($this->_markers[$i]['data'], 0, 29);
+ if ($signature == "http://ns.adobe.com/xap/1.0/\0") {
+ $data = substr($this->_markers[$i]['data'], 29);
+ break;
+ }
+ }
+ }
+
+ if ($data == null) {
+ $this->_info['xmp'] = false;
+ return false;
+ }
+
+ $parser = xml_parser_create();
+ xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
+ xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1);
+ $result = xml_parse_into_struct($parser, $data, $values, $tags);
+ xml_parser_free($parser);
+
+ if ($result == 0) {
+ $this->_info['xmp'] = false;
+ return false;
+ }
+
+ $this->_info['xmp'] = array();
+ $count = count($values);
+ for ($i = 0; $i < $count; $i++) {
+ if ($values[$i]['tag'] == 'rdf:Description' && $values[$i]['type'] == 'open') {
+
+ while ((++$i < $count) && ($values[$i]['tag'] != 'rdf:Description')) {
+ $this->_parseXmpNode($values, $i, $this->_info['xmp'][$values[$i]['tag']], $count);
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Parses XMP nodes by recursion
+ *
+ * @author Hakan Sandell <hakan.sandell@mydata.se>
+ *
+ * @param array $values
+ * @param int $i
+ * @param mixed $meta
+ * @param integer $count
+ */
+ function _parseXmpNode($values, &$i, &$meta, $count) {
+ if ($values[$i]['type'] == 'close') return;
+
+ if ($values[$i]['type'] == 'complete') {
+ // Simple Type property
+ $meta = $values[$i]['value'];
+ return;
+ }
+
+ $i++;
+ if ($i >= $count) return;
+
+ if ($values[$i]['tag'] == 'rdf:Bag' || $values[$i]['tag'] == 'rdf:Seq') {
+ // Array property
+ $meta = array();
+ while ($values[++$i]['tag'] == 'rdf:li') {
+ $this->_parseXmpNode($values, $i, $meta[], $count);
+ }
+ $i++; // skip closing Bag/Seq tag
+
+ } elseif ($values[$i]['tag'] == 'rdf:Alt') {
+ // Language Alternative property, only the first (default) value is used
+ if ($values[$i]['type'] == 'open') {
+ $i++;
+ $this->_parseXmpNode($values, $i, $meta, $count);
+ while ((++$i < $count) && ($values[$i]['tag'] != 'rdf:Alt'));
+ $i++; // skip closing Alt tag
+ }
+
+ } else {
+ // Structure property
+ $meta = array();
+ $startTag = $values[$i-1]['tag'];
+ do {
+ $this->_parseXmpNode($values, $i, $meta[$values[$i]['tag']], $count);
+ } while ((++$i < $count) && ($values[$i]['tag'] != $startTag));
+ }
+ }
+
+ /*************************************************************/
+ function _parseMarkerExif() {
+ if (!isset($this->_markers)) {
+ $this->_readJPEG();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ $data = null;
+ $count = count($this->_markers);
+ for ($i = 0; $i < $count; $i++) {
+ if ($this->_markers[$i]['marker'] == 0xE1) {
+ $signature = $this->_getFixedString($this->_markers[$i]['data'], 0, 6);
+ if ($signature == "Exif\0\0") {
+ $data =& $this->_markers[$i]['data'];
+ break;
+ }
+ }
+ }
+
+ if ($data == null) {
+ $this->_info['exif'] = false;
+ return false;
+ }
+ $pos = 6;
+ $this->_info['exif'] = array();
+
+ // We don't increment $pos after this because Exif uses offsets relative to this point
+
+ $byteAlign = $this->_getShort($data, $pos + 0);
+
+ if ($byteAlign == 0x4949) { // "II"
+ $isBigEndian = false;
+ } elseif ($byteAlign == 0x4D4D) { // "MM"
+ $isBigEndian = true;
+ } else {
+ return false; // Unexpected data
+ }
+
+ $alignCheck = $this->_getShort($data, $pos + 2, $isBigEndian);
+ if ($alignCheck != 0x002A) // That's the expected value
+ return false; // Unexpected data
+
+ if ($isBigEndian) {
+ $this->_info['exif']['ByteAlign'] = "Big Endian";
+ } else {
+ $this->_info['exif']['ByteAlign'] = "Little Endian";
+ }
+
+ $offsetIFD0 = $this->_getLong($data, $pos + 4, $isBigEndian);
+ if ($offsetIFD0 < 8)
+ return false; // Unexpected data
+
+ $offsetIFD1 = $this->_readIFD($data, $pos, $offsetIFD0, $isBigEndian, 'ifd0');
+ if ($offsetIFD1 != 0)
+ $this->_readIFD($data, $pos, $offsetIFD1, $isBigEndian, 'ifd1');
+
+ return true;
+ }
+
+ /*************************************************************/
+
+ /**
+ * @param mixed $data
+ * @param integer $base
+ * @param integer $offset
+ * @param boolean $isBigEndian
+ * @param string $mode
+ *
+ * @return int
+ */
+ function _readIFD($data, $base, $offset, $isBigEndian, $mode) {
+ $EXIFTags = $this->_exifTagNames($mode);
+
+ $numEntries = $this->_getShort($data, $base + $offset, $isBigEndian);
+ $offset += 2;
+
+ $exifTIFFOffset = 0;
+ $exifTIFFLength = 0;
+ $exifThumbnailOffset = 0;
+ $exifThumbnailLength = 0;
+
+ for ($i = 0; $i < $numEntries; $i++) {
+ $tag = $this->_getShort($data, $base + $offset, $isBigEndian);
+ $offset += 2;
+ $type = $this->_getShort($data, $base + $offset, $isBigEndian);
+ $offset += 2;
+ $count = $this->_getLong($data, $base + $offset, $isBigEndian);
+ $offset += 4;
+
+ if (($type < 1) || ($type > 12))
+ return false; // Unexpected Type
+
+ $typeLengths = array( -1, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8 );
+
+ $dataLength = $typeLengths[$type] * $count;
+ if ($dataLength > 4) {
+ $dataOffset = $this->_getLong($data, $base + $offset, $isBigEndian);
+ $rawValue = $this->_getFixedString($data, $base + $dataOffset, $dataLength);
+ } else {
+ $rawValue = $this->_getFixedString($data, $base + $offset, $dataLength);
+ }
+ $offset += 4;
+
+ switch ($type) {
+ case 1: // UBYTE
+ if ($count == 1) {
+ $value = $this->_getByte($rawValue, 0);
+ } else {
+ $value = array();
+ for ($j = 0; $j < $count; $j++)
+ $value[$j] = $this->_getByte($rawValue, $j);
+ }
+ break;
+ case 2: // ASCII
+ $value = $rawValue;
+ break;
+ case 3: // USHORT
+ if ($count == 1) {
+ $value = $this->_getShort($rawValue, 0, $isBigEndian);
+ } else {
+ $value = array();
+ for ($j = 0; $j < $count; $j++)
+ $value[$j] = $this->_getShort($rawValue, $j * 2, $isBigEndian);
+ }
+ break;
+ case 4: // ULONG
+ if ($count == 1) {
+ $value = $this->_getLong($rawValue, 0, $isBigEndian);
+ } else {
+ $value = array();
+ for ($j = 0; $j < $count; $j++)
+ $value[$j] = $this->_getLong($rawValue, $j * 4, $isBigEndian);
+ }
+ break;
+ case 5: // URATIONAL
+ if ($count == 1) {
+ $a = $this->_getLong($rawValue, 0, $isBigEndian);
+ $b = $this->_getLong($rawValue, 4, $isBigEndian);
+ $value = array();
+ $value['val'] = 0;
+ $value['num'] = $a;
+ $value['den'] = $b;
+ if (($a != 0) && ($b != 0)) {
+ $value['val'] = $a / $b;
+ }
+ } else {
+ $value = array();
+ for ($j = 0; $j < $count; $j++) {
+ $a = $this->_getLong($rawValue, $j * 8, $isBigEndian);
+ $b = $this->_getLong($rawValue, ($j * 8) + 4, $isBigEndian);
+ $value = array();
+ $value[$j]['val'] = 0;
+ $value[$j]['num'] = $a;
+ $value[$j]['den'] = $b;
+ if (($a != 0) && ($b != 0))
+ $value[$j]['val'] = $a / $b;
+ }
+ }
+ break;
+ case 6: // SBYTE
+ if ($count == 1) {
+ $value = $this->_getByte($rawValue, 0);
+ } else {
+ $value = array();
+ for ($j = 0; $j < $count; $j++)
+ $value[$j] = $this->_getByte($rawValue, $j);
+ }
+ break;
+ case 7: // UNDEFINED
+ $value = $rawValue;
+ break;
+ case 8: // SSHORT
+ if ($count == 1) {
+ $value = $this->_getShort($rawValue, 0, $isBigEndian);
+ } else {
+ $value = array();
+ for ($j = 0; $j < $count; $j++)
+ $value[$j] = $this->_getShort($rawValue, $j * 2, $isBigEndian);
+ }
+ break;
+ case 9: // SLONG
+ if ($count == 1) {
+ $value = $this->_getLong($rawValue, 0, $isBigEndian);
+ } else {
+ $value = array();
+ for ($j = 0; $j < $count; $j++)
+ $value[$j] = $this->_getLong($rawValue, $j * 4, $isBigEndian);
+ }
+ break;
+ case 10: // SRATIONAL
+ if ($count == 1) {
+ $a = $this->_getLong($rawValue, 0, $isBigEndian);
+ $b = $this->_getLong($rawValue, 4, $isBigEndian);
+ $value = array();
+ $value['val'] = 0;
+ $value['num'] = $a;
+ $value['den'] = $b;
+ if (($a != 0) && ($b != 0))
+ $value['val'] = $a / $b;
+ } else {
+ $value = array();
+ for ($j = 0; $j < $count; $j++) {
+ $a = $this->_getLong($rawValue, $j * 8, $isBigEndian);
+ $b = $this->_getLong($rawValue, ($j * 8) + 4, $isBigEndian);
+ $value = array();
+ $value[$j]['val'] = 0;
+ $value[$j]['num'] = $a;
+ $value[$j]['den'] = $b;
+ if (($a != 0) && ($b != 0))
+ $value[$j]['val'] = $a / $b;
+ }
+ }
+ break;
+ case 11: // FLOAT
+ $value = $rawValue;
+ break;
+
+ case 12: // DFLOAT
+ $value = $rawValue;
+ break;
+ default:
+ return false; // Unexpected Type
+ }
+
+ $tagName = '';
+ if (($mode == 'ifd0') && ($tag == 0x8769)) { // ExifIFDOffset
+ $this->_readIFD($data, $base, $value, $isBigEndian, 'exif');
+ } elseif (($mode == 'ifd0') && ($tag == 0x8825)) { // GPSIFDOffset
+ $this->_readIFD($data, $base, $value, $isBigEndian, 'gps');
+ } elseif (($mode == 'ifd1') && ($tag == 0x0111)) { // TIFFStripOffsets
+ $exifTIFFOffset = $value;
+ } elseif (($mode == 'ifd1') && ($tag == 0x0117)) { // TIFFStripByteCounts
+ $exifTIFFLength = $value;
+ } elseif (($mode == 'ifd1') && ($tag == 0x0201)) { // TIFFJFIFOffset
+ $exifThumbnailOffset = $value;
+ } elseif (($mode == 'ifd1') && ($tag == 0x0202)) { // TIFFJFIFLength
+ $exifThumbnailLength = $value;
+ } elseif (($mode == 'exif') && ($tag == 0xA005)) { // InteropIFDOffset
+ $this->_readIFD($data, $base, $value, $isBigEndian, 'interop');
+ }
+ // elseif (($mode == 'exif') && ($tag == 0x927C)) { // MakerNote
+ // }
+ else {
+ if (isset($EXIFTags[$tag])) {
+ $tagName = $EXIFTags[$tag];
+ if (isset($this->_info['exif'][$tagName])) {
+ if (!is_array($this->_info['exif'][$tagName])) {
+ $aux = array();
+ $aux[0] = $this->_info['exif'][$tagName];
+ $this->_info['exif'][$tagName] = $aux;
+ }
+
+ $this->_info['exif'][$tagName][count($this->_info['exif'][$tagName])] = $value;
+ } else {
+ $this->_info['exif'][$tagName] = $value;
+ }
+ }
+ /*
+ else {
+ echo sprintf("<h1>Unknown tag %02x (t: %d l: %d) %s in %s</h1>", $tag, $type, $count, $mode, $this->_fileName);
+ // Unknown Tags will be ignored!!!
+ // That's because the tag might be a pointer (like the Exif tag)
+ // and saving it without saving the data it points to might
+ // create an invalid file.
+ }
+ */
+ }
+ }
+
+ if (($exifThumbnailOffset > 0) && ($exifThumbnailLength > 0)) {
+ $this->_info['exif']['JFIFThumbnail'] = $this->_getFixedString($data, $base + $exifThumbnailOffset, $exifThumbnailLength);
+ }
+
+ if (($exifTIFFOffset > 0) && ($exifTIFFLength > 0)) {
+ $this->_info['exif']['TIFFStrips'] = $this->_getFixedString($data, $base + $exifTIFFOffset, $exifTIFFLength);
+ }
+
+ $nextOffset = $this->_getLong($data, $base + $offset, $isBigEndian);
+ return $nextOffset;
+ }
+
+ /*************************************************************/
+ function & _createMarkerExif() {
+ $data = null;
+ $count = count($this->_markers);
+ for ($i = 0; $i < $count; $i++) {
+ if ($this->_markers[$i]['marker'] == 0xE1) {
+ $signature = $this->_getFixedString($this->_markers[$i]['data'], 0, 6);
+ if ($signature == "Exif\0\0") {
+ $data =& $this->_markers[$i]['data'];
+ break;
+ }
+ }
+ }
+
+ if (!isset($this->_info['exif'])) {
+ return false;
+ }
+
+ $data = "Exif\0\0";
+ $pos = 6;
+ $offsetBase = 6;
+
+ if (isset($this->_info['exif']['ByteAlign']) && ($this->_info['exif']['ByteAlign'] == "Big Endian")) {
+ $isBigEndian = true;
+ $aux = "MM";
+ $pos = $this->_putString($data, $pos, $aux);
+ } else {
+ $isBigEndian = false;
+ $aux = "II";
+ $pos = $this->_putString($data, $pos, $aux);
+ }
+ $pos = $this->_putShort($data, $pos, 0x002A, $isBigEndian);
+ $pos = $this->_putLong($data, $pos, 0x00000008, $isBigEndian); // IFD0 Offset is always 8
+
+ $ifd0 =& $this->_getIFDEntries($isBigEndian, 'ifd0');
+ $ifd1 =& $this->_getIFDEntries($isBigEndian, 'ifd1');
+
+ $pos = $this->_writeIFD($data, $pos, $offsetBase, $ifd0, $isBigEndian, true);
+ $pos = $this->_writeIFD($data, $pos, $offsetBase, $ifd1, $isBigEndian, false);
+
+ return $data;
+ }
+
+ /*************************************************************/
+
+ /**
+ * @param mixed $data
+ * @param integer $pos
+ * @param integer $offsetBase
+ * @param array $entries
+ * @param boolean $isBigEndian
+ * @param boolean $hasNext
+ *
+ * @return mixed
+ */
+ function _writeIFD(&$data, $pos, $offsetBase, &$entries, $isBigEndian, $hasNext) {
+ $tiffData = null;
+ $tiffDataOffsetPos = -1;
+
+ $entryCount = count($entries);
+
+ $dataPos = $pos + 2 + ($entryCount * 12) + 4;
+ $pos = $this->_putShort($data, $pos, $entryCount, $isBigEndian);
+
+ for ($i = 0; $i < $entryCount; $i++) {
+ $tag = $entries[$i]['tag'];
+ $type = $entries[$i]['type'];
+
+ if ($type == -99) { // SubIFD
+ $pos = $this->_putShort($data, $pos, $tag, $isBigEndian);
+ $pos = $this->_putShort($data, $pos, 0x04, $isBigEndian); // LONG
+ $pos = $this->_putLong($data, $pos, 0x01, $isBigEndian); // Count = 1
+ $pos = $this->_putLong($data, $pos, $dataPos - $offsetBase, $isBigEndian);
+
+ $dataPos = $this->_writeIFD($data, $dataPos, $offsetBase, $entries[$i]['value'], $isBigEndian, false);
+ } elseif ($type == -98) { // TIFF Data
+ $pos = $this->_putShort($data, $pos, $tag, $isBigEndian);
+ $pos = $this->_putShort($data, $pos, 0x04, $isBigEndian); // LONG
+ $pos = $this->_putLong($data, $pos, 0x01, $isBigEndian); // Count = 1
+ $tiffDataOffsetPos = $pos;
+ $pos = $this->_putLong($data, $pos, 0x00, $isBigEndian); // For Now
+ $tiffData =& $entries[$i]['value'] ;
+ } else { // Regular Entry
+ $pos = $this->_putShort($data, $pos, $tag, $isBigEndian);
+ $pos = $this->_putShort($data, $pos, $type, $isBigEndian);
+ $pos = $this->_putLong($data, $pos, $entries[$i]['count'], $isBigEndian);
+ if (strlen($entries[$i]['value']) > 4) {
+ $pos = $this->_putLong($data, $pos, $dataPos - $offsetBase, $isBigEndian);
+ $dataPos = $this->_putString($data, $dataPos, $entries[$i]['value']);
+ } else {
+ $val = str_pad($entries[$i]['value'], 4, "\0");
+ $pos = $this->_putString($data, $pos, $val);
+ }
+ }
+ }
+
+ if ($tiffData != null) {
+ $this->_putLong($data, $tiffDataOffsetPos, $dataPos - $offsetBase, $isBigEndian);
+ $dataPos = $this->_putString($data, $dataPos, $tiffData);
+ }
+
+ if ($hasNext) {
+ $pos = $this->_putLong($data, $pos, $dataPos - $offsetBase, $isBigEndian);
+ } else {
+ $pos = $this->_putLong($data, $pos, 0, $isBigEndian);
+ }
+
+ return $dataPos;
+ }
+
+ /*************************************************************/
+
+ /**
+ * @param boolean $isBigEndian
+ * @param string $mode
+ *
+ * @return array
+ */
+ function & _getIFDEntries($isBigEndian, $mode) {
+ $EXIFNames = $this->_exifTagNames($mode);
+ $EXIFTags = $this->_exifNameTags($mode);
+ $EXIFTypeInfo = $this->_exifTagTypes($mode);
+
+ $ifdEntries = array();
+ $entryCount = 0;
+
+ foreach($EXIFNames as $tag => $name) {
+ $type = $EXIFTypeInfo[$tag][0];
+ $count = $EXIFTypeInfo[$tag][1];
+ $value = null;
+
+ if (($mode == 'ifd0') && ($tag == 0x8769)) { // ExifIFDOffset
+ if (isset($this->_info['exif']['EXIFVersion'])) {
+ $value =& $this->_getIFDEntries($isBigEndian, "exif");
+ $type = -99;
+ }
+ else {
+ $value = null;
+ }
+ } elseif (($mode == 'ifd0') && ($tag == 0x8825)) { // GPSIFDOffset
+ if (isset($this->_info['exif']['GPSVersionID'])) {
+ $value =& $this->_getIFDEntries($isBigEndian, "gps");
+ $type = -99;
+ } else {
+ $value = null;
+ }
+ } elseif (($mode == 'ifd1') && ($tag == 0x0111)) { // TIFFStripOffsets
+ if (isset($this->_info['exif']['TIFFStrips'])) {
+ $value =& $this->_info['exif']['TIFFStrips'];
+ $type = -98;
+ } else {
+ $value = null;
+ }
+ } elseif (($mode == 'ifd1') && ($tag == 0x0117)) { // TIFFStripByteCounts
+ if (isset($this->_info['exif']['TIFFStrips'])) {
+ $value = strlen($this->_info['exif']['TIFFStrips']);
+ } else {
+ $value = null;
+ }
+ } elseif (($mode == 'ifd1') && ($tag == 0x0201)) { // TIFFJFIFOffset
+ if (isset($this->_info['exif']['JFIFThumbnail'])) {
+ $value =& $this->_info['exif']['JFIFThumbnail'];
+ $type = -98;
+ } else {
+ $value = null;
+ }
+ } elseif (($mode == 'ifd1') && ($tag == 0x0202)) { // TIFFJFIFLength
+ if (isset($this->_info['exif']['JFIFThumbnail'])) {
+ $value = strlen($this->_info['exif']['JFIFThumbnail']);
+ } else {
+ $value = null;
+ }
+ } elseif (($mode == 'exif') && ($tag == 0xA005)) { // InteropIFDOffset
+ if (isset($this->_info['exif']['InteroperabilityIndex'])) {
+ $value =& $this->_getIFDEntries($isBigEndian, "interop");
+ $type = -99;
+ } else {
+ $value = null;
+ }
+ } elseif (isset($this->_info['exif'][$name])) {
+ $origValue =& $this->_info['exif'][$name];
+
+ // This makes it easier to process variable size elements
+ if (!is_array($origValue) || isset($origValue['val'])) {
+ unset($origValue); // Break the reference
+ $origValue = array($this->_info['exif'][$name]);
+ }
+ $origCount = count($origValue);
+
+ if ($origCount == 0 ) {
+ $type = -1; // To ignore this field
+ }
+
+ $value = " ";
+
+ switch ($type) {
+ case 1: // UBYTE
+ if ($count == 0) {
+ $count = $origCount;
+ }
+
+ $j = 0;
+ while (($j < $count) && ($j < $origCount)) {
+
+ $this->_putByte($value, $j, $origValue[$j]);
+ $j++;
+ }
+
+ while ($j < $count) {
+ $this->_putByte($value, $j, 0);
+ $j++;
+ }
+ break;
+ case 2: // ASCII
+ $v = strval($origValue[0]);
+ if (($count != 0) && (strlen($v) > $count)) {
+ $v = substr($v, 0, $count);
+ }
+ elseif (($count > 0) && (strlen($v) < $count)) {
+ $v = str_pad($v, $count, "\0");
+ }
+
+ $count = strlen($v);
+
+ $this->_putString($value, 0, $v);
+ break;
+ case 3: // USHORT
+ if ($count == 0) {
+ $count = $origCount;
+ }
+
+ $j = 0;
+ while (($j < $count) && ($j < $origCount)) {
+ $this->_putShort($value, $j * 2, $origValue[$j], $isBigEndian);
+ $j++;
+ }
+
+ while ($j < $count) {
+ $this->_putShort($value, $j * 2, 0, $isBigEndian);
+ $j++;
+ }
+ break;
+ case 4: // ULONG
+ if ($count == 0) {
+ $count = $origCount;
+ }
+
+ $j = 0;
+ while (($j < $count) && ($j < $origCount)) {
+ $this->_putLong($value, $j * 4, $origValue[$j], $isBigEndian);
+ $j++;
+ }
+
+ while ($j < $count) {
+ $this->_putLong($value, $j * 4, 0, $isBigEndian);
+ $j++;
+ }
+ break;
+ case 5: // URATIONAL
+ if ($count == 0) {
+ $count = $origCount;
+ }
+
+ $j = 0;
+ while (($j < $count) && ($j < $origCount)) {
+ $v = $origValue[$j];
+ if (is_array($v)) {
+ $a = $v['num'];
+ $b = $v['den'];
+ }
+ else {
+ $a = 0;
+ $b = 0;
+ // TODO: Allow other types and convert them
+ }
+ $this->_putLong($value, $j * 8, $a, $isBigEndian);
+ $this->_putLong($value, ($j * 8) + 4, $b, $isBigEndian);
+ $j++;
+ }
+
+ while ($j < $count) {
+ $this->_putLong($value, $j * 8, 0, $isBigEndian);
+ $this->_putLong($value, ($j * 8) + 4, 0, $isBigEndian);
+ $j++;
+ }
+ break;
+ case 6: // SBYTE
+ if ($count == 0) {
+ $count = $origCount;
+ }
+
+ $j = 0;
+ while (($j < $count) && ($j < $origCount)) {
+ $this->_putByte($value, $j, $origValue[$j]);
+ $j++;
+ }
+
+ while ($j < $count) {
+ $this->_putByte($value, $j, 0);
+ $j++;
+ }
+ break;
+ case 7: // UNDEFINED
+ $v = strval($origValue[0]);
+ if (($count != 0) && (strlen($v) > $count)) {
+ $v = substr($v, 0, $count);
+ }
+ elseif (($count > 0) && (strlen($v) < $count)) {
+ $v = str_pad($v, $count, "\0");
+ }
+
+ $count = strlen($v);
+
+ $this->_putString($value, 0, $v);
+ break;
+ case 8: // SSHORT
+ if ($count == 0) {
+ $count = $origCount;
+ }
+
+ $j = 0;
+ while (($j < $count) && ($j < $origCount)) {
+ $this->_putShort($value, $j * 2, $origValue[$j], $isBigEndian);
+ $j++;
+ }
+
+ while ($j < $count) {
+ $this->_putShort($value, $j * 2, 0, $isBigEndian);
+ $j++;
+ }
+ break;
+ case 9: // SLONG
+ if ($count == 0) {
+ $count = $origCount;
+ }
+
+ $j = 0;
+ while (($j < $count) && ($j < $origCount)) {
+ $this->_putLong($value, $j * 4, $origValue[$j], $isBigEndian);
+ $j++;
+ }
+
+ while ($j < $count) {
+ $this->_putLong($value, $j * 4, 0, $isBigEndian);
+ $j++;
+ }
+ break;
+ case 10: // SRATIONAL
+ if ($count == 0) {
+ $count = $origCount;
+ }
+
+ $j = 0;
+ while (($j < $count) && ($j < $origCount)) {
+ $v = $origValue[$j];
+ if (is_array($v)) {
+ $a = $v['num'];
+ $b = $v['den'];
+ }
+ else {
+ $a = 0;
+ $b = 0;
+ // TODO: Allow other types and convert them
+ }
+
+ $this->_putLong($value, $j * 8, $a, $isBigEndian);
+ $this->_putLong($value, ($j * 8) + 4, $b, $isBigEndian);
+ $j++;
+ }
+
+ while ($j < $count) {
+ $this->_putLong($value, $j * 8, 0, $isBigEndian);
+ $this->_putLong($value, ($j * 8) + 4, 0, $isBigEndian);
+ $j++;
+ }
+ break;
+ case 11: // FLOAT
+ if ($count == 0) {
+ $count = $origCount;
+ }
+
+ $j = 0;
+ while (($j < $count) && ($j < $origCount)) {
+ $v = strval($origValue[$j]);
+ if (strlen($v) > 4) {
+ $v = substr($v, 0, 4);
+ }
+ elseif (strlen($v) < 4) {
+ $v = str_pad($v, 4, "\0");
+ }
+ $this->_putString($value, $j * 4, $v);
+ $j++;
+ }
+
+ while ($j < $count) {
+ $v = "\0\0\0\0";
+ $this->_putString($value, $j * 4, $v);
+ $j++;
+ }
+ break;
+ case 12: // DFLOAT
+ if ($count == 0) {
+ $count = $origCount;
+ }
+
+ $j = 0;
+ while (($j < $count) && ($j < $origCount)) {
+ $v = strval($origValue[$j]);
+ if (strlen($v) > 8) {
+ $v = substr($v, 0, 8);
+ }
+ elseif (strlen($v) < 8) {
+ $v = str_pad($v, 8, "\0");
+ }
+ $this->_putString($value, $j * 8, $v);
+ $j++;
+ }
+
+ while ($j < $count) {
+ $v = "\0\0\0\0\0\0\0\0";
+ $this->_putString($value, $j * 8, $v);
+ $j++;
+ }
+ break;
+ default:
+ $value = null;
+ break;
+ }
+ }
+
+ if ($value != null) {
+ $ifdEntries[$entryCount] = array();
+ $ifdEntries[$entryCount]['tag'] = $tag;
+ $ifdEntries[$entryCount]['type'] = $type;
+ $ifdEntries[$entryCount]['count'] = $count;
+ $ifdEntries[$entryCount]['value'] = $value;
+
+ $entryCount++;
+ }
+ }
+
+ return $ifdEntries;
+ }
+
+ /*************************************************************/
+ function _parseMarkerAdobe() {
+ if (!isset($this->_markers)) {
+ $this->_readJPEG();
+ }
+
+ if ($this->_markers == null) {
+ return false;
+ }
+
+ $data = null;
+ $count = count($this->_markers);
+ for ($i = 0; $i < $count; $i++) {
+ if ($this->_markers[$i]['marker'] == 0xED) {
+ $signature = $this->_getFixedString($this->_markers[$i]['data'], 0, 14);
+ if ($signature == "Photoshop 3.0\0") {
+ $data =& $this->_markers[$i]['data'];
+ break;
+ }
+ }
+ }
+
+ if ($data == null) {
+ $this->_info['adobe'] = false;
+ $this->_info['iptc'] = false;
+ return false;
+ }
+ $pos = 14;
+ $this->_info['adobe'] = array();
+ $this->_info['adobe']['raw'] = array();
+ $this->_info['iptc'] = array();
+
+ $datasize = strlen($data);
+
+ while ($pos < $datasize) {
+ $signature = $this->_getFixedString($data, $pos, 4);
+ if ($signature != '8BIM')
+ return false;
+ $pos += 4;
+
+ $type = $this->_getShort($data, $pos);
+ $pos += 2;
+
+ $strlen = $this->_getByte($data, $pos);
+ $pos += 1;
+ $header = '';
+ for ($i = 0; $i < $strlen; $i++) {
+ $header .= $data[$pos + $i];
+ }
+ $pos += $strlen + 1 - ($strlen % 2); // The string is padded to even length, counting the length byte itself
+
+ $length = $this->_getLong($data, $pos);
+ $pos += 4;
+
+ $basePos = $pos;
+
+ switch ($type) {
+ case 0x0404: // Caption (IPTC Data)
+ $pos = $this->_readIPTC($data, $pos);
+ if ($pos == false)
+ return false;
+ break;
+ case 0x040A: // CopyrightFlag
+ $this->_info['adobe']['CopyrightFlag'] = $this->_getByte($data, $pos);
+ $pos += $length;
+ break;
+ case 0x040B: // ImageURL
+ $this->_info['adobe']['ImageURL'] = $this->_getFixedString($data, $pos, $length);
+ $pos += $length;
+ break;
+ case 0x040C: // Thumbnail
+ $aux = $this->_getLong($data, $pos);
+ $pos += 4;
+ if ($aux == 1) {
+ $this->_info['adobe']['ThumbnailWidth'] = $this->_getLong($data, $pos);
+ $pos += 4;
+ $this->_info['adobe']['ThumbnailHeight'] = $this->_getLong($data, $pos);
+ $pos += 4;
+
+ $pos += 16; // Skip some data
+
+ $this->_info['adobe']['ThumbnailData'] = $this->_getFixedString($data, $pos, $length - 28);
+ $pos += $length - 28;
+ }
+ break;
+ default:
+ break;
+ }
+
+ // We save all blocks, even those we recognized
+ $label = sprintf('8BIM_0x%04x', $type);
+ $this->_info['adobe']['raw'][$label] = array();
+ $this->_info['adobe']['raw'][$label]['type'] = $type;
+ $this->_info['adobe']['raw'][$label]['header'] = $header;
+ $this->_info['adobe']['raw'][$label]['data'] =& $this->_getFixedString($data, $basePos, $length);
+
+ $pos = $basePos + $length + ($length % 2); // Even padding
+ }
+
+ }
+
+ /*************************************************************/
+ function _readIPTC(&$data, $pos = 0) {
+ $totalLength = strlen($data);
+
+ $IPTCTags = $this->_iptcTagNames();
+
+ while ($pos < ($totalLength - 5)) {
+ $signature = $this->_getShort($data, $pos);
+ if ($signature != 0x1C02)
+ return $pos;
+ $pos += 2;
+
+ $type = $this->_getByte($data, $pos);
+ $pos += 1;
+ $length = $this->_getShort($data, $pos);
+ $pos += 2;
+
+ $basePos = $pos;
+ $label = '';
+
+ if (isset($IPTCTags[$type])) {
+ $label = $IPTCTags[$type];
+ } else {
+ $label = sprintf('IPTC_0x%02x', $type);
+ }
+
+ if ($label != '') {
+ if (isset($this->_info['iptc'][$label])) {
+ if (!is_array($this->_info['iptc'][$label])) {
+ $aux = array();
+ $aux[0] = $this->_info['iptc'][$label];
+ $this->_info['iptc'][$label] = $aux;
+ }
+ $this->_info['iptc'][$label][ count($this->_info['iptc'][$label]) ] = $this->_getFixedString($data, $pos, $length);
+ } else {
+ $this->_info['iptc'][$label] = $this->_getFixedString($data, $pos, $length);
+ }
+ }
+
+ $pos = $basePos + $length; // No padding
+ }
+ return $pos;
+ }
+
+ /*************************************************************/
+ function & _createMarkerAdobe() {
+ if (isset($this->_info['iptc'])) {
+ if (!isset($this->_info['adobe'])) {
+ $this->_info['adobe'] = array();
+ }
+ if (!isset($this->_info['adobe']['raw'])) {
+ $this->_info['adobe']['raw'] = array();
+ }
+ if (!isset($this->_info['adobe']['raw']['8BIM_0x0404'])) {
+ $this->_info['adobe']['raw']['8BIM_0x0404'] = array();
+ }
+ $this->_info['adobe']['raw']['8BIM_0x0404']['type'] = 0x0404;
+ $this->_info['adobe']['raw']['8BIM_0x0404']['header'] = "Caption";
+ $this->_info['adobe']['raw']['8BIM_0x0404']['data'] =& $this->_writeIPTC();
+ }
+
+ if (isset($this->_info['adobe']['raw']) && (count($this->_info['adobe']['raw']) > 0)) {
+ $data = "Photoshop 3.0\0";
+ $pos = 14;
+
+ reset($this->_info['adobe']['raw']);
+ foreach ($this->_info['adobe']['raw'] as $value){
+ $pos = $this->_write8BIM(
+ $data,
+ $pos,
+ $value['type'],
+ $value['header'],
+ $value['data'] );
+ }
+ }
+
+ return $data;
+ }
+
+ /*************************************************************/
+
+ /**
+ * @param mixed $data
+ * @param integer $pos
+ *
+ * @param string $type
+ * @param string $header
+ * @param mixed $value
+ *
+ * @return int|mixed
+ */
+ function _write8BIM(&$data, $pos, $type, $header, &$value) {
+ $signature = "8BIM";
+
+ $pos = $this->_putString($data, $pos, $signature);
+ $pos = $this->_putShort($data, $pos, $type);
+
+ $len = strlen($header);
+
+ $pos = $this->_putByte($data, $pos, $len);
+ $pos = $this->_putString($data, $pos, $header);
+ if (($len % 2) == 0) { // Even padding, including the length byte
+ $pos = $this->_putByte($data, $pos, 0);
+ }
+
+ $len = strlen($value);
+ $pos = $this->_putLong($data, $pos, $len);
+ $pos = $this->_putString($data, $pos, $value);
+ if (($len % 2) != 0) { // Even padding
+ $pos = $this->_putByte($data, $pos, 0);
+ }
+ return $pos;
+ }
+
+ /*************************************************************/
+ function & _writeIPTC() {
+ $data = " ";
+ $pos = 0;
+
+ $IPTCNames =& $this->_iptcNameTags();
+
+ foreach($this->_info['iptc'] as $label => $value) {
+ $value =& $this->_info['iptc'][$label];
+ $type = -1;
+
+ if (isset($IPTCNames[$label])) {
+ $type = $IPTCNames[$label];
+ }
+ elseif (substr($label, 0, 7) == "IPTC_0x") {
+ $type = hexdec(substr($label, 7, 2));
+ }
+
+ if ($type != -1) {
+ if (is_array($value)) {
+ $vcnt = count($value);
+ for ($i = 0; $i < $vcnt; $i++) {
+ $pos = $this->_writeIPTCEntry($data, $pos, $type, $value[$i]);
+ }
+ }
+ else {
+ $pos = $this->_writeIPTCEntry($data, $pos, $type, $value);
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ /*************************************************************/
+
+ /**
+ * @param mixed $data
+ * @param integer $pos
+ *
+ * @param string $type
+ * @param mixed $value
+ *
+ * @return int|mixed
+ */
+ function _writeIPTCEntry(&$data, $pos, $type, &$value) {
+ $pos = $this->_putShort($data, $pos, 0x1C02);
+ $pos = $this->_putByte($data, $pos, $type);
+ $pos = $this->_putShort($data, $pos, strlen($value));
+ $pos = $this->_putString($data, $pos, $value);
+
+ return $pos;
+ }
+
+ /*************************************************************/
+ function _exifTagNames($mode) {
+ $tags = array();
+
+ if ($mode == 'ifd0') {
+ $tags[0x010E] = 'ImageDescription';
+ $tags[0x010F] = 'Make';
+ $tags[0x0110] = 'Model';
+ $tags[0x0112] = 'Orientation';
+ $tags[0x011A] = 'XResolution';
+ $tags[0x011B] = 'YResolution';
+ $tags[0x0128] = 'ResolutionUnit';
+ $tags[0x0131] = 'Software';
+ $tags[0x0132] = 'DateTime';
+ $tags[0x013B] = 'Artist';
+ $tags[0x013E] = 'WhitePoint';
+ $tags[0x013F] = 'PrimaryChromaticities';
+ $tags[0x0211] = 'YCbCrCoefficients';
+ $tags[0x0212] = 'YCbCrSubSampling';
+ $tags[0x0213] = 'YCbCrPositioning';
+ $tags[0x0214] = 'ReferenceBlackWhite';
+ $tags[0x8298] = 'Copyright';
+ $tags[0x8769] = 'ExifIFDOffset';
+ $tags[0x8825] = 'GPSIFDOffset';
+ }
+ if ($mode == 'ifd1') {
+ $tags[0x00FE] = 'TIFFNewSubfileType';
+ $tags[0x00FF] = 'TIFFSubfileType';
+ $tags[0x0100] = 'TIFFImageWidth';
+ $tags[0x0101] = 'TIFFImageHeight';
+ $tags[0x0102] = 'TIFFBitsPerSample';
+ $tags[0x0103] = 'TIFFCompression';
+ $tags[0x0106] = 'TIFFPhotometricInterpretation';
+ $tags[0x0107] = 'TIFFThreshholding';
+ $tags[0x0108] = 'TIFFCellWidth';
+ $tags[0x0109] = 'TIFFCellLength';
+ $tags[0x010A] = 'TIFFFillOrder';
+ $tags[0x010E] = 'TIFFImageDescription';
+ $tags[0x010F] = 'TIFFMake';
+ $tags[0x0110] = 'TIFFModel';
+ $tags[0x0111] = 'TIFFStripOffsets';
+ $tags[0x0112] = 'TIFFOrientation';
+ $tags[0x0115] = 'TIFFSamplesPerPixel';
+ $tags[0x0116] = 'TIFFRowsPerStrip';
+ $tags[0x0117] = 'TIFFStripByteCounts';
+ $tags[0x0118] = 'TIFFMinSampleValue';
+ $tags[0x0119] = 'TIFFMaxSampleValue';
+ $tags[0x011A] = 'TIFFXResolution';
+ $tags[0x011B] = 'TIFFYResolution';
+ $tags[0x011C] = 'TIFFPlanarConfiguration';
+ $tags[0x0122] = 'TIFFGrayResponseUnit';
+ $tags[0x0123] = 'TIFFGrayResponseCurve';
+ $tags[0x0128] = 'TIFFResolutionUnit';
+ $tags[0x0131] = 'TIFFSoftware';
+ $tags[0x0132] = 'TIFFDateTime';
+ $tags[0x013B] = 'TIFFArtist';
+ $tags[0x013C] = 'TIFFHostComputer';
+ $tags[0x0140] = 'TIFFColorMap';
+ $tags[0x0152] = 'TIFFExtraSamples';
+ $tags[0x0201] = 'TIFFJFIFOffset';
+ $tags[0x0202] = 'TIFFJFIFLength';
+ $tags[0x0211] = 'TIFFYCbCrCoefficients';
+ $tags[0x0212] = 'TIFFYCbCrSubSampling';
+ $tags[0x0213] = 'TIFFYCbCrPositioning';
+ $tags[0x0214] = 'TIFFReferenceBlackWhite';
+ $tags[0x8298] = 'TIFFCopyright';
+ $tags[0x9286] = 'TIFFUserComment';
+ } elseif ($mode == 'exif') {
+ $tags[0x829A] = 'ExposureTime';
+ $tags[0x829D] = 'FNumber';
+ $tags[0x8822] = 'ExposureProgram';
+ $tags[0x8824] = 'SpectralSensitivity';
+ $tags[0x8827] = 'ISOSpeedRatings';
+ $tags[0x8828] = 'OECF';
+ $tags[0x9000] = 'EXIFVersion';
+ $tags[0x9003] = 'DateTimeOriginal';
+ $tags[0x9004] = 'DateTimeDigitized';
+ $tags[0x9101] = 'ComponentsConfiguration';
+ $tags[0x9102] = 'CompressedBitsPerPixel';
+ $tags[0x9201] = 'ShutterSpeedValue';
+ $tags[0x9202] = 'ApertureValue';
+ $tags[0x9203] = 'BrightnessValue';
+ $tags[0x9204] = 'ExposureBiasValue';
+ $tags[0x9205] = 'MaxApertureValue';
+ $tags[0x9206] = 'SubjectDistance';
+ $tags[0x9207] = 'MeteringMode';
+ $tags[0x9208] = 'LightSource';
+ $tags[0x9209] = 'Flash';
+ $tags[0x920A] = 'FocalLength';
+ $tags[0x927C] = 'MakerNote';
+ $tags[0x9286] = 'UserComment';
+ $tags[0x9290] = 'SubSecTime';
+ $tags[0x9291] = 'SubSecTimeOriginal';
+ $tags[0x9292] = 'SubSecTimeDigitized';
+ $tags[0xA000] = 'FlashPixVersion';
+ $tags[0xA001] = 'ColorSpace';
+ $tags[0xA002] = 'PixelXDimension';
+ $tags[0xA003] = 'PixelYDimension';
+ $tags[0xA004] = 'RelatedSoundFile';
+ $tags[0xA005] = 'InteropIFDOffset';
+ $tags[0xA20B] = 'FlashEnergy';
+ $tags[0xA20C] = 'SpatialFrequencyResponse';
+ $tags[0xA20E] = 'FocalPlaneXResolution';
+ $tags[0xA20F] = 'FocalPlaneYResolution';
+ $tags[0xA210] = 'FocalPlaneResolutionUnit';
+ $tags[0xA214] = 'SubjectLocation';
+ $tags[0xA215] = 'ExposureIndex';
+ $tags[0xA217] = 'SensingMethod';
+ $tags[0xA300] = 'FileSource';
+ $tags[0xA301] = 'SceneType';
+ $tags[0xA302] = 'CFAPattern';
+ } elseif ($mode == 'interop') {
+ $tags[0x0001] = 'InteroperabilityIndex';
+ $tags[0x0002] = 'InteroperabilityVersion';
+ $tags[0x1000] = 'RelatedImageFileFormat';
+ $tags[0x1001] = 'RelatedImageWidth';
+ $tags[0x1002] = 'RelatedImageLength';
+ } elseif ($mode == 'gps') {
+ $tags[0x0000] = 'GPSVersionID';
+ $tags[0x0001] = 'GPSLatitudeRef';
+ $tags[0x0002] = 'GPSLatitude';
+ $tags[0x0003] = 'GPSLongitudeRef';
+ $tags[0x0004] = 'GPSLongitude';
+ $tags[0x0005] = 'GPSAltitudeRef';
+ $tags[0x0006] = 'GPSAltitude';
+ $tags[0x0007] = 'GPSTimeStamp';
+ $tags[0x0008] = 'GPSSatellites';
+ $tags[0x0009] = 'GPSStatus';
+ $tags[0x000A] = 'GPSMeasureMode';
+ $tags[0x000B] = 'GPSDOP';
+ $tags[0x000C] = 'GPSSpeedRef';
+ $tags[0x000D] = 'GPSSpeed';
+ $tags[0x000E] = 'GPSTrackRef';
+ $tags[0x000F] = 'GPSTrack';
+ $tags[0x0010] = 'GPSImgDirectionRef';
+ $tags[0x0011] = 'GPSImgDirection';
+ $tags[0x0012] = 'GPSMapDatum';
+ $tags[0x0013] = 'GPSDestLatitudeRef';
+ $tags[0x0014] = 'GPSDestLatitude';
+ $tags[0x0015] = 'GPSDestLongitudeRef';
+ $tags[0x0016] = 'GPSDestLongitude';
+ $tags[0x0017] = 'GPSDestBearingRef';
+ $tags[0x0018] = 'GPSDestBearing';
+ $tags[0x0019] = 'GPSDestDistanceRef';
+ $tags[0x001A] = 'GPSDestDistance';
+ }
+
+ return $tags;
+ }
+
+ /*************************************************************/
+ function _exifTagTypes($mode) {
+ $tags = array();
+
+ if ($mode == 'ifd0') {
+ $tags[0x010E] = array(2, 0); // ImageDescription -> ASCII, Any
+ $tags[0x010F] = array(2, 0); // Make -> ASCII, Any
+ $tags[0x0110] = array(2, 0); // Model -> ASCII, Any
+ $tags[0x0112] = array(3, 1); // Orientation -> SHORT, 1
+ $tags[0x011A] = array(5, 1); // XResolution -> RATIONAL, 1
+ $tags[0x011B] = array(5, 1); // YResolution -> RATIONAL, 1
+ $tags[0x0128] = array(3, 1); // ResolutionUnit -> SHORT
+ $tags[0x0131] = array(2, 0); // Software -> ASCII, Any
+ $tags[0x0132] = array(2, 20); // DateTime -> ASCII, 20
+ $tags[0x013B] = array(2, 0); // Artist -> ASCII, Any
+ $tags[0x013E] = array(5, 2); // WhitePoint -> RATIONAL, 2
+ $tags[0x013F] = array(5, 6); // PrimaryChromaticities -> RATIONAL, 6
+ $tags[0x0211] = array(5, 3); // YCbCrCoefficients -> RATIONAL, 3
+ $tags[0x0212] = array(3, 2); // YCbCrSubSampling -> SHORT, 2
+ $tags[0x0213] = array(3, 1); // YCbCrPositioning -> SHORT, 1
+ $tags[0x0214] = array(5, 6); // ReferenceBlackWhite -> RATIONAL, 6
+ $tags[0x8298] = array(2, 0); // Copyright -> ASCII, Any
+ $tags[0x8769] = array(4, 1); // ExifIFDOffset -> LONG, 1
+ $tags[0x8825] = array(4, 1); // GPSIFDOffset -> LONG, 1
+ }
+ if ($mode == 'ifd1') {
+ $tags[0x00FE] = array(4, 1); // TIFFNewSubfileType -> LONG, 1
+ $tags[0x00FF] = array(3, 1); // TIFFSubfileType -> SHORT, 1
+ $tags[0x0100] = array(4, 1); // TIFFImageWidth -> LONG (or SHORT), 1
+ $tags[0x0101] = array(4, 1); // TIFFImageHeight -> LONG (or SHORT), 1
+ $tags[0x0102] = array(3, 3); // TIFFBitsPerSample -> SHORT, 3
+ $tags[0x0103] = array(3, 1); // TIFFCompression -> SHORT, 1
+ $tags[0x0106] = array(3, 1); // TIFFPhotometricInterpretation -> SHORT, 1
+ $tags[0x0107] = array(3, 1); // TIFFThreshholding -> SHORT, 1
+ $tags[0x0108] = array(3, 1); // TIFFCellWidth -> SHORT, 1
+ $tags[0x0109] = array(3, 1); // TIFFCellLength -> SHORT, 1
+ $tags[0x010A] = array(3, 1); // TIFFFillOrder -> SHORT, 1
+ $tags[0x010E] = array(2, 0); // TIFFImageDescription -> ASCII, Any
+ $tags[0x010F] = array(2, 0); // TIFFMake -> ASCII, Any
+ $tags[0x0110] = array(2, 0); // TIFFModel -> ASCII, Any
+ $tags[0x0111] = array(4, 0); // TIFFStripOffsets -> LONG (or SHORT), Any (one per strip)
+ $tags[0x0112] = array(3, 1); // TIFFOrientation -> SHORT, 1
+ $tags[0x0115] = array(3, 1); // TIFFSamplesPerPixel -> SHORT, 1
+ $tags[0x0116] = array(4, 1); // TIFFRowsPerStrip -> LONG (or SHORT), 1
+ $tags[0x0117] = array(4, 0); // TIFFStripByteCounts -> LONG (or SHORT), Any (one per strip)
+ $tags[0x0118] = array(3, 0); // TIFFMinSampleValue -> SHORT, Any (SamplesPerPixel)
+ $tags[0x0119] = array(3, 0); // TIFFMaxSampleValue -> SHORT, Any (SamplesPerPixel)
+ $tags[0x011A] = array(5, 1); // TIFFXResolution -> RATIONAL, 1
+ $tags[0x011B] = array(5, 1); // TIFFYResolution -> RATIONAL, 1
+ $tags[0x011C] = array(3, 1); // TIFFPlanarConfiguration -> SHORT, 1
+ $tags[0x0122] = array(3, 1); // TIFFGrayResponseUnit -> SHORT, 1
+ $tags[0x0123] = array(3, 0); // TIFFGrayResponseCurve -> SHORT, Any (2^BitsPerSample)
+ $tags[0x0128] = array(3, 1); // TIFFResolutionUnit -> SHORT, 1
+ $tags[0x0131] = array(2, 0); // TIFFSoftware -> ASCII, Any
+ $tags[0x0132] = array(2, 20); // TIFFDateTime -> ASCII, 20
+ $tags[0x013B] = array(2, 0); // TIFFArtist -> ASCII, Any
+ $tags[0x013C] = array(2, 0); // TIFFHostComputer -> ASCII, Any
+ $tags[0x0140] = array(3, 0); // TIFFColorMap -> SHORT, Any (3 * 2^BitsPerSample)
+ $tags[0x0152] = array(3, 0); // TIFFExtraSamples -> SHORT, Any (SamplesPerPixel - 3)
+ $tags[0x0201] = array(4, 1); // TIFFJFIFOffset -> LONG, 1
+ $tags[0x0202] = array(4, 1); // TIFFJFIFLength -> LONG, 1
+ $tags[0x0211] = array(5, 3); // TIFFYCbCrCoefficients -> RATIONAL, 3
+ $tags[0x0212] = array(3, 2); // TIFFYCbCrSubSampling -> SHORT, 2
+ $tags[0x0213] = array(3, 1); // TIFFYCbCrPositioning -> SHORT, 1
+ $tags[0x0214] = array(5, 6); // TIFFReferenceBlackWhite -> RATIONAL, 6
+ $tags[0x8298] = array(2, 0); // TIFFCopyright -> ASCII, Any
+ $tags[0x9286] = array(2, 0); // TIFFUserComment -> ASCII, Any
+ } elseif ($mode == 'exif') {
+ $tags[0x829A] = array(5, 1); // ExposureTime -> RATIONAL, 1
+ $tags[0x829D] = array(5, 1); // FNumber -> RATIONAL, 1
+ $tags[0x8822] = array(3, 1); // ExposureProgram -> SHORT, 1
+ $tags[0x8824] = array(2, 0); // SpectralSensitivity -> ASCII, Any
+ $tags[0x8827] = array(3, 0); // ISOSpeedRatings -> SHORT, Any
+ $tags[0x8828] = array(7, 0); // OECF -> UNDEFINED, Any
+ $tags[0x9000] = array(7, 4); // EXIFVersion -> UNDEFINED, 4
+ $tags[0x9003] = array(2, 20); // DateTimeOriginal -> ASCII, 20
+ $tags[0x9004] = array(2, 20); // DateTimeDigitized -> ASCII, 20
+ $tags[0x9101] = array(7, 4); // ComponentsConfiguration -> UNDEFINED, 4
+ $tags[0x9102] = array(5, 1); // CompressedBitsPerPixel -> RATIONAL, 1
+ $tags[0x9201] = array(10, 1); // ShutterSpeedValue -> SRATIONAL, 1
+ $tags[0x9202] = array(5, 1); // ApertureValue -> RATIONAL, 1
+ $tags[0x9203] = array(10, 1); // BrightnessValue -> SRATIONAL, 1
+ $tags[0x9204] = array(10, 1); // ExposureBiasValue -> SRATIONAL, 1
+ $tags[0x9205] = array(5, 1); // MaxApertureValue -> RATIONAL, 1
+ $tags[0x9206] = array(5, 1); // SubjectDistance -> RATIONAL, 1
+ $tags[0x9207] = array(3, 1); // MeteringMode -> SHORT, 1
+ $tags[0x9208] = array(3, 1); // LightSource -> SHORT, 1
+ $tags[0x9209] = array(3, 1); // Flash -> SHORT, 1
+ $tags[0x920A] = array(5, 1); // FocalLength -> RATIONAL, 1
+ $tags[0x927C] = array(7, 0); // MakerNote -> UNDEFINED, Any
+ $tags[0x9286] = array(7, 0); // UserComment -> UNDEFINED, Any
+ $tags[0x9290] = array(2, 0); // SubSecTime -> ASCII, Any
+ $tags[0x9291] = array(2, 0); // SubSecTimeOriginal -> ASCII, Any
+ $tags[0x9292] = array(2, 0); // SubSecTimeDigitized -> ASCII, Any
+ $tags[0xA000] = array(7, 4); // FlashPixVersion -> UNDEFINED, 4
+ $tags[0xA001] = array(3, 1); // ColorSpace -> SHORT, 1
+ $tags[0xA002] = array(4, 1); // PixelXDimension -> LONG (or SHORT), 1
+ $tags[0xA003] = array(4, 1); // PixelYDimension -> LONG (or SHORT), 1
+ $tags[0xA004] = array(2, 13); // RelatedSoundFile -> ASCII, 13
+ $tags[0xA005] = array(4, 1); // InteropIFDOffset -> LONG, 1
+ $tags[0xA20B] = array(5, 1); // FlashEnergy -> RATIONAL, 1
+ $tags[0xA20C] = array(7, 0); // SpatialFrequencyResponse -> UNDEFINED, Any
+ $tags[0xA20E] = array(5, 1); // FocalPlaneXResolution -> RATIONAL, 1
+ $tags[0xA20F] = array(5, 1); // FocalPlaneYResolution -> RATIONAL, 1
+ $tags[0xA210] = array(3, 1); // FocalPlaneResolutionUnit -> SHORT, 1
+ $tags[0xA214] = array(3, 2); // SubjectLocation -> SHORT, 2
+ $tags[0xA215] = array(5, 1); // ExposureIndex -> RATIONAL, 1
+ $tags[0xA217] = array(3, 1); // SensingMethod -> SHORT, 1
+ $tags[0xA300] = array(7, 1); // FileSource -> UNDEFINED, 1
+ $tags[0xA301] = array(7, 1); // SceneType -> UNDEFINED, 1
+ $tags[0xA302] = array(7, 0); // CFAPattern -> UNDEFINED, Any
+ } elseif ($mode == 'interop') {
+ $tags[0x0001] = array(2, 0); // InteroperabilityIndex -> ASCII, Any
+ $tags[0x0002] = array(7, 4); // InteroperabilityVersion -> UNKNOWN, 4
+ $tags[0x1000] = array(2, 0); // RelatedImageFileFormat -> ASCII, Any
+ $tags[0x1001] = array(4, 1); // RelatedImageWidth -> LONG (or SHORT), 1
+ $tags[0x1002] = array(4, 1); // RelatedImageLength -> LONG (or SHORT), 1
+ } elseif ($mode == 'gps') {
+ $tags[0x0000] = array(1, 4); // GPSVersionID -> BYTE, 4
+ $tags[0x0001] = array(2, 2); // GPSLatitudeRef -> ASCII, 2
+ $tags[0x0002] = array(5, 3); // GPSLatitude -> RATIONAL, 3
+ $tags[0x0003] = array(2, 2); // GPSLongitudeRef -> ASCII, 2
+ $tags[0x0004] = array(5, 3); // GPSLongitude -> RATIONAL, 3
+ $tags[0x0005] = array(2, 2); // GPSAltitudeRef -> ASCII, 2
+ $tags[0x0006] = array(5, 1); // GPSAltitude -> RATIONAL, 1
+ $tags[0x0007] = array(5, 3); // GPSTimeStamp -> RATIONAL, 3
+ $tags[0x0008] = array(2, 0); // GPSSatellites -> ASCII, Any
+ $tags[0x0009] = array(2, 2); // GPSStatus -> ASCII, 2
+ $tags[0x000A] = array(2, 2); // GPSMeasureMode -> ASCII, 2
+ $tags[0x000B] = array(5, 1); // GPSDOP -> RATIONAL, 1
+ $tags[0x000C] = array(2, 2); // GPSSpeedRef -> ASCII, 2
+ $tags[0x000D] = array(5, 1); // GPSSpeed -> RATIONAL, 1
+ $tags[0x000E] = array(2, 2); // GPSTrackRef -> ASCII, 2
+ $tags[0x000F] = array(5, 1); // GPSTrack -> RATIONAL, 1
+ $tags[0x0010] = array(2, 2); // GPSImgDirectionRef -> ASCII, 2
+ $tags[0x0011] = array(5, 1); // GPSImgDirection -> RATIONAL, 1
+ $tags[0x0012] = array(2, 0); // GPSMapDatum -> ASCII, Any
+ $tags[0x0013] = array(2, 2); // GPSDestLatitudeRef -> ASCII, 2
+ $tags[0x0014] = array(5, 3); // GPSDestLatitude -> RATIONAL, 3
+ $tags[0x0015] = array(2, 2); // GPSDestLongitudeRef -> ASCII, 2
+ $tags[0x0016] = array(5, 3); // GPSDestLongitude -> RATIONAL, 3
+ $tags[0x0017] = array(2, 2); // GPSDestBearingRef -> ASCII, 2
+ $tags[0x0018] = array(5, 1); // GPSDestBearing -> RATIONAL, 1
+ $tags[0x0019] = array(2, 2); // GPSDestDistanceRef -> ASCII, 2
+ $tags[0x001A] = array(5, 1); // GPSDestDistance -> RATIONAL, 1
+ }
+
+ return $tags;
+ }
+
+ /*************************************************************/
+ function _exifNameTags($mode) {
+ $tags = $this->_exifTagNames($mode);
+ return $this->_names2Tags($tags);
+ }
+
+ /*************************************************************/
+ function _iptcTagNames() {
+ $tags = array();
+ $tags[0x14] = 'SuplementalCategories';
+ $tags[0x19] = 'Keywords';
+ $tags[0x78] = 'Caption';
+ $tags[0x7A] = 'CaptionWriter';
+ $tags[0x69] = 'Headline';
+ $tags[0x28] = 'SpecialInstructions';
+ $tags[0x0F] = 'Category';
+ $tags[0x50] = 'Byline';
+ $tags[0x55] = 'BylineTitle';
+ $tags[0x6E] = 'Credit';
+ $tags[0x73] = 'Source';
+ $tags[0x74] = 'CopyrightNotice';
+ $tags[0x05] = 'ObjectName';
+ $tags[0x5A] = 'City';
+ $tags[0x5C] = 'Sublocation';
+ $tags[0x5F] = 'ProvinceState';
+ $tags[0x65] = 'CountryName';
+ $tags[0x67] = 'OriginalTransmissionReference';
+ $tags[0x37] = 'DateCreated';
+ $tags[0x0A] = 'CopyrightFlag';
+
+ return $tags;
+ }
+
+ /*************************************************************/
+ function & _iptcNameTags() {
+ $tags = $this->_iptcTagNames();
+ return $this->_names2Tags($tags);
+ }
+
+ /*************************************************************/
+ function _names2Tags($tags2Names) {
+ $names2Tags = array();
+
+ foreach($tags2Names as $tag => $name) {
+ $names2Tags[$name] = $tag;
+ }
+
+ return $names2Tags;
+ }
+
+ /*************************************************************/
+
+ /**
+ * @param $data
+ * @param integer $pos
+ *
+ * @return int
+ */
+ function _getByte(&$data, $pos) {
+ return ord($data[$pos]);
+ }
+
+ /*************************************************************/
+
+ /**
+ * @param mixed $data
+ * @param integer $pos
+ *
+ * @param mixed $val
+ *
+ * @return int
+ */
+ function _putByte(&$data, $pos, $val) {
+ $val = intval($val);
+
+ $data[$pos] = chr($val);
+
+ return $pos + 1;
+ }
+
+ /*************************************************************/
+ function _getShort(&$data, $pos, $bigEndian = true) {
+ if ($bigEndian) {
+ return (ord($data[$pos]) << 8)
+ + ord($data[$pos + 1]);
+ } else {
+ return ord($data[$pos])
+ + (ord($data[$pos + 1]) << 8);
+ }
+ }
+
+ /*************************************************************/
+ function _putShort(&$data, $pos = 0, $val = 0, $bigEndian = true) {
+ $val = intval($val);
+
+ if ($bigEndian) {
+ $data[$pos + 0] = chr(($val & 0x0000FF00) >> 8);
+ $data[$pos + 1] = chr(($val & 0x000000FF) >> 0);
+ } else {
+ $data[$pos + 0] = chr(($val & 0x00FF) >> 0);
+ $data[$pos + 1] = chr(($val & 0xFF00) >> 8);
+ }
+
+ return $pos + 2;
+ }
+
+ /*************************************************************/
+
+ /**
+ * @param mixed $data
+ * @param integer $pos
+ *
+ * @param bool $bigEndian
+ *
+ * @return int
+ */
+ function _getLong(&$data, $pos, $bigEndian = true) {
+ if ($bigEndian) {
+ return (ord($data[$pos]) << 24)
+ + (ord($data[$pos + 1]) << 16)
+ + (ord($data[$pos + 2]) << 8)
+ + ord($data[$pos + 3]);
+ } else {
+ return ord($data[$pos])
+ + (ord($data[$pos + 1]) << 8)
+ + (ord($data[$pos + 2]) << 16)
+ + (ord($data[$pos + 3]) << 24);
+ }
+ }
+
+ /*************************************************************/
+
+ /**
+ * @param mixed $data
+ * @param integer $pos
+ *
+ * @param mixed $val
+ * @param bool $bigEndian
+ *
+ * @return int
+ */
+ function _putLong(&$data, $pos, $val, $bigEndian = true) {
+ $val = intval($val);
+
+ if ($bigEndian) {
+ $data[$pos + 0] = chr(($val & 0xFF000000) >> 24);
+ $data[$pos + 1] = chr(($val & 0x00FF0000) >> 16);
+ $data[$pos + 2] = chr(($val & 0x0000FF00) >> 8);
+ $data[$pos + 3] = chr(($val & 0x000000FF) >> 0);
+ } else {
+ $data[$pos + 0] = chr(($val & 0x000000FF) >> 0);
+ $data[$pos + 1] = chr(($val & 0x0000FF00) >> 8);
+ $data[$pos + 2] = chr(($val & 0x00FF0000) >> 16);
+ $data[$pos + 3] = chr(($val & 0xFF000000) >> 24);
+ }
+
+ return $pos + 4;
+ }
+
+ /*************************************************************/
+ function & _getNullString(&$data, $pos) {
+ $str = '';
+ $max = strlen($data);
+
+ while ($pos < $max) {
+ if (ord($data[$pos]) == 0) {
+ return $str;
+ } else {
+ $str .= $data[$pos];
+ }
+ $pos++;
+ }
+
+ return $str;
+ }
+
+ /*************************************************************/
+ function & _getFixedString(&$data, $pos, $length = -1) {
+ if ($length == -1) {
+ $length = strlen($data) - $pos;
+ }
+
+ $rv = substr($data, $pos, $length);
+ return $rv;
+ }
+
+ /*************************************************************/
+ function _putString(&$data, $pos, &$str) {
+ $len = strlen($str);
+ for ($i = 0; $i < $len; $i++) {
+ $data[$pos + $i] = $str[$i];
+ }
+
+ return $pos + $len;
+ }
+
+ /*************************************************************/
+ function _hexDump(&$data, $start = 0, $length = -1) {
+ if (($length == -1) || (($length + $start) > strlen($data))) {
+ $end = strlen($data);
+ } else {
+ $end = $start + $length;
+ }
+
+ $ascii = '';
+ $count = 0;
+
+ echo "<tt>\n";
+
+ while ($start < $end) {
+ if (($count % 16) == 0) {
+ echo sprintf('%04d', $count) . ': ';
+ }
+
+ $c = ord($data[$start]);
+ $count++;
+ $start++;
+
+ $aux = dechex($c);
+ if (strlen($aux) == 1)
+ echo '0';
+ echo $aux . ' ';
+
+ if ($c == 60)
+ $ascii .= '&lt;';
+ elseif ($c == 62)
+ $ascii .= '&gt;';
+ elseif ($c == 32)
+ $ascii .= '&#160;';
+ elseif ($c > 32)
+ $ascii .= chr($c);
+ else
+ $ascii .= '.';
+
+ if (($count % 4) == 0) {
+ echo ' - ';
+ }
+
+ if (($count % 16) == 0) {
+ echo ': ' . $ascii . "<br>\n";
+ $ascii = '';
+ }
+ }
+
+ if ($ascii != '') {
+ while (($count % 16) != 0) {
+ echo '-- ';
+ $count++;
+ if (($count % 4) == 0) {
+ echo ' - ';
+ }
+ }
+ echo ': ' . $ascii . "<br>\n";
+ }
+
+ echo "</tt>\n";
+ }
+
+ /*****************************************************************/
+}
+
+/* vim: set expandtab tabstop=4 shiftwidth=4: */
diff --git a/platform/www/inc/Mailer.class.php b/platform/www/inc/Mailer.class.php
new file mode 100644
index 0000000..dd6cbd3
--- /dev/null
+++ b/platform/www/inc/Mailer.class.php
@@ -0,0 +1,777 @@
+<?php
+/**
+ * A class to build and send multi part mails (with HTML content and embedded
+ * attachments). All mails are assumed to be in UTF-8 encoding.
+ *
+ * Attachments are handled in memory so this shouldn't be used to send huge
+ * files, but then again mail shouldn't be used to send huge files either.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+use dokuwiki\Extension\Event;
+
+// end of line for mail lines - RFC822 says CRLF but postfix (and other MTAs?)
+// think different
+if(!defined('MAILHEADER_EOL')) define('MAILHEADER_EOL', "\n");
+#define('MAILHEADER_ASCIIONLY',1);
+
+/**
+ * Mail Handling
+ */
+class Mailer {
+
+ protected $headers = array();
+ protected $attach = array();
+ protected $html = '';
+ protected $text = '';
+
+ protected $boundary = '';
+ protected $partid = '';
+ protected $sendparam = null;
+
+ protected $allowhtml = true;
+
+ protected $replacements = array('text'=> array(), 'html' => array());
+
+ /**
+ * Constructor
+ *
+ * Initializes the boundary strings, part counters and token replacements
+ */
+ public function __construct() {
+ global $conf;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ $server = parse_url(DOKU_URL, PHP_URL_HOST);
+ if(strpos($server,'.') === false) $server .= '.localhost';
+
+ $this->partid = substr(md5(uniqid(mt_rand(), true)),0, 8).'@'.$server;
+ $this->boundary = '__________'.md5(uniqid(mt_rand(), true));
+
+ $listid = implode('.', array_reverse(explode('/', DOKU_BASE))).$server;
+ $listid = strtolower(trim($listid, '.'));
+
+ $this->allowhtml = (bool)$conf['htmlmail'];
+
+ // add some default headers for mailfiltering FS#2247
+ if(!empty($conf['mailreturnpath'])) {
+ $this->setHeader('Return-Path', $conf['mailreturnpath']);
+ }
+ $this->setHeader('X-Mailer', 'DokuWiki');
+ $this->setHeader('X-DokuWiki-User', $INPUT->server->str('REMOTE_USER'));
+ $this->setHeader('X-DokuWiki-Title', $conf['title']);
+ $this->setHeader('X-DokuWiki-Server', $server);
+ $this->setHeader('X-Auto-Response-Suppress', 'OOF');
+ $this->setHeader('List-Id', $conf['title'].' <'.$listid.'>');
+ $this->setHeader('Date', date('r'), false);
+
+ $this->prepareTokenReplacements();
+ }
+
+ /**
+ * Attach a file
+ *
+ * @param string $path Path to the file to attach
+ * @param string $mime Mimetype of the attached file
+ * @param string $name The filename to use
+ * @param string $embed Unique key to reference this file from the HTML part
+ */
+ public function attachFile($path, $mime, $name = '', $embed = '') {
+ if(!$name) {
+ $name = \dokuwiki\Utf8\PhpString::basename($path);
+ }
+
+ $this->attach[] = array(
+ 'data' => file_get_contents($path),
+ 'mime' => $mime,
+ 'name' => $name,
+ 'embed' => $embed
+ );
+ }
+
+ /**
+ * Attach a file
+ *
+ * @param string $data The file contents to attach
+ * @param string $mime Mimetype of the attached file
+ * @param string $name The filename to use
+ * @param string $embed Unique key to reference this file from the HTML part
+ */
+ public function attachContent($data, $mime, $name = '', $embed = '') {
+ if(!$name) {
+ list(, $ext) = explode('/', $mime);
+ $name = count($this->attach).".$ext";
+ }
+
+ $this->attach[] = array(
+ 'data' => $data,
+ 'mime' => $mime,
+ 'name' => $name,
+ 'embed' => $embed
+ );
+ }
+
+ /**
+ * Callback function to automatically embed images referenced in HTML templates
+ *
+ * @param array $matches
+ * @return string placeholder
+ */
+ protected function autoEmbedCallBack($matches) {
+ static $embeds = 0;
+ $embeds++;
+
+ // get file and mime type
+ $media = cleanID($matches[1]);
+ list(, $mime) = mimetype($media);
+ $file = mediaFN($media);
+ if(!file_exists($file)) return $matches[0]; //bad reference, keep as is
+
+ // attach it and set placeholder
+ $this->attachFile($file, $mime, '', 'autoembed'.$embeds);
+ return '%%autoembed'.$embeds.'%%';
+ }
+
+ /**
+ * Add an arbitrary header to the mail
+ *
+ * If an empy value is passed, the header is removed
+ *
+ * @param string $header the header name (no trailing colon!)
+ * @param string|string[] $value the value of the header
+ * @param bool $clean remove all non-ASCII chars and line feeds?
+ */
+ public function setHeader($header, $value, $clean = true) {
+ $header = str_replace(' ', '-', ucwords(strtolower(str_replace('-', ' ', $header)))); // streamline casing
+ if($clean) {
+ $header = preg_replace('/[^a-zA-Z0-9_ \-\.\+\@]+/', '', $header);
+ $value = preg_replace('/[^a-zA-Z0-9_ \-\.\+\@<>]+/', '', $value);
+ }
+
+ // empty value deletes
+ if(is_array($value)){
+ $value = array_map('trim', $value);
+ $value = array_filter($value);
+ if(!$value) $value = '';
+ }else{
+ $value = trim($value);
+ }
+ if($value === '') {
+ if(isset($this->headers[$header])) unset($this->headers[$header]);
+ } else {
+ $this->headers[$header] = $value;
+ }
+ }
+
+ /**
+ * Set additional parameters to be passed to sendmail
+ *
+ * Whatever is set here is directly passed to PHP's mail() command as last
+ * parameter. Depending on the PHP setup this might break mailing alltogether
+ *
+ * @param string $param
+ */
+ public function setParameters($param) {
+ $this->sendparam = $param;
+ }
+
+ /**
+ * Set the text and HTML body and apply replacements
+ *
+ * This function applies a whole bunch of default replacements in addition
+ * to the ones specified as parameters
+ *
+ * If you pass the HTML part or HTML replacements yourself you have to make
+ * sure you encode all HTML special chars correctly
+ *
+ * @param string $text plain text body
+ * @param array $textrep replacements to apply on the text part
+ * @param array $htmlrep replacements to apply on the HTML part, null to use $textrep (urls wrapped in <a> tags)
+ * @param string $html the HTML body, leave null to create it from $text
+ * @param bool $wrap wrap the HTML in the default header/Footer
+ */
+ public function setBody($text, $textrep = null, $htmlrep = null, $html = null, $wrap = true) {
+
+ $htmlrep = (array)$htmlrep;
+ $textrep = (array)$textrep;
+
+ // create HTML from text if not given
+ if($html === null) {
+ $html = $text;
+ $html = hsc($html);
+ $html = preg_replace('/^----+$/m', '<hr >', $html);
+ $html = nl2br($html);
+ }
+ if($wrap) {
+ $wrapper = rawLocale('mailwrap', 'html');
+ $html = preg_replace('/\n-- <br \/>.*$/s', '', $html); //strip signature
+ $html = str_replace('@EMAILSIGNATURE@', '', $html); //strip @EMAILSIGNATURE@
+ $html = str_replace('@HTMLBODY@', $html, $wrapper);
+ }
+
+ if(strpos($text, '@EMAILSIGNATURE@') === false) {
+ $text .= '@EMAILSIGNATURE@';
+ }
+
+ // copy over all replacements missing for HTML (autolink URLs)
+ foreach($textrep as $key => $value) {
+ if(isset($htmlrep[$key])) continue;
+ if(media_isexternal($value)) {
+ $htmlrep[$key] = '<a href="'.hsc($value).'">'.hsc($value).'</a>';
+ } else {
+ $htmlrep[$key] = hsc($value);
+ }
+ }
+
+ // embed media from templates
+ $html = preg_replace_callback(
+ '/@MEDIA\(([^\)]+)\)@/',
+ array($this, 'autoEmbedCallBack'), $html
+ );
+
+ // add default token replacements
+ $trep = array_merge($this->replacements['text'], (array)$textrep);
+ $hrep = array_merge($this->replacements['html'], (array)$htmlrep);
+
+ // Apply replacements
+ foreach($trep as $key => $substitution) {
+ $text = str_replace('@'.strtoupper($key).'@', $substitution, $text);
+ }
+ foreach($hrep as $key => $substitution) {
+ $html = str_replace('@'.strtoupper($key).'@', $substitution, $html);
+ }
+
+ $this->setHTML($html);
+ $this->setText($text);
+ }
+
+ /**
+ * Set the HTML part of the mail
+ *
+ * Placeholders can be used to reference embedded attachments
+ *
+ * You probably want to use setBody() instead
+ *
+ * @param string $html
+ */
+ public function setHTML($html) {
+ $this->html = $html;
+ }
+
+ /**
+ * Set the plain text part of the mail
+ *
+ * You probably want to use setBody() instead
+ *
+ * @param string $text
+ */
+ public function setText($text) {
+ $this->text = $text;
+ }
+
+ /**
+ * Add the To: recipients
+ *
+ * @see cleanAddress
+ * @param string|string[] $address Multiple adresses separated by commas or as array
+ */
+ public function to($address) {
+ $this->setHeader('To', $address, false);
+ }
+
+ /**
+ * Add the Cc: recipients
+ *
+ * @see cleanAddress
+ * @param string|string[] $address Multiple adresses separated by commas or as array
+ */
+ public function cc($address) {
+ $this->setHeader('Cc', $address, false);
+ }
+
+ /**
+ * Add the Bcc: recipients
+ *
+ * @see cleanAddress
+ * @param string|string[] $address Multiple adresses separated by commas or as array
+ */
+ public function bcc($address) {
+ $this->setHeader('Bcc', $address, false);
+ }
+
+ /**
+ * Add the From: address
+ *
+ * This is set to $conf['mailfrom'] when not specified so you shouldn't need
+ * to call this function
+ *
+ * @see cleanAddress
+ * @param string $address from address
+ */
+ public function from($address) {
+ $this->setHeader('From', $address, false);
+ }
+
+ /**
+ * Add the mail's Subject: header
+ *
+ * @param string $subject the mail subject
+ */
+ public function subject($subject) {
+ $this->headers['Subject'] = $subject;
+ }
+
+ /**
+ * Return a clean name which can be safely used in mail address
+ * fields. That means the name will be enclosed in '"' if it includes
+ * a '"' or a ','. Also a '"' will be escaped as '\"'.
+ *
+ * @param string $name the name to clean-up
+ * @see cleanAddress
+ */
+ public function getCleanName($name) {
+ $name = trim($name, ' \t"');
+ $name = str_replace('"', '\"', $name, $count);
+ if ($count > 0 || strpos($name, ',') !== false) {
+ $name = '"'.$name.'"';
+ }
+ return $name;
+ }
+
+ /**
+ * Sets an email address header with correct encoding
+ *
+ * Unicode characters will be deaccented and encoded base64
+ * for headers. Addresses may not contain Non-ASCII data!
+ *
+ * If @$addresses is a string then it will be split into multiple
+ * addresses. Addresses must be separated by a comma. If the display
+ * name includes a comma then it MUST be properly enclosed by '"' to
+ * prevent spliting at the wrong point.
+ *
+ * Example:
+ * cc("föö <foo@bar.com>, me@somewhere.com","TBcc");
+ * to("foo, Dr." <foo@bar.com>, me@somewhere.com");
+ *
+ * @param string|string[] $addresses Multiple adresses separated by commas or as array
+ * @return false|string the prepared header (can contain multiple lines)
+ */
+ public function cleanAddress($addresses) {
+ $headers = '';
+ if(!is_array($addresses)){
+ $count = preg_match_all('/\s*(?:("[^"]*"[^,]+),*)|([^,]+)\s*,*/', $addresses, $matches, PREG_SET_ORDER);
+ $addresses = array();
+ if ($count !== false && is_array($matches)) {
+ foreach ($matches as $match) {
+ array_push($addresses, rtrim($match[0], ','));
+ }
+ }
+ }
+
+ foreach($addresses as $part) {
+ $part = preg_replace('/[\r\n\0]+/', ' ', $part); // remove attack vectors
+ $part = trim($part);
+
+ // parse address
+ if(preg_match('#(.*?)<(.*?)>#', $part, $matches)) {
+ $text = trim($matches[1]);
+ $addr = $matches[2];
+ } else {
+ $text = '';
+ $addr = $part;
+ }
+ // skip empty ones
+ if(empty($addr)) {
+ continue;
+ }
+
+ // FIXME: is there a way to encode the localpart of a emailaddress?
+ if(!\dokuwiki\Utf8\Clean::isASCII($addr)) {
+ msg(hsc("E-Mail address <$addr> is not ASCII"), -1, __LINE__, __FILE__, MSG_ADMINS_ONLY);
+ continue;
+ }
+
+ if(!mail_isvalid($addr)) {
+ msg(hsc("E-Mail address <$addr> is not valid"), -1, __LINE__, __FILE__, MSG_ADMINS_ONLY);
+ continue;
+ }
+
+ // text was given
+ if(!empty($text) && !isWindows()) { // No named recipients for To: in Windows (see FS#652)
+ // add address quotes
+ $addr = "<$addr>";
+
+ if(defined('MAILHEADER_ASCIIONLY')) {
+ $text = \dokuwiki\Utf8\Clean::deaccent($text);
+ $text = \dokuwiki\Utf8\Clean::strip($text);
+ }
+
+ if(strpos($text, ',') !== false || !\dokuwiki\Utf8\Clean::isASCII($text)) {
+ $text = '=?UTF-8?B?'.base64_encode($text).'?=';
+ }
+ } else {
+ $text = '';
+ }
+
+ // add to header comma seperated
+ if($headers != '') {
+ $headers .= ', ';
+ }
+ $headers .= $text.' '.$addr;
+ }
+
+ $headers = trim($headers);
+ if(empty($headers)) return false;
+
+ return $headers;
+ }
+
+
+ /**
+ * Prepare the mime multiparts for all attachments
+ *
+ * Replaces placeholders in the HTML with the correct CIDs
+ *
+ * @return string mime multiparts
+ */
+ protected function prepareAttachments() {
+ $mime = '';
+ $part = 1;
+ // embedded attachments
+ foreach($this->attach as $media) {
+ $media['name'] = str_replace(':', '_', cleanID($media['name'], true));
+
+ // create content id
+ $cid = 'part'.$part.'.'.$this->partid;
+
+ // replace wildcards
+ if($media['embed']) {
+ $this->html = str_replace('%%'.$media['embed'].'%%', 'cid:'.$cid, $this->html);
+ }
+
+ $mime .= '--'.$this->boundary.MAILHEADER_EOL;
+ $mime .= $this->wrappedHeaderLine('Content-Type', $media['mime'].'; id="'.$cid.'"');
+ $mime .= $this->wrappedHeaderLine('Content-Transfer-Encoding', 'base64');
+ $mime .= $this->wrappedHeaderLine('Content-ID',"<$cid>");
+ if($media['embed']) {
+ $mime .= $this->wrappedHeaderLine('Content-Disposition', 'inline; filename='.$media['name']);
+ } else {
+ $mime .= $this->wrappedHeaderLine('Content-Disposition', 'attachment; filename='.$media['name']);
+ }
+ $mime .= MAILHEADER_EOL; //end of headers
+ $mime .= chunk_split(base64_encode($media['data']), 74, MAILHEADER_EOL);
+
+ $part++;
+ }
+ return $mime;
+ }
+
+ /**
+ * Build the body and handles multi part mails
+ *
+ * Needs to be called before prepareHeaders!
+ *
+ * @return string the prepared mail body, false on errors
+ */
+ protected function prepareBody() {
+
+ // no HTML mails allowed? remove HTML body
+ if(!$this->allowhtml) {
+ $this->html = '';
+ }
+
+ // check for body
+ if(!$this->text && !$this->html) {
+ return false;
+ }
+
+ // add general headers
+ $this->headers['MIME-Version'] = '1.0';
+
+ $body = '';
+
+ if(!$this->html && !count($this->attach)) { // we can send a simple single part message
+ $this->headers['Content-Type'] = 'text/plain; charset=UTF-8';
+ $this->headers['Content-Transfer-Encoding'] = 'base64';
+ $body .= chunk_split(base64_encode($this->text), 72, MAILHEADER_EOL);
+ } else { // multi part it is
+ $body .= "This is a multi-part message in MIME format.".MAILHEADER_EOL;
+
+ // prepare the attachments
+ $attachments = $this->prepareAttachments();
+
+ // do we have alternative text content?
+ if($this->text && $this->html) {
+ $this->headers['Content-Type'] = 'multipart/alternative;'.MAILHEADER_EOL.
+ ' boundary="'.$this->boundary.'XX"';
+ $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL;
+ $body .= 'Content-Type: text/plain; charset=UTF-8'.MAILHEADER_EOL;
+ $body .= 'Content-Transfer-Encoding: base64'.MAILHEADER_EOL;
+ $body .= MAILHEADER_EOL;
+ $body .= chunk_split(base64_encode($this->text), 72, MAILHEADER_EOL);
+ $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL;
+ $body .= 'Content-Type: multipart/related;'.MAILHEADER_EOL.
+ ' boundary="'.$this->boundary.'";'.MAILHEADER_EOL.
+ ' type="text/html"'.MAILHEADER_EOL;
+ $body .= MAILHEADER_EOL;
+ }
+
+ $body .= '--'.$this->boundary.MAILHEADER_EOL;
+ $body .= 'Content-Type: text/html; charset=UTF-8'.MAILHEADER_EOL;
+ $body .= 'Content-Transfer-Encoding: base64'.MAILHEADER_EOL;
+ $body .= MAILHEADER_EOL;
+ $body .= chunk_split(base64_encode($this->html), 72, MAILHEADER_EOL);
+ $body .= MAILHEADER_EOL;
+ $body .= $attachments;
+ $body .= '--'.$this->boundary.'--'.MAILHEADER_EOL;
+
+ // close open multipart/alternative boundary
+ if($this->text && $this->html) {
+ $body .= '--'.$this->boundary.'XX--'.MAILHEADER_EOL;
+ }
+ }
+
+ return $body;
+ }
+
+ /**
+ * Cleanup and encode the headers array
+ */
+ protected function cleanHeaders() {
+ global $conf;
+
+ // clean up addresses
+ if(empty($this->headers['From'])) $this->from($conf['mailfrom']);
+ $addrs = array('To', 'From', 'Cc', 'Bcc', 'Reply-To', 'Sender');
+ foreach($addrs as $addr) {
+ if(isset($this->headers[$addr])) {
+ $this->headers[$addr] = $this->cleanAddress($this->headers[$addr]);
+ }
+ }
+
+ if(isset($this->headers['Subject'])) {
+ // add prefix to subject
+ if(empty($conf['mailprefix'])) {
+ if(\dokuwiki\Utf8\PhpString::strlen($conf['title']) < 20) {
+ $prefix = '['.$conf['title'].']';
+ } else {
+ $prefix = '['.\dokuwiki\Utf8\PhpString::substr($conf['title'], 0, 20).'...]';
+ }
+ } else {
+ $prefix = '['.$conf['mailprefix'].']';
+ }
+ $len = strlen($prefix);
+ if(substr($this->headers['Subject'], 0, $len) != $prefix) {
+ $this->headers['Subject'] = $prefix.' '.$this->headers['Subject'];
+ }
+
+ // encode subject
+ if(defined('MAILHEADER_ASCIIONLY')) {
+ $this->headers['Subject'] = \dokuwiki\Utf8\Clean::deaccent($this->headers['Subject']);
+ $this->headers['Subject'] = \dokuwiki\Utf8\Clean::strip($this->headers['Subject']);
+ }
+ if(!\dokuwiki\Utf8\Clean::isASCII($this->headers['Subject'])) {
+ $this->headers['Subject'] = '=?UTF-8?B?'.base64_encode($this->headers['Subject']).'?=';
+ }
+ }
+
+ }
+
+ /**
+ * Returns a complete, EOL terminated header line, wraps it if necessary
+ *
+ * @param string $key
+ * @param string $val
+ * @return string line
+ */
+ protected function wrappedHeaderLine($key, $val){
+ return wordwrap("$key: $val", 78, MAILHEADER_EOL.' ').MAILHEADER_EOL;
+ }
+
+ /**
+ * Create a string from the headers array
+ *
+ * @returns string the headers
+ */
+ protected function prepareHeaders() {
+ $headers = '';
+ foreach($this->headers as $key => $val) {
+ if ($val === '' || $val === null) continue;
+ $headers .= $this->wrappedHeaderLine($key, $val);
+ }
+ return $headers;
+ }
+
+ /**
+ * return a full email with all headers
+ *
+ * This is mainly intended for debugging and testing but could also be
+ * used for MHT exports
+ *
+ * @return string the mail, false on errors
+ */
+ public function dump() {
+ $this->cleanHeaders();
+ $body = $this->prepareBody();
+ if($body === false) return false;
+ $headers = $this->prepareHeaders();
+
+ return $headers.MAILHEADER_EOL.$body;
+ }
+
+ /**
+ * Prepare default token replacement strings
+ *
+ * Populates the '$replacements' property.
+ * Should be called by the class constructor
+ */
+ protected function prepareTokenReplacements() {
+ global $INFO;
+ global $conf;
+ /* @var Input $INPUT */
+ global $INPUT;
+ global $lang;
+
+ $ip = clientIP();
+ $cip = gethostsbyaddrs($ip);
+ $name = isset($INFO) ? $INFO['userinfo']['name'] : '';
+ $mail = isset($INFO) ? $INFO['userinfo']['mail'] : '';
+
+ $this->replacements['text'] = array(
+ 'DATE' => dformat(),
+ 'BROWSER' => $INPUT->server->str('HTTP_USER_AGENT'),
+ 'IPADDRESS' => $ip,
+ 'HOSTNAME' => $cip,
+ 'TITLE' => $conf['title'],
+ 'DOKUWIKIURL' => DOKU_URL,
+ 'USER' => $INPUT->server->str('REMOTE_USER'),
+ 'NAME' => $name,
+ 'MAIL' => $mail
+ );
+ $signature = str_replace(
+ '@DOKUWIKIURL@',
+ $this->replacements['text']['DOKUWIKIURL'],
+ $lang['email_signature_text']
+ );
+ $this->replacements['text']['EMAILSIGNATURE'] = "\n-- \n" . $signature . "\n";
+
+ $this->replacements['html'] = array(
+ 'DATE' => '<i>' . hsc(dformat()) . '</i>',
+ 'BROWSER' => hsc($INPUT->server->str('HTTP_USER_AGENT')),
+ 'IPADDRESS' => '<code>' . hsc($ip) . '</code>',
+ 'HOSTNAME' => '<code>' . hsc($cip) . '</code>',
+ 'TITLE' => hsc($conf['title']),
+ 'DOKUWIKIURL' => '<a href="' . DOKU_URL . '">' . DOKU_URL . '</a>',
+ 'USER' => hsc($INPUT->server->str('REMOTE_USER')),
+ 'NAME' => hsc($name),
+ 'MAIL' => '<a href="mailto:"' . hsc($mail) . '">' .
+ hsc($mail) . '</a>'
+ );
+ $signature = $lang['email_signature_text'];
+ if(!empty($lang['email_signature_html'])) {
+ $signature = $lang['email_signature_html'];
+ }
+ $signature = str_replace(
+ array(
+ '@DOKUWIKIURL@',
+ "\n"
+ ),
+ array(
+ $this->replacements['html']['DOKUWIKIURL'],
+ '<br />'
+ ),
+ $signature
+ );
+ $this->replacements['html']['EMAILSIGNATURE'] = $signature;
+ }
+
+ /**
+ * Send the mail
+ *
+ * Call this after all data was set
+ *
+ * @triggers MAIL_MESSAGE_SEND
+ * @return bool true if the mail was successfully passed to the MTA
+ */
+ public function send() {
+ global $lang;
+ $success = false;
+
+ // prepare hook data
+ $data = array(
+ // pass the whole mail class to plugin
+ 'mail' => $this,
+ // pass references for backward compatibility
+ 'to' => &$this->headers['To'],
+ 'cc' => &$this->headers['Cc'],
+ 'bcc' => &$this->headers['Bcc'],
+ 'from' => &$this->headers['From'],
+ 'subject' => &$this->headers['Subject'],
+ 'body' => &$this->text,
+ 'params' => &$this->sendparam,
+ 'headers' => '', // plugins shouldn't use this
+ // signal if we mailed successfully to AFTER event
+ 'success' => &$success,
+ );
+
+ // do our thing if BEFORE hook approves
+ $evt = new Event('MAIL_MESSAGE_SEND', $data);
+ if($evt->advise_before(true)) {
+ // clean up before using the headers
+ $this->cleanHeaders();
+
+ // any recipients?
+ if(trim($this->headers['To']) === '' &&
+ trim($this->headers['Cc']) === '' &&
+ trim($this->headers['Bcc']) === ''
+ ) return false;
+
+ // The To: header is special
+ if(array_key_exists('To', $this->headers)) {
+ $to = (string)$this->headers['To'];
+ unset($this->headers['To']);
+ } else {
+ $to = '';
+ }
+
+ // so is the subject
+ if(array_key_exists('Subject', $this->headers)) {
+ $subject = (string)$this->headers['Subject'];
+ unset($this->headers['Subject']);
+ } else {
+ $subject = '';
+ }
+
+ // make the body
+ $body = $this->prepareBody();
+ if($body === false) return false;
+
+ // cook the headers
+ $headers = $this->prepareHeaders();
+ // add any headers set by legacy plugins
+ if(trim($data['headers'])) {
+ $headers .= MAILHEADER_EOL.trim($data['headers']);
+ }
+
+ if(!function_exists('mail')){
+ $emsg = $lang['email_fail'] . $subject;
+ error_log($emsg);
+ msg(hsc($emsg), -1, __LINE__, __FILE__, MSG_MANAGERS_ONLY);
+ $evt->advise_after();
+ return false;
+ }
+
+ // send the thing
+ if($this->sendparam === null) {
+ $success = @mail($to, $subject, $body, $headers);
+ } else {
+ $success = @mail($to, $subject, $body, $headers, $this->sendparam);
+ }
+ }
+ // any AFTER actions?
+ $evt->advise_after();
+ return $success;
+ }
+}
diff --git a/platform/www/inc/Manifest.php b/platform/www/inc/Manifest.php
new file mode 100644
index 0000000..29e7f26
--- /dev/null
+++ b/platform/www/inc/Manifest.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace dokuwiki;
+
+use dokuwiki\Extension\Event;
+
+class Manifest
+{
+ public function sendManifest()
+ {
+ $manifest = retrieveConfig('manifest', 'jsonToArray');
+
+ global $conf;
+
+ $manifest['scope'] = DOKU_REL;
+
+ if (empty($manifest['name'])) {
+ $manifest['name'] = $conf['title'];
+ }
+
+ if (empty($manifest['short_name'])) {
+ $manifest['short_name'] = $conf['title'];
+ }
+
+ if (empty($manifest['description'])) {
+ $manifest['description'] = $conf['tagline'];
+ }
+
+ if (empty($manifest['start_url'])) {
+ $manifest['start_url'] = DOKU_REL;
+ }
+
+ $styleUtil = new \dokuwiki\StyleUtils();
+ $styleIni = $styleUtil->cssStyleini();
+ $replacements = $styleIni['replacements'];
+
+ if (empty($manifest['background_color'])) {
+ $manifest['background_color'] = $replacements['__background__'];
+ }
+
+ if (empty($manifest['theme_color'])) {
+ $manifest['theme_color'] = !empty($replacements['__theme_color__'])
+ ? $replacements['__theme_color__']
+ : $replacements['__background_alt__'];
+ }
+
+ if (empty($manifest['icons'])) {
+ $manifest['icons'] = [];
+ if (file_exists(mediaFN(':wiki:favicon.ico'))) {
+ $url = ml(':wiki:favicon.ico', '', true, '', true);
+ $manifest['icons'][] = [
+ 'src' => $url,
+ 'sizes' => '16x16',
+ ];
+ }
+
+ $look = [
+ ':wiki:logo.svg',
+ ':logo.svg',
+ ':wiki:dokuwiki.svg'
+ ];
+
+ foreach ($look as $svgLogo) {
+
+ $svgLogoFN = mediaFN($svgLogo);
+
+ if (file_exists($svgLogoFN)) {
+ $url = ml($svgLogo, '', true, '', true);
+ $manifest['icons'][] = [
+ 'src' => $url,
+ 'sizes' => '17x17 512x512',
+ 'type' => 'image/svg+xml',
+ ];
+ break;
+ };
+ }
+ }
+
+ Event::createAndTrigger('MANIFEST_SEND', $manifest);
+
+ header('Content-Type: application/manifest+json');
+ echo json_encode($manifest);
+ }
+}
diff --git a/platform/www/inc/Menu/AbstractMenu.php b/platform/www/inc/Menu/AbstractMenu.php
new file mode 100644
index 0000000..37e5d2c
--- /dev/null
+++ b/platform/www/inc/Menu/AbstractMenu.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace dokuwiki\Menu;
+
+use dokuwiki\Extension\Event;
+use dokuwiki\Menu\Item\AbstractItem;
+
+/**
+ * Class AbstractMenu
+ *
+ * Basic menu functionality. A menu defines a list of AbstractItem that shall be shown.
+ * It contains convenience functions to display the menu in HTML, but template authors can also
+ * just accesst the items via getItems() and create the HTML as however they see fit.
+ */
+abstract class AbstractMenu implements MenuInterface {
+
+ /** @var string[] list of Item classes to load */
+ protected $types = array();
+
+ /** @var int the context this menu is used in */
+ protected $context = AbstractItem::CTX_DESKTOP;
+
+ /** @var string view identifier to be set in the event */
+ protected $view = '';
+
+ /**
+ * AbstractMenu constructor.
+ *
+ * @param int $context the context this menu is used in
+ */
+ public function __construct($context = AbstractItem::CTX_DESKTOP) {
+ $this->context = $context;
+ }
+
+ /**
+ * Get the list of action items in this menu
+ *
+ * @return AbstractItem[]
+ * @triggers MENU_ITEMS_ASSEMBLY
+ */
+ public function getItems() {
+ $data = array(
+ 'view' => $this->view,
+ 'items' => array(),
+ );
+ Event::createAndTrigger('MENU_ITEMS_ASSEMBLY', $data, array($this, 'loadItems'));
+ return $data['items'];
+ }
+
+ /**
+ * Default action for the MENU_ITEMS_ASSEMBLY event
+ *
+ * @see getItems()
+ * @param array $data The plugin data
+ */
+ public function loadItems(&$data) {
+ foreach($this->types as $class) {
+ try {
+ $class = "\\dokuwiki\\Menu\\Item\\$class";
+ /** @var AbstractItem $item */
+ $item = new $class();
+ if(!$item->visibleInContext($this->context)) continue;
+ $data['items'][] = $item;
+ } catch(\RuntimeException $ignored) {
+ // item not available
+ }
+ }
+ }
+
+ /**
+ * Generate HTML list items for this menu
+ *
+ * This is a convenience method for template authors. If you need more fine control over the
+ * output, use getItems() and build the HTML yourself
+ *
+ * @param string|false $classprefix create a class from type with this prefix, false for no class
+ * @param bool $svg add the SVG link
+ * @return string
+ */
+ public function getListItems($classprefix = '', $svg = true) {
+ $html = '';
+ foreach($this->getItems() as $item) {
+ if($classprefix !== false) {
+ $class = ' class="' . $classprefix . $item->getType() . '"';
+ } else {
+ $class = '';
+ }
+
+ $html .= "<li$class>";
+ $html .= $item->asHtmlLink(false, $svg);
+ $html .= '</li>';
+ }
+ return $html;
+ }
+
+}
diff --git a/platform/www/inc/Menu/DetailMenu.php b/platform/www/inc/Menu/DetailMenu.php
new file mode 100644
index 0000000..27c0c6f
--- /dev/null
+++ b/platform/www/inc/Menu/DetailMenu.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace dokuwiki\Menu;
+
+/**
+ * Class DetailMenu
+ *
+ * This menu offers options on an image detail view. It usually displayed similar to
+ * the PageMenu.
+ */
+class DetailMenu extends AbstractMenu {
+
+ protected $view = 'detail';
+
+ protected $types = array(
+ 'MediaManager',
+ 'ImgBackto',
+ 'Top',
+ );
+
+}
diff --git a/platform/www/inc/Menu/Item/AbstractItem.php b/platform/www/inc/Menu/Item/AbstractItem.php
new file mode 100644
index 0000000..c6b04bf
--- /dev/null
+++ b/platform/www/inc/Menu/Item/AbstractItem.php
@@ -0,0 +1,253 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class AbstractItem
+ *
+ * This class defines a single Item to be displayed in one of DokuWiki's menus. Plugins
+ * can extend those menus through action plugins and add their own instances of this class,
+ * overwriting some of its properties.
+ *
+ * Items may be shown multiple times in different contexts. Eg. for the default template
+ * all menus are shown in a Dropdown list on mobile, but are split into several places on
+ * desktop. The item's $context property can be used to hide the item depending on the current
+ * context.
+ *
+ * Children usually just need to overwrite the different properties, but for complex things
+ * the accessors may be overwritten instead.
+ */
+abstract class AbstractItem {
+
+ /** menu item is to be shown on desktop screens only */
+ const CTX_DESKTOP = 1;
+ /** menu item is to be shown on mobile screens only */
+ const CTX_MOBILE = 2;
+ /** menu item is to be shown in all contexts */
+ const CTX_ALL = 3;
+
+ /** @var string name of the action, usually the lowercase class name */
+ protected $type = '';
+ /** @var string optional keyboard shortcut */
+ protected $accesskey = '';
+ /** @var string the page id this action links to */
+ protected $id = '';
+ /** @var string the method to be used when this action is used in a form */
+ protected $method = 'get';
+ /** @var array parameters for the action (should contain the do parameter) */
+ protected $params = array();
+ /** @var bool when true, a rel=nofollow should be used */
+ protected $nofollow = true;
+ /** @var string this item's label may contain a placeholder, which is replaced with this */
+ protected $replacement = '';
+ /** @var string the full path to the SVG icon of this menu item */
+ protected $svg = DOKU_INC . 'lib/images/menu/00-default_checkbox-blank-circle-outline.svg';
+ /** @var string can be set to overwrite the default lookup in $lang.btn_* */
+ protected $label = '';
+ /** @var string the tooltip title, defaults to $label */
+ protected $title = '';
+ /** @var int the context this titme is shown in */
+ protected $context = self::CTX_ALL;
+
+ /**
+ * AbstractItem constructor.
+ *
+ * Sets the dynamic properties
+ *
+ * Children should always call the parent constructor!
+ *
+ * @throws \RuntimeException when the action is disabled
+ */
+ public function __construct() {
+ global $ID;
+ $this->id = $ID;
+ $this->type = $this->getType();
+ $this->params['do'] = $this->type;
+
+ if(!actionOK($this->type)) throw new \RuntimeException("action disabled: {$this->type}");
+ }
+
+ /**
+ * Return this item's label
+ *
+ * When the label property was set, it is simply returned. Otherwise, the action's type
+ * is used to look up the translation in the main language file and, if used, the replacement
+ * is applied.
+ *
+ * @return string
+ */
+ public function getLabel() {
+ if($this->label !== '') return $this->label;
+
+ /** @var array $lang */
+ global $lang;
+ $label = $lang['btn_' . $this->type];
+ if(strpos($label, '%s')) {
+ $label = sprintf($label, $this->replacement);
+ }
+ if($label === '') $label = '[' . $this->type . ']';
+ return $label;
+ }
+
+ /**
+ * Return this item's title
+ *
+ * This title should be used to display a tooltip (using the HTML title attribute). If
+ * a title property was not explicitly set, the label will be returned.
+ *
+ * @return string
+ */
+ public function getTitle() {
+ if($this->title === '') return $this->getLabel();
+ return $this->title;
+ }
+
+ /**
+ * Return the link this item links to
+ *
+ * Basically runs wl() on $id and $params. However if the ID is a hash it is used directly
+ * as the link
+ *
+ * Please note that the generated URL is *not* XML escaped.
+ *
+ * @see wl()
+ * @return string
+ */
+ public function getLink() {
+ if($this->id && $this->id[0] == '#') {
+ return $this->id;
+ } else {
+ return wl($this->id, $this->params, false, '&');
+ }
+ }
+
+ /**
+ * Convenience method to get the attributes for constructing an <a> element
+ *
+ * @see buildAttributes()
+ * @param string|false $classprefix create a class from type with this prefix, false for no class
+ * @return array
+ */
+ public function getLinkAttributes($classprefix = 'menuitem ') {
+ $attr = array(
+ 'href' => $this->getLink(),
+ 'title' => $this->getTitle(),
+ );
+ if($this->isNofollow()) $attr['rel'] = 'nofollow';
+ if($this->getAccesskey()) {
+ $attr['accesskey'] = $this->getAccesskey();
+ $attr['title'] .= ' [' . $this->getAccesskey() . ']';
+ }
+ if($classprefix !== false) $attr['class'] = $classprefix . $this->getType();
+
+ return $attr;
+ }
+
+ /**
+ * Convenience method to create a full <a> element
+ *
+ * Wraps around the label and SVG image
+ *
+ * @param string|false $classprefix create a class from type with this prefix, false for no class
+ * @param bool $svg add SVG icon to the link
+ * @return string
+ */
+ public function asHtmlLink($classprefix = 'menuitem ', $svg = true) {
+ $attr = buildAttributes($this->getLinkAttributes($classprefix));
+ $html = "<a $attr>";
+ if($svg) {
+ $html .= '<span>' . hsc($this->getLabel()) . '</span>';
+ $html .= inlineSVG($this->getSvg());
+ } else {
+ $html .= hsc($this->getLabel());
+ }
+ $html .= "</a>";
+
+ return $html;
+ }
+
+ /**
+ * Convenience method to create a <button> element inside it's own form element
+ *
+ * Uses html_btn()
+ *
+ * @return string
+ */
+ public function asHtmlButton() {
+ return html_btn(
+ $this->getType(),
+ $this->id,
+ $this->getAccesskey(),
+ $this->getParams(),
+ $this->method,
+ $this->getTitle(),
+ $this->getLabel(),
+ $this->getSvg()
+ );
+ }
+
+ /**
+ * Should this item be shown in the given context
+ *
+ * @param int $ctx the current context
+ * @return bool
+ */
+ public function visibleInContext($ctx) {
+ return (bool) ($ctx & $this->context);
+ }
+
+ /**
+ * @return string the name of this item
+ */
+ public function getType() {
+ if($this->type === '') {
+ $this->type = strtolower(substr(strrchr(get_class($this), '\\'), 1));
+ }
+ return $this->type;
+ }
+
+ /**
+ * @return string
+ */
+ public function getAccesskey() {
+ return $this->accesskey;
+ }
+
+ /**
+ * @return array
+ */
+ public function getParams() {
+ return $this->params;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isNofollow() {
+ return $this->nofollow;
+ }
+
+ /**
+ * @return string
+ */
+ public function getSvg() {
+ return $this->svg;
+ }
+
+ /**
+ * Return this Item's settings as an array as used in tpl_get_action()
+ *
+ * @return array
+ */
+ public function getLegacyData() {
+ return array(
+ 'accesskey' => $this->accesskey ?: null,
+ 'type' => $this->type,
+ 'id' => $this->id,
+ 'method' => $this->method,
+ 'params' => $this->params,
+ 'nofollow' => $this->nofollow,
+ 'replacement' => $this->replacement
+ );
+ }
+}
diff --git a/platform/www/inc/Menu/Item/Admin.php b/platform/www/inc/Menu/Item/Admin.php
new file mode 100644
index 0000000..e5506c2
--- /dev/null
+++ b/platform/www/inc/Menu/Item/Admin.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Admin
+ *
+ * Opens the Admin screen. Only shown to managers or above
+ */
+class Admin extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ parent::__construct();
+
+ $this->svg = DOKU_INC . 'lib/images/menu/settings.svg';
+ }
+
+ /** @inheritdoc */
+ public function visibleInContext($ctx)
+ {
+ global $INFO;
+ if(!$INFO['ismanager']) return false;
+
+ return parent::visibleInContext($ctx);
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/Back.php b/platform/www/inc/Menu/Item/Back.php
new file mode 100644
index 0000000..a7cc1d9
--- /dev/null
+++ b/platform/www/inc/Menu/Item/Back.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Back
+ *
+ * Navigates back up one namepspace. This is currently not used in any menu. Templates
+ * would need to add this item manually.
+ */
+class Back extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ global $ID;
+ parent::__construct();
+
+ $parent = tpl_getparent($ID);
+ if(!$parent) {
+ throw new \RuntimeException("No parent for back action");
+ }
+
+ $this->id = $parent;
+ $this->params = array('do' => '');
+ $this->accesskey = 'b';
+ $this->svg = DOKU_INC . 'lib/images/menu/12-back_arrow-left.svg';
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/Backlink.php b/platform/www/inc/Menu/Item/Backlink.php
new file mode 100644
index 0000000..6dc242b
--- /dev/null
+++ b/platform/www/inc/Menu/Item/Backlink.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Backlink
+ *
+ * Shows the backlinks for the current page
+ */
+class Backlink extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ parent::__construct();
+ $this->svg = DOKU_INC . 'lib/images/menu/08-backlink_link-variant.svg';
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/Edit.php b/platform/www/inc/Menu/Item/Edit.php
new file mode 100644
index 0000000..15d9543
--- /dev/null
+++ b/platform/www/inc/Menu/Item/Edit.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Edit
+ *
+ * Most complex item. Shows the edit button but mutates to show, draft and create based on
+ * current state.
+ */
+class Edit extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ global $ACT;
+ global $INFO;
+ global $REV;
+
+ parent::__construct();
+
+ if($ACT === 'show') {
+ $this->method = 'post';
+ if($INFO['writable']) {
+ $this->accesskey = 'e';
+ if(!empty($INFO['draft'])) {
+ $this->type = 'draft';
+ $this->params['do'] = 'draft';
+ } else {
+ $this->params['rev'] = $REV;
+ if(!$INFO['exists']) {
+ $this->type = 'create';
+ }
+ }
+ } else {
+ if(!actionOK("source")) throw new \RuntimeException("action disabled: source");
+ $params['rev'] = $REV;
+ $this->type = 'source';
+ $this->accesskey = 'v';
+ }
+ } else {
+ $this->params = array('do' => '');
+ $this->type = 'show';
+ $this->accesskey = 'v';
+ }
+
+ $this->setIcon();
+ }
+
+ /**
+ * change the icon according to what type the edit button has
+ */
+ protected function setIcon() {
+ $icons = array(
+ 'edit' => '01-edit_pencil.svg',
+ 'create' => '02-create_pencil.svg',
+ 'draft' => '03-draft_android-studio.svg',
+ 'show' => '04-show_file-document.svg',
+ 'source' => '05-source_file-xml.svg',
+ );
+ if(isset($icons[$this->type])) {
+ $this->svg = DOKU_INC . 'lib/images/menu/' . $icons[$this->type];
+ }
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/ImgBackto.php b/platform/www/inc/Menu/Item/ImgBackto.php
new file mode 100644
index 0000000..72820a5
--- /dev/null
+++ b/platform/www/inc/Menu/Item/ImgBackto.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class ImgBackto
+ *
+ * Links back to the originating page from a detail image view
+ */
+class ImgBackto extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ global $ID;
+ parent::__construct();
+
+ $this->svg = DOKU_INC . 'lib/images/menu/12-back_arrow-left.svg';
+ $this->type = 'img_backto';
+ $this->params = array();
+ $this->accesskey = 'b';
+ $this->replacement = $ID;
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/Index.php b/platform/www/inc/Menu/Item/Index.php
new file mode 100644
index 0000000..4132673
--- /dev/null
+++ b/platform/www/inc/Menu/Item/Index.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Index
+ *
+ * Shows the sitemap
+ */
+class Index extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ global $conf;
+ global $ID;
+ parent::__construct();
+
+ $this->accesskey = 'x';
+ $this->svg = DOKU_INC . 'lib/images/menu/file-tree.svg';
+
+ // allow searchbots to get to the sitemap from the homepage (when dokuwiki isn't providing a sitemap.xml)
+ if($conf['start'] == $ID && !$conf['sitemap']) {
+ $this->nofollow = false;
+ }
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/Login.php b/platform/www/inc/Menu/Item/Login.php
new file mode 100644
index 0000000..671f6a7
--- /dev/null
+++ b/platform/www/inc/Menu/Item/Login.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Login
+ *
+ * Show a login or logout item, based on the current state
+ */
+class Login extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ global $INPUT;
+ parent::__construct();
+
+ $this->svg = DOKU_INC . 'lib/images/menu/login.svg';
+ $this->params['sectok'] = getSecurityToken();
+ if($INPUT->server->has('REMOTE_USER')) {
+ if(!actionOK('logout')) {
+ throw new \RuntimeException("logout disabled");
+ }
+ $this->params['do'] = 'logout';
+ $this->type = 'logout';
+ $this->svg = DOKU_INC . 'lib/images/menu/logout.svg';
+ }
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/Media.php b/platform/www/inc/Menu/Item/Media.php
new file mode 100644
index 0000000..0e5f47b
--- /dev/null
+++ b/platform/www/inc/Menu/Item/Media.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Media
+ *
+ * Opens the media manager
+ */
+class Media extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ global $ID;
+ parent::__construct();
+
+ $this->svg = DOKU_INC . 'lib/images/menu/folder-multiple-image.svg';
+ $this->params['ns'] = getNS($ID);
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/MediaManager.php b/platform/www/inc/Menu/Item/MediaManager.php
new file mode 100644
index 0000000..8549d20
--- /dev/null
+++ b/platform/www/inc/Menu/Item/MediaManager.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class MediaManager
+ *
+ * Opens the current image in the media manager. Used on image detail view.
+ */
+class MediaManager extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ global $IMG;
+ parent::__construct();
+
+ $imgNS = getNS($IMG);
+ $authNS = auth_quickaclcheck("$imgNS:*");
+ if($authNS < AUTH_UPLOAD) {
+ throw new \RuntimeException("media manager link only with upload permissions");
+ }
+
+ $this->svg = DOKU_INC . 'lib/images/menu/11-mediamanager_folder-image.svg';
+ $this->type = 'mediaManager';
+ $this->params = array(
+ 'ns' => $imgNS,
+ 'image' => $IMG,
+ 'do' => 'media'
+ );
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/Profile.php b/platform/www/inc/Menu/Item/Profile.php
new file mode 100644
index 0000000..2b4ceeb
--- /dev/null
+++ b/platform/www/inc/Menu/Item/Profile.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Profile
+ *
+ * Open the user's profile
+ */
+class Profile extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ global $INPUT;
+ parent::__construct();
+
+ if(!$INPUT->server->str('REMOTE_USER')) {
+ throw new \RuntimeException("profile is only for logged in users");
+ }
+
+ $this->svg = DOKU_INC . 'lib/images/menu/account-card-details.svg';
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/Recent.php b/platform/www/inc/Menu/Item/Recent.php
new file mode 100644
index 0000000..ff90ce6
--- /dev/null
+++ b/platform/www/inc/Menu/Item/Recent.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Recent
+ *
+ * Show the site wide recent changes
+ */
+class Recent extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ parent::__construct();
+
+ $this->accesskey = 'r';
+ $this->svg = DOKU_INC . 'lib/images/menu/calendar-clock.svg';
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/Register.php b/platform/www/inc/Menu/Item/Register.php
new file mode 100644
index 0000000..615146e
--- /dev/null
+++ b/platform/www/inc/Menu/Item/Register.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Register
+ *
+ * Open the view to register a new account
+ */
+class Register extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ global $INPUT;
+ parent::__construct();
+
+ if($INPUT->server->str('REMOTE_USER')) {
+ throw new \RuntimeException("no register when already logged in");
+ }
+
+ $this->svg = DOKU_INC . 'lib/images/menu/account-plus.svg';
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/Resendpwd.php b/platform/www/inc/Menu/Item/Resendpwd.php
new file mode 100644
index 0000000..7ddc6b0
--- /dev/null
+++ b/platform/www/inc/Menu/Item/Resendpwd.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Resendpwd
+ *
+ * Access the "forgot password" dialog
+ */
+class Resendpwd extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ global $INPUT;
+ parent::__construct();
+
+ if($INPUT->server->str('REMOTE_USER')) {
+ throw new \RuntimeException("no resendpwd when already logged in");
+ }
+
+ $this->svg = DOKU_INC . 'lib/images/menu/lock-reset.svg';
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/Revert.php b/platform/www/inc/Menu/Item/Revert.php
new file mode 100644
index 0000000..7d57df0
--- /dev/null
+++ b/platform/www/inc/Menu/Item/Revert.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Revert
+ *
+ * Quick revert to the currently shown page revision
+ */
+class Revert extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ global $REV;
+ global $INFO;
+ parent::__construct();
+
+ if(!$REV || !$INFO['writable']) {
+ throw new \RuntimeException('revert not available');
+ }
+ $this->params['rev'] = $REV;
+ $this->params['sectok'] = getSecurityToken();
+ $this->svg = DOKU_INC . 'lib/images/menu/06-revert_replay.svg';
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/Revisions.php b/platform/www/inc/Menu/Item/Revisions.php
new file mode 100644
index 0000000..3009a79
--- /dev/null
+++ b/platform/www/inc/Menu/Item/Revisions.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Revisions
+ *
+ * Access the old revisions of the current page
+ */
+class Revisions extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ parent::__construct();
+
+ $this->accesskey = 'o';
+ $this->type = 'revs';
+ $this->svg = DOKU_INC . 'lib/images/menu/07-revisions_history.svg';
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/Subscribe.php b/platform/www/inc/Menu/Item/Subscribe.php
new file mode 100644
index 0000000..1c9d335
--- /dev/null
+++ b/platform/www/inc/Menu/Item/Subscribe.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Subscribe
+ *
+ * Access the subscription management view
+ */
+class Subscribe extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ global $INPUT;
+ parent::__construct();
+
+ if(!$INPUT->server->str('REMOTE_USER')) {
+ throw new \RuntimeException("subscribe is only for logged in users");
+ }
+
+ $this->svg = DOKU_INC . 'lib/images/menu/09-subscribe_email-outline.svg';
+ }
+
+}
diff --git a/platform/www/inc/Menu/Item/Top.php b/platform/www/inc/Menu/Item/Top.php
new file mode 100644
index 0000000..a05c4f1
--- /dev/null
+++ b/platform/www/inc/Menu/Item/Top.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Top
+ *
+ * Scroll back to the top. Uses a hash as $id which is handled special in getLink().
+ * Not shown in mobile context
+ */
+class Top extends AbstractItem {
+
+ /** @inheritdoc */
+ public function __construct() {
+ parent::__construct();
+
+ $this->svg = DOKU_INC . 'lib/images/menu/10-top_arrow-up.svg';
+ $this->accesskey = 't';
+ $this->params = array('do' => '');
+ $this->id = '#dokuwiki__top';
+ $this->context = self::CTX_DESKTOP;
+ }
+
+ /**
+ * Convenience method to create a <button> element
+ *
+ * Uses html_topbtn()
+ *
+ * @todo this does currently not support the SVG icon
+ * @return string
+ */
+ public function asHtmlButton() {
+ return html_topbtn();
+ }
+
+}
diff --git a/platform/www/inc/Menu/MenuInterface.php b/platform/www/inc/Menu/MenuInterface.php
new file mode 100644
index 0000000..91dde9d
--- /dev/null
+++ b/platform/www/inc/Menu/MenuInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace dokuwiki\Menu;
+
+use dokuwiki\Menu\Item\AbstractItem;
+
+/**
+ * Interface MenuInterface
+ *
+ * Defines what a Menu provides
+ */
+Interface MenuInterface {
+
+ /**
+ * Get the list of action items in this menu
+ *
+ * @return AbstractItem[]
+ */
+ public function getItems();
+}
diff --git a/platform/www/inc/Menu/MobileMenu.php b/platform/www/inc/Menu/MobileMenu.php
new file mode 100644
index 0000000..2098056
--- /dev/null
+++ b/platform/www/inc/Menu/MobileMenu.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace dokuwiki\Menu;
+
+use dokuwiki\Menu\Item\AbstractItem;
+
+/**
+ * Class MobileMenu
+ *
+ * Note: this does not inherit from AbstractMenu because it is not working like the other
+ * menus. This is a meta menu, aggregating the items from the other menus and offering a combined
+ * view. The idea is to use this on mobile devices, thus the context is fixed to CTX_MOBILE
+ */
+class MobileMenu implements MenuInterface {
+
+ /**
+ * Returns all items grouped by view
+ *
+ * @return AbstractItem[][]
+ */
+ public function getGroupedItems() {
+ $pagemenu = new PageMenu(AbstractItem::CTX_MOBILE);
+ $sitemenu = new SiteMenu(AbstractItem::CTX_MOBILE);
+ $usermenu = new UserMenu(AbstractItem::CTX_MOBILE);
+
+ return array(
+ 'page' => $pagemenu->getItems(),
+ 'site' => $sitemenu->getItems(),
+ 'user' => $usermenu->getItems()
+ );
+ }
+
+ /**
+ * Get all items in a flat array
+ *
+ * This returns the same format as AbstractMenu::getItems()
+ *
+ * @return AbstractItem[]
+ */
+ public function getItems() {
+ $menu = $this->getGroupedItems();
+ return call_user_func_array('array_merge', array_values($menu));
+ }
+
+ /**
+ * Print a dropdown menu with all DokuWiki actions
+ *
+ * Note: this will not use any pretty URLs
+ *
+ * @param string $empty empty option label
+ * @param string $button submit button label
+ * @return string
+ */
+ public function getDropdown($empty = '', $button = '&gt;') {
+ global $ID;
+ global $REV;
+ /** @var string[] $lang */
+ global $lang;
+ global $INPUT;
+
+ $html = '<form action="' . script() . '" method="get" accept-charset="utf-8">';
+ $html .= '<div class="no">';
+ $html .= '<input type="hidden" name="id" value="' . $ID . '" />';
+ if($REV) $html .= '<input type="hidden" name="rev" value="' . $REV . '" />';
+ if($INPUT->server->str('REMOTE_USER')) {
+ $html .= '<input type="hidden" name="sectok" value="' . getSecurityToken() . '" />';
+ }
+
+ $html .= '<select name="do" class="edit quickselect" title="' . $lang['tools'] . '">';
+ $html .= '<option value="">' . $empty . '</option>';
+
+ foreach($this->getGroupedItems() as $tools => $items) {
+ if (count($items)) {
+ $html .= '<optgroup label="' . $lang[$tools . '_tools'] . '">';
+ foreach($items as $item) {
+ $params = $item->getParams();
+ $html .= '<option value="' . $params['do'] . '">';
+ $html .= hsc($item->getLabel());
+ $html .= '</option>';
+ }
+ $html .= '</optgroup>';
+ }
+ }
+
+ $html .= '</select>';
+ $html .= '<button type="submit">' . $button . '</button>';
+ $html .= '</div>';
+ $html .= '</form>';
+
+ return $html;
+ }
+
+}
diff --git a/platform/www/inc/Menu/PageMenu.php b/platform/www/inc/Menu/PageMenu.php
new file mode 100644
index 0000000..9c0a55e
--- /dev/null
+++ b/platform/www/inc/Menu/PageMenu.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace dokuwiki\Menu;
+
+/**
+ * Class PageMenu
+ *
+ * Actions manipulating the current page. Shown as a floating menu in the dokuwiki template
+ */
+class PageMenu extends AbstractMenu {
+
+ protected $view = 'page';
+
+ protected $types = array(
+ 'Edit',
+ 'Revert',
+ 'Revisions',
+ 'Backlink',
+ 'Subscribe',
+ 'Top',
+ );
+
+}
diff --git a/platform/www/inc/Menu/SiteMenu.php b/platform/www/inc/Menu/SiteMenu.php
new file mode 100644
index 0000000..dba6888
--- /dev/null
+++ b/platform/www/inc/Menu/SiteMenu.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace dokuwiki\Menu;
+
+/**
+ * Class SiteMenu
+ *
+ * Actions that are not bound to an individual page but provide toolsfor the whole wiki.
+ */
+class SiteMenu extends AbstractMenu {
+
+ protected $view = 'site';
+
+ protected $types = array(
+ 'Recent',
+ 'Media',
+ 'Index'
+ );
+
+}
diff --git a/platform/www/inc/Menu/UserMenu.php b/platform/www/inc/Menu/UserMenu.php
new file mode 100644
index 0000000..01028d3
--- /dev/null
+++ b/platform/www/inc/Menu/UserMenu.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace dokuwiki\Menu;
+
+/**
+ * Class UserMenu
+ *
+ * Actions related to the current user
+ */
+class UserMenu extends AbstractMenu {
+
+ protected $view = 'user';
+
+ protected $types = array(
+ 'Profile',
+ 'Admin',
+ 'Register',
+ 'Login',
+ );
+
+}
diff --git a/platform/www/inc/Parsing/Handler/AbstractRewriter.php b/platform/www/inc/Parsing/Handler/AbstractRewriter.php
new file mode 100644
index 0000000..d9becbf
--- /dev/null
+++ b/platform/www/inc/Parsing/Handler/AbstractRewriter.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace dokuwiki\Parsing\Handler;
+
+/**
+ * Basic implementation of the rewriter interface to be specialized by children
+ */
+abstract class AbstractRewriter implements ReWriterInterface
+{
+ /** @var CallWriterInterface original CallWriter */
+ protected $callWriter;
+
+ /** @var array[] list of calls */
+ public $calls = array();
+
+ /** @inheritdoc */
+ public function __construct(CallWriterInterface $callWriter)
+ {
+ $this->callWriter = $callWriter;
+ }
+
+ /** @inheritdoc */
+ public function writeCall($call)
+ {
+ $this->calls[] = $call;
+ }
+
+ /** * @inheritdoc */
+ public function writeCalls($calls)
+ {
+ $this->calls = array_merge($this->calls, $calls);
+ }
+
+ /** @inheritDoc */
+ public function getCallWriter()
+ {
+ return $this->callWriter;
+ }
+}
diff --git a/platform/www/inc/Parsing/Handler/Block.php b/platform/www/inc/Parsing/Handler/Block.php
new file mode 100644
index 0000000..4cfa686
--- /dev/null
+++ b/platform/www/inc/Parsing/Handler/Block.php
@@ -0,0 +1,211 @@
+<?php
+
+namespace dokuwiki\Parsing\Handler;
+
+/**
+ * Handler for paragraphs
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ */
+class Block
+{
+ protected $calls = array();
+ protected $skipEol = false;
+ protected $inParagraph = false;
+
+ // Blocks these should not be inside paragraphs
+ protected $blockOpen = array(
+ 'header',
+ 'listu_open','listo_open','listitem_open','listcontent_open',
+ 'table_open','tablerow_open','tablecell_open','tableheader_open','tablethead_open',
+ 'quote_open',
+ 'code','file','hr','preformatted','rss',
+ 'htmlblock','phpblock',
+ 'footnote_open',
+ );
+
+ protected $blockClose = array(
+ 'header',
+ 'listu_close','listo_close','listitem_close','listcontent_close',
+ 'table_close','tablerow_close','tablecell_close','tableheader_close','tablethead_close',
+ 'quote_close',
+ 'code','file','hr','preformatted','rss',
+ 'htmlblock','phpblock',
+ 'footnote_close',
+ );
+
+ // Stacks can contain paragraphs
+ protected $stackOpen = array(
+ 'section_open',
+ );
+
+ protected $stackClose = array(
+ 'section_close',
+ );
+
+
+ /**
+ * Constructor. Adds loaded syntax plugins to the block and stack
+ * arrays
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function __construct()
+ {
+ global $DOKU_PLUGINS;
+ //check if syntax plugins were loaded
+ if (empty($DOKU_PLUGINS['syntax'])) return;
+ foreach ($DOKU_PLUGINS['syntax'] as $n => $p) {
+ $ptype = $p->getPType();
+ if ($ptype == 'block') {
+ $this->blockOpen[] = 'plugin_'.$n;
+ $this->blockClose[] = 'plugin_'.$n;
+ } elseif ($ptype == 'stack') {
+ $this->stackOpen[] = 'plugin_'.$n;
+ $this->stackClose[] = 'plugin_'.$n;
+ }
+ }
+ }
+
+ protected function openParagraph($pos)
+ {
+ if ($this->inParagraph) return;
+ $this->calls[] = array('p_open',array(), $pos);
+ $this->inParagraph = true;
+ $this->skipEol = true;
+ }
+
+ /**
+ * Close a paragraph if needed
+ *
+ * This function makes sure there are no empty paragraphs on the stack
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string|integer $pos
+ */
+ protected function closeParagraph($pos)
+ {
+ if (!$this->inParagraph) return;
+ // look back if there was any content - we don't want empty paragraphs
+ $content = '';
+ $ccount = count($this->calls);
+ for ($i=$ccount-1; $i>=0; $i--) {
+ if ($this->calls[$i][0] == 'p_open') {
+ break;
+ } elseif ($this->calls[$i][0] == 'cdata') {
+ $content .= $this->calls[$i][1][0];
+ } else {
+ $content = 'found markup';
+ break;
+ }
+ }
+
+ if (trim($content)=='') {
+ //remove the whole paragraph
+ //array_splice($this->calls,$i); // <- this is much slower than the loop below
+ for ($x=$ccount; $x>$i;
+ $x--) array_pop($this->calls);
+ } else {
+ // remove ending linebreaks in the paragraph
+ $i=count($this->calls)-1;
+ if ($this->calls[$i][0] == 'cdata') $this->calls[$i][1][0] = rtrim($this->calls[$i][1][0], "\n");
+ $this->calls[] = array('p_close',array(), $pos);
+ }
+
+ $this->inParagraph = false;
+ $this->skipEol = true;
+ }
+
+ protected function addCall($call)
+ {
+ $key = count($this->calls);
+ if ($key and ($call[0] == 'cdata') and ($this->calls[$key-1][0] == 'cdata')) {
+ $this->calls[$key-1][1][0] .= $call[1][0];
+ } else {
+ $this->calls[] = $call;
+ }
+ }
+
+ // simple version of addCall, without checking cdata
+ protected function storeCall($call)
+ {
+ $this->calls[] = $call;
+ }
+
+ /**
+ * Processes the whole instruction stack to open and close paragraphs
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $calls
+ *
+ * @return array
+ */
+ public function process($calls)
+ {
+ // open first paragraph
+ $this->openParagraph(0);
+ foreach ($calls as $key => $call) {
+ $cname = $call[0];
+ if ($cname == 'plugin') {
+ $cname='plugin_'.$call[1][0];
+ $plugin = true;
+ $plugin_open = (($call[1][2] == DOKU_LEXER_ENTER) || ($call[1][2] == DOKU_LEXER_SPECIAL));
+ $plugin_close = (($call[1][2] == DOKU_LEXER_EXIT) || ($call[1][2] == DOKU_LEXER_SPECIAL));
+ } else {
+ $plugin = false;
+ }
+ /* stack */
+ if (in_array($cname, $this->stackClose) && (!$plugin || $plugin_close)) {
+ $this->closeParagraph($call[2]);
+ $this->storeCall($call);
+ $this->openParagraph($call[2]);
+ continue;
+ }
+ if (in_array($cname, $this->stackOpen) && (!$plugin || $plugin_open)) {
+ $this->closeParagraph($call[2]);
+ $this->storeCall($call);
+ $this->openParagraph($call[2]);
+ continue;
+ }
+ /* block */
+ // If it's a substition it opens and closes at the same call.
+ // To make sure next paragraph is correctly started, let close go first.
+ if (in_array($cname, $this->blockClose) && (!$plugin || $plugin_close)) {
+ $this->closeParagraph($call[2]);
+ $this->storeCall($call);
+ $this->openParagraph($call[2]);
+ continue;
+ }
+ if (in_array($cname, $this->blockOpen) && (!$plugin || $plugin_open)) {
+ $this->closeParagraph($call[2]);
+ $this->storeCall($call);
+ continue;
+ }
+ /* eol */
+ if ($cname == 'eol') {
+ // Check this isn't an eol instruction to skip...
+ if (!$this->skipEol) {
+ // Next is EOL => double eol => mark as paragraph
+ if (isset($calls[$key+1]) && $calls[$key+1][0] == 'eol') {
+ $this->closeParagraph($call[2]);
+ $this->openParagraph($call[2]);
+ } else {
+ //if this is just a single eol make a space from it
+ $this->addCall(array('cdata',array("\n"), $call[2]));
+ }
+ }
+ continue;
+ }
+ /* normal */
+ $this->addCall($call);
+ $this->skipEol = false;
+ }
+ // close last paragraph
+ $call = end($this->calls);
+ $this->closeParagraph($call[2]);
+ return $this->calls;
+ }
+}
diff --git a/platform/www/inc/Parsing/Handler/CallWriter.php b/platform/www/inc/Parsing/Handler/CallWriter.php
new file mode 100644
index 0000000..2457143
--- /dev/null
+++ b/platform/www/inc/Parsing/Handler/CallWriter.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace dokuwiki\Parsing\Handler;
+
+class CallWriter implements CallWriterInterface
+{
+
+ /** @var \Doku_Handler $Handler */
+ protected $Handler;
+
+ /**
+ * @param \Doku_Handler $Handler
+ */
+ public function __construct(\Doku_Handler $Handler)
+ {
+ $this->Handler = $Handler;
+ }
+
+ /** @inheritdoc */
+ public function writeCall($call)
+ {
+ $this->Handler->calls[] = $call;
+ }
+
+ /** @inheritdoc */
+ public function writeCalls($calls)
+ {
+ $this->Handler->calls = array_merge($this->Handler->calls, $calls);
+ }
+
+ /**
+ * @inheritdoc
+ * function is required, but since this call writer is first/highest in
+ * the chain it is not required to do anything
+ */
+ public function finalise()
+ {
+ unset($this->Handler);
+ }
+}
diff --git a/platform/www/inc/Parsing/Handler/CallWriterInterface.php b/platform/www/inc/Parsing/Handler/CallWriterInterface.php
new file mode 100644
index 0000000..ffc2468
--- /dev/null
+++ b/platform/www/inc/Parsing/Handler/CallWriterInterface.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace dokuwiki\Parsing\Handler;
+
+interface CallWriterInterface
+{
+ /**
+ * Add a call to our call list
+ *
+ * @param array $call the call to be added
+ */
+ public function writeCall($call);
+
+ /**
+ * Append a list of calls to our call list
+ *
+ * @param array[] $calls list of calls to be appended
+ */
+ public function writeCalls($calls);
+
+ /**
+ * Explicit request to finish up and clean up NOW!
+ * (probably because document end has been reached)
+ *
+ * If part of a CallWriter chain, call finalise on
+ * the original call writer
+ *
+ */
+ public function finalise();
+}
diff --git a/platform/www/inc/Parsing/Handler/Lists.php b/platform/www/inc/Parsing/Handler/Lists.php
new file mode 100644
index 0000000..282ddfb
--- /dev/null
+++ b/platform/www/inc/Parsing/Handler/Lists.php
@@ -0,0 +1,186 @@
+<?php
+
+namespace dokuwiki\Parsing\Handler;
+
+class Lists extends AbstractRewriter
+{
+ protected $listCalls = array();
+ protected $listStack = array();
+
+ protected $initialDepth = 0;
+
+ const NODE = 1;
+
+ /** @inheritdoc */
+ public function finalise()
+ {
+ $last_call = end($this->calls);
+ $this->writeCall(array('list_close',array(), $last_call[2]));
+
+ $this->process();
+ $this->callWriter->finalise();
+ unset($this->callWriter);
+ }
+
+ /** @inheritdoc */
+ public function process()
+ {
+
+ foreach ($this->calls as $call) {
+ switch ($call[0]) {
+ case 'list_item':
+ $this->listOpen($call);
+ break;
+ case 'list_open':
+ $this->listStart($call);
+ break;
+ case 'list_close':
+ $this->listEnd($call);
+ break;
+ default:
+ $this->listContent($call);
+ break;
+ }
+ }
+
+ $this->callWriter->writeCalls($this->listCalls);
+ return $this->callWriter;
+ }
+
+ protected function listStart($call)
+ {
+ $depth = $this->interpretSyntax($call[1][0], $listType);
+
+ $this->initialDepth = $depth;
+ // array(list type, current depth, index of current listitem_open)
+ $this->listStack[] = array($listType, $depth, 1);
+
+ $this->listCalls[] = array('list'.$listType.'_open',array(),$call[2]);
+ $this->listCalls[] = array('listitem_open',array(1),$call[2]);
+ $this->listCalls[] = array('listcontent_open',array(),$call[2]);
+ }
+
+
+ protected function listEnd($call)
+ {
+ $closeContent = true;
+
+ while ($list = array_pop($this->listStack)) {
+ if ($closeContent) {
+ $this->listCalls[] = array('listcontent_close',array(),$call[2]);
+ $closeContent = false;
+ }
+ $this->listCalls[] = array('listitem_close',array(),$call[2]);
+ $this->listCalls[] = array('list'.$list[0].'_close', array(), $call[2]);
+ }
+ }
+
+ protected function listOpen($call)
+ {
+ $depth = $this->interpretSyntax($call[1][0], $listType);
+ $end = end($this->listStack);
+ $key = key($this->listStack);
+
+ // Not allowed to be shallower than initialDepth
+ if ($depth < $this->initialDepth) {
+ $depth = $this->initialDepth;
+ }
+
+ if ($depth == $end[1]) {
+ // Just another item in the list...
+ if ($listType == $end[0]) {
+ $this->listCalls[] = array('listcontent_close',array(),$call[2]);
+ $this->listCalls[] = array('listitem_close',array(),$call[2]);
+ $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]);
+ $this->listCalls[] = array('listcontent_open',array(),$call[2]);
+
+ // new list item, update list stack's index into current listitem_open
+ $this->listStack[$key][2] = count($this->listCalls) - 2;
+
+ // Switched list type...
+ } else {
+ $this->listCalls[] = array('listcontent_close',array(),$call[2]);
+ $this->listCalls[] = array('listitem_close',array(),$call[2]);
+ $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]);
+ $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]);
+ $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]);
+ $this->listCalls[] = array('listcontent_open',array(),$call[2]);
+
+ array_pop($this->listStack);
+ $this->listStack[] = array($listType, $depth, count($this->listCalls) - 2);
+ }
+ } elseif ($depth > $end[1]) { // Getting deeper...
+ $this->listCalls[] = array('listcontent_close',array(),$call[2]);
+ $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]);
+ $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]);
+ $this->listCalls[] = array('listcontent_open',array(),$call[2]);
+
+ // set the node/leaf state of this item's parent listitem_open to NODE
+ $this->listCalls[$this->listStack[$key][2]][1][1] = self::NODE;
+
+ $this->listStack[] = array($listType, $depth, count($this->listCalls) - 2);
+ } else { // Getting shallower ( $depth < $end[1] )
+ $this->listCalls[] = array('listcontent_close',array(),$call[2]);
+ $this->listCalls[] = array('listitem_close',array(),$call[2]);
+ $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]);
+
+ // Throw away the end - done
+ array_pop($this->listStack);
+
+ while (1) {
+ $end = end($this->listStack);
+ $key = key($this->listStack);
+
+ if ($end[1] <= $depth) {
+ // Normalize depths
+ $depth = $end[1];
+
+ $this->listCalls[] = array('listitem_close',array(),$call[2]);
+
+ if ($end[0] == $listType) {
+ $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]);
+ $this->listCalls[] = array('listcontent_open',array(),$call[2]);
+
+ // new list item, update list stack's index into current listitem_open
+ $this->listStack[$key][2] = count($this->listCalls) - 2;
+ } else {
+ // Switching list type...
+ $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]);
+ $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]);
+ $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]);
+ $this->listCalls[] = array('listcontent_open',array(),$call[2]);
+
+ array_pop($this->listStack);
+ $this->listStack[] = array($listType, $depth, count($this->listCalls) - 2);
+ }
+
+ break;
+
+ // Haven't dropped down far enough yet.... ( $end[1] > $depth )
+ } else {
+ $this->listCalls[] = array('listitem_close',array(),$call[2]);
+ $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]);
+
+ array_pop($this->listStack);
+ }
+ }
+ }
+ }
+
+ protected function listContent($call)
+ {
+ $this->listCalls[] = $call;
+ }
+
+ protected function interpretSyntax($match, & $type)
+ {
+ if (substr($match, -1) == '*') {
+ $type = 'u';
+ } else {
+ $type = 'o';
+ }
+ // Is the +1 needed? It used to be count(explode(...))
+ // but I don't think the number is seen outside this handler
+ return substr_count(str_replace("\t", ' ', $match), ' ') + 1;
+ }
+}
diff --git a/platform/www/inc/Parsing/Handler/Nest.php b/platform/www/inc/Parsing/Handler/Nest.php
new file mode 100644
index 0000000..98d2134
--- /dev/null
+++ b/platform/www/inc/Parsing/Handler/Nest.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace dokuwiki\Parsing\Handler;
+
+/**
+ * Generic call writer class to handle nesting of rendering instructions
+ * within a render instruction. Also see nest() method of renderer base class
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ */
+class Nest extends AbstractRewriter
+{
+ protected $closingInstruction;
+
+ /**
+ * @inheritdoc
+ *
+ * @param CallWriterInterface $CallWriter the parser's current call writer, i.e. the one above us in the chain
+ * @param string $close closing instruction name, this is required to properly terminate the
+ * syntax mode if the document ends without a closing pattern
+ */
+ public function __construct(CallWriterInterface $CallWriter, $close = "nest_close")
+ {
+ parent::__construct($CallWriter);
+ $this->closingInstruction = $close;
+ }
+
+ /** @inheritdoc */
+ public function writeCall($call)
+ {
+ $this->calls[] = $call;
+ }
+
+ /** @inheritdoc */
+ public function writeCalls($calls)
+ {
+ $this->calls = array_merge($this->calls, $calls);
+ }
+
+ /** @inheritdoc */
+ public function finalise()
+ {
+ $last_call = end($this->calls);
+ $this->writeCall(array($this->closingInstruction,array(), $last_call[2]));
+
+ $this->process();
+ $this->callWriter->finalise();
+ unset($this->callWriter);
+ }
+
+ /** @inheritdoc */
+ public function process()
+ {
+ // merge consecutive cdata
+ $unmerged_calls = $this->calls;
+ $this->calls = array();
+
+ foreach ($unmerged_calls as $call) $this->addCall($call);
+
+ $first_call = reset($this->calls);
+ $this->callWriter->writeCall(array("nest", array($this->calls), $first_call[2]));
+
+ return $this->callWriter;
+ }
+
+ /**
+ * @param array $call
+ */
+ protected function addCall($call)
+ {
+ $key = count($this->calls);
+ if ($key and ($call[0] == 'cdata') and ($this->calls[$key-1][0] == 'cdata')) {
+ $this->calls[$key-1][1][0] .= $call[1][0];
+ } elseif ($call[0] == 'eol') {
+ // do nothing (eol shouldn't be allowed, to counter preformatted fix in #1652 & #1699)
+ } else {
+ $this->calls[] = $call;
+ }
+ }
+
+
+}
diff --git a/platform/www/inc/Parsing/Handler/Preformatted.php b/platform/www/inc/Parsing/Handler/Preformatted.php
new file mode 100644
index 0000000..41beb66
--- /dev/null
+++ b/platform/www/inc/Parsing/Handler/Preformatted.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace dokuwiki\Parsing\Handler;
+
+class Preformatted extends AbstractRewriter
+{
+
+ protected $pos;
+ protected $text ='';
+
+ /** @inheritdoc */
+ public function finalise()
+ {
+ $last_call = end($this->calls);
+ $this->writeCall(array('preformatted_end',array(), $last_call[2]));
+
+ $this->process();
+ $this->callWriter->finalise();
+ unset($this->callWriter);
+ }
+
+ /** @inheritdoc */
+ public function process()
+ {
+ foreach ($this->calls as $call) {
+ switch ($call[0]) {
+ case 'preformatted_start':
+ $this->pos = $call[2];
+ break;
+ case 'preformatted_newline':
+ $this->text .= "\n";
+ break;
+ case 'preformatted_content':
+ $this->text .= $call[1][0];
+ break;
+ case 'preformatted_end':
+ if (trim($this->text)) {
+ $this->callWriter->writeCall(array('preformatted', array($this->text), $this->pos));
+ }
+ // see FS#1699 & FS#1652, add 'eol' instructions to ensure proper triggering of following p_open
+ $this->callWriter->writeCall(array('eol', array(), $this->pos));
+ $this->callWriter->writeCall(array('eol', array(), $this->pos));
+ break;
+ }
+ }
+
+ return $this->callWriter;
+ }
+}
diff --git a/platform/www/inc/Parsing/Handler/Quote.php b/platform/www/inc/Parsing/Handler/Quote.php
new file mode 100644
index 0000000..74861b1
--- /dev/null
+++ b/platform/www/inc/Parsing/Handler/Quote.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace dokuwiki\Parsing\Handler;
+
+class Quote extends AbstractRewriter
+{
+ protected $quoteCalls = array();
+
+ /** @inheritdoc */
+ public function finalise()
+ {
+ $last_call = end($this->calls);
+ $this->writeCall(array('quote_end',array(), $last_call[2]));
+
+ $this->process();
+ $this->callWriter->finalise();
+ unset($this->callWriter);
+ }
+
+ /** @inheritdoc */
+ public function process()
+ {
+
+ $quoteDepth = 1;
+
+ foreach ($this->calls as $call) {
+ switch ($call[0]) {
+
+ /** @noinspection PhpMissingBreakStatementInspection */
+ case 'quote_start':
+ $this->quoteCalls[] = array('quote_open',array(),$call[2]);
+ // fallthrough
+ case 'quote_newline':
+ $quoteLength = $this->getDepth($call[1][0]);
+
+ if ($quoteLength > $quoteDepth) {
+ $quoteDiff = $quoteLength - $quoteDepth;
+ for ($i = 1; $i <= $quoteDiff; $i++) {
+ $this->quoteCalls[] = array('quote_open',array(),$call[2]);
+ }
+ } elseif ($quoteLength < $quoteDepth) {
+ $quoteDiff = $quoteDepth - $quoteLength;
+ for ($i = 1; $i <= $quoteDiff; $i++) {
+ $this->quoteCalls[] = array('quote_close',array(),$call[2]);
+ }
+ } else {
+ if ($call[0] != 'quote_start') $this->quoteCalls[] = array('linebreak',array(),$call[2]);
+ }
+
+ $quoteDepth = $quoteLength;
+
+ break;
+
+ case 'quote_end':
+ if ($quoteDepth > 1) {
+ $quoteDiff = $quoteDepth - 1;
+ for ($i = 1; $i <= $quoteDiff; $i++) {
+ $this->quoteCalls[] = array('quote_close',array(),$call[2]);
+ }
+ }
+
+ $this->quoteCalls[] = array('quote_close',array(),$call[2]);
+
+ $this->callWriter->writeCalls($this->quoteCalls);
+ break;
+
+ default:
+ $this->quoteCalls[] = $call;
+ break;
+ }
+ }
+
+ return $this->callWriter;
+ }
+
+ /**
+ * @param string $marker
+ * @return int
+ */
+ protected function getDepth($marker)
+ {
+ preg_match('/>{1,}/', $marker, $matches);
+ $quoteLength = strlen($matches[0]);
+ return $quoteLength;
+ }
+}
diff --git a/platform/www/inc/Parsing/Handler/ReWriterInterface.php b/platform/www/inc/Parsing/Handler/ReWriterInterface.php
new file mode 100644
index 0000000..2fa7b25
--- /dev/null
+++ b/platform/www/inc/Parsing/Handler/ReWriterInterface.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace dokuwiki\Parsing\Handler;
+
+/**
+ * A ReWriter takes over from the orignal call writer and handles all new calls itself until
+ * the process method is called and control is given back to the original writer.
+ *
+ * @property array[] $calls The list of current calls
+ */
+interface ReWriterInterface extends CallWriterInterface
+{
+ /**
+ * ReWriterInterface constructor.
+ *
+ * This rewriter will be registered as the new call writer in the Handler.
+ * The original is passed as parameter
+ *
+ * @param CallWriterInterface $callWriter the original callwriter
+ */
+ public function __construct(CallWriterInterface $callWriter);
+
+ /**
+ * Process any calls that have been added and add them to the
+ * original call writer
+ *
+ * @return CallWriterInterface the orignal call writer
+ */
+ public function process();
+
+ /**
+ * Accessor for this rewriter's original CallWriter
+ *
+ * @return CallWriterInterface
+ */
+ public function getCallWriter();
+}
diff --git a/platform/www/inc/Parsing/Handler/Table.php b/platform/www/inc/Parsing/Handler/Table.php
new file mode 100644
index 0000000..35ff5a8
--- /dev/null
+++ b/platform/www/inc/Parsing/Handler/Table.php
@@ -0,0 +1,320 @@
+<?php
+
+namespace dokuwiki\Parsing\Handler;
+
+class Table extends AbstractRewriter
+{
+
+ protected $tableCalls = array();
+ protected $maxCols = 0;
+ protected $maxRows = 1;
+ protected $currentCols = 0;
+ protected $firstCell = false;
+ protected $lastCellType = 'tablecell';
+ protected $inTableHead = true;
+ protected $currentRow = array('tableheader' => 0, 'tablecell' => 0);
+ protected $countTableHeadRows = 0;
+
+ /** @inheritdoc */
+ public function finalise()
+ {
+ $last_call = end($this->calls);
+ $this->writeCall(array('table_end',array(), $last_call[2]));
+
+ $this->process();
+ $this->callWriter->finalise();
+ unset($this->callWriter);
+ }
+
+ /** @inheritdoc */
+ public function process()
+ {
+ foreach ($this->calls as $call) {
+ switch ($call[0]) {
+ case 'table_start':
+ $this->tableStart($call);
+ break;
+ case 'table_row':
+ $this->tableRowClose($call);
+ $this->tableRowOpen(array('tablerow_open',$call[1],$call[2]));
+ break;
+ case 'tableheader':
+ case 'tablecell':
+ $this->tableCell($call);
+ break;
+ case 'table_end':
+ $this->tableRowClose($call);
+ $this->tableEnd($call);
+ break;
+ default:
+ $this->tableDefault($call);
+ break;
+ }
+ }
+ $this->callWriter->writeCalls($this->tableCalls);
+
+ return $this->callWriter;
+ }
+
+ protected function tableStart($call)
+ {
+ $this->tableCalls[] = array('table_open',$call[1],$call[2]);
+ $this->tableCalls[] = array('tablerow_open',array(),$call[2]);
+ $this->firstCell = true;
+ }
+
+ protected function tableEnd($call)
+ {
+ $this->tableCalls[] = array('table_close',$call[1],$call[2]);
+ $this->finalizeTable();
+ }
+
+ protected function tableRowOpen($call)
+ {
+ $this->tableCalls[] = $call;
+ $this->currentCols = 0;
+ $this->firstCell = true;
+ $this->lastCellType = 'tablecell';
+ $this->maxRows++;
+ if ($this->inTableHead) {
+ $this->currentRow = array('tablecell' => 0, 'tableheader' => 0);
+ }
+ }
+
+ protected function tableRowClose($call)
+ {
+ if ($this->inTableHead && ($this->inTableHead = $this->isTableHeadRow())) {
+ $this->countTableHeadRows++;
+ }
+ // Strip off final cell opening and anything after it
+ while ($discard = array_pop($this->tableCalls)) {
+ if ($discard[0] == 'tablecell_open' || $discard[0] == 'tableheader_open') {
+ break;
+ }
+ if (!empty($this->currentRow[$discard[0]])) {
+ $this->currentRow[$discard[0]]--;
+ }
+ }
+ $this->tableCalls[] = array('tablerow_close', array(), $call[2]);
+
+ if ($this->currentCols > $this->maxCols) {
+ $this->maxCols = $this->currentCols;
+ }
+ }
+
+ protected function isTableHeadRow()
+ {
+ $td = $this->currentRow['tablecell'];
+ $th = $this->currentRow['tableheader'];
+
+ if (!$th || $td > 2) return false;
+ if (2*$td > $th) return false;
+
+ return true;
+ }
+
+ protected function tableCell($call)
+ {
+ if ($this->inTableHead) {
+ $this->currentRow[$call[0]]++;
+ }
+ if (!$this->firstCell) {
+ // Increase the span
+ $lastCall = end($this->tableCalls);
+
+ // A cell call which follows an open cell means an empty cell so span
+ if ($lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open') {
+ $this->tableCalls[] = array('colspan',array(),$call[2]);
+ }
+
+ $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]);
+ $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]);
+ $this->lastCellType = $call[0];
+ } else {
+ $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]);
+ $this->lastCellType = $call[0];
+ $this->firstCell = false;
+ }
+
+ $this->currentCols++;
+ }
+
+ protected function tableDefault($call)
+ {
+ $this->tableCalls[] = $call;
+ }
+
+ protected function finalizeTable()
+ {
+
+ // Add the max cols and rows to the table opening
+ if ($this->tableCalls[0][0] == 'table_open') {
+ // Adjust to num cols not num col delimeters
+ $this->tableCalls[0][1][] = $this->maxCols - 1;
+ $this->tableCalls[0][1][] = $this->maxRows;
+ $this->tableCalls[0][1][] = array_shift($this->tableCalls[0][1]);
+ } else {
+ trigger_error('First element in table call list is not table_open');
+ }
+
+ $lastRow = 0;
+ $lastCell = 0;
+ $cellKey = array();
+ $toDelete = array();
+
+ // if still in tableheader, then there can be no table header
+ // as all rows can't be within <THEAD>
+ if ($this->inTableHead) {
+ $this->inTableHead = false;
+ $this->countTableHeadRows = 0;
+ }
+
+ // Look for the colspan elements and increment the colspan on the
+ // previous non-empty opening cell. Once done, delete all the cells
+ // that contain colspans
+ for ($key = 0; $key < count($this->tableCalls); ++$key) {
+ $call = $this->tableCalls[$key];
+
+ switch ($call[0]) {
+ case 'table_open':
+ if ($this->countTableHeadRows) {
+ array_splice($this->tableCalls, $key+1, 0, array(
+ array('tablethead_open', array(), $call[2])));
+ }
+ break;
+
+ case 'tablerow_open':
+ $lastRow++;
+ $lastCell = 0;
+ break;
+
+ case 'tablecell_open':
+ case 'tableheader_open':
+ $lastCell++;
+ $cellKey[$lastRow][$lastCell] = $key;
+ break;
+
+ case 'table_align':
+ $prev = in_array($this->tableCalls[$key-1][0], array('tablecell_open', 'tableheader_open'));
+ $next = in_array($this->tableCalls[$key+1][0], array('tablecell_close', 'tableheader_close'));
+ // If the cell is empty, align left
+ if ($prev && $next) {
+ $this->tableCalls[$key-1][1][1] = 'left';
+
+ // If the previous element was a cell open, align right
+ } elseif ($prev) {
+ $this->tableCalls[$key-1][1][1] = 'right';
+
+ // If the next element is the close of an element, align either center or left
+ } elseif ($next) {
+ if ($this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] == 'right') {
+ $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'center';
+ } else {
+ $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'left';
+ }
+ }
+
+ // Now convert the whitespace back to cdata
+ $this->tableCalls[$key][0] = 'cdata';
+ break;
+
+ case 'colspan':
+ $this->tableCalls[$key-1][1][0] = false;
+
+ for ($i = $key-2; $i >= $cellKey[$lastRow][1]; $i--) {
+ if ($this->tableCalls[$i][0] == 'tablecell_open' ||
+ $this->tableCalls[$i][0] == 'tableheader_open'
+ ) {
+ if (false !== $this->tableCalls[$i][1][0]) {
+ $this->tableCalls[$i][1][0]++;
+ break;
+ }
+ }
+ }
+
+ $toDelete[] = $key-1;
+ $toDelete[] = $key;
+ $toDelete[] = $key+1;
+ break;
+
+ case 'rowspan':
+ if ($this->tableCalls[$key-1][0] == 'cdata') {
+ // ignore rowspan if previous call was cdata (text mixed with :::)
+ // we don't have to check next call as that wont match regex
+ $this->tableCalls[$key][0] = 'cdata';
+ } else {
+ $spanning_cell = null;
+
+ // can't cross thead/tbody boundary
+ if (!$this->countTableHeadRows || ($lastRow-1 != $this->countTableHeadRows)) {
+ for ($i = $lastRow-1; $i > 0; $i--) {
+ if ($this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tablecell_open' ||
+ $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tableheader_open'
+ ) {
+ if ($this->tableCalls[$cellKey[$i][$lastCell]][1][2] >= $lastRow - $i) {
+ $spanning_cell = $i;
+ break;
+ }
+ }
+ }
+ }
+ if (is_null($spanning_cell)) {
+ // No spanning cell found, so convert this cell to
+ // an empty one to avoid broken tables
+ $this->tableCalls[$key][0] = 'cdata';
+ $this->tableCalls[$key][1][0] = '';
+ break;
+ }
+ $this->tableCalls[$cellKey[$spanning_cell][$lastCell]][1][2]++;
+
+ $this->tableCalls[$key-1][1][2] = false;
+
+ $toDelete[] = $key-1;
+ $toDelete[] = $key;
+ $toDelete[] = $key+1;
+ }
+ break;
+
+ case 'tablerow_close':
+ // Fix broken tables by adding missing cells
+ $moreCalls = array();
+ while (++$lastCell < $this->maxCols) {
+ $moreCalls[] = array('tablecell_open', array(1, null, 1), $call[2]);
+ $moreCalls[] = array('cdata', array(''), $call[2]);
+ $moreCalls[] = array('tablecell_close', array(), $call[2]);
+ }
+ $moreCallsLength = count($moreCalls);
+ if ($moreCallsLength) {
+ array_splice($this->tableCalls, $key, 0, $moreCalls);
+ $key += $moreCallsLength;
+ }
+
+ if ($this->countTableHeadRows == $lastRow) {
+ array_splice($this->tableCalls, $key+1, 0, array(
+ array('tablethead_close', array(), $call[2])));
+ }
+ break;
+ }
+ }
+
+ // condense cdata
+ $cnt = count($this->tableCalls);
+ for ($key = 0; $key < $cnt; $key++) {
+ if ($this->tableCalls[$key][0] == 'cdata') {
+ $ckey = $key;
+ $key++;
+ while ($this->tableCalls[$key][0] == 'cdata') {
+ $this->tableCalls[$ckey][1][0] .= $this->tableCalls[$key][1][0];
+ $toDelete[] = $key;
+ $key++;
+ }
+ continue;
+ }
+ }
+
+ foreach ($toDelete as $delete) {
+ unset($this->tableCalls[$delete]);
+ }
+ $this->tableCalls = array_values($this->tableCalls);
+ }
+}
diff --git a/platform/www/inc/Parsing/Lexer/Lexer.php b/platform/www/inc/Parsing/Lexer/Lexer.php
new file mode 100644
index 0000000..edcd251
--- /dev/null
+++ b/platform/www/inc/Parsing/Lexer/Lexer.php
@@ -0,0 +1,349 @@
+<?php
+/**
+ * Lexer adapted from Simple Test: http://sourceforge.net/projects/simpletest/
+ * For an intro to the Lexer see:
+ * https://web.archive.org/web/20120125041816/http://www.phppatterns.com/docs/develop/simple_test_lexer_notes
+ *
+ * @author Marcus Baker http://www.lastcraft.com
+ */
+
+namespace dokuwiki\Parsing\Lexer;
+
+/**
+ * Accepts text and breaks it into tokens.
+ *
+ * Some optimisation to make the sure the content is only scanned by the PHP regex
+ * parser once. Lexer modes must not start with leading underscores.
+ */
+class Lexer
+{
+ /** @var ParallelRegex[] */
+ protected $regexes;
+ /** @var \Doku_Handler */
+ protected $handler;
+ /** @var StateStack */
+ protected $modeStack;
+ /** @var array mode "rewrites" */
+ protected $mode_handlers;
+ /** @var bool case sensitive? */
+ protected $case;
+
+ /**
+ * Sets up the lexer in case insensitive matching by default.
+ *
+ * @param \Doku_Handler $handler Handling strategy by reference.
+ * @param string $start Starting handler.
+ * @param boolean $case True for case sensitive.
+ */
+ public function __construct($handler, $start = "accept", $case = false)
+ {
+ $this->case = $case;
+ $this->regexes = array();
+ $this->handler = $handler;
+ $this->modeStack = new StateStack($start);
+ $this->mode_handlers = array();
+ }
+
+ /**
+ * Adds a token search pattern for a particular parsing mode.
+ *
+ * The pattern does not change the current mode.
+ *
+ * @param string $pattern Perl style regex, but ( and )
+ * lose the usual meaning.
+ * @param string $mode Should only apply this
+ * pattern when dealing with
+ * this type of input.
+ */
+ public function addPattern($pattern, $mode = "accept")
+ {
+ if (! isset($this->regexes[$mode])) {
+ $this->regexes[$mode] = new ParallelRegex($this->case);
+ }
+ $this->regexes[$mode]->addPattern($pattern);
+ }
+
+ /**
+ * Adds a pattern that will enter a new parsing mode.
+ *
+ * Useful for entering parenthesis, strings, tags, etc.
+ *
+ * @param string $pattern Perl style regex, but ( and ) lose the usual meaning.
+ * @param string $mode Should only apply this pattern when dealing with this type of input.
+ * @param string $new_mode Change parsing to this new nested mode.
+ */
+ public function addEntryPattern($pattern, $mode, $new_mode)
+ {
+ if (! isset($this->regexes[$mode])) {
+ $this->regexes[$mode] = new ParallelRegex($this->case);
+ }
+ $this->regexes[$mode]->addPattern($pattern, $new_mode);
+ }
+
+ /**
+ * Adds a pattern that will exit the current mode and re-enter the previous one.
+ *
+ * @param string $pattern Perl style regex, but ( and ) lose the usual meaning.
+ * @param string $mode Mode to leave.
+ */
+ public function addExitPattern($pattern, $mode)
+ {
+ if (! isset($this->regexes[$mode])) {
+ $this->regexes[$mode] = new ParallelRegex($this->case);
+ }
+ $this->regexes[$mode]->addPattern($pattern, "__exit");
+ }
+
+ /**
+ * Adds a pattern that has a special mode.
+ *
+ * Acts as an entry and exit pattern in one go, effectively calling a special
+ * parser handler for this token only.
+ *
+ * @param string $pattern Perl style regex, but ( and ) lose the usual meaning.
+ * @param string $mode Should only apply this pattern when dealing with this type of input.
+ * @param string $special Use this mode for this one token.
+ */
+ public function addSpecialPattern($pattern, $mode, $special)
+ {
+ if (! isset($this->regexes[$mode])) {
+ $this->regexes[$mode] = new ParallelRegex($this->case);
+ }
+ $this->regexes[$mode]->addPattern($pattern, "_$special");
+ }
+
+ /**
+ * Adds a mapping from a mode to another handler.
+ *
+ * @param string $mode Mode to be remapped.
+ * @param string $handler New target handler.
+ */
+ public function mapHandler($mode, $handler)
+ {
+ $this->mode_handlers[$mode] = $handler;
+ }
+
+ /**
+ * Splits the page text into tokens.
+ *
+ * Will fail if the handlers report an error or if no content is consumed. If successful then each
+ * unparsed and parsed token invokes a call to the held listener.
+ *
+ * @param string $raw Raw HTML text.
+ * @return boolean True on success, else false.
+ */
+ public function parse($raw)
+ {
+ if (! isset($this->handler)) {
+ return false;
+ }
+ $initialLength = strlen($raw);
+ $length = $initialLength;
+ $pos = 0;
+ while (is_array($parsed = $this->reduce($raw))) {
+ list($unmatched, $matched, $mode) = $parsed;
+ $currentLength = strlen($raw);
+ $matchPos = $initialLength - $currentLength - strlen($matched);
+ if (! $this->dispatchTokens($unmatched, $matched, $mode, $pos, $matchPos)) {
+ return false;
+ }
+ if ($currentLength == $length) {
+ return false;
+ }
+ $length = $currentLength;
+ $pos = $initialLength - $currentLength;
+ }
+ if (!$parsed) {
+ return false;
+ }
+ return $this->invokeHandler($raw, DOKU_LEXER_UNMATCHED, $pos);
+ }
+
+ /**
+ * Gives plugins access to the mode stack
+ *
+ * @return StateStack
+ */
+ public function getModeStack()
+ {
+ return $this->modeStack;
+ }
+
+ /**
+ * Sends the matched token and any leading unmatched
+ * text to the parser changing the lexer to a new
+ * mode if one is listed.
+ *
+ * @param string $unmatched Unmatched leading portion.
+ * @param string $matched Actual token match.
+ * @param bool|string $mode Mode after match. A boolean false mode causes no change.
+ * @param int $initialPos
+ * @param int $matchPos Current byte index location in raw doc thats being parsed
+ * @return boolean False if there was any error from the parser.
+ */
+ protected function dispatchTokens($unmatched, $matched, $mode, $initialPos, $matchPos)
+ {
+ if (! $this->invokeHandler($unmatched, DOKU_LEXER_UNMATCHED, $initialPos)) {
+ return false;
+ }
+ if ($this->isModeEnd($mode)) {
+ if (! $this->invokeHandler($matched, DOKU_LEXER_EXIT, $matchPos)) {
+ return false;
+ }
+ return $this->modeStack->leave();
+ }
+ if ($this->isSpecialMode($mode)) {
+ $this->modeStack->enter($this->decodeSpecial($mode));
+ if (! $this->invokeHandler($matched, DOKU_LEXER_SPECIAL, $matchPos)) {
+ return false;
+ }
+ return $this->modeStack->leave();
+ }
+ if (is_string($mode)) {
+ $this->modeStack->enter($mode);
+ return $this->invokeHandler($matched, DOKU_LEXER_ENTER, $matchPos);
+ }
+ return $this->invokeHandler($matched, DOKU_LEXER_MATCHED, $matchPos);
+ }
+
+ /**
+ * Tests to see if the new mode is actually to leave the current mode and pop an item from the matching
+ * mode stack.
+ *
+ * @param string $mode Mode to test.
+ * @return boolean True if this is the exit mode.
+ */
+ protected function isModeEnd($mode)
+ {
+ return ($mode === "__exit");
+ }
+
+ /**
+ * Test to see if the mode is one where this mode is entered for this token only and automatically
+ * leaves immediately afterwoods.
+ *
+ * @param string $mode Mode to test.
+ * @return boolean True if this is the exit mode.
+ */
+ protected function isSpecialMode($mode)
+ {
+ return (strncmp($mode, "_", 1) == 0);
+ }
+
+ /**
+ * Strips the magic underscore marking single token modes.
+ *
+ * @param string $mode Mode to decode.
+ * @return string Underlying mode name.
+ */
+ protected function decodeSpecial($mode)
+ {
+ return substr($mode, 1);
+ }
+
+ /**
+ * Calls the parser method named after the current mode.
+ *
+ * Empty content will be ignored. The lexer has a parser handler for each mode in the lexer.
+ *
+ * @param string $content Text parsed.
+ * @param boolean $is_match Token is recognised rather
+ * than unparsed data.
+ * @param int $pos Current byte index location in raw doc
+ * thats being parsed
+ * @return bool
+ */
+ protected function invokeHandler($content, $is_match, $pos)
+ {
+ if (($content === "") || ($content === false)) {
+ return true;
+ }
+ $handler = $this->modeStack->getCurrent();
+ if (isset($this->mode_handlers[$handler])) {
+ $handler = $this->mode_handlers[$handler];
+ }
+
+ // modes starting with plugin_ are all handled by the same
+ // handler but with an additional parameter
+ if (substr($handler, 0, 7)=='plugin_') {
+ list($handler,$plugin) = explode('_', $handler, 2);
+ return $this->handler->$handler($content, $is_match, $pos, $plugin);
+ }
+
+ return $this->handler->$handler($content, $is_match, $pos);
+ }
+
+ /**
+ * Tries to match a chunk of text and if successful removes the recognised chunk and any leading
+ * unparsed data. Empty strings will not be matched.
+ *
+ * @param string $raw The subject to parse. This is the content that will be eaten.
+ * @return array|bool Three item list of unparsed content followed by the
+ * recognised token and finally the action the parser is to take.
+ * True if no match, false if there is a parsing error.
+ */
+ protected function reduce(&$raw)
+ {
+ if (! isset($this->regexes[$this->modeStack->getCurrent()])) {
+ return false;
+ }
+ if ($raw === "") {
+ return true;
+ }
+ if ($action = $this->regexes[$this->modeStack->getCurrent()]->split($raw, $split)) {
+ list($unparsed, $match, $raw) = $split;
+ return array($unparsed, $match, $action);
+ }
+ return true;
+ }
+
+ /**
+ * Escapes regex characters other than (, ) and /
+ *
+ * @param string $str
+ * @return string
+ */
+ public static function escape($str)
+ {
+ $chars = array(
+ '/\\\\/',
+ '/\./',
+ '/\+/',
+ '/\*/',
+ '/\?/',
+ '/\[/',
+ '/\^/',
+ '/\]/',
+ '/\$/',
+ '/\{/',
+ '/\}/',
+ '/\=/',
+ '/\!/',
+ '/\</',
+ '/\>/',
+ '/\|/',
+ '/\:/'
+ );
+
+ $escaped = array(
+ '\\\\\\\\',
+ '\.',
+ '\+',
+ '\*',
+ '\?',
+ '\[',
+ '\^',
+ '\]',
+ '\$',
+ '\{',
+ '\}',
+ '\=',
+ '\!',
+ '\<',
+ '\>',
+ '\|',
+ '\:'
+ );
+ return preg_replace($chars, $escaped, $str);
+ }
+}
diff --git a/platform/www/inc/Parsing/Lexer/ParallelRegex.php b/platform/www/inc/Parsing/Lexer/ParallelRegex.php
new file mode 100644
index 0000000..96f61a1
--- /dev/null
+++ b/platform/www/inc/Parsing/Lexer/ParallelRegex.php
@@ -0,0 +1,203 @@
+<?php
+/**
+ * Lexer adapted from Simple Test: http://sourceforge.net/projects/simpletest/
+ * For an intro to the Lexer see:
+ * https://web.archive.org/web/20120125041816/http://www.phppatterns.com/docs/develop/simple_test_lexer_notes
+ *
+ * @author Marcus Baker http://www.lastcraft.com
+ */
+
+namespace dokuwiki\Parsing\Lexer;
+
+/**
+ * Compounded regular expression.
+ *
+ * Any of the contained patterns could match and when one does it's label is returned.
+ */
+class ParallelRegex
+{
+ /** @var string[] patterns to match */
+ protected $patterns;
+ /** @var string[] labels for above patterns */
+ protected $labels;
+ /** @var string the compound regex matching all patterns */
+ protected $regex;
+ /** @var bool case sensitive matching? */
+ protected $case;
+
+ /**
+ * Constructor. Starts with no patterns.
+ *
+ * @param boolean $case True for case sensitive, false
+ * for insensitive.
+ */
+ public function __construct($case)
+ {
+ $this->case = $case;
+ $this->patterns = array();
+ $this->labels = array();
+ $this->regex = null;
+ }
+
+ /**
+ * Adds a pattern with an optional label.
+ *
+ * @param mixed $pattern Perl style regex. Must be UTF-8
+ * encoded. If its a string, the (, )
+ * lose their meaning unless they
+ * form part of a lookahead or
+ * lookbehind assertation.
+ * @param bool|string $label Label of regex to be returned
+ * on a match. Label must be ASCII
+ */
+ public function addPattern($pattern, $label = true)
+ {
+ $count = count($this->patterns);
+ $this->patterns[$count] = $pattern;
+ $this->labels[$count] = $label;
+ $this->regex = null;
+ }
+
+ /**
+ * Attempts to match all patterns at once against a string.
+ *
+ * @param string $subject String to match against.
+ * @param string $match First matched portion of
+ * subject.
+ * @return bool|string False if no match found, label if label exists, true if not
+ */
+ public function match($subject, &$match)
+ {
+ if (count($this->patterns) == 0) {
+ return false;
+ }
+ if (! preg_match($this->getCompoundedRegex(), $subject, $matches)) {
+ $match = "";
+ return false;
+ }
+
+ $match = $matches[0];
+ $size = count($matches);
+ // FIXME this could be made faster by storing the labels as keys in a hashmap
+ for ($i = 1; $i < $size; $i++) {
+ if ($matches[$i] && isset($this->labels[$i - 1])) {
+ return $this->labels[$i - 1];
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Attempts to split the string against all patterns at once
+ *
+ * @param string $subject String to match against.
+ * @param array $split The split result: array containing, pre-match, match & post-match strings
+ * @return boolean True on success.
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+ public function split($subject, &$split)
+ {
+ if (count($this->patterns) == 0) {
+ return false;
+ }
+
+ if (! preg_match($this->getCompoundedRegex(), $subject, $matches)) {
+ if (function_exists('preg_last_error')) {
+ $err = preg_last_error();
+ switch ($err) {
+ case PREG_BACKTRACK_LIMIT_ERROR:
+ msg('A PCRE backtrack error occured. Try to increase the pcre.backtrack_limit in php.ini', -1);
+ break;
+ case PREG_RECURSION_LIMIT_ERROR:
+ msg('A PCRE recursion error occured. Try to increase the pcre.recursion_limit in php.ini', -1);
+ break;
+ case PREG_BAD_UTF8_ERROR:
+ msg('A PCRE UTF-8 error occured. This might be caused by a faulty plugin', -1);
+ break;
+ case PREG_INTERNAL_ERROR:
+ msg('A PCRE internal error occured. This might be caused by a faulty plugin', -1);
+ break;
+ }
+ }
+
+ $split = array($subject, "", "");
+ return false;
+ }
+
+ $idx = count($matches)-2;
+ list($pre, $post) = preg_split($this->patterns[$idx].$this->getPerlMatchingFlags(), $subject, 2);
+ $split = array($pre, $matches[0], $post);
+
+ return isset($this->labels[$idx]) ? $this->labels[$idx] : true;
+ }
+
+ /**
+ * Compounds the patterns into a single
+ * regular expression separated with the
+ * "or" operator. Caches the regex.
+ * Will automatically escape (, ) and / tokens.
+ *
+ * @return null|string
+ */
+ protected function getCompoundedRegex()
+ {
+ if ($this->regex == null) {
+ $cnt = count($this->patterns);
+ for ($i = 0; $i < $cnt; $i++) {
+ /*
+ * decompose the input pattern into "(", "(?", ")",
+ * "[...]", "[]..]", "[^]..]", "[...[:...:]..]", "\x"...
+ * elements.
+ */
+ preg_match_all('/\\\\.|' .
+ '\(\?|' .
+ '[()]|' .
+ '\[\^?\]?(?:\\\\.|\[:[^]]*:\]|[^]\\\\])*\]|' .
+ '[^[()\\\\]+/', $this->patterns[$i], $elts);
+
+ $pattern = "";
+ $level = 0;
+
+ foreach ($elts[0] as $elt) {
+ /*
+ * for "(", ")" remember the nesting level, add "\"
+ * only to the non-"(?" ones.
+ */
+
+ switch ($elt) {
+ case '(':
+ $pattern .= '\(';
+ break;
+ case ')':
+ if ($level > 0)
+ $level--; /* closing (? */
+ else $pattern .= '\\';
+ $pattern .= ')';
+ break;
+ case '(?':
+ $level++;
+ $pattern .= '(?';
+ break;
+ default:
+ if (substr($elt, 0, 1) == '\\')
+ $pattern .= $elt;
+ else $pattern .= str_replace('/', '\/', $elt);
+ }
+ }
+ $this->patterns[$i] = "($pattern)";
+ }
+ $this->regex = "/" . implode("|", $this->patterns) . "/" . $this->getPerlMatchingFlags();
+ }
+ return $this->regex;
+ }
+
+ /**
+ * Accessor for perl regex mode flags to use.
+ * @return string Perl regex flags.
+ */
+ protected function getPerlMatchingFlags()
+ {
+ return ($this->case ? "msS" : "msSi");
+ }
+}
diff --git a/platform/www/inc/Parsing/Lexer/StateStack.php b/platform/www/inc/Parsing/Lexer/StateStack.php
new file mode 100644
index 0000000..325412b
--- /dev/null
+++ b/platform/www/inc/Parsing/Lexer/StateStack.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * Lexer adapted from Simple Test: http://sourceforge.net/projects/simpletest/
+ * For an intro to the Lexer see:
+ * https://web.archive.org/web/20120125041816/http://www.phppatterns.com/docs/develop/simple_test_lexer_notes
+ *
+ * @author Marcus Baker http://www.lastcraft.com
+ */
+
+namespace dokuwiki\Parsing\Lexer;
+
+/**
+ * States for a stack machine.
+ */
+class StateStack
+{
+ protected $stack;
+
+ /**
+ * Constructor. Starts in named state.
+ * @param string $start Starting state name.
+ */
+ public function __construct($start)
+ {
+ $this->stack = array($start);
+ }
+
+ /**
+ * Accessor for current state.
+ * @return string State.
+ */
+ public function getCurrent()
+ {
+ return $this->stack[count($this->stack) - 1];
+ }
+
+ /**
+ * Adds a state to the stack and sets it to be the current state.
+ *
+ * @param string $state New state.
+ */
+ public function enter($state)
+ {
+ array_push($this->stack, $state);
+ }
+
+ /**
+ * Leaves the current state and reverts
+ * to the previous one.
+ * @return boolean false if we attempt to drop off the bottom of the list.
+ */
+ public function leave()
+ {
+ if (count($this->stack) == 1) {
+ return false;
+ }
+ array_pop($this->stack);
+ return true;
+ }
+}
diff --git a/platform/www/inc/Parsing/Parser.php b/platform/www/inc/Parsing/Parser.php
new file mode 100644
index 0000000..63f0141
--- /dev/null
+++ b/platform/www/inc/Parsing/Parser.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace dokuwiki\Parsing;
+
+use Doku_Handler;
+use dokuwiki\Parsing\Lexer\Lexer;
+use dokuwiki\Parsing\ParserMode\Base;
+use dokuwiki\Parsing\ParserMode\ModeInterface;
+
+/**
+ * Sets up the Lexer with modes and points it to the Handler
+ * For an intro to the Lexer see: wiki:parser
+ */
+class Parser {
+
+ /** @var Doku_Handler */
+ protected $handler;
+
+ /** @var Lexer $lexer */
+ protected $lexer;
+
+ /** @var ModeInterface[] $modes */
+ protected $modes = array();
+
+ /** @var bool mode connections may only be set up once */
+ protected $connected = false;
+
+ /**
+ * dokuwiki\Parsing\Doku_Parser constructor.
+ *
+ * @param Doku_Handler $handler
+ */
+ public function __construct(Doku_Handler $handler) {
+ $this->handler = $handler;
+ }
+
+ /**
+ * Adds the base mode and initialized the lexer
+ *
+ * @param Base $BaseMode
+ */
+ protected function addBaseMode($BaseMode) {
+ $this->modes['base'] = $BaseMode;
+ if(!$this->lexer) {
+ $this->lexer = new Lexer($this->handler, 'base', true);
+ }
+ $this->modes['base']->Lexer = $this->lexer;
+ }
+
+ /**
+ * Add a new syntax element (mode) to the parser
+ *
+ * PHP preserves order of associative elements
+ * Mode sequence is important
+ *
+ * @param string $name
+ * @param ModeInterface $Mode
+ */
+ public function addMode($name, ModeInterface $Mode) {
+ if(!isset($this->modes['base'])) {
+ $this->addBaseMode(new Base());
+ }
+ $Mode->Lexer = $this->lexer; // FIXME should be done by setter
+ $this->modes[$name] = $Mode;
+ }
+
+ /**
+ * Connect all modes with each other
+ *
+ * This is the last step before actually parsing.
+ */
+ protected function connectModes() {
+
+ if($this->connected) {
+ return;
+ }
+
+ foreach(array_keys($this->modes) as $mode) {
+ // Base isn't connected to anything
+ if($mode == 'base') {
+ continue;
+ }
+ $this->modes[$mode]->preConnect();
+
+ foreach(array_keys($this->modes) as $cm) {
+
+ if($this->modes[$cm]->accepts($mode)) {
+ $this->modes[$mode]->connectTo($cm);
+ }
+
+ }
+
+ $this->modes[$mode]->postConnect();
+ }
+
+ $this->connected = true;
+ }
+
+ /**
+ * Parses wiki syntax to instructions
+ *
+ * @param string $doc the wiki syntax text
+ * @return array instructions
+ */
+ public function parse($doc) {
+ $this->connectModes();
+ // Normalize CRs and pad doc
+ $doc = "\n" . str_replace("\r\n", "\n", $doc) . "\n";
+ $this->lexer->parse($doc);
+
+ if (!method_exists($this->handler, 'finalize')) {
+ /** @deprecated 2019-10 we have a legacy handler from a plugin, assume legacy _finalize exists */
+
+ \dokuwiki\Debug\DebugHelper::dbgCustomDeprecationEvent(
+ 'finalize()',
+ get_class($this->handler) . '::_finalize()',
+ __METHOD__,
+ __FILE__,
+ __LINE__
+ );
+ $this->handler->_finalize();
+ } else {
+ $this->handler->finalize();
+ }
+ return $this->handler->calls;
+ }
+
+}
diff --git a/platform/www/inc/Parsing/ParserMode/AbstractMode.php b/platform/www/inc/Parsing/ParserMode/AbstractMode.php
new file mode 100644
index 0000000..15fc9fe
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/AbstractMode.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+/**
+ * This class and all the subclasses below are used to reduce the effort required to register
+ * modes with the Lexer.
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ */
+abstract class AbstractMode implements ModeInterface
+{
+ /** @var \dokuwiki\Parsing\Lexer\Lexer $Lexer will be injected on loading FIXME this should be done by setter */
+ public $Lexer;
+ protected $allowedModes = array();
+
+ /** @inheritdoc */
+ abstract public function getSort();
+
+ /** @inheritdoc */
+ public function preConnect()
+ {
+ }
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ }
+
+ /** @inheritdoc */
+ public function postConnect()
+ {
+ }
+
+ /** @inheritdoc */
+ public function accepts($mode)
+ {
+ return in_array($mode, (array) $this->allowedModes);
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Acronym.php b/platform/www/inc/Parsing/ParserMode/Acronym.php
new file mode 100644
index 0000000..b42a7b5
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Acronym.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Acronym extends AbstractMode
+{
+ // A list
+ protected $acronyms = array();
+ protected $pattern = '';
+
+ /**
+ * Acronym constructor.
+ *
+ * @param string[] $acronyms
+ */
+ public function __construct($acronyms)
+ {
+ usort($acronyms, array($this,'compare'));
+ $this->acronyms = $acronyms;
+ }
+
+ /** @inheritdoc */
+ public function preConnect()
+ {
+ if (!count($this->acronyms)) return;
+
+ $bound = '[\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]';
+ $acronyms = array_map(['\\dokuwiki\\Parsing\\Lexer\\Lexer', 'escape'], $this->acronyms);
+ $this->pattern = '(?<=^|'.$bound.')(?:'.join('|', $acronyms).')(?='.$bound.')';
+ }
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ if (!count($this->acronyms)) return;
+
+ if (strlen($this->pattern) > 0) {
+ $this->Lexer->addSpecialPattern($this->pattern, $mode, 'acronym');
+ }
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 240;
+ }
+
+ /**
+ * sort callback to order by string length descending
+ *
+ * @param string $a
+ * @param string $b
+ *
+ * @return int
+ */
+ protected function compare($a, $b)
+ {
+ $a_len = strlen($a);
+ $b_len = strlen($b);
+ if ($a_len > $b_len) {
+ return -1;
+ } elseif ($a_len < $b_len) {
+ return 1;
+ }
+
+ return 0;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Base.php b/platform/www/inc/Parsing/ParserMode/Base.php
new file mode 100644
index 0000000..5622756
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Base.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Base extends AbstractMode
+{
+
+ /**
+ * Base constructor.
+ */
+ public function __construct()
+ {
+ global $PARSER_MODES;
+
+ $this->allowedModes = array_merge(
+ $PARSER_MODES['container'],
+ $PARSER_MODES['baseonly'],
+ $PARSER_MODES['paragraphs'],
+ $PARSER_MODES['formatting'],
+ $PARSER_MODES['substition'],
+ $PARSER_MODES['protected'],
+ $PARSER_MODES['disabled']
+ );
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 0;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Camelcaselink.php b/platform/www/inc/Parsing/ParserMode/Camelcaselink.php
new file mode 100644
index 0000000..ef0b325
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Camelcaselink.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Camelcaselink extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addSpecialPattern(
+ '\b[A-Z]+[a-z]+[A-Z][A-Za-z]*\b',
+ $mode,
+ 'camelcaselink'
+ );
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 290;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Code.php b/platform/www/inc/Parsing/ParserMode/Code.php
new file mode 100644
index 0000000..aa49437
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Code.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Code extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addEntryPattern('<code\b(?=.*</code>)', $mode, 'code');
+ }
+
+ /** @inheritdoc */
+ public function postConnect()
+ {
+ $this->Lexer->addExitPattern('</code>', 'code');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 200;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Emaillink.php b/platform/www/inc/Parsing/ParserMode/Emaillink.php
new file mode 100644
index 0000000..f9af28c
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Emaillink.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Emaillink extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ // pattern below is defined in inc/mail.php
+ $this->Lexer->addSpecialPattern('<'.PREG_PATTERN_VALID_EMAIL.'>', $mode, 'emaillink');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 340;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Entity.php b/platform/www/inc/Parsing/ParserMode/Entity.php
new file mode 100644
index 0000000..b670124
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Entity.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+use dokuwiki\Parsing\Lexer\Lexer;
+
+class Entity extends AbstractMode
+{
+
+ protected $entities = array();
+ protected $pattern = '';
+
+ /**
+ * Entity constructor.
+ * @param string[] $entities
+ */
+ public function __construct($entities)
+ {
+ $this->entities = $entities;
+ }
+
+
+ /** @inheritdoc */
+ public function preConnect()
+ {
+ if (!count($this->entities) || $this->pattern != '') return;
+
+ $sep = '';
+ foreach ($this->entities as $entity) {
+ $this->pattern .= $sep. Lexer::escape($entity);
+ $sep = '|';
+ }
+ }
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ if (!count($this->entities)) return;
+
+ if (strlen($this->pattern) > 0) {
+ $this->Lexer->addSpecialPattern($this->pattern, $mode, 'entity');
+ }
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 260;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Eol.php b/platform/www/inc/Parsing/ParserMode/Eol.php
new file mode 100644
index 0000000..a5886b5
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Eol.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Eol extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $badModes = array('listblock','table');
+ if (in_array($mode, $badModes)) {
+ return;
+ }
+ // see FS#1652, pattern extended to swallow preceding whitespace to avoid
+ // issues with lines that only contain whitespace
+ $this->Lexer->addSpecialPattern('(?:^[ \t]*)?\n', $mode, 'eol');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 370;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Externallink.php b/platform/www/inc/Parsing/ParserMode/Externallink.php
new file mode 100644
index 0000000..7475745
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Externallink.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Externallink extends AbstractMode
+{
+ protected $schemes = array();
+ protected $patterns = array();
+
+ /** @inheritdoc */
+ public function preConnect()
+ {
+ if (count($this->patterns)) return;
+
+ $ltrs = '\w';
+ $gunk = '/\#~:.?+=&%@!\-\[\]';
+ $punc = '.:?\-;,';
+ $host = $ltrs.$punc;
+ $any = $ltrs.$gunk.$punc;
+
+ $this->schemes = getSchemes();
+ foreach ($this->schemes as $scheme) {
+ $this->patterns[] = '\b(?i)'.$scheme.'(?-i)://['.$any.']+?(?=['.$punc.']*[^'.$any.'])';
+ }
+
+ $this->patterns[] = '(?<=\s)(?i)www?(?-i)\.['.$host.']+?\.['.$host.']+?['.$any.']+?(?=['.$punc.']*[^'.$any.'])';
+ $this->patterns[] = '(?<=\s)(?i)ftp?(?-i)\.['.$host.']+?\.['.$host.']+?['.$any.']+?(?=['.$punc.']*[^'.$any.'])';
+ }
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+
+ foreach ($this->patterns as $pattern) {
+ $this->Lexer->addSpecialPattern($pattern, $mode, 'externallink');
+ }
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 330;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/File.php b/platform/www/inc/Parsing/ParserMode/File.php
new file mode 100644
index 0000000..1491341
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/File.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class File extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addEntryPattern('<file\b(?=.*</file>)', $mode, 'file');
+ }
+
+ /** @inheritdoc */
+ public function postConnect()
+ {
+ $this->Lexer->addExitPattern('</file>', 'file');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 210;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Filelink.php b/platform/www/inc/Parsing/ParserMode/Filelink.php
new file mode 100644
index 0000000..3cd86cb
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Filelink.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Filelink extends AbstractMode
+{
+
+ protected $pattern;
+
+ /** @inheritdoc */
+ public function preConnect()
+ {
+
+ $ltrs = '\w';
+ $gunk = '/\#~:.?+=&%@!\-';
+ $punc = '.:?\-;,';
+ $host = $ltrs.$punc;
+ $any = $ltrs.$gunk.$punc;
+
+ $this->pattern = '\b(?i)file(?-i)://['.$any.']+?['.
+ $punc.']*[^'.$any.']';
+ }
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addSpecialPattern(
+ $this->pattern,
+ $mode,
+ 'filelink'
+ );
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 360;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Footnote.php b/platform/www/inc/Parsing/ParserMode/Footnote.php
new file mode 100644
index 0000000..c399f98
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Footnote.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Footnote extends AbstractMode
+{
+
+ /**
+ * Footnote constructor.
+ */
+ public function __construct()
+ {
+ global $PARSER_MODES;
+
+ $this->allowedModes = array_merge(
+ $PARSER_MODES['container'],
+ $PARSER_MODES['formatting'],
+ $PARSER_MODES['substition'],
+ $PARSER_MODES['protected'],
+ $PARSER_MODES['disabled']
+ );
+
+ unset($this->allowedModes[array_search('footnote', $this->allowedModes)]);
+ }
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addEntryPattern(
+ '\x28\x28(?=.*\x29\x29)',
+ $mode,
+ 'footnote'
+ );
+ }
+
+ /** @inheritdoc */
+ public function postConnect()
+ {
+ $this->Lexer->addExitPattern(
+ '\x29\x29',
+ 'footnote'
+ );
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 150;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Formatting.php b/platform/www/inc/Parsing/ParserMode/Formatting.php
new file mode 100644
index 0000000..a3c465c
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Formatting.php
@@ -0,0 +1,115 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+/**
+ * This class sets the markup for bold (=strong),
+ * italic (=emphasis), underline etc.
+ */
+class Formatting extends AbstractMode
+{
+ protected $type;
+
+ protected $formatting = array(
+ 'strong' => array(
+ 'entry' => '\*\*(?=.*\*\*)',
+ 'exit' => '\*\*',
+ 'sort' => 70
+ ),
+
+ 'emphasis' => array(
+ 'entry' => '//(?=[^\x00]*[^:])', //hack for bugs #384 #763 #1468
+ 'exit' => '//',
+ 'sort' => 80
+ ),
+
+ 'underline' => array(
+ 'entry' => '__(?=.*__)',
+ 'exit' => '__',
+ 'sort' => 90
+ ),
+
+ 'monospace' => array(
+ 'entry' => '\x27\x27(?=.*\x27\x27)',
+ 'exit' => '\x27\x27',
+ 'sort' => 100
+ ),
+
+ 'subscript' => array(
+ 'entry' => '<sub>(?=.*</sub>)',
+ 'exit' => '</sub>',
+ 'sort' => 110
+ ),
+
+ 'superscript' => array(
+ 'entry' => '<sup>(?=.*</sup>)',
+ 'exit' => '</sup>',
+ 'sort' => 120
+ ),
+
+ 'deleted' => array(
+ 'entry' => '<del>(?=.*</del>)',
+ 'exit' => '</del>',
+ 'sort' => 130
+ ),
+ );
+
+ /**
+ * @param string $type
+ */
+ public function __construct($type)
+ {
+ global $PARSER_MODES;
+
+ if (!array_key_exists($type, $this->formatting)) {
+ trigger_error('Invalid formatting type ' . $type, E_USER_WARNING);
+ }
+
+ $this->type = $type;
+
+ // formatting may contain other formatting but not it self
+ $modes = $PARSER_MODES['formatting'];
+ $key = array_search($type, $modes);
+ if (is_int($key)) {
+ unset($modes[$key]);
+ }
+
+ $this->allowedModes = array_merge(
+ $modes,
+ $PARSER_MODES['substition'],
+ $PARSER_MODES['disabled']
+ );
+ }
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+
+ // Can't nest formatting in itself
+ if ($mode == $this->type) {
+ return;
+ }
+
+ $this->Lexer->addEntryPattern(
+ $this->formatting[$this->type]['entry'],
+ $mode,
+ $this->type
+ );
+ }
+
+ /** @inheritdoc */
+ public function postConnect()
+ {
+
+ $this->Lexer->addExitPattern(
+ $this->formatting[$this->type]['exit'],
+ $this->type
+ );
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return $this->formatting[$this->type]['sort'];
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Header.php b/platform/www/inc/Parsing/ParserMode/Header.php
new file mode 100644
index 0000000..854b317
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Header.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Header extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ //we're not picky about the closing ones, two are enough
+ $this->Lexer->addSpecialPattern(
+ '[ \t]*={2,}[^\n]+={2,}[ \t]*(?=\n)',
+ $mode,
+ 'header'
+ );
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 50;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Hr.php b/platform/www/inc/Parsing/ParserMode/Hr.php
new file mode 100644
index 0000000..e4f0b44
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Hr.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Hr extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addSpecialPattern('\n[ \t]*-{4,}[ \t]*(?=\n)', $mode, 'hr');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 160;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Html.php b/platform/www/inc/Parsing/ParserMode/Html.php
new file mode 100644
index 0000000..f5b63ef
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Html.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Html extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addEntryPattern('<html>(?=.*</html>)', $mode, 'html');
+ $this->Lexer->addEntryPattern('<HTML>(?=.*</HTML>)', $mode, 'htmlblock');
+ }
+
+ /** @inheritdoc */
+ public function postConnect()
+ {
+ $this->Lexer->addExitPattern('</html>', 'html');
+ $this->Lexer->addExitPattern('</HTML>', 'htmlblock');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 190;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Internallink.php b/platform/www/inc/Parsing/ParserMode/Internallink.php
new file mode 100644
index 0000000..6def0d9
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Internallink.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Internallink extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ // Word boundaries?
+ $this->Lexer->addSpecialPattern("\[\[.*?\]\](?!\])", $mode, 'internallink');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 300;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Linebreak.php b/platform/www/inc/Parsing/ParserMode/Linebreak.php
new file mode 100644
index 0000000..dd95cc3
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Linebreak.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Linebreak extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addSpecialPattern('\x5C{2}(?:[ \t]|(?=\n))', $mode, 'linebreak');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 140;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Listblock.php b/platform/www/inc/Parsing/ParserMode/Listblock.php
new file mode 100644
index 0000000..eef7627
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Listblock.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Listblock extends AbstractMode
+{
+
+ /**
+ * Listblock constructor.
+ */
+ public function __construct()
+ {
+ global $PARSER_MODES;
+
+ $this->allowedModes = array_merge(
+ $PARSER_MODES['formatting'],
+ $PARSER_MODES['substition'],
+ $PARSER_MODES['disabled'],
+ $PARSER_MODES['protected']
+ );
+ }
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addEntryPattern('[ \t]*\n {2,}[\-\*]', $mode, 'listblock');
+ $this->Lexer->addEntryPattern('[ \t]*\n\t{1,}[\-\*]', $mode, 'listblock');
+
+ $this->Lexer->addPattern('\n {2,}[\-\*]', 'listblock');
+ $this->Lexer->addPattern('\n\t{1,}[\-\*]', 'listblock');
+ }
+
+ /** @inheritdoc */
+ public function postConnect()
+ {
+ $this->Lexer->addExitPattern('\n', 'listblock');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 10;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Media.php b/platform/www/inc/Parsing/ParserMode/Media.php
new file mode 100644
index 0000000..f93f947
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Media.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Media extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ // Word boundaries?
+ $this->Lexer->addSpecialPattern("\{\{(?:[^\}]|(?:\}[^\}]))+\}\}", $mode, 'media');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 320;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/ModeInterface.php b/platform/www/inc/Parsing/ParserMode/ModeInterface.php
new file mode 100644
index 0000000..7cca038
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/ModeInterface.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+/**
+ * Defines a mode (syntax component) in the Parser
+ */
+interface ModeInterface
+{
+ /**
+ * returns a number used to determine in which order modes are added
+ *
+ * @return int;
+ */
+ public function getSort();
+
+ /**
+ * Called before any calls to connectTo
+ *
+ * @return void
+ */
+ public function preConnect();
+
+ /**
+ * Connects the mode
+ *
+ * @param string $mode
+ * @return void
+ */
+ public function connectTo($mode);
+
+ /**
+ * Called after all calls to connectTo
+ *
+ * @return void
+ */
+ public function postConnect();
+
+ /**
+ * Check if given mode is accepted inside this mode
+ *
+ * @param string $mode
+ * @return bool
+ */
+ public function accepts($mode);
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Multiplyentity.php b/platform/www/inc/Parsing/ParserMode/Multiplyentity.php
new file mode 100644
index 0000000..89df136
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Multiplyentity.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+/**
+ * Implements the 640x480 replacement
+ */
+class Multiplyentity extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+
+ $this->Lexer->addSpecialPattern(
+ '(?<=\b)(?:[1-9]|\d{2,})[xX]\d+(?=\b)',
+ $mode,
+ 'multiplyentity'
+ );
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 270;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Nocache.php b/platform/www/inc/Parsing/ParserMode/Nocache.php
new file mode 100644
index 0000000..fa6db83
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Nocache.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Nocache extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addSpecialPattern('~~NOCACHE~~', $mode, 'nocache');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 40;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Notoc.php b/platform/www/inc/Parsing/ParserMode/Notoc.php
new file mode 100644
index 0000000..5956207
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Notoc.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Notoc extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addSpecialPattern('~~NOTOC~~', $mode, 'notoc');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 30;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Php.php b/platform/www/inc/Parsing/ParserMode/Php.php
new file mode 100644
index 0000000..914648b
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Php.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Php extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addEntryPattern('<php>(?=.*</php>)', $mode, 'php');
+ $this->Lexer->addEntryPattern('<PHP>(?=.*</PHP>)', $mode, 'phpblock');
+ }
+
+ /** @inheritdoc */
+ public function postConnect()
+ {
+ $this->Lexer->addExitPattern('</php>', 'php');
+ $this->Lexer->addExitPattern('</PHP>', 'phpblock');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 180;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Plugin.php b/platform/www/inc/Parsing/ParserMode/Plugin.php
new file mode 100644
index 0000000..c885c60
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Plugin.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+/**
+ * @fixme do we need this anymore or could the syntax plugin inherit directly from abstract mode?
+ */
+abstract class Plugin extends AbstractMode {}
diff --git a/platform/www/inc/Parsing/ParserMode/Preformatted.php b/platform/www/inc/Parsing/ParserMode/Preformatted.php
new file mode 100644
index 0000000..7dfc474
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Preformatted.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Preformatted extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ // Has hard coded awareness of lists...
+ $this->Lexer->addEntryPattern('\n (?![\*\-])', $mode, 'preformatted');
+ $this->Lexer->addEntryPattern('\n\t(?![\*\-])', $mode, 'preformatted');
+
+ // How to effect a sub pattern with the Lexer!
+ $this->Lexer->addPattern('\n ', 'preformatted');
+ $this->Lexer->addPattern('\n\t', 'preformatted');
+ }
+
+ /** @inheritdoc */
+ public function postConnect()
+ {
+ $this->Lexer->addExitPattern('\n', 'preformatted');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 20;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Quote.php b/platform/www/inc/Parsing/ParserMode/Quote.php
new file mode 100644
index 0000000..65525b2
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Quote.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Quote extends AbstractMode
+{
+
+ /**
+ * Quote constructor.
+ */
+ public function __construct()
+ {
+ global $PARSER_MODES;
+
+ $this->allowedModes = array_merge(
+ $PARSER_MODES['formatting'],
+ $PARSER_MODES['substition'],
+ $PARSER_MODES['disabled'],
+ $PARSER_MODES['protected']
+ );
+ }
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addEntryPattern('\n>{1,}', $mode, 'quote');
+ }
+
+ /** @inheritdoc */
+ public function postConnect()
+ {
+ $this->Lexer->addPattern('\n>{1,}', 'quote');
+ $this->Lexer->addExitPattern('\n', 'quote');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 220;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Quotes.php b/platform/www/inc/Parsing/ParserMode/Quotes.php
new file mode 100644
index 0000000..13db2e6
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Quotes.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Quotes extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ global $conf;
+
+ $ws = '\s/\#~:+=&%@\-\x28\x29\]\[{}><"\''; // whitespace
+ $punc = ';,\.?!';
+
+ if ($conf['typography'] == 2) {
+ $this->Lexer->addSpecialPattern(
+ "(?<=^|[$ws])'(?=[^$ws$punc])",
+ $mode,
+ 'singlequoteopening'
+ );
+ $this->Lexer->addSpecialPattern(
+ "(?<=^|[^$ws]|[$punc])'(?=$|[$ws$punc])",
+ $mode,
+ 'singlequoteclosing'
+ );
+ $this->Lexer->addSpecialPattern(
+ "(?<=^|[^$ws$punc])'(?=$|[^$ws$punc])",
+ $mode,
+ 'apostrophe'
+ );
+ }
+
+ $this->Lexer->addSpecialPattern(
+ "(?<=^|[$ws])\"(?=[^$ws$punc])",
+ $mode,
+ 'doublequoteopening'
+ );
+ $this->Lexer->addSpecialPattern(
+ "\"",
+ $mode,
+ 'doublequoteclosing'
+ );
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 280;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Rss.php b/platform/www/inc/Parsing/ParserMode/Rss.php
new file mode 100644
index 0000000..a62d9b8
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Rss.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Rss extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addSpecialPattern("\{\{rss>[^\}]+\}\}", $mode, 'rss');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 310;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Smiley.php b/platform/www/inc/Parsing/ParserMode/Smiley.php
new file mode 100644
index 0000000..084ccc9
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Smiley.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+use dokuwiki\Parsing\Lexer\Lexer;
+
+class Smiley extends AbstractMode
+{
+ protected $smileys = array();
+ protected $pattern = '';
+
+ /**
+ * Smiley constructor.
+ * @param string[] $smileys
+ */
+ public function __construct($smileys)
+ {
+ $this->smileys = $smileys;
+ }
+
+ /** @inheritdoc */
+ public function preConnect()
+ {
+ if (!count($this->smileys) || $this->pattern != '') return;
+
+ $sep = '';
+ foreach ($this->smileys as $smiley) {
+ $this->pattern .= $sep.'(?<=\W|^)'. Lexer::escape($smiley).'(?=\W|$)';
+ $sep = '|';
+ }
+ }
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ if (!count($this->smileys)) return;
+
+ if (strlen($this->pattern) > 0) {
+ $this->Lexer->addSpecialPattern($this->pattern, $mode, 'smiley');
+ }
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 230;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Table.php b/platform/www/inc/Parsing/ParserMode/Table.php
new file mode 100644
index 0000000..b4b5123
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Table.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Table extends AbstractMode
+{
+
+ /**
+ * Table constructor.
+ */
+ public function __construct()
+ {
+ global $PARSER_MODES;
+
+ $this->allowedModes = array_merge(
+ $PARSER_MODES['formatting'],
+ $PARSER_MODES['substition'],
+ $PARSER_MODES['disabled'],
+ $PARSER_MODES['protected']
+ );
+ }
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addEntryPattern('[\t ]*\n\^', $mode, 'table');
+ $this->Lexer->addEntryPattern('[\t ]*\n\|', $mode, 'table');
+ }
+
+ /** @inheritdoc */
+ public function postConnect()
+ {
+ $this->Lexer->addPattern('\n\^', 'table');
+ $this->Lexer->addPattern('\n\|', 'table');
+ $this->Lexer->addPattern('[\t ]*:::[\t ]*(?=[\|\^])', 'table');
+ $this->Lexer->addPattern('[\t ]+', 'table');
+ $this->Lexer->addPattern('\^', 'table');
+ $this->Lexer->addPattern('\|', 'table');
+ $this->Lexer->addExitPattern('\n', 'table');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 60;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Unformatted.php b/platform/www/inc/Parsing/ParserMode/Unformatted.php
new file mode 100644
index 0000000..1bc2826
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Unformatted.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Unformatted extends AbstractMode
+{
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addEntryPattern('<nowiki>(?=.*</nowiki>)', $mode, 'unformatted');
+ $this->Lexer->addEntryPattern('%%(?=.*%%)', $mode, 'unformattedalt');
+ }
+
+ /** @inheritdoc */
+ public function postConnect()
+ {
+ $this->Lexer->addExitPattern('</nowiki>', 'unformatted');
+ $this->Lexer->addExitPattern('%%', 'unformattedalt');
+ $this->Lexer->mapHandler('unformattedalt', 'unformatted');
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 170;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Windowssharelink.php b/platform/www/inc/Parsing/ParserMode/Windowssharelink.php
new file mode 100644
index 0000000..747d4d8
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Windowssharelink.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+class Windowssharelink extends AbstractMode
+{
+
+ protected $pattern;
+
+ /** @inheritdoc */
+ public function preConnect()
+ {
+ $this->pattern = "\\\\\\\\\w+?(?:\\\\[\w\-$]+)+";
+ }
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addSpecialPattern(
+ $this->pattern,
+ $mode,
+ 'windowssharelink'
+ );
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 350;
+ }
+}
diff --git a/platform/www/inc/Parsing/ParserMode/Wordblock.php b/platform/www/inc/Parsing/ParserMode/Wordblock.php
new file mode 100644
index 0000000..50b24b2
--- /dev/null
+++ b/platform/www/inc/Parsing/ParserMode/Wordblock.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace dokuwiki\Parsing\ParserMode;
+
+use dokuwiki\Parsing\Lexer\Lexer;
+
+/**
+ * @fixme is this actually used?
+ */
+class Wordblock extends AbstractMode
+{
+ protected $badwords = array();
+ protected $pattern = '';
+
+ /**
+ * Wordblock constructor.
+ * @param $badwords
+ */
+ public function __construct($badwords)
+ {
+ $this->badwords = $badwords;
+ }
+
+ /** @inheritdoc */
+ public function preConnect()
+ {
+
+ if (count($this->badwords) == 0 || $this->pattern != '') {
+ return;
+ }
+
+ $sep = '';
+ foreach ($this->badwords as $badword) {
+ $this->pattern .= $sep.'(?<=\b)(?i)'. Lexer::escape($badword).'(?-i)(?=\b)';
+ $sep = '|';
+ }
+ }
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ if (strlen($this->pattern) > 0) {
+ $this->Lexer->addSpecialPattern($this->pattern, $mode, 'wordblock');
+ }
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 250;
+ }
+}
diff --git a/platform/www/inc/PassHash.php b/platform/www/inc/PassHash.php
new file mode 100644
index 0000000..1189da0
--- /dev/null
+++ b/platform/www/inc/PassHash.php
@@ -0,0 +1,808 @@
+<?php
+// phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
+
+namespace dokuwiki;
+
+/**
+ * Password Hashing Class
+ *
+ * This class implements various mechanisms used to hash passwords
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
+ * @license LGPL2
+ */
+class PassHash {
+ /**
+ * Verifies a cleartext password against a crypted hash
+ *
+ * The method and salt used for the crypted hash is determined automatically,
+ * then the clear text password is crypted using the same method. If both hashs
+ * match true is is returned else false
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
+ *
+ * @param string $clear Clear-Text password
+ * @param string $hash Hash to compare against
+ * @return bool
+ */
+ public function verify_hash($clear, $hash) {
+ $method = '';
+ $salt = '';
+ $magic = '';
+
+ //determine the used method and salt
+ if (substr($hash, 0, 2) == 'U$') {
+ // This may be an updated password from user_update_7000(). Such hashes
+ // have 'U' added as the first character and need an extra md5().
+ $hash = substr($hash, 1);
+ $clear = md5($clear);
+ }
+ $len = strlen($hash);
+ if(preg_match('/^\$1\$([^\$]{0,8})\$/', $hash, $m)) {
+ $method = 'smd5';
+ $salt = $m[1];
+ $magic = '1';
+ } elseif(preg_match('/^\$apr1\$([^\$]{0,8})\$/', $hash, $m)) {
+ $method = 'apr1';
+ $salt = $m[1];
+ $magic = 'apr1';
+ } elseif(preg_match('/^\$S\$(.{52})$/', $hash, $m)) {
+ $method = 'drupal_sha512';
+ $salt = $m[1];
+ $magic = 'S';
+ } elseif(preg_match('/^\$P\$(.{31})$/', $hash, $m)) {
+ $method = 'pmd5';
+ $salt = $m[1];
+ $magic = 'P';
+ } elseif(preg_match('/^\$H\$(.{31})$/', $hash, $m)) {
+ $method = 'pmd5';
+ $salt = $m[1];
+ $magic = 'H';
+ } elseif(preg_match('/^pbkdf2_(\w+?)\$(\d+)\$(.{12})\$/', $hash, $m)) {
+ $method = 'djangopbkdf2';
+ $magic = array(
+ 'algo' => $m[1],
+ 'iter' => $m[2],
+ );
+ $salt = $m[3];
+ } elseif(preg_match('/^PBKDF2(SHA\d+)\$(\d+)\$([[:xdigit:]]+)\$([[:xdigit:]]+)$/', $hash, $m)) {
+ $method = 'seafilepbkdf2';
+ $magic = array(
+ 'algo' => $m[1],
+ 'iter' => $m[2],
+ );
+ $salt = $m[3];
+ } elseif(preg_match('/^sha1\$(.{5})\$/', $hash, $m)) {
+ $method = 'djangosha1';
+ $salt = $m[1];
+ } elseif(preg_match('/^md5\$(.{5})\$/', $hash, $m)) {
+ $method = 'djangomd5';
+ $salt = $m[1];
+ } elseif(preg_match('/^\$2(a|y)\$(.{2})\$/', $hash, $m)) {
+ $method = 'bcrypt';
+ $salt = $hash;
+ } elseif(substr($hash, 0, 6) == '{SSHA}') {
+ $method = 'ssha';
+ $salt = substr(base64_decode(substr($hash, 6)), 20);
+ } elseif(substr($hash, 0, 6) == '{SMD5}') {
+ $method = 'lsmd5';
+ $salt = substr(base64_decode(substr($hash, 6)), 16);
+ } elseif(preg_match('/^:B:(.+?):.{32}$/', $hash, $m)) {
+ $method = 'mediawiki';
+ $salt = $m[1];
+ } elseif(preg_match('/^\$6\$(rounds=\d+)?\$?(.+?)\$/', $hash, $m)) {
+ $method = 'sha512';
+ $salt = $m[2];
+ $magic = $m[1];
+ } elseif(preg_match('/^\$(argon2id?)/', $hash, $m)) {
+ if(!defined('PASSWORD_'.strtoupper($m[1]))) {
+ throw new \Exception('This PHP installation has no '.strtoupper($m[1]).' support');
+ }
+ return password_verify($clear,$hash);
+ } elseif($len == 32) {
+ $method = 'md5';
+ } elseif($len == 40) {
+ $method = 'sha1';
+ } elseif($len == 16) {
+ $method = 'mysql';
+ } elseif($len == 41 && $hash[0] == '*') {
+ $method = 'my411';
+ } elseif($len == 34) {
+ $method = 'kmd5';
+ $salt = $hash;
+ } else {
+ $method = 'crypt';
+ $salt = substr($hash, 0, 2);
+ }
+
+ //crypt and compare
+ $call = 'hash_'.$method;
+ $newhash = $this->$call($clear, $salt, $magic);
+ if(\hash_equals($newhash, $hash)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Create a random salt
+ *
+ * @param int $len The length of the salt
+ * @return string
+ */
+ public function gen_salt($len = 32) {
+ $salt = '';
+ $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+ for($i = 0; $i < $len; $i++) {
+ $salt .= $chars[$this->random(0, 61)];
+ }
+ return $salt;
+ }
+
+ /**
+ * Initialize the passed variable with a salt if needed.
+ *
+ * If $salt is not null, the value is kept, but the lenght restriction is
+ * applied (unless, $cut is false).
+ *
+ * @param string|null &$salt The salt, pass null if you want one generated
+ * @param int $len The length of the salt
+ * @param bool $cut Apply length restriction to existing salt?
+ */
+ public function init_salt(&$salt, $len = 32, $cut = true) {
+ if(is_null($salt)) {
+ $salt = $this->gen_salt($len);
+ $cut = true; // for new hashes we alway apply length restriction
+ }
+ if(strlen($salt) > $len && $cut) $salt = substr($salt, 0, $len);
+ }
+
+ // Password hashing methods follow below
+
+ /**
+ * Password hashing method 'smd5'
+ *
+ * Uses salted MD5 hashs. Salt is 8 bytes long.
+ *
+ * The same mechanism is used by Apache's 'apr1' method. This will
+ * fallback to a implementation in pure PHP if MD5 support is not
+ * available in crypt()
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author <mikey_nich at hotmail dot com>
+ * @link http://php.net/manual/en/function.crypt.php#73619
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @return string Hashed password
+ */
+ public function hash_smd5($clear, $salt = null) {
+ $this->init_salt($salt, 8);
+
+ if(defined('CRYPT_MD5') && CRYPT_MD5 && $salt !== '') {
+ return crypt($clear, '$1$'.$salt.'$');
+ } else {
+ // Fall back to PHP-only implementation
+ return $this->hash_apr1($clear, $salt, '1');
+ }
+ }
+
+ /**
+ * Password hashing method 'lsmd5'
+ *
+ * Uses salted MD5 hashs. Salt is 8 bytes long.
+ *
+ * This is the format used by LDAP.
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @return string Hashed password
+ */
+ public function hash_lsmd5($clear, $salt = null) {
+ $this->init_salt($salt, 8);
+ return "{SMD5}".base64_encode(md5($clear.$salt, true).$salt);
+ }
+
+ /**
+ * Password hashing method 'apr1'
+ *
+ * Uses salted MD5 hashs. Salt is 8 bytes long.
+ *
+ * This is basically the same as smd1 above, but as used by Apache.
+ *
+ * @author <mikey_nich at hotmail dot com>
+ * @link http://php.net/manual/en/function.crypt.php#73619
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @param string $magic The hash identifier (apr1 or 1)
+ * @return string Hashed password
+ */
+ public function hash_apr1($clear, $salt = null, $magic = 'apr1') {
+ $this->init_salt($salt, 8);
+
+ $len = strlen($clear);
+ $text = $clear.'$'.$magic.'$'.$salt;
+ $bin = pack("H32", md5($clear.$salt.$clear));
+ for($i = $len; $i > 0; $i -= 16) {
+ $text .= substr($bin, 0, min(16, $i));
+ }
+ for($i = $len; $i > 0; $i >>= 1) {
+ $text .= ($i & 1) ? chr(0) : $clear[0];
+ }
+ $bin = pack("H32", md5($text));
+ for($i = 0; $i < 1000; $i++) {
+ $new = ($i & 1) ? $clear : $bin;
+ if($i % 3) $new .= $salt;
+ if($i % 7) $new .= $clear;
+ $new .= ($i & 1) ? $bin : $clear;
+ $bin = pack("H32", md5($new));
+ }
+ $tmp = '';
+ for($i = 0; $i < 5; $i++) {
+ $k = $i + 6;
+ $j = $i + 12;
+ if($j == 16) $j = 5;
+ $tmp = $bin[$i].$bin[$k].$bin[$j].$tmp;
+ }
+ $tmp = chr(0).chr(0).$bin[11].$tmp;
+ $tmp = strtr(
+ strrev(substr(base64_encode($tmp), 2)),
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
+ "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+ );
+ return '$'.$magic.'$'.$salt.'$'.$tmp;
+ }
+
+ /**
+ * Password hashing method 'md5'
+ *
+ * Uses MD5 hashs.
+ *
+ * @param string $clear The clear text to hash
+ * @return string Hashed password
+ */
+ public function hash_md5($clear) {
+ return md5($clear);
+ }
+
+ /**
+ * Password hashing method 'sha1'
+ *
+ * Uses SHA1 hashs.
+ *
+ * @param string $clear The clear text to hash
+ * @return string Hashed password
+ */
+ public function hash_sha1($clear) {
+ return sha1($clear);
+ }
+
+ /**
+ * Password hashing method 'ssha' as used by LDAP
+ *
+ * Uses salted SHA1 hashs. Salt is 4 bytes long.
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @return string Hashed password
+ */
+ public function hash_ssha($clear, $salt = null) {
+ $this->init_salt($salt, 4);
+ return '{SSHA}'.base64_encode(pack("H*", sha1($clear.$salt)).$salt);
+ }
+
+ /**
+ * Password hashing method 'crypt'
+ *
+ * Uses salted crypt hashs. Salt is 2 bytes long.
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @return string Hashed password
+ */
+ public function hash_crypt($clear, $salt = null) {
+ $this->init_salt($salt, 2);
+ return crypt($clear, $salt);
+ }
+
+ /**
+ * Password hashing method 'mysql'
+ *
+ * This method was used by old MySQL systems
+ *
+ * @link http://php.net/mysql
+ * @author <soren at byu dot edu>
+ * @param string $clear The clear text to hash
+ * @return string Hashed password
+ */
+ public function hash_mysql($clear) {
+ $nr = 0x50305735;
+ $nr2 = 0x12345671;
+ $add = 7;
+ $charArr = preg_split("//", $clear);
+ foreach($charArr as $char) {
+ if(($char == '') || ($char == ' ') || ($char == '\t')) continue;
+ $charVal = ord($char);
+ $nr ^= ((($nr & 63) + $add) * $charVal) + ($nr << 8);
+ $nr2 += ($nr2 << 8) ^ $nr;
+ $add += $charVal;
+ }
+ return sprintf("%08x%08x", ($nr & 0x7fffffff), ($nr2 & 0x7fffffff));
+ }
+
+ /**
+ * Password hashing method 'my411'
+ *
+ * Uses SHA1 hashs. This method is used by MySQL 4.11 and above
+ *
+ * @param string $clear The clear text to hash
+ * @return string Hashed password
+ */
+ public function hash_my411($clear) {
+ return '*'.strtoupper(sha1(pack("H*", sha1($clear))));
+ }
+
+ /**
+ * Password hashing method 'kmd5'
+ *
+ * Uses salted MD5 hashs.
+ *
+ * Salt is 2 bytes long, but stored at position 16, so you need to pass at
+ * least 18 bytes. You can pass the crypted hash as salt.
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @return string Hashed password
+ */
+ public function hash_kmd5($clear, $salt = null) {
+ $this->init_salt($salt);
+
+ $key = substr($salt, 16, 2);
+ $hash1 = strtolower(md5($key.md5($clear)));
+ $hash2 = substr($hash1, 0, 16).$key.substr($hash1, 16);
+ return $hash2;
+ }
+
+ /**
+ * Password stretched hashing wrapper.
+ *
+ * Initial hash is repeatedly rehashed with same password.
+ * Any salted hash algorithm supported by PHP hash() can be used. Salt
+ * is 1+8 bytes long, 1st byte is the iteration count when given. For null
+ * salts $compute is used.
+ *
+ * The actual iteration count is 2 to the power of the given count,
+ * maximum is 30 (-> 2^30 = 1_073_741_824). If a higher one is given,
+ * the function throws an exception.
+ * This iteration count is expected to grow with increasing power of
+ * new computers.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
+ * @link http://www.openwall.com/phpass/
+ *
+ * @param string $algo The hash algorithm to be used
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @param string $magic The hash identifier (P or H)
+ * @param int $compute The iteration count for new passwords
+ * @throws \Exception
+ * @return string Hashed password
+ */
+ protected function stretched_hash($algo, $clear, $salt = null, $magic = 'P', $compute = 8) {
+ $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+ if(is_null($salt)) {
+ $this->init_salt($salt);
+ $salt = $itoa64[$compute].$salt; // prefix iteration count
+ }
+ $iterc = $salt[0]; // pos 0 of salt is log2(iteration count)
+ $iter = strpos($itoa64, $iterc);
+
+ if($iter > 30) {
+ throw new \Exception("Too high iteration count ($iter) in ".
+ __CLASS__.'::'.__FUNCTION__);
+ }
+
+ $iter = 1 << $iter;
+ $salt = substr($salt, 1, 8);
+
+ // iterate
+ $hash = hash($algo, $salt . $clear, TRUE);
+ do {
+ $hash = hash($algo, $hash.$clear, true);
+ } while(--$iter);
+
+ // encode
+ $output = '';
+ $count = strlen($hash);
+ $i = 0;
+ do {
+ $value = ord($hash[$i++]);
+ $output .= $itoa64[$value & 0x3f];
+ if($i < $count)
+ $value |= ord($hash[$i]) << 8;
+ $output .= $itoa64[($value >> 6) & 0x3f];
+ if($i++ >= $count)
+ break;
+ if($i < $count)
+ $value |= ord($hash[$i]) << 16;
+ $output .= $itoa64[($value >> 12) & 0x3f];
+ if($i++ >= $count)
+ break;
+ $output .= $itoa64[($value >> 18) & 0x3f];
+ } while($i < $count);
+
+ return '$'.$magic.'$'.$iterc.$salt.$output;
+ }
+
+ /**
+ * Password hashing method 'pmd5'
+ *
+ * Repeatedly uses salted MD5 hashs. See stretched_hash() for the
+ * details.
+ *
+ *
+ * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
+ * @link http://www.openwall.com/phpass/
+ * @see PassHash::stretched_hash() for the implementation details.
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @param string $magic The hash identifier (P or H)
+ * @param int $compute The iteration count for new passwords
+ * @throws Exception
+ * @return string Hashed password
+ */
+ public function hash_pmd5($clear, $salt = null, $magic = 'P', $compute = 8) {
+ return $this->stretched_hash('md5', $clear, $salt, $magic, $compute);
+ }
+
+ /**
+ * Password hashing method 'drupal_sha512'
+ *
+ * Implements Drupal salted sha512 hashs. Drupal truncates the hash at 55
+ * characters. See stretched_hash() for the details;
+ *
+ * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
+ * @link https://api.drupal.org/api/drupal/includes%21password.inc/7.x
+ * @see PassHash::stretched_hash() for the implementation details.
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @param string $magic The hash identifier (S)
+ * @param int $compute The iteration count for new passwords (defautl is drupal 7's)
+ * @throws Exception
+ * @return string Hashed password
+ */
+ public function hash_drupal_sha512($clear, $salt = null, $magic = 'S', $compute = 15) {
+ return substr($this->stretched_hash('sha512', $clear, $salt, $magic, $compute), 0, 55);
+ }
+
+ /**
+ * Alias for hash_pmd5
+ *
+ * @param string $clear
+ * @param null|string $salt
+ * @param string $magic
+ * @param int $compute
+ *
+ * @return string
+ * @throws \Exception
+ */
+ public function hash_hmd5($clear, $salt = null, $magic = 'H', $compute = 8) {
+ return $this->hash_pmd5($clear, $salt, $magic, $compute);
+ }
+
+ /**
+ * Password hashing method 'djangosha1'
+ *
+ * Uses salted SHA1 hashs. Salt is 5 bytes long.
+ * This is used by the Django Python framework
+ *
+ * @link http://docs.djangoproject.com/en/dev/topics/auth/#passwords
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @return string Hashed password
+ */
+ public function hash_djangosha1($clear, $salt = null) {
+ $this->init_salt($salt, 5);
+ return 'sha1$'.$salt.'$'.sha1($salt.$clear);
+ }
+
+ /**
+ * Password hashing method 'djangomd5'
+ *
+ * Uses salted MD5 hashs. Salt is 5 bytes long.
+ * This is used by the Django Python framework
+ *
+ * @link http://docs.djangoproject.com/en/dev/topics/auth/#passwords
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @return string Hashed password
+ */
+ public function hash_djangomd5($clear, $salt = null) {
+ $this->init_salt($salt, 5);
+ return 'md5$'.$salt.'$'.md5($salt.$clear);
+ }
+
+ /**
+ * Password hashing method 'seafilepbkdf2'
+ *
+ * An algorithm and iteration count should be given in the opts array.
+ *
+ * Hash algorithm is the string that is in the password string in seafile
+ * database. It has to be converted to a php algo name.
+ *
+ * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
+ * @see https://stackoverflow.com/a/23670177
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @param array $opts ('algo' => hash algorithm, 'iter' => iterations)
+ * @return string Hashed password
+ * @throws Exception when PHP is missing support for the method/algo
+ */
+ public function hash_seafilepbkdf2($clear, $salt=null, $opts=array()) {
+ $this->init_salt($salt, 64);
+ if(empty($opts['algo'])) {
+ $prefixalgo='SHA256';
+ } else {
+ $prefixalgo=$opts['algo'];
+ }
+ $algo = strtolower($prefixalgo);
+ if(empty($opts['iter'])) {
+ $iter = 10000;
+ } else {
+ $iter = (int) $opts['iter'];
+ }
+ if(!function_exists('hash_pbkdf2')) {
+ throw new Exception('This PHP installation has no PBKDF2 support');
+ }
+ if(!in_array($algo, hash_algos())) {
+ throw new Exception("This PHP installation has no $algo support");
+ }
+
+ $hash = hash_pbkdf2($algo, $clear, hex2bin($salt), $iter, 0);
+ return "PBKDF2$prefixalgo\$$iter\$$salt\$$hash";
+ }
+
+ /**
+ * Password hashing method 'djangopbkdf2'
+ *
+ * An algorithm and iteration count should be given in the opts array.
+ * Defaults to sha256 and 24000 iterations
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @param array $opts ('algo' => hash algorithm, 'iter' => iterations)
+ * @return string Hashed password
+ * @throws \Exception when PHP is missing support for the method/algo
+ */
+ public function hash_djangopbkdf2($clear, $salt=null, $opts=array()) {
+ $this->init_salt($salt, 12);
+ if(empty($opts['algo'])) {
+ $algo = 'sha256';
+ } else {
+ $algo = $opts['algo'];
+ }
+ if(empty($opts['iter'])) {
+ $iter = 24000;
+ } else {
+ $iter = (int) $opts['iter'];
+ }
+ if(!function_exists('hash_pbkdf2')) {
+ throw new \Exception('This PHP installation has no PBKDF2 support');
+ }
+ if(!in_array($algo, hash_algos())) {
+ throw new \Exception("This PHP installation has no $algo support");
+ }
+
+ $hash = base64_encode(hash_pbkdf2($algo, $clear, $salt, $iter, 0, true));
+ return "pbkdf2_$algo\$$iter\$$salt\$$hash";
+ }
+
+ /**
+ * Alias for djangopbkdf2 defaulting to sha256 as hash algorithm
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @param array $opts ('iter' => iterations)
+ * @return string Hashed password
+ * @throws \Exception when PHP is missing support for the method/algo
+ */
+ public function hash_djangopbkdf2_sha256($clear, $salt=null, $opts=array()) {
+ $opts['algo'] = 'sha256';
+ return $this->hash_djangopbkdf2($clear, $salt, $opts);
+ }
+
+ /**
+ * Alias for djangopbkdf2 defaulting to sha1 as hash algorithm
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @param array $opts ('iter' => iterations)
+ * @return string Hashed password
+ * @throws \Exception when PHP is missing support for the method/algo
+ */
+ public function hash_djangopbkdf2_sha1($clear, $salt=null, $opts=array()) {
+ $opts['algo'] = 'sha1';
+ return $this->hash_djangopbkdf2($clear, $salt, $opts);
+ }
+
+ /**
+ * Passwordhashing method 'bcrypt'
+ *
+ * Uses a modified blowfish algorithm called eksblowfish
+ * This method works on PHP 5.3+ only and will throw an exception
+ * if the needed crypt support isn't available
+ *
+ * A full hash should be given as salt (starting with $a2$) or this
+ * will break. When no salt is given, the iteration count can be set
+ * through the $compute variable.
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @param int $compute The iteration count (between 4 and 31)
+ * @throws \Exception
+ * @return string Hashed password
+ */
+ public function hash_bcrypt($clear, $salt = null, $compute = 10) {
+ if(!defined('CRYPT_BLOWFISH') || CRYPT_BLOWFISH != 1) {
+ throw new \Exception('This PHP installation has no bcrypt support');
+ }
+
+ if(is_null($salt)) {
+ if($compute < 4 || $compute > 31) $compute = 8;
+ $salt = '$2y$'.str_pad($compute, 2, '0', STR_PAD_LEFT).'$'.
+ $this->gen_salt(22);
+ }
+
+ return crypt($clear, $salt);
+ }
+
+ /**
+ * Password hashing method SHA512
+ *
+ * This is only supported on PHP 5.3.2 or higher and will throw an exception if
+ * the needed crypt support is not available
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @param string $magic The rounds for sha512 (for example "rounds=3000"), null for default value
+ * @return string Hashed password
+ * @throws \Exception
+ */
+ public function hash_sha512($clear, $salt = null, $magic = null) {
+ if(!defined('CRYPT_SHA512') || CRYPT_SHA512 != 1) {
+ throw new \Exception('This PHP installation has no SHA512 support');
+ }
+ $this->init_salt($salt, 8, false);
+ if(empty($magic)) {
+ return crypt($clear, '$6$'.$salt.'$');
+ }else{
+ return crypt($clear, '$6$'.$magic.'$'.$salt.'$');
+ }
+ }
+
+ /**
+ * Password hashing method 'mediawiki'
+ *
+ * Uses salted MD5, this is referred to as Method B in MediaWiki docs. Unsalted md5
+ * method 'A' is not supported.
+ *
+ * @link http://www.mediawiki.org/wiki/Manual_talk:User_table#user_password_column
+ *
+ * @param string $clear The clear text to hash
+ * @param string $salt The salt to use, null for random
+ * @return string Hashed password
+ */
+ public function hash_mediawiki($clear, $salt = null) {
+ $this->init_salt($salt, 8, false);
+ return ':B:'.$salt.':'.md5($salt.'-'.md5($clear));
+ }
+
+
+ /**
+ * Password hashing method 'argon2i'
+ *
+ * Uses php's own password_hash function to create argon2i password hash
+ * Default Cost and thread options are used for now.
+ *
+ * @link https://www.php.net/manual/de/function.password-hash.php
+ *
+ * @param string $clear The clear text to hash
+ * @return string Hashed password
+ */
+ public function hash_argon2i($clear) {
+ if(!defined('PASSWORD_ARGON2I')) {
+ throw new \Exception('This PHP installation has no ARGON2I support');
+ }
+ return password_hash($clear,PASSWORD_ARGON2I);
+ }
+
+ /**
+ * Password hashing method 'argon2id'
+ *
+ * Uses php's own password_hash function to create argon2id password hash
+ * Default Cost and thread options are used for now.
+ *
+ * @link https://www.php.net/manual/de/function.password-hash.php
+ *
+ * @param string $clear The clear text to hash
+ * @return string Hashed password
+ */
+ public function hash_argon2id($clear) {
+ if(!defined('PASSWORD_ARGON2ID')) {
+ throw new \Exception('This PHP installation has no ARGON2ID support');
+ }
+ return password_hash($clear,PASSWORD_ARGON2ID);
+ }
+
+ /**
+ * Wraps around native hash_hmac() or reimplents it
+ *
+ * This is not directly used as password hashing method, and thus isn't callable via the
+ * verify_hash() method. It should be used to create signatures and might be used in other
+ * password hashing methods.
+ *
+ * @see hash_hmac()
+ * @author KC Cloyd
+ * @link http://php.net/manual/en/function.hash-hmac.php#93440
+ *
+ * @param string $algo Name of selected hashing algorithm (i.e. "md5", "sha256", "haval160,4",
+ * etc..) See hash_algos() for a list of supported algorithms.
+ * @param string $data Message to be hashed.
+ * @param string $key Shared secret key used for generating the HMAC variant of the message digest.
+ * @param bool $raw_output When set to TRUE, outputs raw binary data. FALSE outputs lowercase hexits.
+ * @return string
+ */
+ public static function hmac($algo, $data, $key, $raw_output = false) {
+ // use native function if available and not in unit test
+ if(function_exists('hash_hmac') && !defined('SIMPLE_TEST')){
+ return hash_hmac($algo, $data, $key, $raw_output);
+ }
+
+ $algo = strtolower($algo);
+ $pack = 'H' . strlen($algo('test'));
+ $size = 64;
+ $opad = str_repeat(chr(0x5C), $size);
+ $ipad = str_repeat(chr(0x36), $size);
+
+ if(strlen($key) > $size) {
+ $key = str_pad(pack($pack, $algo($key)), $size, chr(0x00));
+ } else {
+ $key = str_pad($key, $size, chr(0x00));
+ }
+
+ for($i = 0; $i < strlen($key) - 1; $i++) {
+ $opad[$i] = $opad[$i] ^ $key[$i];
+ $ipad[$i] = $ipad[$i] ^ $key[$i];
+ }
+
+ $output = $algo($opad . pack($pack, $algo($ipad . $data)));
+
+ return ($raw_output) ? pack($pack, $output) : $output;
+ }
+
+ /**
+ * Use a secure random generator
+ *
+ * @param int $min
+ * @param int $max
+ * @return int
+ */
+ protected function random($min, $max){
+ try {
+ return random_int($min, $max);
+ } catch (\Exception $e) {
+ // availability of random source is checked elsewhere in DokuWiki
+ // we demote this to an unchecked runtime exception here
+ throw new \RuntimeException($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+}
diff --git a/platform/www/inc/Remote/AccessDeniedException.php b/platform/www/inc/Remote/AccessDeniedException.php
new file mode 100644
index 0000000..65f6689
--- /dev/null
+++ b/platform/www/inc/Remote/AccessDeniedException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace dokuwiki\Remote;
+
+/**
+ * Class AccessDeniedException
+ */
+class AccessDeniedException extends RemoteException
+{
+}
diff --git a/platform/www/inc/Remote/Api.php b/platform/www/inc/Remote/Api.php
new file mode 100644
index 0000000..3b52656
--- /dev/null
+++ b/platform/www/inc/Remote/Api.php
@@ -0,0 +1,410 @@
+<?php
+
+namespace dokuwiki\Remote;
+
+use dokuwiki\Extension\Event;
+use dokuwiki\Extension\RemotePlugin;
+
+/**
+ * This class provides information about remote access to the wiki.
+ *
+ * == Types of methods ==
+ * There are two types of remote methods. The first is the core methods.
+ * These are always available and provided by dokuwiki.
+ * The other is plugin methods. These are provided by remote plugins.
+ *
+ * == Information structure ==
+ * The information about methods will be given in an array with the following structure:
+ * array(
+ * 'method.remoteName' => array(
+ * 'args' => array(
+ * 'type eg. string|int|...|date|file',
+ * )
+ * 'name' => 'method name in class',
+ * 'return' => 'type',
+ * 'public' => 1/0 - method bypass default group check (used by login)
+ * ['doc' = 'method documentation'],
+ * )
+ * )
+ *
+ * plugin names are formed the following:
+ * core methods begin by a 'dokuwiki' or 'wiki' followed by a . and the method name itself.
+ * i.e.: dokuwiki.version or wiki.getPage
+ *
+ * plugin methods are formed like 'plugin.<plugin name>.<method name>'.
+ * i.e.: plugin.clock.getTime or plugin.clock_gmt.getTime
+ */
+class Api
+{
+
+ /**
+ * @var ApiCore
+ */
+ private $coreMethods = null;
+
+ /**
+ * @var array remote methods provided by dokuwiki plugins - will be filled lazy via
+ * {@see dokuwiki\Remote\RemoteAPI#getPluginMethods}
+ */
+ private $pluginMethods = null;
+
+ /**
+ * @var array contains custom calls to the api. Plugins can use the XML_CALL_REGISTER event.
+ * The data inside is 'custom.call.something' => array('plugin name', 'remote method name')
+ *
+ * The remote method name is the same as in the remote name returned by _getMethods().
+ */
+ private $pluginCustomCalls = null;
+
+ private $dateTransformation;
+ private $fileTransformation;
+
+ /**
+ * constructor
+ */
+ public function __construct()
+ {
+ $this->dateTransformation = array($this, 'dummyTransformation');
+ $this->fileTransformation = array($this, 'dummyTransformation');
+ }
+
+ /**
+ * Get all available methods with remote access.
+ *
+ * @return array with information to all available methods
+ * @throws RemoteException
+ */
+ public function getMethods()
+ {
+ return array_merge($this->getCoreMethods(), $this->getPluginMethods());
+ }
+
+ /**
+ * Call a method via remote api.
+ *
+ * @param string $method name of the method to call.
+ * @param array $args arguments to pass to the given method
+ * @return mixed result of method call, must be a primitive type.
+ * @throws RemoteException
+ */
+ public function call($method, $args = array())
+ {
+ if ($args === null) {
+ $args = array();
+ }
+ list($type, $pluginName, /* $call */) = explode('.', $method, 3);
+ if ($type === 'plugin') {
+ return $this->callPlugin($pluginName, $method, $args);
+ }
+ if ($this->coreMethodExist($method)) {
+ return $this->callCoreMethod($method, $args);
+ }
+ return $this->callCustomCallPlugin($method, $args);
+ }
+
+ /**
+ * Check existance of core methods
+ *
+ * @param string $name name of the method
+ * @return bool if method exists
+ */
+ private function coreMethodExist($name)
+ {
+ $coreMethods = $this->getCoreMethods();
+ return array_key_exists($name, $coreMethods);
+ }
+
+ /**
+ * Try to call custom methods provided by plugins
+ *
+ * @param string $method name of method
+ * @param array $args
+ * @return mixed
+ * @throws RemoteException if method not exists
+ */
+ private function callCustomCallPlugin($method, $args)
+ {
+ $customCalls = $this->getCustomCallPlugins();
+ if (!array_key_exists($method, $customCalls)) {
+ throw new RemoteException('Method does not exist', -32603);
+ }
+ $customCall = $customCalls[$method];
+ return $this->callPlugin($customCall[0], $customCall[1], $args);
+ }
+
+ /**
+ * Returns plugin calls that are registered via RPC_CALL_ADD action
+ *
+ * @return array with pairs of custom plugin calls
+ * @triggers RPC_CALL_ADD
+ */
+ private function getCustomCallPlugins()
+ {
+ if ($this->pluginCustomCalls === null) {
+ $data = array();
+ Event::createAndTrigger('RPC_CALL_ADD', $data);
+ $this->pluginCustomCalls = $data;
+ }
+ return $this->pluginCustomCalls;
+ }
+
+ /**
+ * Call a plugin method
+ *
+ * @param string $pluginName
+ * @param string $method method name
+ * @param array $args
+ * @return mixed return of custom method
+ * @throws RemoteException
+ */
+ private function callPlugin($pluginName, $method, $args)
+ {
+ $plugin = plugin_load('remote', $pluginName);
+ $methods = $this->getPluginMethods();
+ if (!$plugin) {
+ throw new RemoteException('Method does not exist', -32603);
+ }
+ $this->checkAccess($methods[$method]);
+ $name = $this->getMethodName($methods, $method);
+ try {
+ set_error_handler(array($this, "argumentWarningHandler"), E_WARNING); // for PHP <7.1
+ return call_user_func_array(array($plugin, $name), $args);
+ } catch (\ArgumentCountError $th) {
+ throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
+ } finally {
+ restore_error_handler();
+ }
+ }
+
+ /**
+ * Call a core method
+ *
+ * @param string $method name of method
+ * @param array $args
+ * @return mixed
+ * @throws RemoteException if method not exist
+ */
+ private function callCoreMethod($method, $args)
+ {
+ $coreMethods = $this->getCoreMethods();
+ $this->checkAccess($coreMethods[$method]);
+ if (!isset($coreMethods[$method])) {
+ throw new RemoteException('Method does not exist', -32603);
+ }
+ $this->checkArgumentLength($coreMethods[$method], $args);
+ try {
+ set_error_handler(array($this, "argumentWarningHandler"), E_WARNING); // for PHP <7.1
+ return call_user_func_array(array($this->coreMethods, $this->getMethodName($coreMethods, $method)), $args);
+ } catch (\ArgumentCountError $th) {
+ throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
+ } finally {
+ restore_error_handler();
+ }
+ }
+
+ /**
+ * Check if access should be checked
+ *
+ * @param array $methodMeta data about the method
+ * @throws AccessDeniedException
+ */
+ private function checkAccess($methodMeta)
+ {
+ if (!isset($methodMeta['public'])) {
+ $this->forceAccess();
+ } else {
+ if ($methodMeta['public'] == '0') {
+ $this->forceAccess();
+ }
+ }
+ }
+
+ /**
+ * Check the number of parameters
+ *
+ * @param array $methodMeta data about the method
+ * @param array $args
+ * @throws RemoteException if wrong parameter count
+ */
+ private function checkArgumentLength($methodMeta, $args)
+ {
+ if (count($methodMeta['args']) < count($args)) {
+ throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
+ }
+ }
+
+ /**
+ * Determine the name of the real method
+ *
+ * @param array $methodMeta list of data of the methods
+ * @param string $method name of method
+ * @return string
+ */
+ private function getMethodName($methodMeta, $method)
+ {
+ if (isset($methodMeta[$method]['name'])) {
+ return $methodMeta[$method]['name'];
+ }
+ $method = explode('.', $method);
+ return $method[count($method) - 1];
+ }
+
+ /**
+ * Perform access check for current user
+ *
+ * @return bool true if the current user has access to remote api.
+ * @throws AccessDeniedException If remote access disabled
+ */
+ public function hasAccess()
+ {
+ global $conf;
+ global $USERINFO;
+ /** @var \dokuwiki\Input\Input $INPUT */
+ global $INPUT;
+
+ if (!$conf['remote']) {
+ throw new AccessDeniedException('server error. RPC server not enabled.', -32604);
+ }
+ if (trim($conf['remoteuser']) == '!!not set!!') {
+ return false;
+ }
+ if (!$conf['useacl']) {
+ return true;
+ }
+ if (trim($conf['remoteuser']) == '') {
+ return true;
+ }
+
+ return auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array) $USERINFO['grps']);
+ }
+
+ /**
+ * Requests access
+ *
+ * @return void
+ * @throws AccessDeniedException On denied access.
+ */
+ public function forceAccess()
+ {
+ if (!$this->hasAccess()) {
+ throw new AccessDeniedException('server error. not authorized to call method', -32604);
+ }
+ }
+
+ /**
+ * Collects all the methods of the enabled Remote Plugins
+ *
+ * @return array all plugin methods.
+ * @throws RemoteException if not implemented
+ */
+ public function getPluginMethods()
+ {
+ if ($this->pluginMethods === null) {
+ $this->pluginMethods = array();
+ $plugins = plugin_list('remote');
+
+ foreach ($plugins as $pluginName) {
+ /** @var RemotePlugin $plugin */
+ $plugin = plugin_load('remote', $pluginName);
+ if (!is_subclass_of($plugin, 'dokuwiki\Extension\RemotePlugin')) {
+ throw new RemoteException(
+ "Plugin $pluginName does not implement dokuwiki\Plugin\DokuWiki_Remote_Plugin"
+ );
+ }
+
+ try {
+ $methods = $plugin->_getMethods();
+ } catch (\ReflectionException $e) {
+ throw new RemoteException('Automatic aggregation of available remote methods failed', 0, $e);
+ }
+
+ foreach ($methods as $method => $meta) {
+ $this->pluginMethods["plugin.$pluginName.$method"] = $meta;
+ }
+ }
+ }
+ return $this->pluginMethods;
+ }
+
+ /**
+ * Collects all the core methods
+ *
+ * @param ApiCore $apiCore this parameter is used for testing. Here you can pass a non-default RemoteAPICore
+ * instance. (for mocking)
+ * @return array all core methods.
+ */
+ public function getCoreMethods($apiCore = null)
+ {
+ if ($this->coreMethods === null) {
+ if ($apiCore === null) {
+ $this->coreMethods = new ApiCore($this);
+ } else {
+ $this->coreMethods = $apiCore;
+ }
+ }
+ return $this->coreMethods->__getRemoteInfo();
+ }
+
+ /**
+ * Transform file to xml
+ *
+ * @param mixed $data
+ * @return mixed
+ */
+ public function toFile($data)
+ {
+ return call_user_func($this->fileTransformation, $data);
+ }
+
+ /**
+ * Transform date to xml
+ *
+ * @param mixed $data
+ * @return mixed
+ */
+ public function toDate($data)
+ {
+ return call_user_func($this->dateTransformation, $data);
+ }
+
+ /**
+ * A simple transformation
+ *
+ * @param mixed $data
+ * @return mixed
+ */
+ public function dummyTransformation($data)
+ {
+ return $data;
+ }
+
+ /**
+ * Set the transformer function
+ *
+ * @param callback $dateTransformation
+ */
+ public function setDateTransformation($dateTransformation)
+ {
+ $this->dateTransformation = $dateTransformation;
+ }
+
+ /**
+ * Set the transformer function
+ *
+ * @param callback $fileTransformation
+ */
+ public function setFileTransformation($fileTransformation)
+ {
+ $this->fileTransformation = $fileTransformation;
+ }
+
+ /**
+ * The error handler that catches argument-related warnings
+ */
+ public function argumentWarningHandler($errno, $errstr)
+ {
+ if (substr($errstr, 0, 17) == 'Missing argument ') {
+ throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
+ }
+ }
+}
diff --git a/platform/www/inc/Remote/ApiCore.php b/platform/www/inc/Remote/ApiCore.php
new file mode 100644
index 0000000..3aa7861
--- /dev/null
+++ b/platform/www/inc/Remote/ApiCore.php
@@ -0,0 +1,1025 @@
+<?php
+
+namespace dokuwiki\Remote;
+
+use Doku_Renderer_xhtml;
+use dokuwiki\ChangeLog\MediaChangeLog;
+use dokuwiki\ChangeLog\PageChangeLog;
+use dokuwiki\Extension\Event;
+
+define('DOKU_API_VERSION', 10);
+
+/**
+ * Provides the core methods for the remote API.
+ * The methods are ordered in 'wiki.<method>' and 'dokuwiki.<method>' namespaces
+ */
+class ApiCore
+{
+ /** @var int Increased whenever the API is changed */
+ const API_VERSION = 10;
+
+
+ /** @var Api */
+ private $api;
+
+ /**
+ * @param Api $api
+ */
+ public function __construct(Api $api)
+ {
+ $this->api = $api;
+ }
+
+ /**
+ * Returns details about the core methods
+ *
+ * @return array
+ */
+ public function __getRemoteInfo()
+ {
+ return array(
+ 'dokuwiki.getVersion' => array(
+ 'args' => array(),
+ 'return' => 'string',
+ 'doc' => 'Returns the running DokuWiki version.'
+ ), 'dokuwiki.login' => array(
+ 'args' => array('string', 'string'),
+ 'return' => 'int',
+ 'doc' => 'Tries to login with the given credentials and sets auth cookies.',
+ 'public' => '1'
+ ), 'dokuwiki.logoff' => array(
+ 'args' => array(),
+ 'return' => 'int',
+ 'doc' => 'Tries to logoff by expiring auth cookies and the associated PHP session.'
+ ), 'dokuwiki.getPagelist' => array(
+ 'args' => array('string', 'array'),
+ 'return' => 'array',
+ 'doc' => 'List all pages within the given namespace.',
+ 'name' => 'readNamespace'
+ ), 'dokuwiki.search' => array(
+ 'args' => array('string'),
+ 'return' => 'array',
+ 'doc' => 'Perform a fulltext search and return a list of matching pages'
+ ), 'dokuwiki.getTime' => array(
+ 'args' => array(),
+ 'return' => 'int',
+ 'doc' => 'Returns the current time at the remote wiki server as Unix timestamp.',
+ ), 'dokuwiki.setLocks' => array(
+ 'args' => array('array'),
+ 'return' => 'array',
+ 'doc' => 'Lock or unlock pages.'
+ ), 'dokuwiki.getTitle' => array(
+ 'args' => array(),
+ 'return' => 'string',
+ 'doc' => 'Returns the wiki title.',
+ 'public' => '1'
+ ), 'dokuwiki.appendPage' => array(
+ 'args' => array('string', 'string', 'array'),
+ 'return' => 'bool',
+ 'doc' => 'Append text to a wiki page.'
+ ), 'dokuwiki.deleteUsers' => array(
+ 'args' => array('array'),
+ 'return' => 'bool',
+ 'doc' => 'Remove one or more users from the list of registered users.'
+ ), 'wiki.getPage' => array(
+ 'args' => array('string'),
+ 'return' => 'string',
+ 'doc' => 'Get the raw Wiki text of page, latest version.',
+ 'name' => 'rawPage',
+ ), 'wiki.getPageVersion' => array(
+ 'args' => array('string', 'int'),
+ 'name' => 'rawPage',
+ 'return' => 'string',
+ 'doc' => 'Return a raw wiki page'
+ ), 'wiki.getPageHTML' => array(
+ 'args' => array('string'),
+ 'return' => 'string',
+ 'doc' => 'Return page in rendered HTML, latest version.',
+ 'name' => 'htmlPage'
+ ), 'wiki.getPageHTMLVersion' => array(
+ 'args' => array('string', 'int'),
+ 'return' => 'string',
+ 'doc' => 'Return page in rendered HTML.',
+ 'name' => 'htmlPage'
+ ), 'wiki.getAllPages' => array(
+ 'args' => array(),
+ 'return' => 'array',
+ 'doc' => 'Returns a list of all pages. The result is an array of utf8 pagenames.',
+ 'name' => 'listPages'
+ ), 'wiki.getAttachments' => array(
+ 'args' => array('string', 'array'),
+ 'return' => 'array',
+ 'doc' => 'Returns a list of all media files.',
+ 'name' => 'listAttachments'
+ ), 'wiki.getBackLinks' => array(
+ 'args' => array('string'),
+ 'return' => 'array',
+ 'doc' => 'Returns the pages that link to this page.',
+ 'name' => 'listBackLinks'
+ ), 'wiki.getPageInfo' => array(
+ 'args' => array('string'),
+ 'return' => 'array',
+ 'doc' => 'Returns a struct with info about the page, latest version.',
+ 'name' => 'pageInfo'
+ ), 'wiki.getPageInfoVersion' => array(
+ 'args' => array('string', 'int'),
+ 'return' => 'array',
+ 'doc' => 'Returns a struct with info about the page.',
+ 'name' => 'pageInfo'
+ ), 'wiki.getPageVersions' => array(
+ 'args' => array('string', 'int'),
+ 'return' => 'array',
+ 'doc' => 'Returns the available revisions of the page.',
+ 'name' => 'pageVersions'
+ ), 'wiki.putPage' => array(
+ 'args' => array('string', 'string', 'array'),
+ 'return' => 'bool',
+ 'doc' => 'Saves a wiki page.'
+ ), 'wiki.listLinks' => array(
+ 'args' => array('string'),
+ 'return' => 'array',
+ 'doc' => 'Lists all links contained in a wiki page.'
+ ), 'wiki.getRecentChanges' => array(
+ 'args' => array('int'),
+ 'return' => 'array',
+ 'Returns a struct about all recent changes since given timestamp.'
+ ), 'wiki.getRecentMediaChanges' => array(
+ 'args' => array('int'),
+ 'return' => 'array',
+ 'Returns a struct about all recent media changes since given timestamp.'
+ ), 'wiki.aclCheck' => array(
+ 'args' => array('string', 'string', 'array'),
+ 'return' => 'int',
+ 'doc' => 'Returns the permissions of a given wiki page. By default, for current user/groups'
+ ), 'wiki.putAttachment' => array(
+ 'args' => array('string', 'file', 'array'),
+ 'return' => 'array',
+ 'doc' => 'Upload a file to the wiki.'
+ ), 'wiki.deleteAttachment' => array(
+ 'args' => array('string'),
+ 'return' => 'int',
+ 'doc' => 'Delete a file from the wiki.'
+ ), 'wiki.getAttachment' => array(
+ 'args' => array('string'),
+ 'doc' => 'Return a media file',
+ 'return' => 'file',
+ 'name' => 'getAttachment',
+ ), 'wiki.getAttachmentInfo' => array(
+ 'args' => array('string'),
+ 'return' => 'array',
+ 'doc' => 'Returns a struct with info about the attachment.'
+ ), 'dokuwiki.getXMLRPCAPIVersion' => array(
+ 'args' => array(),
+ 'name' => 'getAPIVersion',
+ 'return' => 'int',
+ 'doc' => 'Returns the XMLRPC API version.',
+ 'public' => '1',
+ ), 'wiki.getRPCVersionSupported' => array(
+ 'args' => array(),
+ 'name' => 'wikiRpcVersion',
+ 'return' => 'int',
+ 'doc' => 'Returns 2 with the supported RPC API version.',
+ 'public' => '1'
+ ),
+
+ );
+ }
+
+ /**
+ * @return string
+ */
+ public function getVersion()
+ {
+ return getVersion();
+ }
+
+ /**
+ * @return int unix timestamp
+ */
+ public function getTime()
+ {
+ return time();
+ }
+
+ /**
+ * Return a raw wiki page
+ *
+ * @param string $id wiki page id
+ * @param int|string $rev revision timestamp of the page or empty string
+ * @return string page text.
+ * @throws AccessDeniedException if no permission for page
+ */
+ public function rawPage($id, $rev = '')
+ {
+ $id = $this->resolvePageId($id);
+ if (auth_quickaclcheck($id) < AUTH_READ) {
+ throw new AccessDeniedException('You are not allowed to read this file', 111);
+ }
+ $text = rawWiki($id, $rev);
+ if (!$text) {
+ return pageTemplate($id);
+ } else {
+ return $text;
+ }
+ }
+
+ /**
+ * Return a media file
+ *
+ * @author Gina Haeussge <osd@foosel.net>
+ *
+ * @param string $id file id
+ * @return mixed media file
+ * @throws AccessDeniedException no permission for media
+ * @throws RemoteException not exist
+ */
+ public function getAttachment($id)
+ {
+ $id = cleanID($id);
+ if (auth_quickaclcheck(getNS($id) . ':*') < AUTH_READ) {
+ throw new AccessDeniedException('You are not allowed to read this file', 211);
+ }
+
+ $file = mediaFN($id);
+ if (!@ file_exists($file)) {
+ throw new RemoteException('The requested file does not exist', 221);
+ }
+
+ $data = io_readFile($file, false);
+ return $this->api->toFile($data);
+ }
+
+ /**
+ * Return info about a media file
+ *
+ * @author Gina Haeussge <osd@foosel.net>
+ *
+ * @param string $id page id
+ * @return array
+ */
+ public function getAttachmentInfo($id)
+ {
+ $id = cleanID($id);
+ $info = array(
+ 'lastModified' => $this->api->toDate(0),
+ 'size' => 0,
+ );
+
+ $file = mediaFN($id);
+ if (auth_quickaclcheck(getNS($id) . ':*') >= AUTH_READ) {
+ if (file_exists($file)) {
+ $info['lastModified'] = $this->api->toDate(filemtime($file));
+ $info['size'] = filesize($file);
+ } else {
+ //Is it deleted media with changelog?
+ $medialog = new MediaChangeLog($id);
+ $revisions = $medialog->getRevisions(0, 1);
+ if (!empty($revisions)) {
+ $info['lastModified'] = $this->api->toDate($revisions[0]);
+ }
+ }
+ }
+
+ return $info;
+ }
+
+ /**
+ * Return a wiki page rendered to html
+ *
+ * @param string $id page id
+ * @param string|int $rev revision timestamp or empty string
+ * @return null|string html
+ * @throws AccessDeniedException no access to page
+ */
+ public function htmlPage($id, $rev = '')
+ {
+ $id = $this->resolvePageId($id);
+ if (auth_quickaclcheck($id) < AUTH_READ) {
+ throw new AccessDeniedException('You are not allowed to read this page', 111);
+ }
+ return p_wiki_xhtml($id, $rev, false);
+ }
+
+ /**
+ * List all pages - we use the indexer list here
+ *
+ * @return array
+ */
+ public function listPages()
+ {
+ $list = array();
+ $pages = idx_get_indexer()->getPages();
+ $pages = array_filter(array_filter($pages, 'isVisiblePage'), 'page_exists');
+
+ foreach (array_keys($pages) as $idx) {
+ $perm = auth_quickaclcheck($pages[$idx]);
+ if ($perm < AUTH_READ) {
+ continue;
+ }
+ $page = array();
+ $page['id'] = trim($pages[$idx]);
+ $page['perms'] = $perm;
+ $page['size'] = @filesize(wikiFN($pages[$idx]));
+ $page['lastModified'] = $this->api->toDate(@filemtime(wikiFN($pages[$idx])));
+ $list[] = $page;
+ }
+
+ return $list;
+ }
+
+ /**
+ * List all pages in the given namespace (and below)
+ *
+ * @param string $ns
+ * @param array $opts
+ * $opts['depth'] recursion level, 0 for all
+ * $opts['hash'] do md5 sum of content?
+ * @return array
+ */
+ public function readNamespace($ns, $opts = array())
+ {
+ global $conf;
+
+ if (!is_array($opts)) $opts = array();
+
+ $ns = cleanID($ns);
+ $dir = utf8_encodeFN(str_replace(':', '/', $ns));
+ $data = array();
+ $opts['skipacl'] = 0; // no ACL skipping for XMLRPC
+ search($data, $conf['datadir'], 'search_allpages', $opts, $dir);
+ return $data;
+ }
+
+ /**
+ * List all pages in the given namespace (and below)
+ *
+ * @param string $query
+ * @return array
+ */
+ public function search($query)
+ {
+ $regex = array();
+ $data = ft_pageSearch($query, $regex);
+ $pages = array();
+
+ // prepare additional data
+ $idx = 0;
+ foreach ($data as $id => $score) {
+ $file = wikiFN($id);
+
+ if ($idx < FT_SNIPPET_NUMBER) {
+ $snippet = ft_snippet($id, $regex);
+ $idx++;
+ } else {
+ $snippet = '';
+ }
+
+ $pages[] = array(
+ 'id' => $id,
+ 'score' => intval($score),
+ 'rev' => filemtime($file),
+ 'mtime' => filemtime($file),
+ 'size' => filesize($file),
+ 'snippet' => $snippet,
+ 'title' => useHeading('navigation') ? p_get_first_heading($id) : $id
+ );
+ }
+ return $pages;
+ }
+
+ /**
+ * Returns the wiki title.
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ global $conf;
+ return $conf['title'];
+ }
+
+ /**
+ * List all media files.
+ *
+ * Available options are 'recursive' for also including the subnamespaces
+ * in the listing, and 'pattern' for filtering the returned files against
+ * a regular expression matching their name.
+ *
+ * @author Gina Haeussge <osd@foosel.net>
+ *
+ * @param string $ns
+ * @param array $options
+ * $options['depth'] recursion level, 0 for all
+ * $options['showmsg'] shows message if invalid media id is used
+ * $options['pattern'] check given pattern
+ * $options['hash'] add hashes to result list
+ * @return array
+ * @throws AccessDeniedException no access to the media files
+ */
+ public function listAttachments($ns, $options = array())
+ {
+ global $conf;
+
+ $ns = cleanID($ns);
+
+ if (!is_array($options)) $options = array();
+ $options['skipacl'] = 0; // no ACL skipping for XMLRPC
+
+ if (auth_quickaclcheck($ns . ':*') >= AUTH_READ) {
+ $dir = utf8_encodeFN(str_replace(':', '/', $ns));
+
+ $data = array();
+ search($data, $conf['mediadir'], 'search_media', $options, $dir);
+ $len = count($data);
+ if (!$len) return array();
+
+ for ($i = 0; $i < $len; $i++) {
+ unset($data[$i]['meta']);
+ $data[$i]['perms'] = $data[$i]['perm'];
+ unset($data[$i]['perm']);
+ $data[$i]['lastModified'] = $this->api->toDate($data[$i]['mtime']);
+ }
+ return $data;
+ } else {
+ throw new AccessDeniedException('You are not allowed to list media files.', 215);
+ }
+ }
+
+ /**
+ * Return a list of backlinks
+ *
+ * @param string $id page id
+ * @return array
+ */
+ public function listBackLinks($id)
+ {
+ return ft_backlinks($this->resolvePageId($id));
+ }
+
+ /**
+ * Return some basic data about a page
+ *
+ * @param string $id page id
+ * @param string|int $rev revision timestamp or empty string
+ * @return array
+ * @throws AccessDeniedException no access for page
+ * @throws RemoteException page not exist
+ */
+ public function pageInfo($id, $rev = '')
+ {
+ $id = $this->resolvePageId($id);
+ if (auth_quickaclcheck($id) < AUTH_READ) {
+ throw new AccessDeniedException('You are not allowed to read this page', 111);
+ }
+ $file = wikiFN($id, $rev);
+ $time = @filemtime($file);
+ if (!$time) {
+ throw new RemoteException('The requested page does not exist', 121);
+ }
+
+ // set revision to current version if empty, use revision otherwise
+ // as the timestamps of old files are not necessarily correct
+ if ($rev === '') {
+ $rev = $time;
+ }
+
+ $pagelog = new PageChangeLog($id, 1024);
+ $info = $pagelog->getRevisionInfo($rev);
+
+ $data = array(
+ 'name' => $id,
+ 'lastModified' => $this->api->toDate($rev),
+ 'author' => is_array($info) ? (($info['user']) ? $info['user'] : $info['ip']) : null,
+ 'version' => $rev
+ );
+
+ return ($data);
+ }
+
+ /**
+ * Save a wiki page
+ *
+ * @author Michael Klier <chi@chimeric.de>
+ *
+ * @param string $id page id
+ * @param string $text wiki text
+ * @param array $params parameters: summary, minor edit
+ * @return bool
+ * @throws AccessDeniedException no write access for page
+ * @throws RemoteException no id, empty new page or locked
+ */
+ public function putPage($id, $text, $params = array())
+ {
+ global $TEXT;
+ global $lang;
+
+ $id = $this->resolvePageId($id);
+ $TEXT = cleanText($text);
+ $sum = $params['sum'];
+ $minor = $params['minor'];
+
+ if (empty($id)) {
+ throw new RemoteException('Empty page ID', 131);
+ }
+
+ if (!page_exists($id) && trim($TEXT) == '') {
+ throw new RemoteException('Refusing to write an empty new wiki page', 132);
+ }
+
+ if (auth_quickaclcheck($id) < AUTH_EDIT) {
+ throw new AccessDeniedException('You are not allowed to edit this page', 112);
+ }
+
+ // Check, if page is locked
+ if (checklock($id)) {
+ throw new RemoteException('The page is currently locked', 133);
+ }
+
+ // SPAM check
+ if (checkwordblock()) {
+ throw new RemoteException('Positive wordblock check', 134);
+ }
+
+ // autoset summary on new pages
+ if (!page_exists($id) && empty($sum)) {
+ $sum = $lang['created'];
+ }
+
+ // autoset summary on deleted pages
+ if (page_exists($id) && empty($TEXT) && empty($sum)) {
+ $sum = $lang['deleted'];
+ }
+
+ lock($id);
+
+ saveWikiText($id, $TEXT, $sum, $minor);
+
+ unlock($id);
+
+ // run the indexer if page wasn't indexed yet
+ idx_addPage($id);
+
+ return true;
+ }
+
+ /**
+ * Appends text to a wiki page.
+ *
+ * @param string $id page id
+ * @param string $text wiki text
+ * @param array $params such as summary,minor
+ * @return bool|string
+ * @throws RemoteException
+ */
+ public function appendPage($id, $text, $params = array())
+ {
+ $currentpage = $this->rawPage($id);
+ if (!is_string($currentpage)) {
+ return $currentpage;
+ }
+ return $this->putPage($id, $currentpage . $text, $params);
+ }
+
+ /**
+ * Remove one or more users from the list of registered users
+ *
+ * @param string[] $usernames List of usernames to remove
+ *
+ * @return bool
+ *
+ * @throws AccessDeniedException
+ */
+ public function deleteUsers($usernames)
+ {
+ if (!auth_isadmin()) {
+ throw new AccessDeniedException('Only admins are allowed to delete users', 114);
+ }
+ /** @var \dokuwiki\Extension\AuthPlugin $auth */
+ global $auth;
+ return (bool)$auth->triggerUserMod('delete', array($usernames));
+ }
+
+ /**
+ * Uploads a file to the wiki.
+ *
+ * Michael Klier <chi@chimeric.de>
+ *
+ * @param string $id page id
+ * @param string $file
+ * @param array $params such as overwrite
+ * @return false|string
+ * @throws RemoteException
+ */
+ public function putAttachment($id, $file, $params = array())
+ {
+ $id = cleanID($id);
+ $auth = auth_quickaclcheck(getNS($id) . ':*');
+
+ if (!isset($id)) {
+ throw new RemoteException('Filename not given.', 231);
+ }
+
+ global $conf;
+
+ $ftmp = $conf['tmpdir'] . '/' . md5($id . clientIP());
+
+ // save temporary file
+ @unlink($ftmp);
+ io_saveFile($ftmp, $file);
+
+ $res = media_save(array('name' => $ftmp), $id, $params['ow'], $auth, 'rename');
+ if (is_array($res)) {
+ throw new RemoteException($res[0], -$res[1]);
+ } else {
+ return $res;
+ }
+ }
+
+ /**
+ * Deletes a file from the wiki.
+ *
+ * @author Gina Haeussge <osd@foosel.net>
+ *
+ * @param string $id page id
+ * @return int
+ * @throws AccessDeniedException no permissions
+ * @throws RemoteException file in use or not deleted
+ */
+ public function deleteAttachment($id)
+ {
+ $id = cleanID($id);
+ $auth = auth_quickaclcheck(getNS($id) . ':*');
+ $res = media_delete($id, $auth);
+ if ($res & DOKU_MEDIA_DELETED) {
+ return 0;
+ } elseif ($res & DOKU_MEDIA_NOT_AUTH) {
+ throw new AccessDeniedException('You don\'t have permissions to delete files.', 212);
+ } elseif ($res & DOKU_MEDIA_INUSE) {
+ throw new RemoteException('File is still referenced', 232);
+ } else {
+ throw new RemoteException('Could not delete file', 233);
+ }
+ }
+
+ /**
+ * Returns the permissions of a given wiki page for the current user or another user
+ *
+ * @param string $id page id
+ * @param string|null $user username
+ * @param array|null $groups array of groups
+ * @return int permission level
+ */
+ public function aclCheck($id, $user = null, $groups = null)
+ {
+ /** @var \dokuwiki\Extension\AuthPlugin $auth */
+ global $auth;
+
+ $id = $this->resolvePageId($id);
+ if ($user === null) {
+ return auth_quickaclcheck($id);
+ } else {
+ if ($groups === null) {
+ $userinfo = $auth->getUserData($user);
+ if ($userinfo === false) {
+ $groups = array();
+ } else {
+ $groups = $userinfo['grps'];
+ }
+ }
+ return auth_aclcheck($id, $user, $groups);
+ }
+ }
+
+ /**
+ * Lists all links contained in a wiki page
+ *
+ * @author Michael Klier <chi@chimeric.de>
+ *
+ * @param string $id page id
+ * @return array
+ * @throws AccessDeniedException no read access for page
+ */
+ public function listLinks($id)
+ {
+ $id = $this->resolvePageId($id);
+ if (auth_quickaclcheck($id) < AUTH_READ) {
+ throw new AccessDeniedException('You are not allowed to read this page', 111);
+ }
+ $links = array();
+
+ // resolve page instructions
+ $ins = p_cached_instructions(wikiFN($id));
+
+ // instantiate new Renderer - needed for interwiki links
+ $Renderer = new Doku_Renderer_xhtml();
+ $Renderer->interwiki = getInterwiki();
+
+ // parse parse instructions
+ foreach ($ins as $in) {
+ $link = array();
+ switch ($in[0]) {
+ case 'internallink':
+ $link['type'] = 'local';
+ $link['page'] = $in[1][0];
+ $link['href'] = wl($in[1][0]);
+ array_push($links, $link);
+ break;
+ case 'externallink':
+ $link['type'] = 'extern';
+ $link['page'] = $in[1][0];
+ $link['href'] = $in[1][0];
+ array_push($links, $link);
+ break;
+ case 'interwikilink':
+ $url = $Renderer->_resolveInterWiki($in[1][2], $in[1][3]);
+ $link['type'] = 'extern';
+ $link['page'] = $url;
+ $link['href'] = $url;
+ array_push($links, $link);
+ break;
+ }
+ }
+
+ return ($links);
+ }
+
+ /**
+ * Returns a list of recent changes since give timestamp
+ *
+ * @author Michael Hamann <michael@content-space.de>
+ * @author Michael Klier <chi@chimeric.de>
+ *
+ * @param int $timestamp unix timestamp
+ * @return array
+ * @throws RemoteException no valid timestamp
+ */
+ public function getRecentChanges($timestamp)
+ {
+ if (strlen($timestamp) != 10) {
+ throw new RemoteException('The provided value is not a valid timestamp', 311);
+ }
+
+ $recents = getRecentsSince($timestamp);
+
+ $changes = array();
+
+ foreach ($recents as $recent) {
+ $change = array();
+ $change['name'] = $recent['id'];
+ $change['lastModified'] = $this->api->toDate($recent['date']);
+ $change['author'] = $recent['user'];
+ $change['version'] = $recent['date'];
+ $change['perms'] = $recent['perms'];
+ $change['size'] = @filesize(wikiFN($recent['id']));
+ array_push($changes, $change);
+ }
+
+ if (!empty($changes)) {
+ return $changes;
+ } else {
+ // in case we still have nothing at this point
+ throw new RemoteException('There are no changes in the specified timeframe', 321);
+ }
+ }
+
+ /**
+ * Returns a list of recent media changes since give timestamp
+ *
+ * @author Michael Hamann <michael@content-space.de>
+ * @author Michael Klier <chi@chimeric.de>
+ *
+ * @param int $timestamp unix timestamp
+ * @return array
+ * @throws RemoteException no valid timestamp
+ */
+ public function getRecentMediaChanges($timestamp)
+ {
+ if (strlen($timestamp) != 10)
+ throw new RemoteException('The provided value is not a valid timestamp', 311);
+
+ $recents = getRecentsSince($timestamp, null, '', RECENTS_MEDIA_CHANGES);
+
+ $changes = array();
+
+ foreach ($recents as $recent) {
+ $change = array();
+ $change['name'] = $recent['id'];
+ $change['lastModified'] = $this->api->toDate($recent['date']);
+ $change['author'] = $recent['user'];
+ $change['version'] = $recent['date'];
+ $change['perms'] = $recent['perms'];
+ $change['size'] = @filesize(mediaFN($recent['id']));
+ array_push($changes, $change);
+ }
+
+ if (!empty($changes)) {
+ return $changes;
+ } else {
+ // in case we still have nothing at this point
+ throw new RemoteException('There are no changes in the specified timeframe', 321);
+ }
+ }
+
+ /**
+ * Returns a list of available revisions of a given wiki page
+ * Number of returned pages is set by $conf['recent']
+ * However not accessible pages are skipped, so less than $conf['recent'] could be returned
+ *
+ * @author Michael Klier <chi@chimeric.de>
+ *
+ * @param string $id page id
+ * @param int $first skip the first n changelog lines
+ * 0 = from current(if exists)
+ * 1 = from 1st old rev
+ * 2 = from 2nd old rev, etc
+ * @return array
+ * @throws AccessDeniedException no read access for page
+ * @throws RemoteException empty id
+ */
+ public function pageVersions($id, $first = 0)
+ {
+ $id = $this->resolvePageId($id);
+ if (auth_quickaclcheck($id) < AUTH_READ) {
+ throw new AccessDeniedException('You are not allowed to read this page', 111);
+ }
+ global $conf;
+
+ $versions = array();
+
+ if (empty($id)) {
+ throw new RemoteException('Empty page ID', 131);
+ }
+
+ $first = (int) $first;
+ $first_rev = $first - 1;
+ $first_rev = $first_rev < 0 ? 0 : $first_rev;
+ $pagelog = new PageChangeLog($id);
+ $revisions = $pagelog->getRevisions($first_rev, $conf['recent']);
+
+ if ($first == 0) {
+ array_unshift($revisions, ''); // include current revision
+ if (count($revisions) > $conf['recent']) {
+ array_pop($revisions); // remove extra log entry
+ }
+ }
+
+ if (!empty($revisions)) {
+ foreach ($revisions as $rev) {
+ $file = wikiFN($id, $rev);
+ $time = @filemtime($file);
+ // we check if the page actually exists, if this is not the
+ // case this can lead to less pages being returned than
+ // specified via $conf['recent']
+ if ($time) {
+ $pagelog->setChunkSize(1024);
+ $info = $pagelog->getRevisionInfo($rev ? $rev : $time);
+ if (!empty($info)) {
+ $data = array();
+ $data['user'] = $info['user'];
+ $data['ip'] = $info['ip'];
+ $data['type'] = $info['type'];
+ $data['sum'] = $info['sum'];
+ $data['modified'] = $this->api->toDate($info['date']);
+ $data['version'] = $info['date'];
+ array_push($versions, $data);
+ }
+ }
+ }
+ return $versions;
+ } else {
+ return array();
+ }
+ }
+
+ /**
+ * The version of Wiki RPC API supported
+ */
+ public function wikiRpcVersion()
+ {
+ return 2;
+ }
+
+ /**
+ * Locks or unlocks a given batch of pages
+ *
+ * Give an associative array with two keys: lock and unlock. Both should contain a
+ * list of pages to lock or unlock
+ *
+ * Returns an associative array with the keys locked, lockfail, unlocked and
+ * unlockfail, each containing lists of pages.
+ *
+ * @param array[] $set list pages with array('lock' => array, 'unlock' => array)
+ * @return array
+ */
+ public function setLocks($set)
+ {
+ $locked = array();
+ $lockfail = array();
+ $unlocked = array();
+ $unlockfail = array();
+
+ foreach ((array) $set['lock'] as $id) {
+ $id = $this->resolvePageId($id);
+ if (auth_quickaclcheck($id) < AUTH_EDIT || checklock($id)) {
+ $lockfail[] = $id;
+ } else {
+ lock($id);
+ $locked[] = $id;
+ }
+ }
+
+ foreach ((array) $set['unlock'] as $id) {
+ $id = $this->resolvePageId($id);
+ if (auth_quickaclcheck($id) < AUTH_EDIT || !unlock($id)) {
+ $unlockfail[] = $id;
+ } else {
+ $unlocked[] = $id;
+ }
+ }
+
+ return array(
+ 'locked' => $locked,
+ 'lockfail' => $lockfail,
+ 'unlocked' => $unlocked,
+ 'unlockfail' => $unlockfail,
+ );
+ }
+
+ /**
+ * Return API version
+ *
+ * @return int
+ */
+ public function getAPIVersion()
+ {
+ return self::API_VERSION;
+ }
+
+ /**
+ * Login
+ *
+ * @param string $user
+ * @param string $pass
+ * @return int
+ */
+ public function login($user, $pass)
+ {
+ global $conf;
+ /** @var \dokuwiki\Extension\AuthPlugin $auth */
+ global $auth;
+
+ if (!$conf['useacl']) return 0;
+ if (!$auth) return 0;
+
+ @session_start(); // reopen session for login
+ $ok = null;
+ if ($auth->canDo('external')) {
+ $ok = $auth->trustExternal($user, $pass, false);
+ }
+ if ($ok === null){
+ $evdata = array(
+ 'user' => $user,
+ 'password' => $pass,
+ 'sticky' => false,
+ 'silent' => true,
+ );
+ $ok = Event::createAndTrigger('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper');
+ }
+ session_write_close(); // we're done with the session
+
+ return $ok;
+ }
+
+ /**
+ * Log off
+ *
+ * @return int
+ */
+ public function logoff()
+ {
+ global $conf;
+ global $auth;
+ if (!$conf['useacl']) return 0;
+ if (!$auth) return 0;
+
+ auth_logoff();
+
+ return 1;
+ }
+
+ /**
+ * Resolve page id
+ *
+ * @param string $id page id
+ * @return string
+ */
+ private function resolvePageId($id)
+ {
+ $id = cleanID($id);
+ if (empty($id)) {
+ global $conf;
+ $id = cleanID($conf['start']);
+ }
+ return $id;
+ }
+}
diff --git a/platform/www/inc/Remote/RemoteException.php b/platform/www/inc/Remote/RemoteException.php
new file mode 100644
index 0000000..129a6c2
--- /dev/null
+++ b/platform/www/inc/Remote/RemoteException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace dokuwiki\Remote;
+
+/**
+ * Class RemoteException
+ */
+class RemoteException extends \Exception
+{
+}
diff --git a/platform/www/inc/Remote/XmlRpcServer.php b/platform/www/inc/Remote/XmlRpcServer.php
new file mode 100644
index 0000000..0a16af1
--- /dev/null
+++ b/platform/www/inc/Remote/XmlRpcServer.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace dokuwiki\Remote;
+
+/**
+ * Contains needed wrapper functions and registers all available XMLRPC functions.
+ */
+class XmlRpcServer extends \IXR_Server
+{
+ protected $remote;
+
+ /**
+ * Constructor. Register methods and run Server
+ */
+ public function __construct($wait=false)
+ {
+ $this->remote = new Api();
+ $this->remote->setDateTransformation(array($this, 'toDate'));
+ $this->remote->setFileTransformation(array($this, 'toFile'));
+ parent::__construct(false, false, $wait);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function call($methodname, $args)
+ {
+ try {
+ $result = $this->remote->call($methodname, $args);
+ return $result;
+ } /** @noinspection PhpRedundantCatchClauseInspection */ catch (AccessDeniedException $e) {
+ if (!isset($_SERVER['REMOTE_USER'])) {
+ http_status(401);
+ return new \IXR_Error(-32603, "server error. not authorized to call method $methodname");
+ } else {
+ http_status(403);
+ return new \IXR_Error(-32604, "server error. forbidden to call the method $methodname");
+ }
+ } catch (RemoteException $e) {
+ return new \IXR_Error($e->getCode(), $e->getMessage());
+ }
+ }
+
+ /**
+ * @param string|int $data iso date(yyyy[-]mm[-]dd[ hh:mm[:ss]]) or timestamp
+ * @return \IXR_Date
+ */
+ public function toDate($data)
+ {
+ return new \IXR_Date($data);
+ }
+
+ /**
+ * @param string $data
+ * @return \IXR_Base64
+ */
+ public function toFile($data)
+ {
+ return new \IXR_Base64($data);
+ }
+}
diff --git a/platform/www/inc/SafeFN.class.php b/platform/www/inc/SafeFN.class.php
new file mode 100644
index 0000000..c5489b1
--- /dev/null
+++ b/platform/www/inc/SafeFN.class.php
@@ -0,0 +1,158 @@
+<?php
+
+/**
+ * Class to safely store UTF-8 in a Filename
+ *
+ * Encodes a utf8 string using only the following characters 0-9a-z_.-%
+ * characters 0-9a-z in the original string are preserved, "plain".
+ * all other characters are represented in a substring that starts
+ * with '%' are "converted".
+ * The transition from converted substrings to plain characters is
+ * marked with a '.'
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ * @date 2010-04-02
+ */
+class SafeFN {
+
+ // 'safe' characters are a superset of $plain, $pre_indicator and $post_indicator
+ private static $plain = '-./[_0123456789abcdefghijklmnopqrstuvwxyz'; // these characters aren't converted
+ private static $pre_indicator = '%';
+ private static $post_indicator = ']';
+
+ /**
+ * Convert an UTF-8 string to a safe ASCII String
+ *
+ * conversion process
+ * - if codepoint is a plain or post_indicator character,
+ * - if previous character was "converted", append post_indicator to output, clear "converted" flag
+ * - append ascii byte for character to output
+ * (continue to next character)
+ *
+ * - if codepoint is a pre_indicator character,
+ * - append ascii byte for character to output, set "converted" flag
+ * (continue to next character)
+ *
+ * (all remaining characters)
+ * - reduce codepoint value for non-printable ASCII characters (0x00 - 0x1f). Space becomes our zero.
+ * - convert reduced value to base36 (0-9a-z)
+ * - append $pre_indicator characater followed by base36 string to output, set converted flag
+ * (continue to next character)
+ *
+ * @param string $filename a utf8 string, should only include printable characters - not 0x00-0x1f
+ * @return string an encoded representation of $filename using only 'safe' ASCII characters
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+ public static function encode($filename) {
+ return self::unicodeToSafe(\dokuwiki\Utf8\Unicode::fromUtf8($filename));
+ }
+
+ /**
+ * decoding process
+ * - split the string into substrings at any occurrence of pre or post indicator characters
+ * - check the first character of the substring
+ * - if its not a pre_indicator character
+ * - if previous character was converted, skip over post_indicator character
+ * - copy codepoint values of remaining characters to the output array
+ * - clear any converted flag
+ * (continue to next substring)
+ *
+ * _ else (its a pre_indicator character)
+ * - if string length is 1, copy the post_indicator character to the output array
+ * (continue to next substring)
+ *
+ * - else (string length > 1)
+ * - skip the pre-indicator character and convert remaining string from base36 to base10
+ * - increase codepoint value for non-printable ASCII characters (add 0x20)
+ * - append codepoint to output array
+ * (continue to next substring)
+ *
+ * @param string $filename a 'safe' encoded ASCII string,
+ * @return string decoded utf8 representation of $filename
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+ public static function decode($filename) {
+ return \dokuwiki\Utf8\Unicode::toUtf8(self::safeToUnicode(strtolower($filename)));
+ }
+
+ public static function validatePrintableUtf8($printable_utf8) {
+ return !preg_match('#[\x01-\x1f]#',$printable_utf8);
+ }
+
+ public static function validateSafe($safe) {
+ return !preg_match('#[^'.self::$plain.self::$post_indicator.self::$pre_indicator.']#',$safe);
+ }
+
+ /**
+ * convert an array of unicode codepoints into 'safe_filename' format
+ *
+ * @param array int $unicode an array of unicode codepoints
+ * @return string the unicode represented in 'safe_filename' format
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+ private static function unicodeToSafe($unicode) {
+
+ $safe = '';
+ $converted = false;
+
+ foreach ($unicode as $codepoint) {
+ if ($codepoint < 127 && (strpos(self::$plain.self::$post_indicator,chr($codepoint))!==false)) {
+ if ($converted) {
+ $safe .= self::$post_indicator;
+ $converted = false;
+ }
+ $safe .= chr($codepoint);
+
+ } else if ($codepoint == ord(self::$pre_indicator)) {
+ $safe .= self::$pre_indicator;
+ $converted = true;
+ } else {
+ $safe .= self::$pre_indicator.base_convert((string)($codepoint-32),10,36);
+ $converted = true;
+ }
+ }
+ if($converted) $safe .= self::$post_indicator;
+ return $safe;
+ }
+
+ /**
+ * convert a 'safe_filename' string into an array of unicode codepoints
+ *
+ * @param string $safe a filename in 'safe_filename' format
+ * @return array int an array of unicode codepoints
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+ private static function safeToUnicode($safe) {
+
+ $unicode = array();
+ $split = preg_split('#(?=['.self::$post_indicator.self::$pre_indicator.'])#',$safe,-1,PREG_SPLIT_NO_EMPTY);
+
+ $converted = false;
+ foreach ($split as $sub) {
+ $len = strlen($sub);
+ if ($sub[0] != self::$pre_indicator) {
+ // plain (unconverted) characters, optionally starting with a post_indicator
+ // set initial value to skip any post_indicator
+ for ($i=($converted?1:0); $i < $len; $i++) {
+ $unicode[] = ord($sub[$i]);
+ }
+ $converted = false;
+ } else if ($len==1) {
+ // a pre_indicator character in the real data
+ $unicode[] = ord($sub);
+ $converted = true;
+ } else {
+ // a single codepoint in base36, adjusted for initial 32 non-printable chars
+ $unicode[] = 32 + (int)base_convert(substr($sub,1),36,10);
+ $converted = true;
+ }
+ }
+
+ return $unicode;
+ }
+
+}
diff --git a/platform/www/inc/Search/Indexer.php b/platform/www/inc/Search/Indexer.php
new file mode 100644
index 0000000..a29e5b2
--- /dev/null
+++ b/platform/www/inc/Search/Indexer.php
@@ -0,0 +1,1214 @@
+<?php
+
+namespace dokuwiki\Search;
+
+use dokuwiki\Extension\Event;
+
+/**
+ * Class that encapsulates operations on the indexer database.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+class Indexer {
+ /**
+ * @var array $pidCache Cache for getPID()
+ */
+ protected $pidCache = array();
+
+ /**
+ * Adds the contents of a page to the fulltext index
+ *
+ * The added text replaces previous words for the same page.
+ * An empty value erases the page.
+ *
+ * @param string $page a page name
+ * @param string $text the body of the page
+ * @return string|boolean the function completed successfully
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function addPageWords($page, $text) {
+ if (!$this->lock())
+ return "locked";
+
+ // load known documents
+ $pid = $this->getPIDNoLock($page);
+ if ($pid === false) {
+ $this->unlock();
+ return false;
+ }
+
+ $pagewords = array();
+ // get word usage in page
+ $words = $this->getPageWords($text);
+ if ($words === false) {
+ $this->unlock();
+ return false;
+ }
+
+ if (!empty($words)) {
+ foreach (array_keys($words) as $wlen) {
+ $index = $this->getIndex('i', $wlen);
+ foreach ($words[$wlen] as $wid => $freq) {
+ $idx = ($wid<count($index)) ? $index[$wid] : '';
+ $index[$wid] = $this->updateTuple($idx, $pid, $freq);
+ $pagewords[] = "$wlen*$wid";
+ }
+ if (!$this->saveIndex('i', $wlen, $index)) {
+ $this->unlock();
+ return false;
+ }
+ }
+ }
+
+ // Remove obsolete index entries
+ $pageword_idx = $this->getIndexKey('pageword', '', $pid);
+ if ($pageword_idx !== '') {
+ $oldwords = explode(':',$pageword_idx);
+ $delwords = array_diff($oldwords, $pagewords);
+ $upwords = array();
+ foreach ($delwords as $word) {
+ if ($word != '') {
+ list($wlen, $wid) = explode('*', $word);
+ $wid = (int)$wid;
+ $upwords[$wlen][] = $wid;
+ }
+ }
+ foreach ($upwords as $wlen => $widx) {
+ $index = $this->getIndex('i', $wlen);
+ foreach ($widx as $wid) {
+ $index[$wid] = $this->updateTuple($index[$wid], $pid, 0);
+ }
+ $this->saveIndex('i', $wlen, $index);
+ }
+ }
+ // Save the reverse index
+ $pageword_idx = join(':', $pagewords);
+ if (!$this->saveIndexKey('pageword', '', $pid, $pageword_idx)) {
+ $this->unlock();
+ return false;
+ }
+
+ $this->unlock();
+ return true;
+ }
+
+ /**
+ * Split the words in a page and add them to the index.
+ *
+ * @param string $text content of the page
+ * @return array list of word IDs and number of times used
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ protected function getPageWords($text) {
+
+ $tokens = $this->tokenizer($text);
+ $tokens = array_count_values($tokens); // count the frequency of each token
+
+ $words = array();
+ foreach ($tokens as $w=>$c) {
+ $l = wordlen($w);
+ if (isset($words[$l])){
+ $words[$l][$w] = $c + (isset($words[$l][$w]) ? $words[$l][$w] : 0);
+ }else{
+ $words[$l] = array($w => $c);
+ }
+ }
+
+ // arrive here with $words = array(wordlen => array(word => frequency))
+ $word_idx_modified = false;
+ $index = array(); //resulting index
+ foreach (array_keys($words) as $wlen) {
+ $word_idx = $this->getIndex('w', $wlen);
+ foreach ($words[$wlen] as $word => $freq) {
+ $word = (string)$word;
+ $wid = array_search($word, $word_idx, true);
+ if ($wid === false) {
+ $wid = count($word_idx);
+ $word_idx[] = $word;
+ $word_idx_modified = true;
+ }
+ if (!isset($index[$wlen]))
+ $index[$wlen] = array();
+ $index[$wlen][$wid] = $freq;
+ }
+ // save back the word index
+ if ($word_idx_modified && !$this->saveIndex('w', $wlen, $word_idx))
+ return false;
+ }
+
+ return $index;
+ }
+
+ /**
+ * Add/update keys to/of the metadata index.
+ *
+ * Adding new keys does not remove other keys for the page.
+ * An empty value will erase the key.
+ * The $key parameter can be an array to add multiple keys. $value will
+ * not be used if $key is an array.
+ *
+ * @param string $page a page name
+ * @param mixed $key a key string or array of key=>value pairs
+ * @param mixed $value the value or list of values
+ * @return boolean|string the function completed successfully
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ * @author Michael Hamann <michael@content-space.de>
+ */
+ public function addMetaKeys($page, $key, $value=null) {
+ if (!is_array($key)) {
+ $key = array($key => $value);
+ } elseif (!is_null($value)) {
+ // $key is array, but $value is not null
+ trigger_error("array passed to addMetaKeys but value is not null", E_USER_WARNING);
+ }
+
+ if (!$this->lock())
+ return "locked";
+
+ // load known documents
+ $pid = $this->getPIDNoLock($page);
+ if ($pid === false) {
+ $this->unlock();
+ return false;
+ }
+
+ // Special handling for titles so the index file is simpler
+ if (array_key_exists('title', $key)) {
+ $value = $key['title'];
+ if (is_array($value)) {
+ $value = $value[0];
+ }
+ $this->saveIndexKey('title', '', $pid, $value);
+ unset($key['title']);
+ }
+
+ foreach ($key as $name => $values) {
+ $metaname = idx_cleanName($name);
+ $this->addIndexKey('metadata', '', $metaname);
+ $metaidx = $this->getIndex($metaname.'_i', '');
+ $metawords = $this->getIndex($metaname.'_w', '');
+ $addwords = false;
+
+ if (!is_array($values)) $values = array($values);
+
+ $val_idx = $this->getIndexKey($metaname.'_p', '', $pid);
+ if ($val_idx !== '') {
+ $val_idx = explode(':', $val_idx);
+ // -1 means remove, 0 keep, 1 add
+ $val_idx = array_combine($val_idx, array_fill(0, count($val_idx), -1));
+ } else {
+ $val_idx = array();
+ }
+
+ foreach ($values as $val) {
+ $val = (string)$val;
+ if ($val !== "") {
+ $id = array_search($val, $metawords, true);
+ if ($id === false) {
+ // didn't find $val, so we'll add it to the end of metawords and create a placeholder in metaidx
+ $id = count($metawords);
+ $metawords[$id] = $val;
+ $metaidx[$id] = '';
+ $addwords = true;
+ }
+ // test if value is already in the index
+ if (isset($val_idx[$id]) && $val_idx[$id] <= 0){
+ $val_idx[$id] = 0;
+ } else { // else add it
+ $val_idx[$id] = 1;
+ }
+ }
+ }
+
+ if ($addwords) {
+ $this->saveIndex($metaname.'_w', '', $metawords);
+ }
+ $vals_changed = false;
+ foreach ($val_idx as $id => $action) {
+ if ($action == -1) {
+ $metaidx[$id] = $this->updateTuple($metaidx[$id], $pid, 0);
+ $vals_changed = true;
+ unset($val_idx[$id]);
+ } elseif ($action == 1) {
+ $metaidx[$id] = $this->updateTuple($metaidx[$id], $pid, 1);
+ $vals_changed = true;
+ }
+ }
+
+ if ($vals_changed) {
+ $this->saveIndex($metaname.'_i', '', $metaidx);
+ $val_idx = implode(':', array_keys($val_idx));
+ $this->saveIndexKey($metaname.'_p', '', $pid, $val_idx);
+ }
+
+ unset($metaidx);
+ unset($metawords);
+ }
+
+ $this->unlock();
+ return true;
+ }
+
+ /**
+ * Rename a page in the search index without changing the indexed content. This function doesn't check if the
+ * old or new name exists in the filesystem. It returns an error if the old page isn't in the page list of the
+ * indexer and it deletes all previously indexed content of the new page.
+ *
+ * @param string $oldpage The old page name
+ * @param string $newpage The new page name
+ * @return string|bool If the page was successfully renamed, can be a message in the case of an error
+ */
+ public function renamePage($oldpage, $newpage) {
+ if (!$this->lock()) return 'locked';
+
+ $pages = $this->getPages();
+
+ $id = array_search($oldpage, $pages, true);
+ if ($id === false) {
+ $this->unlock();
+ return 'page is not in index';
+ }
+
+ $new_id = array_search($newpage, $pages, true);
+ if ($new_id !== false) {
+ // make sure the page is not in the index anymore
+ if ($this->deletePageNoLock($newpage) !== true) {
+ return false;
+ }
+
+ $pages[$new_id] = 'deleted:'.time().rand(0, 9999);
+ }
+
+ $pages[$id] = $newpage;
+
+ // update index
+ if (!$this->saveIndex('page', '', $pages)) {
+ $this->unlock();
+ return false;
+ }
+
+ // reset the pid cache
+ $this->pidCache = array();
+
+ $this->unlock();
+ return true;
+ }
+
+ /**
+ * Renames a meta value in the index. This doesn't change the meta value in the pages, it assumes that all pages
+ * will be updated.
+ *
+ * @param string $key The metadata key of which a value shall be changed
+ * @param string $oldvalue The old value that shall be renamed
+ * @param string $newvalue The new value to which the old value shall be renamed, if exists values will be merged
+ * @return bool|string If renaming the value has been successful, false or error message on error.
+ */
+ public function renameMetaValue($key, $oldvalue, $newvalue) {
+ if (!$this->lock()) return 'locked';
+
+ // change the relation references index
+ $metavalues = $this->getIndex($key, '_w');
+ $oldid = array_search($oldvalue, $metavalues, true);
+ if ($oldid !== false) {
+ $newid = array_search($newvalue, $metavalues, true);
+ if ($newid !== false) {
+ // free memory
+ unset ($metavalues);
+
+ // okay, now we have two entries for the same value. we need to merge them.
+ $indexline = $this->getIndexKey($key.'_i', '', $oldid);
+ if ($indexline != '') {
+ $newindexline = $this->getIndexKey($key.'_i', '', $newid);
+ $pagekeys = $this->getIndex($key.'_p', '');
+ $parts = explode(':', $indexline);
+ foreach ($parts as $part) {
+ list($id, $count) = explode('*', $part);
+ $newindexline = $this->updateTuple($newindexline, $id, $count);
+
+ $keyline = explode(':', $pagekeys[$id]);
+ // remove old meta value
+ $keyline = array_diff($keyline, array($oldid));
+ // add new meta value when not already present
+ if (!in_array($newid, $keyline)) {
+ array_push($keyline, $newid);
+ }
+ $pagekeys[$id] = implode(':', $keyline);
+ }
+ $this->saveIndex($key.'_p', '', $pagekeys);
+ unset($pagekeys);
+ $this->saveIndexKey($key.'_i', '', $oldid, '');
+ $this->saveIndexKey($key.'_i', '', $newid, $newindexline);
+ }
+ } else {
+ $metavalues[$oldid] = $newvalue;
+ if (!$this->saveIndex($key.'_w', '', $metavalues)) {
+ $this->unlock();
+ return false;
+ }
+ }
+ }
+
+ $this->unlock();
+ return true;
+ }
+
+ /**
+ * Remove a page from the index
+ *
+ * Erases entries in all known indexes.
+ *
+ * @param string $page a page name
+ * @return string|boolean the function completed successfully
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ public function deletePage($page) {
+ if (!$this->lock())
+ return "locked";
+
+ $result = $this->deletePageNoLock($page);
+
+ $this->unlock();
+
+ return $result;
+ }
+
+ /**
+ * Remove a page from the index without locking the index, only use this function if the index is already locked
+ *
+ * Erases entries in all known indexes.
+ *
+ * @param string $page a page name
+ * @return boolean the function completed successfully
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ protected function deletePageNoLock($page) {
+ // load known documents
+ $pid = $this->getPIDNoLock($page);
+ if ($pid === false) {
+ return false;
+ }
+
+ // Remove obsolete index entries
+ $pageword_idx = $this->getIndexKey('pageword', '', $pid);
+ if ($pageword_idx !== '') {
+ $delwords = explode(':',$pageword_idx);
+ $upwords = array();
+ foreach ($delwords as $word) {
+ if ($word != '') {
+ list($wlen,$wid) = explode('*', $word);
+ $wid = (int)$wid;
+ $upwords[$wlen][] = $wid;
+ }
+ }
+ foreach ($upwords as $wlen => $widx) {
+ $index = $this->getIndex('i', $wlen);
+ foreach ($widx as $wid) {
+ $index[$wid] = $this->updateTuple($index[$wid], $pid, 0);
+ }
+ $this->saveIndex('i', $wlen, $index);
+ }
+ }
+ // Save the reverse index
+ if (!$this->saveIndexKey('pageword', '', $pid, "")) {
+ return false;
+ }
+
+ $this->saveIndexKey('title', '', $pid, "");
+ $keyidx = $this->getIndex('metadata', '');
+ foreach ($keyidx as $metaname) {
+ $val_idx = explode(':', $this->getIndexKey($metaname.'_p', '', $pid));
+ $meta_idx = $this->getIndex($metaname.'_i', '');
+ foreach ($val_idx as $id) {
+ if ($id === '') continue;
+ $meta_idx[$id] = $this->updateTuple($meta_idx[$id], $pid, 0);
+ }
+ $this->saveIndex($metaname.'_i', '', $meta_idx);
+ $this->saveIndexKey($metaname.'_p', '', $pid, '');
+ }
+
+ return true;
+ }
+
+ /**
+ * Clear the whole index
+ *
+ * @return bool If the index has been cleared successfully
+ */
+ public function clear() {
+ global $conf;
+
+ if (!$this->lock()) return false;
+
+ @unlink($conf['indexdir'].'/page.idx');
+ @unlink($conf['indexdir'].'/title.idx');
+ @unlink($conf['indexdir'].'/pageword.idx');
+ @unlink($conf['indexdir'].'/metadata.idx');
+ $dir = @opendir($conf['indexdir']);
+ if($dir!==false){
+ while(($f = readdir($dir)) !== false){
+ if(substr($f,-4)=='.idx' &&
+ (substr($f,0,1)=='i' || substr($f,0,1)=='w'
+ || substr($f,-6)=='_w.idx' || substr($f,-6)=='_i.idx' || substr($f,-6)=='_p.idx'))
+ @unlink($conf['indexdir']."/$f");
+ }
+ }
+ @unlink($conf['indexdir'].'/lengths.idx');
+
+ // clear the pid cache
+ $this->pidCache = array();
+
+ $this->unlock();
+ return true;
+ }
+
+ /**
+ * Split the text into words for fulltext search
+ *
+ * TODO: does this also need &$stopwords ?
+ *
+ * @triggers INDEXER_TEXT_PREPARE
+ * This event allows plugins to modify the text before it gets tokenized.
+ * Plugins intercepting this event should also intercept INDEX_VERSION_GET
+ *
+ * @param string $text plain text
+ * @param boolean $wc are wildcards allowed?
+ * @return array list of words in the text
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function tokenizer($text, $wc=false) {
+ $wc = ($wc) ? '' : '\*';
+ $stopwords =& idx_get_stopwords();
+
+ // prepare the text to be tokenized
+ $evt = new Event('INDEXER_TEXT_PREPARE', $text);
+ if ($evt->advise_before(true)) {
+ if (preg_match('/[^0-9A-Za-z ]/u', $text)) {
+ $text = \dokuwiki\Utf8\Asian::separateAsianWords($text);
+ }
+ }
+ $evt->advise_after();
+ unset($evt);
+
+ $text = strtr($text,
+ array(
+ "\r" => ' ',
+ "\n" => ' ',
+ "\t" => ' ',
+ "\xC2\xAD" => '', //soft-hyphen
+ )
+ );
+ if (preg_match('/[^0-9A-Za-z ]/u', $text))
+ $text = \dokuwiki\Utf8\Clean::stripspecials($text, ' ', '\._\-:'.$wc);
+
+ $wordlist = explode(' ', $text);
+ foreach ($wordlist as $i => $word) {
+ $wordlist[$i] = (preg_match('/[^0-9A-Za-z]/u', $word)) ?
+ \dokuwiki\Utf8\PhpString::strtolower($word) : strtolower($word);
+ }
+
+ foreach ($wordlist as $i => $word) {
+ if ((!is_numeric($word) && strlen($word) < IDX_MINWORDLENGTH)
+ || array_search($word, $stopwords, true) !== false)
+ unset($wordlist[$i]);
+ }
+ return array_values($wordlist);
+ }
+
+ /**
+ * Get the numeric PID of a page
+ *
+ * @param string $page The page to get the PID for
+ * @return bool|int The page id on success, false on error
+ */
+ public function getPID($page) {
+ // return PID without locking when it is in the cache
+ if (isset($this->pidCache[$page])) return $this->pidCache[$page];
+
+ if (!$this->lock())
+ return false;
+
+ // load known documents
+ $pid = $this->getPIDNoLock($page);
+ if ($pid === false) {
+ $this->unlock();
+ return false;
+ }
+
+ $this->unlock();
+ return $pid;
+ }
+
+ /**
+ * Get the numeric PID of a page without locking the index.
+ * Only use this function when the index is already locked.
+ *
+ * @param string $page The page to get the PID for
+ * @return bool|int The page id on success, false on error
+ */
+ protected function getPIDNoLock($page) {
+ // avoid expensive addIndexKey operation for the most recently requested pages by using a cache
+ if (isset($this->pidCache[$page])) return $this->pidCache[$page];
+ $pid = $this->addIndexKey('page', '', $page);
+ // limit cache to 10 entries by discarding the oldest element as in DokuWiki usually only the most recently
+ // added item will be requested again
+ if (count($this->pidCache) > 10) array_shift($this->pidCache);
+ $this->pidCache[$page] = $pid;
+ return $pid;
+ }
+
+ /**
+ * Get the page id of a numeric PID
+ *
+ * @param int $pid The PID to get the page id for
+ * @return string The page id
+ */
+ public function getPageFromPID($pid) {
+ return $this->getIndexKey('page', '', $pid);
+ }
+
+ /**
+ * Find pages in the fulltext index containing the words,
+ *
+ * The search words must be pre-tokenized, meaning only letters and
+ * numbers with an optional wildcard
+ *
+ * The returned array will have the original tokens as key. The values
+ * in the returned list is an array with the page names as keys and the
+ * number of times that token appears on the page as value.
+ *
+ * @param array $tokens list of words to search for
+ * @return array list of page names with usage counts
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function lookup(&$tokens) {
+ $result = array();
+ $wids = $this->getIndexWords($tokens, $result);
+ if (empty($wids)) return array();
+ // load known words and documents
+ $page_idx = $this->getIndex('page', '');
+ $docs = array();
+ foreach (array_keys($wids) as $wlen) {
+ $wids[$wlen] = array_unique($wids[$wlen]);
+ $index = $this->getIndex('i', $wlen);
+ foreach($wids[$wlen] as $ixid) {
+ if ($ixid < count($index))
+ $docs["$wlen*$ixid"] = $this->parseTuples($page_idx, $index[$ixid]);
+ }
+ }
+ // merge found pages into final result array
+ $final = array();
+ foreach ($result as $word => $res) {
+ $final[$word] = array();
+ foreach ($res as $wid) {
+ // handle the case when ($ixid < count($index)) has been false
+ // and thus $docs[$wid] hasn't been set.
+ if (!isset($docs[$wid])) continue;
+ $hits = &$docs[$wid];
+ foreach ($hits as $hitkey => $hitcnt) {
+ // make sure the document still exists
+ if (!page_exists($hitkey, '', false)) continue;
+ if (!isset($final[$word][$hitkey]))
+ $final[$word][$hitkey] = $hitcnt;
+ else
+ $final[$word][$hitkey] += $hitcnt;
+ }
+ }
+ }
+ return $final;
+ }
+
+ /**
+ * Find pages containing a metadata key.
+ *
+ * The metadata values are compared as case-sensitive strings. Pass a
+ * callback function that returns true or false to use a different
+ * comparison function. The function will be called with the $value being
+ * searched for as the first argument, and the word in the index as the
+ * second argument. The function preg_match can be used directly if the
+ * values are regexes.
+ *
+ * @param string $key name of the metadata key to look for
+ * @param string $value search term to look for, must be a string or array of strings
+ * @param callback $func comparison function
+ * @return array lists with page names, keys are query values if $value is array
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ * @author Michael Hamann <michael@content-space.de>
+ */
+ public function lookupKey($key, &$value, $func=null) {
+ if (!is_array($value))
+ $value_array = array($value);
+ else
+ $value_array =& $value;
+
+ // the matching ids for the provided value(s)
+ $value_ids = array();
+
+ $metaname = idx_cleanName($key);
+
+ // get all words in order to search the matching ids
+ if ($key == 'title') {
+ $words = $this->getIndex('title', '');
+ } else {
+ $words = $this->getIndex($metaname.'_w', '');
+ }
+
+ if (!is_null($func)) {
+ foreach ($value_array as $val) {
+ foreach ($words as $i => $word) {
+ if (call_user_func_array($func, array($val, $word)))
+ $value_ids[$i][] = $val;
+ }
+ }
+ } else {
+ foreach ($value_array as $val) {
+ $xval = $val;
+ $caret = '^';
+ $dollar = '$';
+ // check for wildcards
+ if (substr($xval, 0, 1) == '*') {
+ $xval = substr($xval, 1);
+ $caret = '';
+ }
+ if (substr($xval, -1, 1) == '*') {
+ $xval = substr($xval, 0, -1);
+ $dollar = '';
+ }
+ if (!$caret || !$dollar) {
+ $re = $caret.preg_quote($xval, '/').$dollar;
+ foreach(array_keys(preg_grep('/'.$re.'/', $words)) as $i)
+ $value_ids[$i][] = $val;
+ } else {
+ if (($i = array_search($val, $words, true)) !== false)
+ $value_ids[$i][] = $val;
+ }
+ }
+ }
+
+ unset($words); // free the used memory
+
+ // initialize the result so it won't be null
+ $result = array();
+ foreach ($value_array as $val) {
+ $result[$val] = array();
+ }
+
+ $page_idx = $this->getIndex('page', '');
+
+ // Special handling for titles
+ if ($key == 'title') {
+ foreach ($value_ids as $pid => $val_list) {
+ $page = $page_idx[$pid];
+ foreach ($val_list as $val) {
+ $result[$val][] = $page;
+ }
+ }
+ } else {
+ // load all lines and pages so the used lines can be taken and matched with the pages
+ $lines = $this->getIndex($metaname.'_i', '');
+
+ foreach ($value_ids as $value_id => $val_list) {
+ // parse the tuples of the form page_id*1:page2_id*1 and so on, return value
+ // is an array with page_id => 1, page2_id => 1 etc. so take the keys only
+ $pages = array_keys($this->parseTuples($page_idx, $lines[$value_id]));
+ foreach ($val_list as $val) {
+ $result[$val] = array_merge($result[$val], $pages);
+ }
+ }
+ }
+ if (!is_array($value)) $result = $result[$value];
+ return $result;
+ }
+
+ /**
+ * Find the index ID of each search term.
+ *
+ * The query terms should only contain valid characters, with a '*' at
+ * either the beginning or end of the word (or both).
+ * The $result parameter can be used to merge the index locations with
+ * the appropriate query term.
+ *
+ * @param array $words The query terms.
+ * @param array $result Set to word => array("length*id" ...)
+ * @return array Set to length => array(id ...)
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ protected function getIndexWords(&$words, &$result) {
+ $tokens = array();
+ $tokenlength = array();
+ $tokenwild = array();
+ foreach ($words as $word) {
+ $result[$word] = array();
+ $caret = '^';
+ $dollar = '$';
+ $xword = $word;
+ $wlen = wordlen($word);
+
+ // check for wildcards
+ if (substr($xword, 0, 1) == '*') {
+ $xword = substr($xword, 1);
+ $caret = '';
+ $wlen -= 1;
+ }
+ if (substr($xword, -1, 1) == '*') {
+ $xword = substr($xword, 0, -1);
+ $dollar = '';
+ $wlen -= 1;
+ }
+ if ($wlen < IDX_MINWORDLENGTH && $caret && $dollar && !is_numeric($xword))
+ continue;
+ if (!isset($tokens[$xword]))
+ $tokenlength[$wlen][] = $xword;
+ if (!$caret || !$dollar) {
+ $re = $caret.preg_quote($xword, '/').$dollar;
+ $tokens[$xword][] = array($word, '/'.$re.'/');
+ if (!isset($tokenwild[$xword]))
+ $tokenwild[$xword] = $wlen;
+ } else {
+ $tokens[$xword][] = array($word, null);
+ }
+ }
+ asort($tokenwild);
+ // $tokens = array( base word => array( [ query term , regexp ] ... ) ... )
+ // $tokenlength = array( base word length => base word ... )
+ // $tokenwild = array( base word => base word length ... )
+ $length_filter = empty($tokenwild) ? $tokenlength : min(array_keys($tokenlength));
+ $indexes_known = $this->indexLengths($length_filter);
+ if (!empty($tokenwild)) sort($indexes_known);
+ // get word IDs
+ $wids = array();
+ foreach ($indexes_known as $ixlen) {
+ $word_idx = $this->getIndex('w', $ixlen);
+ // handle exact search
+ if (isset($tokenlength[$ixlen])) {
+ foreach ($tokenlength[$ixlen] as $xword) {
+ $wid = array_search($xword, $word_idx, true);
+ if ($wid !== false) {
+ $wids[$ixlen][] = $wid;
+ foreach ($tokens[$xword] as $w)
+ $result[$w[0]][] = "$ixlen*$wid";
+ }
+ }
+ }
+ // handle wildcard search
+ foreach ($tokenwild as $xword => $wlen) {
+ if ($wlen >= $ixlen) break;
+ foreach ($tokens[$xword] as $w) {
+ if (is_null($w[1])) continue;
+ foreach(array_keys(preg_grep($w[1], $word_idx)) as $wid) {
+ $wids[$ixlen][] = $wid;
+ $result[$w[0]][] = "$ixlen*$wid";
+ }
+ }
+ }
+ }
+ return $wids;
+ }
+
+ /**
+ * Return a list of all pages
+ * Warning: pages may not exist!
+ *
+ * @param string $key list only pages containing the metadata key (optional)
+ * @return array list of page names
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ public function getPages($key=null) {
+ $page_idx = $this->getIndex('page', '');
+ if (is_null($key)) return $page_idx;
+
+ $metaname = idx_cleanName($key);
+
+ // Special handling for titles
+ if ($key == 'title') {
+ $title_idx = $this->getIndex('title', '');
+ array_splice($page_idx, count($title_idx));
+ foreach ($title_idx as $i => $title)
+ if ($title === "") unset($page_idx[$i]);
+ return array_values($page_idx);
+ }
+
+ $pages = array();
+ $lines = $this->getIndex($metaname.'_i', '');
+ foreach ($lines as $line) {
+ $pages = array_merge($pages, $this->parseTuples($page_idx, $line));
+ }
+ return array_keys($pages);
+ }
+
+ /**
+ * Return a list of words sorted by number of times used
+ *
+ * @param int $min bottom frequency threshold
+ * @param int $max upper frequency limit. No limit if $max<$min
+ * @param int $minlen minimum length of words to count
+ * @param string $key metadata key to list. Uses the fulltext index if not given
+ * @return array list of words as the keys and frequency as values
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ public function histogram($min=1, $max=0, $minlen=3, $key=null) {
+ if ($min < 1)
+ $min = 1;
+ if ($max < $min)
+ $max = 0;
+
+ $result = array();
+
+ if ($key == 'title') {
+ $index = $this->getIndex('title', '');
+ $index = array_count_values($index);
+ foreach ($index as $val => $cnt) {
+ if ($cnt >= $min && (!$max || $cnt <= $max) && strlen($val) >= $minlen)
+ $result[$val] = $cnt;
+ }
+ }
+ elseif (!is_null($key)) {
+ $metaname = idx_cleanName($key);
+ $index = $this->getIndex($metaname.'_i', '');
+ $val_idx = array();
+ foreach ($index as $wid => $line) {
+ $freq = $this->countTuples($line);
+ if ($freq >= $min && (!$max || $freq <= $max))
+ $val_idx[$wid] = $freq;
+ }
+ if (!empty($val_idx)) {
+ $words = $this->getIndex($metaname.'_w', '');
+ foreach ($val_idx as $wid => $freq) {
+ if (strlen($words[$wid]) >= $minlen)
+ $result[$words[$wid]] = $freq;
+ }
+ }
+ }
+ else {
+ $lengths = idx_listIndexLengths();
+ foreach ($lengths as $length) {
+ if ($length < $minlen) continue;
+ $index = $this->getIndex('i', $length);
+ $words = null;
+ foreach ($index as $wid => $line) {
+ $freq = $this->countTuples($line);
+ if ($freq >= $min && (!$max || $freq <= $max)) {
+ if ($words === null)
+ $words = $this->getIndex('w', $length);
+ $result[$words[$wid]] = $freq;
+ }
+ }
+ }
+ }
+
+ arsort($result);
+ return $result;
+ }
+
+ /**
+ * Lock the indexer.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @return bool|string
+ */
+ protected function lock() {
+ global $conf;
+ $status = true;
+ $run = 0;
+ $lock = $conf['lockdir'].'/_indexer.lock';
+ while (!@mkdir($lock, $conf['dmode'])) {
+ usleep(50);
+ if(is_dir($lock) && time()-@filemtime($lock) > 60*5){
+ // looks like a stale lock - remove it
+ if (!@rmdir($lock)) {
+ $status = "removing the stale lock failed";
+ return false;
+ } else {
+ $status = "stale lock removed";
+ }
+ }elseif($run++ == 1000){
+ // we waited 5 seconds for that lock
+ return false;
+ }
+ }
+ if (!empty($conf['dperm'])) {
+ chmod($lock, $conf['dperm']);
+ }
+ return $status;
+ }
+
+ /**
+ * Release the indexer lock.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @return bool
+ */
+ protected function unlock() {
+ global $conf;
+ @rmdir($conf['lockdir'].'/_indexer.lock');
+ return true;
+ }
+
+ /**
+ * Retrieve the entire index.
+ *
+ * The $suffix argument is for an index that is split into
+ * multiple parts. Different index files should use different
+ * base names.
+ *
+ * @param string $idx name of the index
+ * @param string $suffix subpart identifier
+ * @return array list of lines without CR or LF
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ protected function getIndex($idx, $suffix) {
+ global $conf;
+ $fn = $conf['indexdir'].'/'.$idx.$suffix.'.idx';
+ if (!file_exists($fn)) return array();
+ return file($fn, FILE_IGNORE_NEW_LINES);
+ }
+
+ /**
+ * Replace the contents of the index with an array.
+ *
+ * @param string $idx name of the index
+ * @param string $suffix subpart identifier
+ * @param array $lines list of lines without LF
+ * @return bool If saving succeeded
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ protected function saveIndex($idx, $suffix, &$lines) {
+ global $conf;
+ $fn = $conf['indexdir'].'/'.$idx.$suffix;
+ $fh = @fopen($fn.'.tmp', 'w');
+ if (!$fh) return false;
+ fwrite($fh, join("\n", $lines));
+ if (!empty($lines))
+ fwrite($fh, "\n");
+ fclose($fh);
+ if ($conf['fperm'])
+ chmod($fn.'.tmp', $conf['fperm']);
+ io_rename($fn.'.tmp', $fn.'.idx');
+ return true;
+ }
+
+ /**
+ * Retrieve a line from the index.
+ *
+ * @param string $idx name of the index
+ * @param string $suffix subpart identifier
+ * @param int $id the line number
+ * @return string a line with trailing whitespace removed
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ protected function getIndexKey($idx, $suffix, $id) {
+ global $conf;
+ $fn = $conf['indexdir'].'/'.$idx.$suffix.'.idx';
+ if (!file_exists($fn)) return '';
+ $fh = @fopen($fn, 'r');
+ if (!$fh) return '';
+ $ln = -1;
+ while (($line = fgets($fh)) !== false) {
+ if (++$ln == $id) break;
+ }
+ fclose($fh);
+ return rtrim((string)$line);
+ }
+
+ /**
+ * Write a line into the index.
+ *
+ * @param string $idx name of the index
+ * @param string $suffix subpart identifier
+ * @param int $id the line number
+ * @param string $line line to write
+ * @return bool If saving succeeded
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ protected function saveIndexKey($idx, $suffix, $id, $line) {
+ global $conf;
+ if (substr($line, -1) != "\n")
+ $line .= "\n";
+ $fn = $conf['indexdir'].'/'.$idx.$suffix;
+ $fh = @fopen($fn.'.tmp', 'w');
+ if (!$fh) return false;
+ $ih = @fopen($fn.'.idx', 'r');
+ if ($ih) {
+ $ln = -1;
+ while (($curline = fgets($ih)) !== false) {
+ fwrite($fh, (++$ln == $id) ? $line : $curline);
+ }
+ if ($id > $ln) {
+ while ($id > ++$ln)
+ fwrite($fh, "\n");
+ fwrite($fh, $line);
+ }
+ fclose($ih);
+ } else {
+ $ln = -1;
+ while ($id > ++$ln)
+ fwrite($fh, "\n");
+ fwrite($fh, $line);
+ }
+ fclose($fh);
+ if ($conf['fperm'])
+ chmod($fn.'.tmp', $conf['fperm']);
+ io_rename($fn.'.tmp', $fn.'.idx');
+ return true;
+ }
+
+ /**
+ * Retrieve or insert a value in the index.
+ *
+ * @param string $idx name of the index
+ * @param string $suffix subpart identifier
+ * @param string $value line to find in the index
+ * @return int|bool line number of the value in the index or false if writing the index failed
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ protected function addIndexKey($idx, $suffix, $value) {
+ $index = $this->getIndex($idx, $suffix);
+ $id = array_search($value, $index, true);
+ if ($id === false) {
+ $id = count($index);
+ $index[$id] = $value;
+ if (!$this->saveIndex($idx, $suffix, $index)) {
+ trigger_error("Failed to write $idx index", E_USER_ERROR);
+ return false;
+ }
+ }
+ return $id;
+ }
+
+ /**
+ * Get the list of lengths indexed in the wiki.
+ *
+ * Read the index directory or a cache file and returns
+ * a sorted array of lengths of the words used in the wiki.
+ *
+ * @author YoBoY <yoboy.leguesh@gmail.com>
+ *
+ * @return array
+ */
+ protected function listIndexLengths() {
+ return idx_listIndexLengths();
+ }
+
+ /**
+ * Get the word lengths that have been indexed.
+ *
+ * Reads the index directory and returns an array of lengths
+ * that there are indices for.
+ *
+ * @author YoBoY <yoboy.leguesh@gmail.com>
+ *
+ * @param array|int $filter
+ * @return array
+ */
+ protected function indexLengths($filter) {
+ global $conf;
+ $idx = array();
+ if (is_array($filter)) {
+ // testing if index files exist only
+ $path = $conf['indexdir']."/i";
+ foreach ($filter as $key => $value) {
+ if (file_exists($path.$key.'.idx'))
+ $idx[] = $key;
+ }
+ } else {
+ $lengths = idx_listIndexLengths();
+ foreach ($lengths as $key => $length) {
+ // keep all the values equal or superior
+ if ((int)$length >= (int)$filter)
+ $idx[] = $length;
+ }
+ }
+ return $idx;
+ }
+
+ /**
+ * Insert or replace a tuple in a line.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param string $line
+ * @param string|int $id
+ * @param int $count
+ * @return string
+ */
+ protected function updateTuple($line, $id, $count) {
+ if ($line != ''){
+ $line = preg_replace('/(^|:)'.preg_quote($id,'/').'\*\d*/', '', $line);
+ }
+ $line = trim($line, ':');
+ if ($count) {
+ if ($line) {
+ return "$id*$count:".$line;
+ } else {
+ return "$id*$count";
+ }
+ }
+ return $line;
+ }
+
+ /**
+ * Split a line into an array of tuples.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $keys
+ * @param string $line
+ * @return array
+ */
+ protected function parseTuples(&$keys, $line) {
+ $result = array();
+ if ($line == '') return $result;
+ $parts = explode(':', $line);
+ foreach ($parts as $tuple) {
+ if ($tuple === '') continue;
+ list($key, $cnt) = explode('*', $tuple);
+ if (!$cnt) continue;
+ $key = $keys[$key];
+ if ($key === false || is_null($key)) continue;
+ $result[$key] = $cnt;
+ }
+ return $result;
+ }
+
+ /**
+ * Sum the counts in a list of tuples.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param string $line
+ * @return int
+ */
+ protected function countTuples($line) {
+ $freq = 0;
+ $parts = explode(':', $line);
+ foreach ($parts as $tuple) {
+ if ($tuple === '') continue;
+ list(/* $pid */, $cnt) = explode('*', $tuple);
+ $freq += (int)$cnt;
+ }
+ return $freq;
+ }
+}
diff --git a/platform/www/inc/Sitemap/Item.php b/platform/www/inc/Sitemap/Item.php
new file mode 100644
index 0000000..d11bfc1
--- /dev/null
+++ b/platform/www/inc/Sitemap/Item.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace dokuwiki\Sitemap;
+
+/**
+ * An item of a sitemap.
+ *
+ * @author Michael Hamann
+ */
+class Item {
+ public $url;
+ public $lastmod;
+ public $changefreq;
+ public $priority;
+
+ /**
+ * Create a new item.
+ *
+ * @param string $url The url of the item
+ * @param int $lastmod Timestamp of the last modification
+ * @param string $changefreq How frequently the item is likely to change.
+ * Valid values: always, hourly, daily, weekly, monthly, yearly, never.
+ * @param $priority float|string The priority of the item relative to other URLs on your site.
+ * Valid values range from 0.0 to 1.0.
+ */
+ public function __construct($url, $lastmod, $changefreq = null, $priority = null) {
+ $this->url = $url;
+ $this->lastmod = $lastmod;
+ $this->changefreq = $changefreq;
+ $this->priority = $priority;
+ }
+
+ /**
+ * Helper function for creating an item for a wikipage id.
+ *
+ * @param string $id A wikipage id.
+ * @param string $changefreq How frequently the item is likely to change.
+ * Valid values: always, hourly, daily, weekly, monthly, yearly, never.
+ * @param float|string $priority The priority of the item relative to other URLs on your site.
+ * Valid values range from 0.0 to 1.0.
+ * @return Item The sitemap item.
+ */
+ public static function createFromID($id, $changefreq = null, $priority = null) {
+ $id = trim($id);
+ $date = @filemtime(wikiFN($id));
+ if(!$date) return null;
+ return new Item(wl($id, '', true), $date, $changefreq, $priority);
+ }
+
+ /**
+ * Get the XML representation of the sitemap item.
+ *
+ * @return string The XML representation.
+ */
+ public function toXML() {
+ $result = ' <url>'.NL
+ .' <loc>'.hsc($this->url).'</loc>'.NL
+ .' <lastmod>'.date_iso8601($this->lastmod).'</lastmod>'.NL;
+ if ($this->changefreq !== null)
+ $result .= ' <changefreq>'.hsc($this->changefreq).'</changefreq>'.NL;
+ if ($this->priority !== null)
+ $result .= ' <priority>'.hsc($this->priority).'</priority>'.NL;
+ $result .= ' </url>'.NL;
+ return $result;
+ }
+}
diff --git a/platform/www/inc/Sitemap/Mapper.php b/platform/www/inc/Sitemap/Mapper.php
new file mode 100644
index 0000000..2f0567f
--- /dev/null
+++ b/platform/www/inc/Sitemap/Mapper.php
@@ -0,0 +1,164 @@
+<?php
+/**
+ * Sitemap handling functions
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Michael Hamann <michael@content-space.de>
+ */
+
+namespace dokuwiki\Sitemap;
+
+use dokuwiki\HTTP\DokuHTTPClient;
+
+/**
+ * A class for building sitemaps and pinging search engines with the sitemap URL.
+ *
+ * @author Michael Hamann
+ */
+class Mapper {
+ /**
+ * Builds a Google Sitemap of all public pages known to the indexer
+ *
+ * The map is placed in the cache directory named sitemap.xml.gz - This
+ * file needs to be writable!
+ *
+ * @author Michael Hamann
+ * @author Andreas Gohr
+ * @link https://www.google.com/webmasters/sitemaps/docs/en/about.html
+ * @link http://www.sitemaps.org/
+ *
+ * @return bool
+ */
+ public static function generate(){
+ global $conf;
+ if($conf['sitemap'] < 1 || !is_numeric($conf['sitemap'])) return false;
+
+ $sitemap = Mapper::getFilePath();
+
+ if(file_exists($sitemap)){
+ if(!is_writable($sitemap)) return false;
+ }else{
+ if(!is_writable(dirname($sitemap))) return false;
+ }
+
+ if(@filesize($sitemap) &&
+ @filemtime($sitemap) > (time()-($conf['sitemap']*86400))){ // 60*60*24=86400
+ dbglog('Sitemapper::generate(): Sitemap up to date');
+ return false;
+ }
+
+ dbglog("Sitemapper::generate(): using $sitemap");
+
+ $pages = idx_get_indexer()->getPages();
+ dbglog('Sitemapper::generate(): creating sitemap using '.count($pages).' pages');
+ $items = array();
+
+ // build the sitemap items
+ foreach($pages as $id){
+ //skip hidden, non existing and restricted files
+ if(isHiddenPage($id)) continue;
+ if(auth_aclcheck($id,'',array()) < AUTH_READ) continue;
+ $item = Item::createFromID($id);
+ if ($item !== null)
+ $items[] = $item;
+ }
+
+ $eventData = array('items' => &$items, 'sitemap' => &$sitemap);
+ $event = new \dokuwiki\Extension\Event('SITEMAP_GENERATE', $eventData);
+ if ($event->advise_before(true)) {
+ //save the new sitemap
+ $event->result = io_saveFile($sitemap, Mapper::getXML($items));
+ }
+ $event->advise_after();
+
+ return $event->result;
+ }
+
+ /**
+ * Builds the sitemap XML string from the given array auf SitemapItems.
+ *
+ * @param $items array The SitemapItems that shall be included in the sitemap.
+ * @return string The sitemap XML.
+ *
+ * @author Michael Hamann
+ */
+ private static function getXML($items) {
+ ob_start();
+ echo '<?xml version="1.0" encoding="UTF-8"?>'.NL;
+ echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'.NL;
+ foreach ($items as $item) {
+ /** @var Item $item */
+ echo $item->toXML();
+ }
+ echo '</urlset>'.NL;
+ $result = ob_get_contents();
+ ob_end_clean();
+ return $result;
+ }
+
+ /**
+ * Helper function for getting the path to the sitemap file.
+ *
+ * @return string The path to the sitemap file.
+ *
+ * @author Michael Hamann
+ */
+ public static function getFilePath() {
+ global $conf;
+
+ $sitemap = $conf['cachedir'].'/sitemap.xml';
+ if (self::sitemapIsCompressed()) {
+ $sitemap .= '.gz';
+ }
+
+ return $sitemap;
+ }
+
+ /**
+ * Helper function for checking if the sitemap is compressed
+ *
+ * @return bool If the sitemap file is compressed
+ */
+ public static function sitemapIsCompressed() {
+ global $conf;
+ return $conf['compression'] === 'bz2' || $conf['compression'] === 'gz';
+ }
+
+ /**
+ * Pings search engines with the sitemap url. Plugins can add or remove
+ * urls to ping using the SITEMAP_PING event.
+ *
+ * @author Michael Hamann
+ *
+ * @return bool
+ */
+ public static function pingSearchEngines() {
+ //ping search engines...
+ $http = new DokuHTTPClient();
+ $http->timeout = 8;
+
+ $encoded_sitemap_url = urlencode(wl('', array('do' => 'sitemap'), true, '&'));
+ $ping_urls = array(
+ 'google' => 'http://www.google.com/webmasters/sitemaps/ping?sitemap='.$encoded_sitemap_url,
+ 'microsoft' => 'http://www.bing.com/webmaster/ping.aspx?siteMap='.$encoded_sitemap_url,
+ 'yandex' => 'http://blogs.yandex.ru/pings/?status=success&url='.$encoded_sitemap_url
+ );
+
+ $data = array('ping_urls' => $ping_urls,
+ 'encoded_sitemap_url' => $encoded_sitemap_url
+ );
+ $event = new \dokuwiki\Extension\Event('SITEMAP_PING', $data);
+ if ($event->advise_before(true)) {
+ foreach ($data['ping_urls'] as $name => $url) {
+ dbglog("Sitemapper::PingSearchEngines(): pinging $name");
+ $resp = $http->get($url);
+ if($http->error) dbglog("Sitemapper:pingSearchengines(): $http->error");
+ dbglog('Sitemapper:pingSearchengines(): '.preg_replace('/[\n\r]/',' ',strip_tags($resp)));
+ }
+ }
+ $event->advise_after();
+
+ return true;
+ }
+}
+
diff --git a/platform/www/inc/StyleUtils.php b/platform/www/inc/StyleUtils.php
new file mode 100644
index 0000000..d9f19a5
--- /dev/null
+++ b/platform/www/inc/StyleUtils.php
@@ -0,0 +1,194 @@
+<?php
+
+namespace dokuwiki;
+
+/**
+ * Class StyleUtils
+ *
+ * Reads and applies the template's style.ini settings
+ */
+class StyleUtils
+{
+
+ /** @var string current template */
+ protected $tpl;
+ /** @var bool reinitialize styles config */
+ protected $reinit;
+ /** @var bool $preview preview mode */
+ protected $preview;
+ /** @var array default replacements to be merged with custom style configs */
+ protected $defaultReplacements = array(
+ '__text__' => "#000",
+ '__background__' => "#fff",
+ '__text_alt__' => "#999",
+ '__background_alt__' => "#eee",
+ '__text_neu__' => "#666",
+ '__background_neu__' => "#ddd",
+ '__border__' => "#ccc",
+ '__highlight__' => "#ff9",
+ '__link__' => "#00f",
+ );
+
+ /**
+ * StyleUtils constructor.
+ * @param string $tpl template name: if not passed as argument, the default value from $conf will be used
+ * @param bool $preview
+ * @param bool $reinit whether static style conf should be reinitialized
+ */
+ public function __construct($tpl = '', $preview = false, $reinit = false)
+ {
+ if (!$tpl) {
+ global $conf;
+ $tpl = $conf['template'];
+ }
+ $this->tpl = $tpl;
+ $this->reinit = $reinit;
+ $this->preview = $preview;
+ }
+
+ /**
+ * Load style ini contents
+ *
+ * Loads and merges style.ini files from template and config and prepares
+ * the stylesheet modes
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Anna Dabrowska <info@cosmocode.de>
+ *
+ * @return array with keys 'stylesheets' and 'replacements'
+ */
+ public function cssStyleini()
+ {
+ static $combined = [];
+ if (!empty($combined) && !$this->reinit) {
+ return $combined;
+ }
+
+ global $conf;
+ global $config_cascade;
+ $stylesheets = array(); // mode, file => base
+
+ // guaranteed placeholder => value
+ $replacements = $this->defaultReplacements;
+
+ // merge all styles from config cascade
+ if (!is_array($config_cascade['styleini'])) {
+ trigger_error('Missing config cascade for styleini', E_USER_WARNING);
+ }
+
+ // allow replacement overwrites in preview mode
+ if ($this->preview) {
+ $config_cascade['styleini']['local'][] = $conf['cachedir'] . '/preview.ini';
+ }
+
+ $combined['stylesheets'] = [];
+ $combined['replacements'] = [];
+
+ foreach (array('default', 'local', 'protected') as $config_group) {
+ if (empty($config_cascade['styleini'][$config_group])) continue;
+
+ // set proper server dirs
+ $webbase = $this->getWebbase($config_group);
+
+ foreach ($config_cascade['styleini'][$config_group] as $inifile) {
+ // replace the placeholder with the name of the current template
+ $inifile = str_replace('%TEMPLATE%', $this->tpl, $inifile);
+
+ $incbase = dirname($inifile) . '/';
+
+ if (file_exists($inifile)) {
+ $config = parse_ini_file($inifile, true);
+
+ if (is_array($config['stylesheets'])) {
+ foreach ($config['stylesheets'] as $inifile => $mode) {
+ // validate and include style files
+ $stylesheets = array_merge(
+ $stylesheets,
+ $this->getValidatedStyles($stylesheets, $inifile, $mode, $incbase, $webbase)
+ );
+ $combined['stylesheets'] = array_merge($combined['stylesheets'], $stylesheets);
+ }
+ }
+
+ if (is_array($config['replacements'])) {
+ $replacements = array_replace(
+ $replacements,
+ $this->cssFixreplacementurls($config['replacements'], $webbase)
+ );
+ $combined['replacements'] = array_merge($combined['replacements'], $replacements);
+ }
+ }
+ }
+ }
+
+ return $combined;
+ }
+
+ /**
+ * Checks if configured style files exist and, if necessary, adjusts file extensions in config
+ *
+ * @param array $stylesheets
+ * @param string $file
+ * @param string $mode
+ * @param string $incbase
+ * @param string $webbase
+ * @return mixed
+ */
+ protected function getValidatedStyles($stylesheets, $file, $mode, $incbase, $webbase)
+ {
+ global $conf;
+ if (!file_exists($incbase . $file)) {
+ list($extension, $basename) = array_map('strrev', explode('.', strrev($file), 2));
+ $newExtension = $extension === 'css' ? 'less' : 'css';
+ if (file_exists($incbase . $basename . '.' . $newExtension)) {
+ $stylesheets[$mode][$incbase . $basename . '.' . $newExtension] = $webbase;
+ if ($conf['allowdebug']) {
+ msg("Stylesheet $file not found, using $basename.$newExtension instead. " .
+ "Please contact developer of \"$this->tpl\" template.", 2);
+ }
+ } elseif ($conf['allowdebug']) {
+ msg("Stylesheet $file not found, please contact the developer of \"$this->tpl\" template.", 2);
+ }
+ }
+ $stylesheets[$mode][fullpath($incbase . $file)] = $webbase;
+ return $stylesheets;
+ }
+
+ /**
+ * Returns the web base path for the given level/group in config cascade.
+ * Style resources are relative to the template directory for the main (default) styles
+ * but relative to DOKU_BASE for everything else"
+ *
+ * @param string $config_group
+ * @return string
+ */
+ protected function getWebbase($config_group)
+ {
+ if ($config_group === 'default') {
+ return tpl_basedir($this->tpl);
+ } else {
+ return DOKU_BASE;
+ }
+ }
+
+ /**
+ * Amend paths used in replacement relative urls, refer FS#2879
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param array $replacements with key-value pairs
+ * @param string $location
+ * @return array
+ */
+ protected function cssFixreplacementurls($replacements, $location)
+ {
+ foreach ($replacements as $key => $value) {
+ $replacements[$key] = preg_replace(
+ '#(url\([ \'"]*)(?!/|data:|http://|https://| |\'|")#',
+ '\\1' . $location,
+ $value
+ );
+ }
+ return $replacements;
+ }
+}
diff --git a/platform/www/inc/Subscriptions/BulkSubscriptionSender.php b/platform/www/inc/Subscriptions/BulkSubscriptionSender.php
new file mode 100644
index 0000000..672ef90
--- /dev/null
+++ b/platform/www/inc/Subscriptions/BulkSubscriptionSender.php
@@ -0,0 +1,261 @@
+<?php
+
+
+namespace dokuwiki\Subscriptions;
+
+
+use dokuwiki\ChangeLog\PageChangeLog;
+use dokuwiki\Input\Input;
+use DokuWiki_Auth_Plugin;
+
+class BulkSubscriptionSender extends SubscriptionSender
+{
+
+ /**
+ * Send digest and list subscriptions
+ *
+ * This sends mails to all subscribers that have a subscription for namespaces above
+ * the given page if the needed $conf['subscribe_time'] has passed already.
+ *
+ * This function is called form lib/exe/indexer.php
+ *
+ * @param string $page
+ *
+ * @return int number of sent mails
+ */
+ public function sendBulk($page)
+ {
+ $subscriberManager = new SubscriberManager();
+ if (!$subscriberManager->isenabled()) {
+ return 0;
+ }
+
+ /** @var DokuWiki_Auth_Plugin $auth */
+ global $auth;
+ global $conf;
+ global $USERINFO;
+ /** @var Input $INPUT */
+ global $INPUT;
+ $count = 0;
+
+ $subscriptions = $subscriberManager->subscribers($page, null, ['digest', 'list']);
+
+ // remember current user info
+ $olduinfo = $USERINFO;
+ $olduser = $INPUT->server->str('REMOTE_USER');
+
+ foreach ($subscriptions as $target => $users) {
+ if (!$this->lock($target)) {
+ continue;
+ }
+
+ foreach ($users as $user => $info) {
+ list($style, $lastupdate) = $info;
+
+ $lastupdate = (int)$lastupdate;
+ if ($lastupdate + $conf['subscribe_time'] > time()) {
+ // Less than the configured time period passed since last
+ // update.
+ continue;
+ }
+
+ // Work as the user to make sure ACLs apply correctly
+ $USERINFO = $auth->getUserData($user);
+ $INPUT->server->set('REMOTE_USER', $user);
+ if ($USERINFO === false) {
+ continue;
+ }
+ if (!$USERINFO['mail']) {
+ continue;
+ }
+
+ if (substr($target, -1, 1) === ':') {
+ // subscription target is a namespace, get all changes within
+ $changes = getRecentsSince($lastupdate, null, getNS($target));
+ } else {
+ // single page subscription, check ACL ourselves
+ if (auth_quickaclcheck($target) < AUTH_READ) {
+ continue;
+ }
+ $meta = p_get_metadata($target);
+ $changes = [$meta['last_change']];
+ }
+
+ // Filter out pages only changed in small and own edits
+ $change_ids = [];
+ foreach ($changes as $rev) {
+ $n = 0;
+ while (!is_null($rev) && $rev['date'] >= $lastupdate &&
+ ($INPUT->server->str('REMOTE_USER') === $rev['user'] ||
+ $rev['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT)) {
+ $pagelog = new PageChangeLog($rev['id']);
+ $rev = $pagelog->getRevisions($n++, 1);
+ $rev = (count($rev) > 0) ? $rev[0] : null;
+ }
+
+ if (!is_null($rev) && $rev['date'] >= $lastupdate) {
+ // Some change was not a minor one and not by myself
+ $change_ids[] = $rev['id'];
+ }
+ }
+
+ // send it
+ if ($style === 'digest') {
+ foreach ($change_ids as $change_id) {
+ $this->sendDigest(
+ $USERINFO['mail'],
+ $change_id,
+ $lastupdate
+ );
+ $count++;
+ }
+ } else {
+ if ($style === 'list') {
+ $this->sendList($USERINFO['mail'], $change_ids, $target);
+ $count++;
+ }
+ }
+ // TODO: Handle duplicate subscriptions.
+
+ // Update notification time.
+ $subscriberManager->add($target, $user, $style, time());
+ }
+ $this->unlock($target);
+ }
+
+ // restore current user info
+ $USERINFO = $olduinfo;
+ $INPUT->server->set('REMOTE_USER', $olduser);
+ return $count;
+ }
+
+ /**
+ * Lock subscription info
+ *
+ * We don't use io_lock() her because we do not wait for the lock and use a larger stale time
+ *
+ * @param string $id The target page or namespace, specified by id; Namespaces
+ * are identified by appending a colon.
+ *
+ * @return bool true, if you got a succesful lock
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+ protected function lock($id)
+ {
+ global $conf;
+
+ $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock';
+
+ if (is_dir($lock) && time() - @filemtime($lock) > 60 * 5) {
+ // looks like a stale lock - remove it
+ @rmdir($lock);
+ }
+
+ // try creating the lock directory
+ if (!@mkdir($lock, $conf['dmode'])) {
+ return false;
+ }
+
+ if (!empty($conf['dperm'])) {
+ chmod($lock, $conf['dperm']);
+ }
+ return true;
+ }
+
+ /**
+ * Unlock subscription info
+ *
+ * @param string $id The target page or namespace, specified by id; Namespaces
+ * are identified by appending a colon.
+ *
+ * @return bool
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+ protected function unlock($id)
+ {
+ global $conf;
+ $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock';
+ return @rmdir($lock);
+ }
+
+ /**
+ * Send a digest mail
+ *
+ * Sends a digest mail showing a bunch of changes of a single page. Basically the same as sendPageDiff()
+ * but determines the last known revision first
+ *
+ * @param string $subscriber_mail The target mail address
+ * @param string $id The ID
+ * @param int $lastupdate Time of the last notification
+ *
+ * @return bool
+ * @author Adrian Lang <lang@cosmocode.de>
+ *
+ */
+ protected function sendDigest($subscriber_mail, $id, $lastupdate)
+ {
+ $pagelog = new PageChangeLog($id);
+ $n = 0;
+ do {
+ $rev = $pagelog->getRevisions($n++, 1);
+ $rev = (count($rev) > 0) ? $rev[0] : null;
+ } while (!is_null($rev) && $rev > $lastupdate);
+
+ // TODO I'm not happy with the following line and passing $this->mailer around. Not sure how to solve it better
+ $pageSubSender = new PageSubscriptionSender($this->mailer);
+ return $pageSubSender->sendPageDiff(
+ $subscriber_mail,
+ 'subscr_digest',
+ $id,
+ $rev
+ );
+ }
+
+ /**
+ * Send a list mail
+ *
+ * Sends a list mail showing a list of changed pages.
+ *
+ * @param string $subscriber_mail The target mail address
+ * @param array $ids Array of ids
+ * @param string $ns_id The id of the namespace
+ *
+ * @return bool true if a mail was sent
+ * @author Adrian Lang <lang@cosmocode.de>
+ *
+ */
+ protected function sendList($subscriber_mail, $ids, $ns_id)
+ {
+ if (count($ids) === 0) {
+ return false;
+ }
+
+ $tlist = '';
+ $hlist = '<ul>';
+ foreach ($ids as $id) {
+ $link = wl($id, [], true);
+ $tlist .= '* ' . $link . NL;
+ $hlist .= '<li><a href="' . $link . '">' . hsc($id) . '</a></li>' . NL;
+ }
+ $hlist .= '</ul>';
+
+ $id = prettyprint_id($ns_id);
+ $trep = [
+ 'DIFF' => rtrim($tlist),
+ 'PAGE' => $id,
+ 'SUBSCRIBE' => wl($id, ['do' => 'subscribe'], true, '&'),
+ ];
+ $hrep = [
+ 'DIFF' => $hlist,
+ ];
+
+ return $this->send(
+ $subscriber_mail,
+ 'subscribe_list',
+ $ns_id,
+ 'subscr_list',
+ $trep,
+ $hrep
+ );
+ }
+}
diff --git a/platform/www/inc/Subscriptions/MediaSubscriptionSender.php b/platform/www/inc/Subscriptions/MediaSubscriptionSender.php
new file mode 100644
index 0000000..1757c2b
--- /dev/null
+++ b/platform/www/inc/Subscriptions/MediaSubscriptionSender.php
@@ -0,0 +1,47 @@
+<?php
+
+
+namespace dokuwiki\Subscriptions;
+
+
+class MediaSubscriptionSender extends SubscriptionSender
+{
+
+ /**
+ * Send the diff for some media change
+ *
+ * @fixme this should embed thumbnails of images in HTML version
+ *
+ * @param string $subscriber_mail The target mail address
+ * @param string $template Mail template ('uploadmail', ...)
+ * @param string $id Media file for which the notification is
+ * @param int|bool $rev Old revision if any
+ * @param int|bool $current_rev New revision if any
+ */
+ public function sendMediaDiff($subscriber_mail, $template, $id, $rev = false, $current_rev = false)
+ {
+ global $conf;
+
+ $file = mediaFN($id);
+ list($mime, /* $ext */) = mimetype($id);
+
+ $trep = [
+ 'MIME' => $mime,
+ 'MEDIA' => ml($id, $current_rev?('rev='.$current_rev):'', true, '&', true),
+ 'SIZE' => filesize_h(filesize($file)),
+ ];
+
+ if ($rev && $conf['mediarevisions']) {
+ $trep['OLD'] = ml($id, "rev=$rev", true, '&', true);
+ } else {
+ $trep['OLD'] = '---';
+ }
+
+ $headers = ['Message-Id' => $this->getMessageID($id, @filemtime($file))];
+ if ($rev) {
+ $headers['In-Reply-To'] = $this->getMessageID($id, $rev);
+ }
+
+ $this->send($subscriber_mail, 'upload', $id, $template, $trep, null, $headers);
+ }
+}
diff --git a/platform/www/inc/Subscriptions/PageSubscriptionSender.php b/platform/www/inc/Subscriptions/PageSubscriptionSender.php
new file mode 100644
index 0000000..e5577c1
--- /dev/null
+++ b/platform/www/inc/Subscriptions/PageSubscriptionSender.php
@@ -0,0 +1,88 @@
+<?php
+
+
+namespace dokuwiki\Subscriptions;
+
+
+use Diff;
+use InlineDiffFormatter;
+use UnifiedDiffFormatter;
+
+class PageSubscriptionSender extends SubscriptionSender
+{
+
+ /**
+ * Send the diff for some page change
+ *
+ * @param string $subscriber_mail The target mail address
+ * @param string $template Mail template ('subscr_digest', 'subscr_single', 'mailtext', ...)
+ * @param string $id Page for which the notification is
+ * @param int|null $rev Old revision if any
+ * @param string $summary Change summary if any
+ * @param int|null $current_rev New revision if any
+ *
+ * @return bool true if successfully sent
+ */
+ public function sendPageDiff($subscriber_mail, $template, $id, $rev = null, $summary = '', $current_rev = null)
+ {
+ global $DIFF_INLINESTYLES;
+
+ // prepare replacements (keys not set in hrep will be taken from trep)
+ $trep = [
+ 'PAGE' => $id,
+ 'NEWPAGE' => wl($id, $current_rev?('rev='.$current_rev):'', true, '&'),
+ 'SUMMARY' => $summary,
+ 'SUBSCRIBE' => wl($id, ['do' => 'subscribe'], true, '&'),
+ ];
+ $hrep = [];
+
+ if ($rev) {
+ $subject = 'changed';
+ $trep['OLDPAGE'] = wl($id, "rev=$rev", true, '&');
+
+ $old_content = rawWiki($id, $rev);
+ $new_content = rawWiki($id);
+
+ $df = new Diff(
+ explode("\n", $old_content),
+ explode("\n", $new_content)
+ );
+ $dformat = new UnifiedDiffFormatter();
+ $tdiff = $dformat->format($df);
+
+ $DIFF_INLINESTYLES = true;
+ $df = new Diff(
+ explode("\n", $old_content),
+ explode("\n", $new_content)
+ );
+ $dformat = new InlineDiffFormatter();
+ $hdiff = $dformat->format($df);
+ $hdiff = '<table>' . $hdiff . '</table>';
+ $DIFF_INLINESTYLES = false;
+ } else {
+ $subject = 'newpage';
+ $trep['OLDPAGE'] = '---';
+ $tdiff = rawWiki($id);
+ $hdiff = nl2br(hsc($tdiff));
+ }
+
+ $trep['DIFF'] = $tdiff;
+ $hrep['DIFF'] = $hdiff;
+
+ $headers = ['Message-Id' => $this->getMessageID($id)];
+ if ($rev) {
+ $headers['In-Reply-To'] = $this->getMessageID($id, $rev);
+ }
+
+ return $this->send(
+ $subscriber_mail,
+ $subject,
+ $id,
+ $template,
+ $trep,
+ $hrep,
+ $headers
+ );
+ }
+
+}
diff --git a/platform/www/inc/Subscriptions/RegistrationSubscriptionSender.php b/platform/www/inc/Subscriptions/RegistrationSubscriptionSender.php
new file mode 100644
index 0000000..bd48875
--- /dev/null
+++ b/platform/www/inc/Subscriptions/RegistrationSubscriptionSender.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace dokuwiki\Subscriptions;
+
+class RegistrationSubscriptionSender extends SubscriptionSender
+{
+
+ /**
+ * Send a notify mail on new registration
+ *
+ * @param string $login login name of the new user
+ * @param string $fullname full name of the new user
+ * @param string $email email address of the new user
+ *
+ * @return bool true if a mail was sent
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ */
+ public function sendRegister($login, $fullname, $email)
+ {
+ global $conf;
+ if (empty($conf['registernotify'])) {
+ return false;
+ }
+
+ $trep = [
+ 'NEWUSER' => $login,
+ 'NEWNAME' => $fullname,
+ 'NEWEMAIL' => $email,
+ ];
+
+ return $this->send(
+ $conf['registernotify'],
+ 'new_user',
+ $login,
+ 'registermail',
+ $trep
+ );
+ }
+}
diff --git a/platform/www/inc/Subscriptions/SubscriberManager.php b/platform/www/inc/Subscriptions/SubscriberManager.php
new file mode 100644
index 0000000..ded1390
--- /dev/null
+++ b/platform/www/inc/Subscriptions/SubscriberManager.php
@@ -0,0 +1,290 @@
+<?php
+
+namespace dokuwiki\Subscriptions;
+
+use dokuwiki\Input\Input;
+use DokuWiki_Auth_Plugin;
+use Exception;
+
+class SubscriberManager
+{
+
+ /**
+ * Check if subscription system is enabled
+ *
+ * @return bool
+ */
+ public function isenabled()
+ {
+ return actionOK('subscribe');
+ }
+
+ /**
+ * Adds a new subscription for the given page or namespace
+ *
+ * This will automatically overwrite any existent subscription for the given user on this
+ * *exact* page or namespace. It will *not* modify any subscription that may exist in higher namespaces.
+ *
+ * @throws Exception when user or style is empty
+ *
+ * @param string $id The target page or namespace, specified by id; Namespaces
+ * are identified by appending a colon.
+ * @param string $user
+ * @param string $style
+ * @param string $data
+ *
+ * @return bool
+ */
+ public function add($id, $user, $style, $data = '')
+ {
+ if (!$this->isenabled()) {
+ return false;
+ }
+
+ // delete any existing subscription
+ $this->remove($id, $user);
+
+ $user = auth_nameencode(trim($user));
+ $style = trim($style);
+ $data = trim($data);
+
+ if (!$user) {
+ throw new Exception('no subscription user given');
+ }
+ if (!$style) {
+ throw new Exception('no subscription style given');
+ }
+ if (!$data) {
+ $data = time();
+ } //always add current time for new subscriptions
+
+ $line = "$user $style $data\n";
+ $file = $this->file($id);
+ return io_saveFile($file, $line, true);
+ }
+
+
+ /**
+ * Removes a subscription for the given page or namespace
+ *
+ * This removes all subscriptions matching the given criteria on the given page or
+ * namespace. It will *not* modify any subscriptions that may exist in higher
+ * namespaces.
+ *
+ * @param string $id The target object’s (namespace or page) id
+ * @param string|array $user
+ * @param string|array $style
+ * @param string|array $data
+ *
+ * @return bool
+ */
+ public function remove($id, $user = null, $style = null, $data = null)
+ {
+ if (!$this->isenabled()) {
+ return false;
+ }
+
+ $file = $this->file($id);
+ if (!file_exists($file)) {
+ return true;
+ }
+
+ $regexBuilder = new SubscriberRegexBuilder();
+ $re = $regexBuilder->buildRegex($user, $style, $data);
+ return io_deleteFromFile($file, $re, true);
+ }
+
+ /**
+ * Get data for $INFO['subscribed']
+ *
+ * $INFO['subscribed'] is either false if no subscription for the current page
+ * and user is in effect. Else it contains an array of arrays with the fields
+ * “target”, “style”, and optionally “data”.
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ *
+ * @param string $id Page ID, defaults to global $ID
+ * @param string $user User, defaults to $_SERVER['REMOTE_USER']
+ *
+ * @return array|false
+ */
+ public function userSubscription($id = '', $user = '')
+ {
+ if (!$this->isenabled()) {
+ return false;
+ }
+
+ global $ID;
+ /** @var Input $INPUT */
+ global $INPUT;
+ if (!$id) {
+ $id = $ID;
+ }
+ if (!$user) {
+ $user = $INPUT->server->str('REMOTE_USER');
+ }
+
+ if (empty($user)) {
+ // not logged in
+ return false;
+ }
+
+ $subs = $this->subscribers($id, $user);
+ if (!count($subs)) {
+ return false;
+ }
+
+ $result = [];
+ foreach ($subs as $target => $info) {
+ $result[] = [
+ 'target' => $target,
+ 'style' => $info[$user][0],
+ 'data' => $info[$user][1],
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Recursively search for matching subscriptions
+ *
+ * This function searches all relevant subscription files for a page or
+ * namespace.
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ *
+ * @param string $page The target object’s (namespace or page) id
+ * @param string|array $user
+ * @param string|array $style
+ * @param string|array $data
+ *
+ * @return array
+ */
+ public function subscribers($page, $user = null, $style = null, $data = null)
+ {
+ if (!$this->isenabled()) {
+ return [];
+ }
+
+ // Construct list of files which may contain relevant subscriptions.
+ $files = [':' => $this->file(':')];
+ do {
+ $files[$page] = $this->file($page);
+ $page = getNS(rtrim($page, ':')) . ':';
+ } while ($page !== ':');
+
+ $regexBuilder = new SubscriberRegexBuilder();
+ $re = $regexBuilder->buildRegex($user, $style, $data);
+
+ // Handle files.
+ $result = [];
+ foreach ($files as $target => $file) {
+ if (!file_exists($file)) {
+ continue;
+ }
+
+ $lines = file($file);
+ foreach ($lines as $line) {
+ // fix old style subscription files
+ if (strpos($line, ' ') === false) {
+ $line = trim($line) . " every\n";
+ }
+
+ // check for matching entries
+ if (!preg_match($re, $line, $m)) {
+ continue;
+ }
+
+ $u = rawurldecode($m[1]); // decode the user name
+ if (!isset($result[$target])) {
+ $result[$target] = [];
+ }
+ $result[$target][$u] = [$m[2], $m[3]]; // add to result
+ }
+ }
+ return array_reverse($result);
+ }
+
+ /**
+ * Default callback for COMMON_NOTIFY_ADDRESSLIST
+ *
+ * Aggregates all email addresses of user who have subscribed the given page with 'every' style
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ * @author Steven Danz <steven-danz@kc.rr.com>
+ *
+ * @todo move the whole functionality into this class, trigger SUBSCRIPTION_NOTIFY_ADDRESSLIST instead,
+ * use an array for the addresses within it
+ *
+ * @param array &$data Containing the entries:
+ * - $id (the page id),
+ * - $self (whether the author should be notified,
+ * - $addresslist (current email address list)
+ * - $replacements (array of additional string substitutions, @KEY@ to be replaced by value)
+ */
+ public function notifyAddresses(&$data)
+ {
+ if (!$this->isenabled()) {
+ return;
+ }
+
+ /** @var DokuWiki_Auth_Plugin $auth */
+ global $auth;
+ global $conf;
+ /** @var \Input $INPUT */
+ global $INPUT;
+
+ $id = $data['id'];
+ $self = $data['self'];
+ $addresslist = $data['addresslist'];
+
+ $subscriptions = $this->subscribers($id, null, 'every');
+
+ $result = [];
+ foreach ($subscriptions as $target => $users) {
+ foreach ($users as $user => $info) {
+ $userinfo = $auth->getUserData($user);
+ if ($userinfo === false) {
+ continue;
+ }
+ if (!$userinfo['mail']) {
+ continue;
+ }
+ if (!$self && $user == $INPUT->server->str('REMOTE_USER')) {
+ continue;
+ } //skip our own changes
+
+ $level = auth_aclcheck($id, $user, $userinfo['grps']);
+ if ($level >= AUTH_READ) {
+ if (strcasecmp($userinfo['mail'], $conf['notify']) != 0) { //skip user who get notified elsewhere
+ $result[$user] = $userinfo['mail'];
+ }
+ }
+ }
+ }
+ $data['addresslist'] = trim($addresslist . ',' . implode(',', $result), ',');
+ }
+
+ /**
+ * Return the subscription meta file for the given ID
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ *
+ * @param string $id The target page or namespace, specified by id; Namespaces
+ * are identified by appending a colon.
+ *
+ * @return string
+ */
+ protected function file($id)
+ {
+ $meta_fname = '.mlist';
+ if ((substr($id, -1, 1) === ':')) {
+ $meta_froot = getNS($id);
+ $meta_fname = '/' . $meta_fname;
+ } else {
+ $meta_froot = $id;
+ }
+ return metaFN((string)$meta_froot, $meta_fname);
+ }
+}
diff --git a/platform/www/inc/Subscriptions/SubscriberRegexBuilder.php b/platform/www/inc/Subscriptions/SubscriberRegexBuilder.php
new file mode 100644
index 0000000..959702a
--- /dev/null
+++ b/platform/www/inc/Subscriptions/SubscriberRegexBuilder.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace dokuwiki\Subscriptions;
+
+use Exception;
+
+class SubscriberRegexBuilder
+{
+
+ /**
+ * Construct a regular expression for parsing a subscription definition line
+ *
+ * @param string|array $user
+ * @param string|array $style
+ * @param string|array $data
+ *
+ * @return string complete regexp including delimiters
+ * @throws Exception when no data is passed
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ */
+ public function buildRegex($user = null, $style = null, $data = null)
+ {
+ // always work with arrays
+ $user = (array)$user;
+ $style = (array)$style;
+ $data = (array)$data;
+
+ // clean
+ $user = array_filter(array_map('trim', $user));
+ $style = array_filter(array_map('trim', $style));
+ $data = array_filter(array_map('trim', $data));
+
+ // user names are encoded
+ $user = array_map('auth_nameencode', $user);
+
+ // quote
+ $user = array_map('preg_quote_cb', $user);
+ $style = array_map('preg_quote_cb', $style);
+ $data = array_map('preg_quote_cb', $data);
+
+ // join
+ $user = join('|', $user);
+ $style = join('|', $style);
+ $data = join('|', $data);
+
+ // any data at all?
+ if ($user . $style . $data === '') {
+ throw new Exception('no data passed');
+ }
+
+ // replace empty values, set which ones are optional
+ $sopt = '';
+ $dopt = '';
+ if ($user === '') {
+ $user = '\S+';
+ }
+ if ($style === '') {
+ $style = '\S+';
+ $sopt = '?';
+ }
+ if ($data === '') {
+ $data = '\S+';
+ $dopt = '?';
+ }
+
+ // assemble
+ return "/^($user)(?:\\s+($style))$sopt(?:\\s+($data))$dopt$/";
+ }
+}
diff --git a/platform/www/inc/Subscriptions/SubscriptionSender.php b/platform/www/inc/Subscriptions/SubscriptionSender.php
new file mode 100644
index 0000000..afc05bf
--- /dev/null
+++ b/platform/www/inc/Subscriptions/SubscriptionSender.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace dokuwiki\Subscriptions;
+
+use Mailer;
+
+abstract class SubscriptionSender
+{
+ protected $mailer;
+
+ public function __construct(Mailer $mailer = null)
+ {
+ if ($mailer === null) {
+ $mailer = new Mailer();
+ }
+ $this->mailer = $mailer;
+ }
+
+ /**
+ * Get a valid message id for a certain $id and revision (or the current revision)
+ *
+ * @param string $id The id of the page (or media file) the message id should be for
+ * @param string $rev The revision of the page, set to the current revision of the page $id if not set
+ *
+ * @return string
+ */
+ protected function getMessageID($id, $rev = null)
+ {
+ static $listid = null;
+ if (is_null($listid)) {
+ $server = parse_url(DOKU_URL, PHP_URL_HOST);
+ $listid = join('.', array_reverse(explode('/', DOKU_BASE))) . $server;
+ $listid = urlencode($listid);
+ $listid = strtolower(trim($listid, '.'));
+ }
+
+ if (is_null($rev)) {
+ $rev = @filemtime(wikiFN($id));
+ }
+
+ return "<$id?rev=$rev@$listid>";
+ }
+
+ /**
+ * Helper function for sending a mail
+ *
+ * @param string $subscriber_mail The target mail address
+ * @param string $subject The lang id of the mail subject (without the
+ * prefix “mail_”)
+ * @param string $context The context of this mail, eg. page or namespace id
+ * @param string $template The name of the mail template
+ * @param array $trep Predefined parameters used to parse the
+ * template (in text format)
+ * @param array $hrep Predefined parameters used to parse the
+ * template (in HTML format), null to default to $trep
+ * @param array $headers Additional mail headers in the form 'name' => 'value'
+ *
+ * @return bool
+ * @author Adrian Lang <lang@cosmocode.de>
+ *
+ */
+ protected function send($subscriber_mail, $subject, $context, $template, $trep, $hrep = null, $headers = [])
+ {
+ global $lang;
+ global $conf;
+
+ $text = rawLocale($template);
+ $subject = $lang['mail_' . $subject] . ' ' . $context;
+ $mail = $this->mailer;
+ $mail->bcc($subscriber_mail);
+ $mail->subject($subject);
+ $mail->setBody($text, $trep, $hrep);
+ if (in_array($template, ['subscr_list', 'subscr_digest'])) {
+ $mail->from($conf['mailfromnobody']);
+ }
+ if (isset($trep['SUBSCRIBE'])) {
+ $mail->setHeader('List-Unsubscribe', '<' . $trep['SUBSCRIBE'] . '>', false);
+ }
+
+ foreach ($headers as $header => $value) {
+ $mail->setHeader($header, $value);
+ }
+
+ return $mail->send();
+ }
+}
diff --git a/platform/www/inc/TaskRunner.php b/platform/www/inc/TaskRunner.php
new file mode 100644
index 0000000..c5de7e3
--- /dev/null
+++ b/platform/www/inc/TaskRunner.php
@@ -0,0 +1,240 @@
+<?php
+
+namespace dokuwiki;
+
+use dokuwiki\Extension\Event;
+use dokuwiki\Sitemap\Mapper;
+use dokuwiki\Subscriptions\BulkSubscriptionSender;
+
+/**
+ * Class TaskRunner
+ *
+ * Run an asynchronous task.
+ */
+class TaskRunner
+{
+ /**
+ * Run the next task
+ *
+ * @todo refactor to remove dependencies on globals
+ * @triggers INDEXER_TASKS_RUN
+ */
+ public function run()
+ {
+ global $INPUT, $conf, $ID;
+
+ // keep running after browser closes connection
+ @ignore_user_abort(true);
+
+ // check if user abort worked, if yes send output early
+ $defer = !@ignore_user_abort() || $conf['broken_iua'];
+ $output = $INPUT->has('debug') && $conf['allowdebug'];
+ if(!$defer && !$output){
+ $this->sendGIF();
+ }
+
+ $ID = cleanID($INPUT->str('id'));
+
+ // Catch any possible output (e.g. errors)
+ if(!$output) {
+ ob_start();
+ } else {
+ header('Content-Type: text/plain');
+ }
+
+ // run one of the jobs
+ $tmp = []; // No event data
+ $evt = new Event('INDEXER_TASKS_RUN', $tmp);
+ if ($evt->advise_before()) {
+ $this->runIndexer() or
+ $this->runSitemapper() or
+ $this->sendDigest() or
+ $this->runTrimRecentChanges() or
+ $this->runTrimRecentChanges(true) or
+ $evt->advise_after();
+ }
+
+ if(!$output) {
+ ob_end_clean();
+ if($defer) {
+ $this->sendGIF();
+ }
+ }
+ }
+
+ /**
+ * Just send a 1x1 pixel blank gif to the browser
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Harry Fuecks <fuecks@gmail.com>
+ */
+ protected function sendGIF()
+ {
+ $img = base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7');
+ header('Content-Type: image/gif');
+ header('Content-Length: '.strlen($img));
+ header('Connection: Close');
+ print $img;
+ tpl_flush();
+ // Browser should drop connection after this
+ // Thinks it's got the whole image
+ }
+
+ /**
+ * Trims the recent changes cache (or imports the old changelog) as needed.
+ *
+ * @param bool $media_changes If the media changelog shall be trimmed instead of
+ * the page changelog
+ *
+ * @return bool
+ * @triggers TASK_RECENTCHANGES_TRIM
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ */
+ protected function runTrimRecentChanges($media_changes = false)
+ {
+ global $conf;
+
+ echo "runTrimRecentChanges($media_changes): started" . NL;
+
+ $fn = ($media_changes ? $conf['media_changelog'] : $conf['changelog']);
+
+ // Trim the Recent Changes
+ // Trims the recent changes cache to the last $conf['changes_days'] recent
+ // changes or $conf['recent'] items, which ever is larger.
+ // The trimming is only done once a day.
+ if (file_exists($fn) &&
+ (@filemtime($fn . '.trimmed') + 86400) < time() &&
+ !file_exists($fn . '_tmp')) {
+ @touch($fn . '.trimmed');
+ io_lock($fn);
+ $lines = file($fn);
+ if (count($lines) <= $conf['recent']) {
+ // nothing to trim
+ io_unlock($fn);
+ echo "runTrimRecentChanges($media_changes): finished" . NL;
+ return false;
+ }
+
+ io_saveFile($fn . '_tmp', ''); // presave tmp as 2nd lock
+ $trim_time = time() - $conf['recent_days'] * 86400;
+ $out_lines = [];
+ $old_lines = [];
+ for ($i = 0; $i < count($lines); $i++) {
+ $log = parseChangelogLine($lines[$i]);
+ if ($log === false) {
+ continue; // discard junk
+ }
+
+ if ($log['date'] < $trim_time) {
+ // keep old lines for now (append .$i to prevent key collisions)
+ $old_lines[$log['date'] . ".$i"] = $lines[$i];
+ } else {
+ // definitely keep these lines
+ $out_lines[$log['date'] . ".$i"] = $lines[$i];
+ }
+ }
+
+ if (count($lines) == count($out_lines)) {
+ // nothing to trim
+ @unlink($fn . '_tmp');
+ io_unlock($fn);
+ echo "runTrimRecentChanges($media_changes): finished" . NL;
+ return false;
+ }
+
+ // sort the final result, it shouldn't be necessary,
+ // however the extra robustness in making the changelog cache self-correcting is worth it
+ ksort($out_lines);
+ $extra = $conf['recent'] - count($out_lines); // do we need extra lines do bring us up to minimum
+ if ($extra > 0) {
+ ksort($old_lines);
+ $out_lines = array_merge(array_slice($old_lines, -$extra), $out_lines);
+ }
+
+ $eventData = [
+ 'isMedia' => $media_changes,
+ 'trimmedChangelogLines' => $out_lines,
+ 'removedChangelogLines' => $extra > 0 ? array_slice($old_lines, 0, -$extra) : $old_lines,
+ ];
+ Event::createAndTrigger('TASK_RECENTCHANGES_TRIM', $eventData);
+ $out_lines = $eventData['trimmedChangelogLines'];
+
+ // save trimmed changelog
+ io_saveFile($fn . '_tmp', implode('', $out_lines));
+ @unlink($fn);
+ if (!rename($fn . '_tmp', $fn)) {
+ // rename failed so try another way...
+ io_unlock($fn);
+ io_saveFile($fn, implode('', $out_lines));
+ @unlink($fn . '_tmp');
+ } else {
+ io_unlock($fn);
+ }
+ echo "runTrimRecentChanges($media_changes): finished" . NL;
+ return true;
+ }
+
+ // nothing done
+ echo "runTrimRecentChanges($media_changes): finished" . NL;
+ return false;
+ }
+
+
+ /**
+ * Runs the indexer for the current page
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function runIndexer()
+ {
+ global $ID;
+ print 'runIndexer(): started' . NL;
+
+ if ((string) $ID === '') {
+ return false;
+ }
+
+ // do the work
+ return idx_addPage($ID, true);
+ }
+
+ /**
+ * Builds a Google Sitemap of all public pages known to the indexer
+ *
+ * The map is placed in the root directory named sitemap.xml.gz - This
+ * file needs to be writable!
+ *
+ * @author Andreas Gohr
+ * @link https://www.google.com/webmasters/sitemaps/docs/en/about.html
+ */
+ protected function runSitemapper()
+ {
+ print 'runSitemapper(): started' . NL;
+ $result = Mapper::generate() && Mapper::pingSearchEngines();
+ print 'runSitemapper(): finished' . NL;
+ return $result;
+ }
+
+ /**
+ * Send digest and list mails for all subscriptions which are in effect for the
+ * current page
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+ protected function sendDigest()
+ {
+ global $ID;
+
+ echo 'sendDigest(): started' . NL;
+ if (!actionOK('subscribe')) {
+ echo 'sendDigest(): disabled' . NL;
+ return false;
+ }
+ $sub = new BulkSubscriptionSender();
+ $sent = $sub->sendBulk($ID);
+
+ echo "sendDigest(): sent $sent mails" . NL;
+ echo 'sendDigest(): finished' . NL;
+ return (bool)$sent;
+ }
+}
diff --git a/platform/www/inc/Ui/Admin.php b/platform/www/inc/Ui/Admin.php
new file mode 100644
index 0000000..fe319d4
--- /dev/null
+++ b/platform/www/inc/Ui/Admin.php
@@ -0,0 +1,167 @@
+<?php
+namespace dokuwiki\Ui;
+
+/**
+ * Class Admin
+ *
+ * Displays the Admin screen
+ *
+ * @package dokuwiki\Ui
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Håkan Sandell <hakan.sandell@home.se>
+ */
+class Admin extends Ui {
+
+ protected $forAdmins = array('usermanager', 'acl', 'extension', 'config', 'styling');
+ protected $forManagers = array('revert', 'popularity');
+ /** @var array[] */
+ protected $menu;
+
+ /**
+ * Display the UI element
+ *
+ * @return void
+ */
+ public function show() {
+ $this->menu = $this->getPluginList();
+ echo '<div class="ui-admin">';
+ echo p_locale_xhtml('admin');
+ $this->showSecurityCheck();
+ $this->showMenu('admin');
+ $this->showMenu('manager');
+ $this->showVersion();
+ $this->showMenu('other');
+ echo '</div>';
+ }
+
+ /**
+ * Show the given menu of available plugins
+ *
+ * @param string $type admin|manager|other
+ */
+ protected function showMenu($type) {
+ if (!$this->menu[$type]) return;
+
+ if ($type === 'other') {
+ echo p_locale_xhtml('adminplugins');
+ $class = 'admin_plugins';
+ } else {
+ $class = 'admin_tasks';
+ }
+
+ echo "<ul class=\"$class\">";
+ foreach ($this->menu[$type] as $item) {
+ $this->showMenuItem($item);
+ }
+ echo '</ul>';
+ }
+
+ /**
+ * Display the DokuWiki version
+ */
+ protected function showVersion() {
+ echo '<div id="admin__version">';
+ echo getVersion();
+ echo '</div>';
+ }
+
+ /**
+ * data security check
+ *
+ * simple check if the 'savedir' is relative and accessible when appended to DOKU_URL
+ *
+ * it verifies either:
+ * 'savedir' has been moved elsewhere, or
+ * has protection to prevent the webserver serving files from it
+ */
+ protected function showSecurityCheck() {
+ global $conf;
+ if(substr($conf['savedir'], 0, 2) !== './') return;
+ $img = DOKU_URL . $conf['savedir'] .
+ '/dont-panic-if-you-see-this-in-your-logs-it-means-your-directory-permissions-are-correct.png';
+ echo '<a style="border:none; float:right;"
+ href="http://www.dokuwiki.org/security#web_access_security">
+ <img src="' . $img . '" alt="Your data directory seems to be protected properly."
+ onerror="this.parentNode.style.display=\'none\'" /></a>';
+ }
+
+ /**
+ * Display a single Admin menu item
+ *
+ * @param array $item
+ */
+ protected function showMenuItem($item) {
+ global $ID;
+ if(blank($item['prompt'])) return;
+ echo '<li><div class="li">';
+ echo '<a href="' . wl($ID, 'do=admin&amp;page=' . $item['plugin']) . '">';
+ echo '<span class="icon">';
+ echo inlineSVG($item['icon']);
+ echo '</span>';
+ echo '<span class="prompt">';
+ echo $item['prompt'];
+ echo '</span>';
+ echo '</a>';
+ echo '</div></li>';
+ }
+
+ /**
+ * Build list of admin functions from the plugins that handle them
+ *
+ * Checks the current permissions to decide on manager or admin plugins
+ *
+ * @return array list of plugins with their properties
+ */
+ protected function getPluginList() {
+ global $conf;
+
+ $pluginlist = plugin_list('admin');
+ $menu = ['admin' => [], 'manager' => [], 'other' => []];
+
+ foreach($pluginlist as $p) {
+ /** @var \dokuwiki\Extension\AdminPlugin $obj */
+ if(($obj = plugin_load('admin', $p)) === null) continue;
+
+ // check permissions
+ if (!$obj->isAccessibleByCurrentUser()) continue;
+
+ if (in_array($p, $this->forAdmins, true)) {
+ $type = 'admin';
+ } elseif (in_array($p, $this->forManagers, true)){
+ $type = 'manager';
+ } else {
+ $type = 'other';
+ }
+
+ $menu[$type][$p] = array(
+ 'plugin' => $p,
+ 'prompt' => $obj->getMenuText($conf['lang']),
+ 'icon' => $obj->getMenuIcon(),
+ 'sort' => $obj->getMenuSort(),
+ );
+ }
+
+ // sort by name, then sort
+ uasort($menu['admin'], [$this, 'menuSort']);
+ uasort($menu['manager'], [$this, 'menuSort']);
+ uasort($menu['other'], [$this, 'menuSort']);
+
+ return $menu;
+ }
+
+ /**
+ * Custom sorting for admin menu
+ *
+ * We sort alphabetically first, then by sort value
+ *
+ * @param array $a
+ * @param array $b
+ * @return int
+ */
+ protected function menuSort($a, $b) {
+ $strcmp = strcasecmp($a['prompt'], $b['prompt']);
+ if($strcmp != 0) return $strcmp;
+ if($a['sort'] === $b['sort']) return 0;
+ return ($a['sort'] < $b['sort']) ? -1 : 1;
+ }
+}
diff --git a/platform/www/inc/Ui/Search.php b/platform/www/inc/Ui/Search.php
new file mode 100644
index 0000000..e4eef67
--- /dev/null
+++ b/platform/www/inc/Ui/Search.php
@@ -0,0 +1,647 @@
+<?php
+
+namespace dokuwiki\Ui;
+
+use dokuwiki\Extension\Event;
+use dokuwiki\Form\Form;
+
+class Search extends Ui
+{
+ protected $query;
+ protected $parsedQuery;
+ protected $searchState;
+ protected $pageLookupResults = array();
+ protected $fullTextResults = array();
+ protected $highlight = array();
+
+ /**
+ * Search constructor.
+ *
+ * @param array $pageLookupResults pagename lookup results in the form [pagename => pagetitle]
+ * @param array $fullTextResults fulltext search results in the form [pagename => #hits]
+ * @param array $highlight array of strings to be highlighted
+ */
+ public function __construct(array $pageLookupResults, array $fullTextResults, $highlight)
+ {
+ global $QUERY;
+ $Indexer = idx_get_indexer();
+
+ $this->query = $QUERY;
+ $this->parsedQuery = ft_queryParser($Indexer, $QUERY);
+ $this->searchState = new SearchState($this->parsedQuery);
+
+ $this->pageLookupResults = $pageLookupResults;
+ $this->fullTextResults = $fullTextResults;
+ $this->highlight = $highlight;
+ }
+
+ /**
+ * display the search result
+ *
+ * @return void
+ */
+ public function show()
+ {
+ $searchHTML = '';
+
+ $searchHTML .= $this->getSearchIntroHTML($this->query);
+
+ $searchHTML .= $this->getSearchFormHTML($this->query);
+
+ $searchHTML .= $this->getPageLookupHTML($this->pageLookupResults);
+
+ $searchHTML .= $this->getFulltextResultsHTML($this->fullTextResults, $this->highlight);
+
+ echo $searchHTML;
+ }
+
+ /**
+ * Get a form which can be used to adjust/refine the search
+ *
+ * @param string $query
+ *
+ * @return string
+ */
+ protected function getSearchFormHTML($query)
+ {
+ global $lang, $ID, $INPUT;
+
+ $searchForm = (new Form(['method' => 'get'], true))->addClass('search-results-form');
+ $searchForm->setHiddenField('do', 'search');
+ $searchForm->setHiddenField('id', $ID);
+ $searchForm->setHiddenField('sf', '1');
+ if ($INPUT->has('min')) {
+ $searchForm->setHiddenField('min', $INPUT->str('min'));
+ }
+ if ($INPUT->has('max')) {
+ $searchForm->setHiddenField('max', $INPUT->str('max'));
+ }
+ if ($INPUT->has('srt')) {
+ $searchForm->setHiddenField('srt', $INPUT->str('srt'));
+ }
+ $searchForm->addFieldsetOpen()->addClass('search-form');
+ $searchForm->addTextInput('q')->val($query)->useInput(false);
+ $searchForm->addButton('', $lang['btn_search'])->attr('type', 'submit');
+
+ $this->addSearchAssistanceElements($searchForm);
+
+ $searchForm->addFieldsetClose();
+
+ Event::createAndTrigger('FORM_SEARCH_OUTPUT', $searchForm);
+
+ return $searchForm->toHTML();
+ }
+
+ /**
+ * Add elements to adjust how the results are sorted
+ *
+ * @param Form $searchForm
+ */
+ protected function addSortTool(Form $searchForm)
+ {
+ global $INPUT, $lang;
+
+ $options = [
+ 'hits' => [
+ 'label' => $lang['search_sort_by_hits'],
+ 'sort' => '',
+ ],
+ 'mtime' => [
+ 'label' => $lang['search_sort_by_mtime'],
+ 'sort' => 'mtime',
+ ],
+ ];
+ $activeOption = 'hits';
+
+ if ($INPUT->str('srt') === 'mtime') {
+ $activeOption = 'mtime';
+ }
+
+ $searchForm->addTagOpen('div')->addClass('toggle')->attr('aria-haspopup', 'true');
+ // render current
+ $currentWrapper = $searchForm->addTagOpen('div')->addClass('current');
+ if ($activeOption !== 'hits') {
+ $currentWrapper->addClass('changed');
+ }
+ $searchForm->addHTML($options[$activeOption]['label']);
+ $searchForm->addTagClose('div');
+
+ // render options list
+ $searchForm->addTagOpen('ul')->attr('aria-expanded', 'false');
+
+ foreach ($options as $key => $option) {
+ $listItem = $searchForm->addTagOpen('li');
+
+ if ($key === $activeOption) {
+ $listItem->addClass('active');
+ $searchForm->addHTML($option['label']);
+ } else {
+ $link = $this->searchState->withSorting($option['sort'])->getSearchLink($option['label']);
+ $searchForm->addHTML($link);
+ }
+ $searchForm->addTagClose('li');
+ }
+ $searchForm->addTagClose('ul');
+
+ $searchForm->addTagClose('div');
+
+ }
+
+ /**
+ * Check if the query is simple enough to modify its namespace limitations without breaking the rest of the query
+ *
+ * @param array $parsedQuery
+ *
+ * @return bool
+ */
+ protected function isNamespaceAssistanceAvailable(array $parsedQuery) {
+ if (preg_match('/[\(\)\|]/', $parsedQuery['query']) === 1) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if the query is simple enough to modify the fragment search behavior without breaking the rest of the query
+ *
+ * @param array $parsedQuery
+ *
+ * @return bool
+ */
+ protected function isFragmentAssistanceAvailable(array $parsedQuery) {
+ if (preg_match('/[\(\)\|]/', $parsedQuery['query']) === 1) {
+ return false;
+ }
+
+ if (!empty($parsedQuery['phrases'])) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Add the elements to be used for search assistance
+ *
+ * @param Form $searchForm
+ */
+ protected function addSearchAssistanceElements(Form $searchForm)
+ {
+ $searchForm->addTagOpen('div')
+ ->addClass('advancedOptions')
+ ->attr('style', 'display: none;')
+ ->attr('aria-hidden', 'true');
+
+ $this->addFragmentBehaviorLinks($searchForm);
+ $this->addNamespaceSelector($searchForm);
+ $this->addDateSelector($searchForm);
+ $this->addSortTool($searchForm);
+
+ $searchForm->addTagClose('div');
+ }
+
+ /**
+ * Add the elements to adjust the fragment search behavior
+ *
+ * @param Form $searchForm
+ */
+ protected function addFragmentBehaviorLinks(Form $searchForm)
+ {
+ if (!$this->isFragmentAssistanceAvailable($this->parsedQuery)) {
+ return;
+ }
+ global $lang;
+
+ $options = [
+ 'exact' => [
+ 'label' => $lang['search_exact_match'],
+ 'and' => array_map(function ($term) {
+ return trim($term, '*');
+ }, $this->parsedQuery['and']),
+ 'not' => array_map(function ($term) {
+ return trim($term, '*');
+ }, $this->parsedQuery['not']),
+ ],
+ 'starts' => [
+ 'label' => $lang['search_starts_with'],
+ 'and' => array_map(function ($term) {
+ return trim($term, '*') . '*';
+ }, $this->parsedQuery['and']),
+ 'not' => array_map(function ($term) {
+ return trim($term, '*') . '*';
+ }, $this->parsedQuery['not']),
+ ],
+ 'ends' => [
+ 'label' => $lang['search_ends_with'],
+ 'and' => array_map(function ($term) {
+ return '*' . trim($term, '*');
+ }, $this->parsedQuery['and']),
+ 'not' => array_map(function ($term) {
+ return '*' . trim($term, '*');
+ }, $this->parsedQuery['not']),
+ ],
+ 'contains' => [
+ 'label' => $lang['search_contains'],
+ 'and' => array_map(function ($term) {
+ return '*' . trim($term, '*') . '*';
+ }, $this->parsedQuery['and']),
+ 'not' => array_map(function ($term) {
+ return '*' . trim($term, '*') . '*';
+ }, $this->parsedQuery['not']),
+ ]
+ ];
+
+ // detect current
+ $activeOption = 'custom';
+ foreach ($options as $key => $option) {
+ if ($this->parsedQuery['and'] === $option['and']) {
+ $activeOption = $key;
+ }
+ }
+ if ($activeOption === 'custom') {
+ $options = array_merge(['custom' => [
+ 'label' => $lang['search_custom_match'],
+ ]], $options);
+ }
+
+ $searchForm->addTagOpen('div')->addClass('toggle')->attr('aria-haspopup', 'true');
+ // render current
+ $currentWrapper = $searchForm->addTagOpen('div')->addClass('current');
+ if ($activeOption !== 'exact') {
+ $currentWrapper->addClass('changed');
+ }
+ $searchForm->addHTML($options[$activeOption]['label']);
+ $searchForm->addTagClose('div');
+
+ // render options list
+ $searchForm->addTagOpen('ul')->attr('aria-expanded', 'false');
+
+ foreach ($options as $key => $option) {
+ $listItem = $searchForm->addTagOpen('li');
+
+ if ($key === $activeOption) {
+ $listItem->addClass('active');
+ $searchForm->addHTML($option['label']);
+ } else {
+ $link = $this->searchState
+ ->withFragments($option['and'], $option['not'])
+ ->getSearchLink($option['label'])
+ ;
+ $searchForm->addHTML($link);
+ }
+ $searchForm->addTagClose('li');
+ }
+ $searchForm->addTagClose('ul');
+
+ $searchForm->addTagClose('div');
+
+ // render options list
+ }
+
+ /**
+ * Add the elements for the namespace selector
+ *
+ * @param Form $searchForm
+ */
+ protected function addNamespaceSelector(Form $searchForm)
+ {
+ if (!$this->isNamespaceAssistanceAvailable($this->parsedQuery)) {
+ return;
+ }
+
+ global $lang;
+
+ $baseNS = empty($this->parsedQuery['ns']) ? '' : $this->parsedQuery['ns'][0];
+ $extraNS = $this->getAdditionalNamespacesFromResults($baseNS);
+
+ $searchForm->addTagOpen('div')->addClass('toggle')->attr('aria-haspopup', 'true');
+ // render current
+ $currentWrapper = $searchForm->addTagOpen('div')->addClass('current');
+ if ($baseNS) {
+ $currentWrapper->addClass('changed');
+ $searchForm->addHTML('@' . $baseNS);
+ } else {
+ $searchForm->addHTML($lang['search_any_ns']);
+ }
+ $searchForm->addTagClose('div');
+
+ // render options list
+ $searchForm->addTagOpen('ul')->attr('aria-expanded', 'false');
+
+ $listItem = $searchForm->addTagOpen('li');
+ if ($baseNS) {
+ $listItem->addClass('active');
+ $link = $this->searchState->withNamespace('')->getSearchLink($lang['search_any_ns']);
+ $searchForm->addHTML($link);
+ } else {
+ $searchForm->addHTML($lang['search_any_ns']);
+ }
+ $searchForm->addTagClose('li');
+
+ foreach ($extraNS as $ns => $count) {
+ $listItem = $searchForm->addTagOpen('li');
+ $label = $ns . ($count ? " <bdi>($count)</bdi>" : '');
+
+ if ($ns === $baseNS) {
+ $listItem->addClass('active');
+ $searchForm->addHTML($label);
+ } else {
+ $link = $this->searchState->withNamespace($ns)->getSearchLink($label);
+ $searchForm->addHTML($link);
+ }
+ $searchForm->addTagClose('li');
+ }
+ $searchForm->addTagClose('ul');
+
+ $searchForm->addTagClose('div');
+
+ }
+
+ /**
+ * Parse the full text results for their top namespaces below the given base namespace
+ *
+ * @param string $baseNS the namespace within which was searched, empty string for root namespace
+ *
+ * @return array an associative array with namespace => #number of found pages, sorted descending
+ */
+ protected function getAdditionalNamespacesFromResults($baseNS)
+ {
+ $namespaces = [];
+ $baseNSLength = strlen($baseNS);
+ foreach ($this->fullTextResults as $page => $numberOfHits) {
+ $namespace = getNS($page);
+ if (!$namespace) {
+ continue;
+ }
+ if ($namespace === $baseNS) {
+ continue;
+ }
+ $firstColon = strpos((string)$namespace, ':', $baseNSLength + 1) ?: strlen($namespace);
+ $subtopNS = substr($namespace, 0, $firstColon);
+ if (empty($namespaces[$subtopNS])) {
+ $namespaces[$subtopNS] = 0;
+ }
+ $namespaces[$subtopNS] += 1;
+ }
+ ksort($namespaces);
+ arsort($namespaces);
+ return $namespaces;
+ }
+
+ /**
+ * @ToDo: custom date input
+ *
+ * @param Form $searchForm
+ */
+ protected function addDateSelector(Form $searchForm)
+ {
+ global $INPUT, $lang;
+
+ $options = [
+ 'any' => [
+ 'before' => false,
+ 'after' => false,
+ 'label' => $lang['search_any_time'],
+ ],
+ 'week' => [
+ 'before' => false,
+ 'after' => '1 week ago',
+ 'label' => $lang['search_past_7_days'],
+ ],
+ 'month' => [
+ 'before' => false,
+ 'after' => '1 month ago',
+ 'label' => $lang['search_past_month'],
+ ],
+ 'year' => [
+ 'before' => false,
+ 'after' => '1 year ago',
+ 'label' => $lang['search_past_year'],
+ ],
+ ];
+ $activeOption = 'any';
+ foreach ($options as $key => $option) {
+ if ($INPUT->str('min') === $option['after']) {
+ $activeOption = $key;
+ break;
+ }
+ }
+
+ $searchForm->addTagOpen('div')->addClass('toggle')->attr('aria-haspopup', 'true');
+ // render current
+ $currentWrapper = $searchForm->addTagOpen('div')->addClass('current');
+ if ($INPUT->has('max') || $INPUT->has('min')) {
+ $currentWrapper->addClass('changed');
+ }
+ $searchForm->addHTML($options[$activeOption]['label']);
+ $searchForm->addTagClose('div');
+
+ // render options list
+ $searchForm->addTagOpen('ul')->attr('aria-expanded', 'false');
+
+ foreach ($options as $key => $option) {
+ $listItem = $searchForm->addTagOpen('li');
+
+ if ($key === $activeOption) {
+ $listItem->addClass('active');
+ $searchForm->addHTML($option['label']);
+ } else {
+ $link = $this->searchState
+ ->withTimeLimitations($option['after'], $option['before'])
+ ->getSearchLink($option['label'])
+ ;
+ $searchForm->addHTML($link);
+ }
+ $searchForm->addTagClose('li');
+ }
+ $searchForm->addTagClose('ul');
+
+ $searchForm->addTagClose('div');
+ }
+
+
+ /**
+ * Build the intro text for the search page
+ *
+ * @param string $query the search query
+ *
+ * @return string
+ */
+ protected function getSearchIntroHTML($query)
+ {
+ global $lang;
+
+ $intro = p_locale_xhtml('searchpage');
+
+ $queryPagename = $this->createPagenameFromQuery($this->parsedQuery);
+ $createQueryPageLink = html_wikilink($queryPagename . '?do=edit', $queryPagename);
+
+ $pagecreateinfo = '';
+ if (auth_quickaclcheck($queryPagename) >= AUTH_CREATE) {
+ $pagecreateinfo = sprintf($lang['searchcreatepage'], $createQueryPageLink);
+ }
+ $intro = str_replace(
+ array('@QUERY@', '@SEARCH@', '@CREATEPAGEINFO@'),
+ array(hsc(rawurlencode($query)), hsc($query), $pagecreateinfo),
+ $intro
+ );
+
+ return $intro;
+ }
+
+ /**
+ * Create a pagename based the parsed search query
+ *
+ * @param array $parsedQuery
+ *
+ * @return string pagename constructed from the parsed query
+ */
+ public function createPagenameFromQuery($parsedQuery)
+ {
+ $cleanedQuery = cleanID($parsedQuery['query']); // already strtolowered
+ if ($cleanedQuery === \dokuwiki\Utf8\PhpString::strtolower($parsedQuery['query'])) {
+ return ':' . $cleanedQuery;
+ }
+ $pagename = '';
+ if (!empty($parsedQuery['ns'])) {
+ $pagename .= ':' . cleanID($parsedQuery['ns'][0]);
+ }
+ $pagename .= ':' . cleanID(implode(' ' , $parsedQuery['highlight']));
+ return $pagename;
+ }
+
+ /**
+ * Build HTML for a list of pages with matching pagenames
+ *
+ * @param array $data search results
+ *
+ * @return string
+ */
+ protected function getPageLookupHTML($data)
+ {
+ if (empty($data)) {
+ return '';
+ }
+
+ global $lang;
+
+ $html = '<div class="search_quickresult">';
+ $html .= '<h2>' . $lang['quickhits'] . ':</h2>';
+ $html .= '<ul class="search_quickhits">';
+ foreach ($data as $id => $title) {
+ $name = null;
+ if (!useHeading('navigation') && $ns = getNS($id)) {
+ $name = shorten(noNS($id), ' (' . $ns . ')', 30);
+ }
+ $link = html_wikilink(':' . $id, $name);
+ $eventData = [
+ 'listItemContent' => [$link],
+ 'page' => $id,
+ ];
+ Event::createAndTrigger('SEARCH_RESULT_PAGELOOKUP', $eventData);
+ $html .= '<li>' . implode('', $eventData['listItemContent']) . '</li>';
+ }
+ $html .= '</ul> ';
+ //clear float (see http://www.complexspiral.com/publications/containing-floats/)
+ $html .= '<div class="clearer"></div>';
+ $html .= '</div>';
+
+ return $html;
+ }
+
+ /**
+ * Build HTML for fulltext search results or "no results" message
+ *
+ * @param array $data the results of the fulltext search
+ * @param array $highlight the terms to be highlighted in the results
+ *
+ * @return string
+ */
+ protected function getFulltextResultsHTML($data, $highlight)
+ {
+ global $lang;
+
+ if (empty($data)) {
+ return '<div class="nothing">' . $lang['nothingfound'] . '</div>';
+ }
+
+ $html = '<div class="search_fulltextresult">';
+ $html .= '<h2>' . $lang['search_fullresults'] . ':</h2>';
+
+ $html .= '<dl class="search_results">';
+ $num = 0;
+ $position = 0;
+
+ foreach ($data as $id => $cnt) {
+ $position += 1;
+ $resultLink = html_wikilink(':' . $id, null, $highlight);
+
+ $resultHeader = [$resultLink];
+
+
+ $restrictQueryToNSLink = $this->restrictQueryToNSLink(getNS($id));
+ if ($restrictQueryToNSLink) {
+ $resultHeader[] = $restrictQueryToNSLink;
+ }
+
+ $resultBody = [];
+ $mtime = filemtime(wikiFN($id));
+ $lastMod = '<span class="lastmod">' . $lang['lastmod'] . '</span> ';
+ $lastMod .= '<time datetime="' . date_iso8601($mtime) . '" title="' . dformat($mtime) . '">' .
+ dformat($mtime, '%f') .
+ '</time>';
+ $resultBody['meta'] = $lastMod;
+ if ($cnt !== 0) {
+ $num++;
+ $hits = '<span class="hits">' . $cnt . ' ' . $lang['hits'] . '</span>, ';
+ $resultBody['meta'] = $hits . $resultBody['meta'];
+ if ($num <= FT_SNIPPET_NUMBER) { // create snippets for the first number of matches only
+ $resultBody['snippet'] = ft_snippet($id, $highlight);
+ }
+ }
+
+ $eventData = [
+ 'resultHeader' => $resultHeader,
+ 'resultBody' => $resultBody,
+ 'page' => $id,
+ 'position' => $position,
+ ];
+ Event::createAndTrigger('SEARCH_RESULT_FULLPAGE', $eventData);
+ $html .= '<div class="search_fullpage_result">';
+ $html .= '<dt>' . implode(' ', $eventData['resultHeader']) . '</dt>';
+ foreach ($eventData['resultBody'] as $class => $htmlContent) {
+ $html .= "<dd class=\"$class\">$htmlContent</dd>";
+ }
+ $html .= '</div>';
+ }
+ $html .= '</dl>';
+
+ $html .= '</div>';
+
+ return $html;
+ }
+
+ /**
+ * create a link to restrict the current query to a namespace
+ *
+ * @param false|string $ns the namespace to which to restrict the query
+ *
+ * @return false|string
+ */
+ protected function restrictQueryToNSLink($ns)
+ {
+ if (!$ns) {
+ return false;
+ }
+ if (!$this->isNamespaceAssistanceAvailable($this->parsedQuery)) {
+ return false;
+ }
+ if (!empty($this->parsedQuery['ns']) && $this->parsedQuery['ns'][0] === $ns) {
+ return false;
+ }
+
+ $name = '@' . $ns;
+ return $this->searchState->withNamespace($ns)->getSearchLink($name);
+ }
+}
diff --git a/platform/www/inc/Ui/SearchState.php b/platform/www/inc/Ui/SearchState.php
new file mode 100644
index 0000000..eb3f7fa
--- /dev/null
+++ b/platform/www/inc/Ui/SearchState.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace dokuwiki\Ui;
+
+class SearchState
+{
+ /**
+ * @var array
+ */
+ protected $parsedQuery = [];
+
+ /**
+ * SearchState constructor.
+ *
+ * @param array $parsedQuery
+ */
+ public function __construct(array $parsedQuery)
+ {
+ global $INPUT;
+
+ $this->parsedQuery = $parsedQuery;
+ if (!isset($parsedQuery['after'])) {
+ $this->parsedQuery['after'] = $INPUT->str('min');
+ }
+ if (!isset($parsedQuery['before'])) {
+ $this->parsedQuery['before'] = $INPUT->str('max');
+ }
+ if (!isset($parsedQuery['sort'])) {
+ $this->parsedQuery['sort'] = $INPUT->str('srt');
+ }
+ }
+
+ /**
+ * Get a search state for the current search limited to a new namespace
+ *
+ * @param string $ns the namespace to which to limit the search, falsy to remove the limitation
+ * @param array $notns
+ *
+ * @return SearchState
+ */
+ public function withNamespace($ns, array $notns = [])
+ {
+ $parsedQuery = $this->parsedQuery;
+ $parsedQuery['ns'] = $ns ? [$ns] : [];
+ $parsedQuery['notns'] = $notns;
+
+ return new SearchState($parsedQuery);
+ }
+
+ /**
+ * Get a search state for the current search with new search fragments and optionally phrases
+ *
+ * @param array $and
+ * @param array $not
+ * @param array $phrases
+ *
+ * @return SearchState
+ */
+ public function withFragments(array $and, array $not, array $phrases = [])
+ {
+ $parsedQuery = $this->parsedQuery;
+ $parsedQuery['and'] = $and;
+ $parsedQuery['not'] = $not;
+ $parsedQuery['phrases'] = $phrases;
+
+ return new SearchState($parsedQuery);
+ }
+
+ /**
+ * Get a search state for the current search with with adjusted time limitations
+ *
+ * @param $after
+ * @param $before
+ *
+ * @return SearchState
+ */
+ public function withTimeLimitations($after, $before)
+ {
+ $parsedQuery = $this->parsedQuery;
+ $parsedQuery['after'] = $after;
+ $parsedQuery['before'] = $before;
+
+ return new SearchState($parsedQuery);
+ }
+
+ /**
+ * Get a search state for the current search with adjusted sort preference
+ *
+ * @param $sort
+ *
+ * @return SearchState
+ */
+ public function withSorting($sort)
+ {
+ $parsedQuery = $this->parsedQuery;
+ $parsedQuery['sort'] = $sort;
+
+ return new SearchState($parsedQuery);
+ }
+
+ /**
+ * Get a link that represents the current search state
+ *
+ * Note that this represents only a simplified version of the search state.
+ * Grouping with braces and "OR" conditions are not supported.
+ *
+ * @param $label
+ *
+ * @return string
+ */
+ public function getSearchLink($label)
+ {
+ global $ID, $conf;
+ $parsedQuery = $this->parsedQuery;
+
+ $tagAttributes = [
+ 'target' => $conf['target']['wiki'],
+ ];
+
+ $newQuery = ft_queryUnparser_simple(
+ $parsedQuery['and'],
+ $parsedQuery['not'],
+ $parsedQuery['phrases'],
+ $parsedQuery['ns'],
+ $parsedQuery['notns']
+ );
+ $hrefAttributes = ['do' => 'search', 'sf' => '1', 'q' => $newQuery];
+ if ($parsedQuery['after']) {
+ $hrefAttributes['min'] = $parsedQuery['after'];
+ }
+ if ($parsedQuery['before']) {
+ $hrefAttributes['max'] = $parsedQuery['before'];
+ }
+ if ($parsedQuery['sort']) {
+ $hrefAttributes['srt'] = $parsedQuery['sort'];
+ }
+
+ $href = wl($ID, $hrefAttributes, false, '&');
+ return "<a href='$href' " . buildAttributes($tagAttributes, true) . ">$label</a>";
+ }
+}
diff --git a/platform/www/inc/Ui/Ui.php b/platform/www/inc/Ui/Ui.php
new file mode 100644
index 0000000..8aac0de
--- /dev/null
+++ b/platform/www/inc/Ui/Ui.php
@@ -0,0 +1,20 @@
+<?php
+namespace dokuwiki\Ui;
+
+/**
+ * Class Ui
+ *
+ * Abstract base class for all DokuWiki screens
+ *
+ * @package dokuwiki\Ui
+ */
+abstract class Ui {
+
+ /**
+ * Display the UI element
+ *
+ * @return void
+ */
+ abstract public function show();
+
+}
diff --git a/platform/www/inc/Utf8/Asian.php b/platform/www/inc/Utf8/Asian.php
new file mode 100644
index 0000000..c7baa30
--- /dev/null
+++ b/platform/www/inc/Utf8/Asian.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace dokuwiki\Utf8;
+
+/**
+ * Methods and constants to handle Asian "words"
+ *
+ * This uses a crude regexp to determine which parts of an Asian string should be treated as words.
+ * This is necessary because in some Asian languages a single unicode char represents a whole idea
+ * without spaces separating them.
+ */
+class Asian
+{
+
+ /**
+ * This defines a non-capturing group for the use in regular expressions to match any asian character that
+ * needs to be treated as a word. Uses the Unicode-Ranges for Asian characters taken from
+ * http://en.wikipedia.org/wiki/Unicode_block
+ */
+ const REGEXP =
+ '(?:' .
+
+ '[\x{0E00}-\x{0E7F}]' . // Thai
+
+ '|' .
+
+ '[' .
+ '\x{2E80}-\x{3040}' . // CJK -> Hangul
+ '\x{309D}-\x{30A0}' .
+ '\x{30FD}-\x{31EF}\x{3200}-\x{D7AF}' .
+ '\x{F900}-\x{FAFF}' . // CJK Compatibility Ideographs
+ '\x{FE30}-\x{FE4F}' . // CJK Compatibility Forms
+ "\xF0\xA0\x80\x80-\xF0\xAA\x9B\x9F" . // CJK Extension B
+ "\xF0\xAA\x9C\x80-\xF0\xAB\x9C\xBF" . // CJK Extension C
+ "\xF0\xAB\x9D\x80-\xF0\xAB\xA0\x9F" . // CJK Extension D
+ "\xF0\xAF\xA0\x80-\xF0\xAF\xAB\xBF" . // CJK Compatibility Supplement
+ ']' .
+
+ '|' .
+
+ '[' . // Hiragana/Katakana (can be two characters)
+ '\x{3042}\x{3044}\x{3046}\x{3048}' .
+ '\x{304A}-\x{3062}\x{3064}-\x{3082}' .
+ '\x{3084}\x{3086}\x{3088}-\x{308D}' .
+ '\x{308F}-\x{3094}' .
+ '\x{30A2}\x{30A4}\x{30A6}\x{30A8}' .
+ '\x{30AA}-\x{30C2}\x{30C4}-\x{30E2}' .
+ '\x{30E4}\x{30E6}\x{30E8}-\x{30ED}' .
+ '\x{30EF}-\x{30F4}\x{30F7}-\x{30FA}' .
+ '][' .
+ '\x{3041}\x{3043}\x{3045}\x{3047}\x{3049}' .
+ '\x{3063}\x{3083}\x{3085}\x{3087}\x{308E}\x{3095}-\x{309C}' .
+ '\x{30A1}\x{30A3}\x{30A5}\x{30A7}\x{30A9}' .
+ '\x{30C3}\x{30E3}\x{30E5}\x{30E7}\x{30EE}\x{30F5}\x{30F6}\x{30FB}\x{30FC}' .
+ '\x{31F0}-\x{31FF}' .
+ ']?' .
+ ')';
+
+
+ /**
+ * Check if the given term contains Asian word characters
+ *
+ * @param string $term
+ * @return bool
+ */
+ public static function isAsianWords($term)
+ {
+ return (bool)preg_match('/' . self::REGEXP . '/u', $term);
+ }
+
+ /**
+ * Surround all Asian words in the given text with the given separator
+ *
+ * @param string $text Original text containing asian words
+ * @param string $sep the separator to use
+ * @return string Text with separated asian words
+ */
+ public static function separateAsianWords($text, $sep = ' ')
+ {
+ // handle asian chars as single words (may fail on older PHP version)
+ $asia = @preg_replace('/(' . self::REGEXP . ')/u', $sep . '\1' . $sep, $text);
+ if (!is_null($asia)) $text = $asia; // recover from regexp falure
+
+ return $text;
+ }
+
+ /**
+ * Split the given text into separate parts
+ *
+ * Each part is either a non-asian string, or a single asian word
+ *
+ * @param string $term
+ * @return string[]
+ */
+ public static function splitAsianWords($term)
+ {
+ return preg_split('/(' . self::REGEXP . '+)/u', $term, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+ }
+}
diff --git a/platform/www/inc/Utf8/Clean.php b/platform/www/inc/Utf8/Clean.php
new file mode 100644
index 0000000..0975ff5
--- /dev/null
+++ b/platform/www/inc/Utf8/Clean.php
@@ -0,0 +1,204 @@
+<?php
+
+namespace dokuwiki\Utf8;
+
+/**
+ * Methods to assess and clean UTF-8 strings
+ */
+class Clean
+{
+ /**
+ * Checks if a string contains 7bit ASCII only
+ *
+ * @author Andreas Haerter <andreas.haerter@dev.mail-node.com>
+ *
+ * @param string $str
+ * @return bool
+ */
+ public static function isASCII($str)
+ {
+ return (preg_match('/(?:[^\x00-\x7F])/', $str) !== 1);
+ }
+
+ /**
+ * Tries to detect if a string is in Unicode encoding
+ *
+ * @author <bmorel@ssi.fr>
+ * @link http://php.net/manual/en/function.utf8-encode.php
+ *
+ * @param string $str
+ * @return bool
+ */
+ public static function isUtf8($str)
+ {
+ $len = strlen($str);
+ for ($i = 0; $i < $len; $i++) {
+ $b = ord($str[$i]);
+ if ($b < 0x80) continue; # 0bbbbbbb
+ elseif (($b & 0xE0) === 0xC0) $n = 1; # 110bbbbb
+ elseif (($b & 0xF0) === 0xE0) $n = 2; # 1110bbbb
+ elseif (($b & 0xF8) === 0xF0) $n = 3; # 11110bbb
+ elseif (($b & 0xFC) === 0xF8) $n = 4; # 111110bb
+ elseif (($b & 0xFE) === 0xFC) $n = 5; # 1111110b
+ else return false; # Does not match any model
+
+ for ($j = 0; $j < $n; $j++) { # n bytes matching 10bbbbbb follow ?
+ if ((++$i === $len) || ((ord($str[$i]) & 0xC0) !== 0x80))
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Strips all high byte chars
+ *
+ * Returns a pure ASCII7 string
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $str
+ * @return string
+ */
+ public static function strip($str)
+ {
+ $ascii = '';
+ $len = strlen($str);
+ for ($i = 0; $i < $len; $i++) {
+ if (ord($str[$i]) < 128) {
+ $ascii .= $str[$i];
+ }
+ }
+ return $ascii;
+ }
+
+ /**
+ * Removes special characters (nonalphanumeric) from a UTF-8 string
+ *
+ * This function adds the controlchars 0x00 to 0x19 to the array of
+ * stripped chars (they are not included in $UTF8_SPECIAL_CHARS)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $string The UTF8 string to strip of special chars
+ * @param string $repl Replace special with this string
+ * @param string $additional Additional chars to strip (used in regexp char class)
+ * @return string
+ */
+ public static function stripspecials($string, $repl = '', $additional = '')
+ {
+ static $specials = null;
+ if ($specials === null) {
+ $specials = preg_quote(Table::specialChars(), '/');
+ }
+
+ return preg_replace('/[' . $additional . '\x00-\x19' . $specials . ']/u', $repl, $string);
+ }
+
+ /**
+ * Replace bad bytes with an alternative character
+ *
+ * ASCII character is recommended for replacement char
+ *
+ * PCRE Pattern to locate bad bytes in a UTF-8 string
+ * Comes from W3 FAQ: Multilingual Forms
+ * Note: modified to include full ASCII range including control chars
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @see http://www.w3.org/International/questions/qa-forms-utf-8
+ *
+ * @param string $str to search
+ * @param string $replace to replace bad bytes with (defaults to '?') - use ASCII
+ * @return string
+ */
+ public static function replaceBadBytes($str, $replace = '')
+ {
+ $UTF8_BAD =
+ '([\x00-\x7F]' . # ASCII (including control chars)
+ '|[\xC2-\xDF][\x80-\xBF]' . # non-overlong 2-byte
+ '|\xE0[\xA0-\xBF][\x80-\xBF]' . # excluding overlongs
+ '|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}' . # straight 3-byte
+ '|\xED[\x80-\x9F][\x80-\xBF]' . # excluding surrogates
+ '|\xF0[\x90-\xBF][\x80-\xBF]{2}' . # planes 1-3
+ '|[\xF1-\xF3][\x80-\xBF]{3}' . # planes 4-15
+ '|\xF4[\x80-\x8F][\x80-\xBF]{2}' . # plane 16
+ '|(.{1}))'; # invalid byte
+ ob_start();
+ while (preg_match('/' . $UTF8_BAD . '/S', $str, $matches)) {
+ if (!isset($matches[2])) {
+ echo $matches[0];
+ } else {
+ echo $replace;
+ }
+ $str = substr($str, strlen($matches[0]));
+ }
+ return ob_get_clean();
+ }
+
+
+ /**
+ * Replace accented UTF-8 characters by unaccented ASCII-7 equivalents
+ *
+ * Use the optional parameter to just deaccent lower ($case = -1) or upper ($case = 1)
+ * letters. Default is to deaccent both cases ($case = 0)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $string
+ * @param int $case
+ * @return string
+ */
+ public static function deaccent($string, $case = 0)
+ {
+ if ($case <= 0) {
+ $string = strtr($string, Table::lowerAccents());
+ }
+ if ($case >= 0) {
+ $string = strtr($string, Table::upperAccents());
+ }
+ return $string;
+ }
+
+ /**
+ * Romanize a non-latin string
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $string
+ * @return string
+ */
+ public static function romanize($string)
+ {
+ if (self::isASCII($string)) return $string; //nothing to do
+
+ return strtr($string, Table::romanization());
+ }
+
+ /**
+ * adjust a byte index into a utf8 string to a utf8 character boundary
+ *
+ * @author chris smith <chris@jalakai.co.uk>
+ *
+ * @param string $str utf8 character string
+ * @param int $i byte index into $str
+ * @param bool $next direction to search for boundary, false = up (current character) true = down (next character)
+ * @return int byte index into $str now pointing to a utf8 character boundary
+ */
+ public static function correctIdx($str, $i, $next = false)
+ {
+
+ if ($i <= 0) return 0;
+
+ $limit = strlen($str);
+ if ($i >= $limit) return $limit;
+
+ if ($next) {
+ while (($i < $limit) && ((ord($str[$i]) & 0xC0) === 0x80)) $i++;
+ } else {
+ while ($i && ((ord($str[$i]) & 0xC0) === 0x80)) $i--;
+ }
+
+ return $i;
+ }
+
+}
diff --git a/platform/www/inc/Utf8/Conversion.php b/platform/www/inc/Utf8/Conversion.php
new file mode 100644
index 0000000..fad9cd0
--- /dev/null
+++ b/platform/www/inc/Utf8/Conversion.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace dokuwiki\Utf8;
+
+/**
+ * Methods to convert from and to UTF-8 strings
+ */
+class Conversion
+{
+
+ /**
+ * Encodes UTF-8 characters to HTML entities
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ * @author <vpribish at shopping dot com>
+ * @link http://php.net/manual/en/function.utf8-decode.php
+ *
+ * @param string $str
+ * @param bool $all Encode non-utf8 char to HTML as well
+ * @return string
+ */
+ public static function toHtml($str, $all = false)
+ {
+ $ret = '';
+ foreach (Unicode::fromUtf8($str) as $cp) {
+ if ($cp < 0x80 && !$all) {
+ $ret .= chr($cp);
+ } elseif ($cp < 0x100) {
+ $ret .= "&#$cp;";
+ } else {
+ $ret .= '&#x' . dechex($cp) . ';';
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Decodes HTML entities to UTF-8 characters
+ *
+ * Convert any &#..; entity to a codepoint,
+ * The entities flag defaults to only decoding numeric entities.
+ * Pass HTML_ENTITIES and named entities, including &amp; &lt; etc.
+ * are handled as well. Avoids the problem that would occur if you
+ * had to decode "&amp;#38;&#38;amp;#38;"
+ *
+ * unhtmlspecialchars(\dokuwiki\Utf8\Conversion::fromHtml($s)) -> "&#38;&#38;"
+ * \dokuwiki\Utf8\Conversion::fromHtml(unhtmlspecialchars($s)) -> "&&amp#38;"
+ * what it should be -> "&#38;&amp#38;"
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param string $str UTF-8 encoded string
+ * @param boolean $entities decode name entities in addtition to numeric ones
+ * @return string UTF-8 encoded string with numeric (and named) entities replaced.
+ */
+ public static function fromHtml($str, $entities = false)
+ {
+ if (!$entities) {
+ return preg_replace_callback(
+ '/(&#([Xx])?([0-9A-Za-z]+);)/m',
+ [__CLASS__, 'decodeNumericEntity'],
+ $str
+ );
+ }
+
+ return preg_replace_callback(
+ '/&(#)?([Xx])?([0-9A-Za-z]+);/m',
+ [__CLASS__, 'decodeAnyEntity'],
+ $str
+ );
+ }
+
+ /**
+ * Decodes any HTML entity to it's correct UTF-8 char equivalent
+ *
+ * @param string $ent An entity
+ * @return string
+ */
+ protected static function decodeAnyEntity($ent)
+ {
+ // create the named entity lookup table
+ static $table = null;
+ if ($table === null) {
+ $table = get_html_translation_table(HTML_ENTITIES);
+ $table = array_flip($table);
+ $table = array_map(
+ static function ($c) {
+ return Unicode::toUtf8(array(ord($c)));
+ },
+ $table
+ );
+ }
+
+ if ($ent[1] === '#') {
+ return self::decodeNumericEntity($ent);
+ }
+
+ if (array_key_exists($ent[0], $table)) {
+ return $table[$ent[0]];
+ }
+
+ return $ent[0];
+ }
+
+ /**
+ * Decodes numeric HTML entities to their correct UTF-8 characters
+ *
+ * @param $ent string A numeric entity
+ * @return string|false
+ */
+ protected static function decodeNumericEntity($ent)
+ {
+ switch ($ent[2]) {
+ case 'X':
+ case 'x':
+ $cp = hexdec($ent[3]);
+ break;
+ default:
+ $cp = intval($ent[3]);
+ break;
+ }
+ return Unicode::toUtf8(array($cp));
+ }
+
+ /**
+ * UTF-8 to UTF-16BE conversion.
+ *
+ * Maybe really UCS-2 without mb_string due to utf8_to_unicode limits
+ *
+ * @param string $str
+ * @param bool $bom
+ * @return string
+ */
+ public static function toUtf16be($str, $bom = false)
+ {
+ $out = $bom ? "\xFE\xFF" : '';
+ if (UTF8_MBSTRING) {
+ return $out . mb_convert_encoding($str, 'UTF-16BE', 'UTF-8');
+ }
+
+ $uni = Unicode::fromUtf8($str);
+ foreach ($uni as $cp) {
+ $out .= pack('n', $cp);
+ }
+ return $out;
+ }
+
+ /**
+ * UTF-8 to UTF-16BE conversion.
+ *
+ * Maybe really UCS-2 without mb_string due to utf8_to_unicode limits
+ *
+ * @param string $str
+ * @return false|string
+ */
+ public static function fromUtf16be($str)
+ {
+ $uni = unpack('n*', $str);
+ return Unicode::toUtf8($uni);
+ }
+
+}
diff --git a/platform/www/inc/Utf8/PhpString.php b/platform/www/inc/Utf8/PhpString.php
new file mode 100644
index 0000000..5bcd601
--- /dev/null
+++ b/platform/www/inc/Utf8/PhpString.php
@@ -0,0 +1,383 @@
+<?php
+
+namespace dokuwiki\Utf8;
+
+/**
+ * UTF-8 aware equivalents to PHP's string functions
+ */
+class PhpString
+{
+
+ /**
+ * A locale independent basename() implementation
+ *
+ * works around a bug in PHP's basename() implementation
+ *
+ * @param string $path A path
+ * @param string $suffix If the name component ends in suffix this will also be cut off
+ * @return string
+ * @link https://bugs.php.net/bug.php?id=37738
+ *
+ * @see basename()
+ */
+ public static function basename($path, $suffix = '')
+ {
+ $path = trim($path, '\\/');
+ $rpos = max(strrpos($path, '/'), strrpos($path, '\\'));
+ if ($rpos) {
+ $path = substr($path, $rpos + 1);
+ }
+
+ $suflen = strlen($suffix);
+ if ($suflen && (substr($path, -$suflen) === $suffix)) {
+ $path = substr($path, 0, -$suflen);
+ }
+
+ return $path;
+ }
+
+ /**
+ * Unicode aware replacement for strlen()
+ *
+ * utf8_decode() converts characters that are not in ISO-8859-1
+ * to '?', which, for the purpose of counting, is alright - It's
+ * even faster than mb_strlen.
+ *
+ * @param string $string
+ * @return int
+ * @see utf8_decode()
+ *
+ * @author <chernyshevsky at hotmail dot com>
+ * @see strlen()
+ */
+ public static function strlen($string)
+ {
+ if (function_exists('utf8_decode')) {
+ return strlen(utf8_decode($string));
+ }
+
+ if (UTF8_MBSTRING) {
+ return mb_strlen($string, 'UTF-8');
+ }
+
+ if (function_exists('iconv_strlen')) {
+ return iconv_strlen($string, 'UTF-8');
+ }
+
+ return strlen($string);
+ }
+
+ /**
+ * UTF-8 aware alternative to substr
+ *
+ * Return part of a string given character offset (and optionally length)
+ *
+ * @param string $str
+ * @param int $offset number of UTF-8 characters offset (from left)
+ * @param int $length (optional) length in UTF-8 characters from offset
+ * @return string
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ */
+ public static function substr($str, $offset, $length = null)
+ {
+ if (UTF8_MBSTRING) {
+ if ($length === null) {
+ return mb_substr($str, $offset);
+ }
+
+ return mb_substr($str, $offset, $length);
+ }
+
+ /*
+ * Notes:
+ *
+ * no mb string support, so we'll use pcre regex's with 'u' flag
+ * pcre only supports repetitions of less than 65536, in order to accept up to MAXINT values for
+ * offset and length, we'll repeat a group of 65535 characters when needed (ok, up to MAXINT-65536)
+ *
+ * substr documentation states false can be returned in some cases (e.g. offset > string length)
+ * mb_substr never returns false, it will return an empty string instead.
+ *
+ * calculating the number of characters in the string is a relatively expensive operation, so
+ * we only carry it out when necessary. It isn't necessary for +ve offsets and no specified length
+ */
+
+ // cast parameters to appropriate types to avoid multiple notices/warnings
+ $str = (string)$str; // generates E_NOTICE for PHP4 objects, but not PHP5 objects
+ $offset = (int)$offset;
+ if ($length !== null) $length = (int)$length;
+
+ // handle trivial cases
+ if ($length === 0) return '';
+ if ($offset < 0 && $length < 0 && $length < $offset) return '';
+
+ $offset_pattern = '';
+ $length_pattern = '';
+
+ // normalise -ve offsets (we could use a tail anchored pattern, but they are horribly slow!)
+ if ($offset < 0) {
+ $strlen = self::strlen($str); // see notes
+ $offset = $strlen + $offset;
+ if ($offset < 0) $offset = 0;
+ }
+
+ // establish a pattern for offset, a non-captured group equal in length to offset
+ if ($offset > 0) {
+ $Ox = (int)($offset / 65535);
+ $Oy = $offset % 65535;
+
+ if ($Ox) $offset_pattern = '(?:.{65535}){' . $Ox . '}';
+ $offset_pattern = '^(?:' . $offset_pattern . '.{' . $Oy . '})';
+ } else {
+ $offset_pattern = '^'; // offset == 0; just anchor the pattern
+ }
+
+ // establish a pattern for length
+ if ($length === null) {
+ $length_pattern = '(.*)$'; // the rest of the string
+ } else {
+
+ if (!isset($strlen)) $strlen = self::strlen($str); // see notes
+ if ($offset > $strlen) return ''; // another trivial case
+
+ if ($length > 0) {
+
+ // reduce any length that would go past the end of the string
+ $length = min($strlen - $offset, $length);
+
+ $Lx = (int)($length / 65535);
+ $Ly = $length % 65535;
+
+ // +ve length requires ... a captured group of length characters
+ if ($Lx) $length_pattern = '(?:.{65535}){' . $Lx . '}';
+ $length_pattern = '(' . $length_pattern . '.{' . $Ly . '})';
+
+ } else if ($length < 0) {
+
+ if ($length < ($offset - $strlen)) return '';
+
+ $Lx = (int)((-$length) / 65535);
+ $Ly = (-$length) % 65535;
+
+ // -ve length requires ... capture everything except a group of -length characters
+ // anchored at the tail-end of the string
+ if ($Lx) $length_pattern = '(?:.{65535}){' . $Lx . '}';
+ $length_pattern = '(.*)(?:' . $length_pattern . '.{' . $Ly . '})$';
+ }
+ }
+
+ if (!preg_match('#' . $offset_pattern . $length_pattern . '#us', $str, $match)) return '';
+ return $match[1];
+ }
+
+ // phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
+ /**
+ * Unicode aware replacement for substr_replace()
+ *
+ * @param string $string input string
+ * @param string $replacement the replacement
+ * @param int $start the replacing will begin at the start'th offset into string.
+ * @param int $length If given and is positive, it represents the length of the portion of string which is
+ * to be replaced. If length is zero then this function will have the effect of inserting
+ * replacement into string at the given start offset.
+ * @return string
+ * @see substr_replace()
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public static function substr_replace($string, $replacement, $start, $length = 0)
+ {
+ $ret = '';
+ if ($start > 0) $ret .= self::substr($string, 0, $start);
+ $ret .= $replacement;
+ $ret .= self::substr($string, $start + $length);
+ return $ret;
+ }
+ // phpcs:enable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
+
+ /**
+ * Unicode aware replacement for ltrim()
+ *
+ * @param string $str
+ * @param string $charlist
+ * @return string
+ * @see ltrim()
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public static function ltrim($str, $charlist = '')
+ {
+ if ($charlist === '') return ltrim($str);
+
+ //quote charlist for use in a characterclass
+ $charlist = preg_replace('!([\\\\\\-\\]\\[/])!', '\\\${1}', $charlist);
+
+ return preg_replace('/^[' . $charlist . ']+/u', '', $str);
+ }
+
+ /**
+ * Unicode aware replacement for rtrim()
+ *
+ * @param string $str
+ * @param string $charlist
+ * @return string
+ * @see rtrim()
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public static function rtrim($str, $charlist = '')
+ {
+ if ($charlist === '') return rtrim($str);
+
+ //quote charlist for use in a characterclass
+ $charlist = preg_replace('!([\\\\\\-\\]\\[/])!', '\\\${1}', $charlist);
+
+ return preg_replace('/[' . $charlist . ']+$/u', '', $str);
+ }
+
+ /**
+ * Unicode aware replacement for trim()
+ *
+ * @param string $str
+ * @param string $charlist
+ * @return string
+ * @see trim()
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public static function trim($str, $charlist = '')
+ {
+ if ($charlist === '') return trim($str);
+
+ return self::ltrim(self::rtrim($str, $charlist), $charlist);
+ }
+
+ /**
+ * This is a unicode aware replacement for strtolower()
+ *
+ * Uses mb_string extension if available
+ *
+ * @param string $string
+ * @return string
+ * @see \dokuwiki\Utf8\PhpString::strtoupper()
+ *
+ * @author Leo Feyer <leo@typolight.org>
+ * @see strtolower()
+ */
+ public static function strtolower($string)
+ {
+ if (UTF8_MBSTRING) {
+ if (class_exists('Normalizer', $autoload = false)) {
+ return \Normalizer::normalize(mb_strtolower($string, 'utf-8'));
+ }
+ return (mb_strtolower($string, 'utf-8'));
+ }
+ return strtr($string, Table::upperCaseToLowerCase());
+ }
+
+ /**
+ * This is a unicode aware replacement for strtoupper()
+ *
+ * Uses mb_string extension if available
+ *
+ * @param string $string
+ * @return string
+ * @see \dokuwiki\Utf8\PhpString::strtoupper()
+ *
+ * @author Leo Feyer <leo@typolight.org>
+ * @see strtoupper()
+ */
+ public static function strtoupper($string)
+ {
+ if (UTF8_MBSTRING) return mb_strtoupper($string, 'utf-8');
+
+ return strtr($string, Table::lowerCaseToUpperCase());
+ }
+
+
+ /**
+ * UTF-8 aware alternative to ucfirst
+ * Make a string's first character uppercase
+ *
+ * @param string $str
+ * @return string with first character as upper case (if applicable)
+ * @author Harry Fuecks
+ *
+ */
+ public static function ucfirst($str)
+ {
+ switch (self::strlen($str)) {
+ case 0:
+ return '';
+ case 1:
+ return self::strtoupper($str);
+ default:
+ preg_match('/^(.{1})(.*)$/us', $str, $matches);
+ return self::strtoupper($matches[1]) . $matches[2];
+ }
+ }
+
+ /**
+ * UTF-8 aware alternative to ucwords
+ * Uppercase the first character of each word in a string
+ *
+ * @param string $str
+ * @return string with first char of each word uppercase
+ * @author Harry Fuecks
+ * @see http://php.net/ucwords
+ *
+ */
+ public static function ucwords($str)
+ {
+ // Note: [\x0c\x09\x0b\x0a\x0d\x20] matches;
+ // form feeds, horizontal tabs, vertical tabs, linefeeds and carriage returns
+ // This corresponds to the definition of a "word" defined at http://php.net/ucwords
+ $pattern = '/(^|([\x0c\x09\x0b\x0a\x0d\x20]+))([^\x0c\x09\x0b\x0a\x0d\x20]{1})[^\x0c\x09\x0b\x0a\x0d\x20]*/u';
+
+ return preg_replace_callback(
+ $pattern,
+ function ($matches) {
+ $leadingws = $matches[2];
+ $ucfirst = self::strtoupper($matches[3]);
+ $ucword = self::substr_replace(ltrim($matches[0]), $ucfirst, 0, 1);
+ return $leadingws . $ucword;
+ },
+ $str
+ );
+ }
+
+ /**
+ * This is an Unicode aware replacement for strpos
+ *
+ * @param string $haystack
+ * @param string $needle
+ * @param integer $offset
+ * @return integer
+ * @author Leo Feyer <leo@typolight.org>
+ * @see strpos()
+ *
+ */
+ public static function strpos($haystack, $needle, $offset = 0)
+ {
+ $comp = 0;
+ $length = null;
+
+ while ($length === null || $length < $offset) {
+ $pos = strpos($haystack, $needle, $offset + $comp);
+
+ if ($pos === false)
+ return false;
+
+ $length = self::strlen(substr($haystack, 0, $pos));
+
+ if ($length < $offset)
+ $comp = $pos - $length;
+ }
+
+ return $length;
+ }
+
+
+}
diff --git a/platform/www/inc/Utf8/Table.php b/platform/www/inc/Utf8/Table.php
new file mode 100644
index 0000000..8683c92
--- /dev/null
+++ b/platform/www/inc/Utf8/Table.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace dokuwiki\Utf8;
+
+/**
+ * Provides static access to the UTF-8 conversion tables
+ *
+ * Lazy-Loads tables on first access
+ */
+class Table
+{
+
+ /**
+ * Get the upper to lower case conversion table
+ *
+ * @return array
+ */
+ public static function upperCaseToLowerCase()
+ {
+ static $table = null;
+ if ($table === null) $table = include __DIR__ . '/tables/case.php';
+ return $table;
+ }
+
+ /**
+ * Get the lower to upper case conversion table
+ *
+ * @return array
+ */
+ public static function lowerCaseToUpperCase()
+ {
+ static $table = null;
+ if ($table === null) {
+ $uclc = self::upperCaseToLowerCase();
+ $table = array_flip($uclc);
+ }
+ return $table;
+ }
+
+ /**
+ * Get the lower case accent table
+ * @return array
+ */
+ public static function lowerAccents()
+ {
+ static $table = null;
+ if ($table === null) {
+ $table = include __DIR__ . '/tables/loweraccents.php';
+ }
+ return $table;
+ }
+
+ /**
+ * Get the lower case accent table
+ * @return array
+ */
+ public static function upperAccents()
+ {
+ static $table = null;
+ if ($table === null) {
+ $table = include __DIR__ . '/tables/upperaccents.php';
+ }
+ return $table;
+ }
+
+ /**
+ * Get the romanization table
+ * @return array
+ */
+ public static function romanization()
+ {
+ static $table = null;
+ if ($table === null) {
+ $table = include __DIR__ . '/tables/romanization.php';
+ }
+ return $table;
+ }
+
+ /**
+ * Get the special chars as a concatenated string
+ * @return string
+ */
+ public static function specialChars()
+ {
+ static $string = null;
+ if ($string === null) {
+ $table = include __DIR__ . '/tables/specials.php';
+ // FIXME should we cache this to file system?
+ $string = Unicode::toUtf8($table);
+ }
+ return $string;
+ }
+}
diff --git a/platform/www/inc/Utf8/Unicode.php b/platform/www/inc/Utf8/Unicode.php
new file mode 100644
index 0000000..4b64265
--- /dev/null
+++ b/platform/www/inc/Utf8/Unicode.php
@@ -0,0 +1,277 @@
+<?php
+
+namespace dokuwiki\Utf8;
+
+/**
+ * Convert between UTF-8 and a list of Unicode Code Points
+ */
+class Unicode
+{
+
+ /**
+ * Takes an UTF-8 string and returns an array of ints representing the
+ * Unicode characters. Astral planes are supported ie. the ints in the
+ * output can be > 0xFFFF. Occurrances of the BOM are ignored. Surrogates
+ * are not allowed.
+ *
+ * If $strict is set to true the function returns false if the input
+ * string isn't a valid UTF-8 octet sequence and raises a PHP error at
+ * level E_USER_WARNING
+ *
+ * Note: this function has been modified slightly in this library to
+ * trigger errors on encountering bad bytes
+ *
+ * @author <hsivonen@iki.fi>
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @see unicode_to_utf8
+ * @link http://hsivonen.iki.fi/php-utf8/
+ * @link http://sourceforge.net/projects/phputf8/
+ * @todo break into less complex chunks
+ * @todo use exceptions instead of user errors
+ *
+ * @param string $str UTF-8 encoded string
+ * @param boolean $strict Check for invalid sequences?
+ * @return mixed array of unicode code points or false if UTF-8 invalid
+ */
+ public static function fromUtf8($str, $strict = false)
+ {
+ $mState = 0; // cached expected number of octets after the current octet
+ // until the beginning of the next UTF8 character sequence
+ $mUcs4 = 0; // cached Unicode character
+ $mBytes = 1; // cached expected number of octets in the current sequence
+
+ $out = array();
+
+ $len = strlen($str);
+
+ for ($i = 0; $i < $len; $i++) {
+
+ $in = ord($str[$i]);
+
+ if ($mState === 0) {
+
+ // When mState is zero we expect either a US-ASCII character or a
+ // multi-octet sequence.
+ if (0 === (0x80 & $in)) {
+ // US-ASCII, pass straight through.
+ $out[] = $in;
+ $mBytes = 1;
+
+ } else if (0xC0 === (0xE0 & $in)) {
+ // First octet of 2 octet sequence
+ $mUcs4 = $in;
+ $mUcs4 = ($mUcs4 & 0x1F) << 6;
+ $mState = 1;
+ $mBytes = 2;
+
+ } else if (0xE0 === (0xF0 & $in)) {
+ // First octet of 3 octet sequence
+ $mUcs4 = $in;
+ $mUcs4 = ($mUcs4 & 0x0F) << 12;
+ $mState = 2;
+ $mBytes = 3;
+
+ } else if (0xF0 === (0xF8 & $in)) {
+ // First octet of 4 octet sequence
+ $mUcs4 = $in;
+ $mUcs4 = ($mUcs4 & 0x07) << 18;
+ $mState = 3;
+ $mBytes = 4;
+
+ } else if (0xF8 === (0xFC & $in)) {
+ /* First octet of 5 octet sequence.
+ *
+ * This is illegal because the encoded codepoint must be either
+ * (a) not the shortest form or
+ * (b) outside the Unicode range of 0-0x10FFFF.
+ * Rather than trying to resynchronize, we will carry on until the end
+ * of the sequence and let the later error handling code catch it.
+ */
+ $mUcs4 = $in;
+ $mUcs4 = ($mUcs4 & 0x03) << 24;
+ $mState = 4;
+ $mBytes = 5;
+
+ } else if (0xFC === (0xFE & $in)) {
+ // First octet of 6 octet sequence, see comments for 5 octet sequence.
+ $mUcs4 = $in;
+ $mUcs4 = ($mUcs4 & 1) << 30;
+ $mState = 5;
+ $mBytes = 6;
+
+ } elseif ($strict) {
+ /* Current octet is neither in the US-ASCII range nor a legal first
+ * octet of a multi-octet sequence.
+ */
+ trigger_error(
+ 'utf8_to_unicode: Illegal sequence identifier ' .
+ 'in UTF-8 at byte ' . $i,
+ E_USER_WARNING
+ );
+ return false;
+
+ }
+
+ } else {
+
+ // When mState is non-zero, we expect a continuation of the multi-octet
+ // sequence
+ if (0x80 === (0xC0 & $in)) {
+
+ // Legal continuation.
+ $shift = ($mState - 1) * 6;
+ $tmp = $in;
+ $tmp = ($tmp & 0x0000003F) << $shift;
+ $mUcs4 |= $tmp;
+
+ /**
+ * End of the multi-octet sequence. mUcs4 now contains the final
+ * Unicode codepoint to be output
+ */
+ if (0 === --$mState) {
+
+ /*
+ * Check for illegal sequences and codepoints.
+ */
+ // From Unicode 3.1, non-shortest form is illegal
+ if (((2 === $mBytes) && ($mUcs4 < 0x0080)) ||
+ ((3 === $mBytes) && ($mUcs4 < 0x0800)) ||
+ ((4 === $mBytes) && ($mUcs4 < 0x10000)) ||
+ (4 < $mBytes) ||
+ // From Unicode 3.2, surrogate characters are illegal
+ (($mUcs4 & 0xFFFFF800) === 0xD800) ||
+ // Codepoints outside the Unicode range are illegal
+ ($mUcs4 > 0x10FFFF)) {
+
+ if ($strict) {
+ trigger_error(
+ 'utf8_to_unicode: Illegal sequence or codepoint ' .
+ 'in UTF-8 at byte ' . $i,
+ E_USER_WARNING
+ );
+
+ return false;
+ }
+
+ }
+
+ if (0xFEFF !== $mUcs4) {
+ // BOM is legal but we don't want to output it
+ $out[] = $mUcs4;
+ }
+
+ //initialize UTF8 cache
+ $mState = 0;
+ $mUcs4 = 0;
+ $mBytes = 1;
+ }
+
+ } elseif ($strict) {
+ /**
+ *((0xC0 & (*in) != 0x80) && (mState != 0))
+ * Incomplete multi-octet sequence.
+ */
+ trigger_error(
+ 'utf8_to_unicode: Incomplete multi-octet ' .
+ ' sequence in UTF-8 at byte ' . $i,
+ E_USER_WARNING
+ );
+
+ return false;
+ }
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Takes an array of ints representing the Unicode characters and returns
+ * a UTF-8 string. Astral planes are supported ie. the ints in the
+ * input can be > 0xFFFF. Occurrances of the BOM are ignored. Surrogates
+ * are not allowed.
+ *
+ * If $strict is set to true the function returns false if the input
+ * array contains ints that represent surrogates or are outside the
+ * Unicode range and raises a PHP error at level E_USER_WARNING
+ *
+ * Note: this function has been modified slightly in this library to use
+ * output buffering to concatenate the UTF-8 string (faster) as well as
+ * reference the array by it's keys
+ *
+ * @param array $arr of unicode code points representing a string
+ * @param boolean $strict Check for invalid sequences?
+ * @return string|false UTF-8 string or false if array contains invalid code points
+ *
+ * @author <hsivonen@iki.fi>
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @see utf8_to_unicode
+ * @link http://hsivonen.iki.fi/php-utf8/
+ * @link http://sourceforge.net/projects/phputf8/
+ * @todo use exceptions instead of user errors
+ */
+ public static function toUtf8($arr, $strict = false)
+ {
+ if (!is_array($arr)) return '';
+ ob_start();
+
+ foreach (array_keys($arr) as $k) {
+
+ if (($arr[$k] >= 0) && ($arr[$k] <= 0x007f)) {
+ # ASCII range (including control chars)
+
+ echo chr($arr[$k]);
+
+ } else if ($arr[$k] <= 0x07ff) {
+ # 2 byte sequence
+
+ echo chr(0xc0 | ($arr[$k] >> 6));
+ echo chr(0x80 | ($arr[$k] & 0x003f));
+
+ } else if ($arr[$k] == 0xFEFF) {
+ # Byte order mark (skip)
+ // nop -- zap the BOM
+
+ } else if ($arr[$k] >= 0xD800 && $arr[$k] <= 0xDFFF) {
+ # Test for illegal surrogates
+
+ // found a surrogate
+ if ($strict) {
+ trigger_error(
+ 'unicode_to_utf8: Illegal surrogate ' .
+ 'at index: ' . $k . ', value: ' . $arr[$k],
+ E_USER_WARNING
+ );
+ return false;
+ }
+
+ } else if ($arr[$k] <= 0xffff) {
+ # 3 byte sequence
+
+ echo chr(0xe0 | ($arr[$k] >> 12));
+ echo chr(0x80 | (($arr[$k] >> 6) & 0x003f));
+ echo chr(0x80 | ($arr[$k] & 0x003f));
+
+ } else if ($arr[$k] <= 0x10ffff) {
+ # 4 byte sequence
+
+ echo chr(0xf0 | ($arr[$k] >> 18));
+ echo chr(0x80 | (($arr[$k] >> 12) & 0x3f));
+ echo chr(0x80 | (($arr[$k] >> 6) & 0x3f));
+ echo chr(0x80 | ($arr[$k] & 0x3f));
+
+ } elseif ($strict) {
+
+ trigger_error(
+ 'unicode_to_utf8: Codepoint out of Unicode range ' .
+ 'at index: ' . $k . ', value: ' . $arr[$k],
+ E_USER_WARNING
+ );
+
+ // out of range
+ return false;
+ }
+ }
+
+ return ob_get_clean();
+ }
+}
diff --git a/platform/www/inc/Utf8/tables/case.php b/platform/www/inc/Utf8/tables/case.php
new file mode 100644
index 0000000..6c41b58
--- /dev/null
+++ b/platform/www/inc/Utf8/tables/case.php
@@ -0,0 +1,659 @@
+<?php
+/**
+ * UTF-8 Case lookup table
+ *
+ * This lookuptable defines the lower case letters to their corresponding
+ * upper case letter in UTF-8
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+return [
+ 'A' => 'a',
+ 'B' => 'b',
+ 'C' => 'c',
+ 'D' => 'd',
+ 'E' => 'e',
+ 'F' => 'f',
+ 'G' => 'g',
+ 'H' => 'h',
+ 'I' => 'i',
+ 'J' => 'j',
+ 'K' => 'k',
+ 'L' => 'l',
+ 'M' => 'm',
+ 'N' => 'n',
+ 'O' => 'o',
+ 'P' => 'p',
+ 'Q' => 'q',
+ 'R' => 'r',
+ 'S' => 's',
+ 'T' => 't',
+ 'U' => 'u',
+ 'V' => 'v',
+ 'W' => 'w',
+ 'X' => 'x',
+ 'Y' => 'y',
+ 'Z' => 'z',
+ 'À' => 'à',
+ 'Á' => 'á',
+ 'Â' => 'â',
+ 'Ã' => 'ã',
+ 'Ä' => 'ä',
+ 'Å' => 'å',
+ 'Æ' => 'æ',
+ 'Ç' => 'ç',
+ 'È' => 'è',
+ 'É' => 'é',
+ 'Ê' => 'ê',
+ 'Ë' => 'ë',
+ 'Ì' => 'ì',
+ 'Í' => 'í',
+ 'Î' => 'î',
+ 'Ï' => 'ï',
+ 'Ð' => 'ð',
+ 'Ñ' => 'ñ',
+ 'Ò' => 'ò',
+ 'Ó' => 'ó',
+ 'Ô' => 'ô',
+ 'Õ' => 'õ',
+ 'Ö' => 'ö',
+ 'Ø' => 'ø',
+ 'Ù' => 'ù',
+ 'Ú' => 'ú',
+ 'Û' => 'û',
+ 'Ü' => 'ü',
+ 'Ý' => 'ý',
+ 'Þ' => 'þ',
+ 'Ā' => 'ā',
+ 'Ă' => 'ă',
+ 'Ą' => 'ą',
+ 'Ć' => 'ć',
+ 'Ĉ' => 'ĉ',
+ 'Ċ' => 'ċ',
+ 'Č' => 'č',
+ 'Ď' => 'ď',
+ 'Đ' => 'đ',
+ 'Ē' => 'ē',
+ 'Ĕ' => 'ĕ',
+ 'Ė' => 'ė',
+ 'Ę' => 'ę',
+ 'Ě' => 'ě',
+ 'Ĝ' => 'ĝ',
+ 'Ğ' => 'ğ',
+ 'Ġ' => 'ġ',
+ 'Ģ' => 'ģ',
+ 'Ĥ' => 'ĥ',
+ 'Ħ' => 'ħ',
+ 'Ĩ' => 'ĩ',
+ 'Ī' => 'ī',
+ 'Ĭ' => 'ĭ',
+ 'Į' => 'į',
+ 'IJ' => 'ij',
+ 'Ĵ' => 'ĵ',
+ 'Ķ' => 'ķ',
+ 'Ĺ' => 'ĺ',
+ 'Ļ' => 'ļ',
+ 'Ľ' => 'ľ',
+ 'Ŀ' => 'ŀ',
+ 'Ł' => 'ł',
+ 'Ń' => 'ń',
+ 'Ņ' => 'ņ',
+ 'Ň' => 'ň',
+ 'Ŋ' => 'ŋ',
+ 'Ō' => 'ō',
+ 'Ŏ' => 'ŏ',
+ 'Ő' => 'ő',
+ 'Œ' => 'œ',
+ 'Ŕ' => 'ŕ',
+ 'Ŗ' => 'ŗ',
+ 'Ř' => 'ř',
+ 'Ś' => 'ś',
+ 'Ŝ' => 'ŝ',
+ 'Ş' => 'ş',
+ 'Š' => 'š',
+ 'Ţ' => 'ţ',
+ 'Ť' => 'ť',
+ 'Ŧ' => 'ŧ',
+ 'Ũ' => 'ũ',
+ 'Ū' => 'ū',
+ 'Ŭ' => 'ŭ',
+ 'Ů' => 'ů',
+ 'Ű' => 'ű',
+ 'Ų' => 'ų',
+ 'Ŵ' => 'ŵ',
+ 'Ŷ' => 'ŷ',
+ 'Ÿ' => 'ÿ',
+ 'Ź' => 'ź',
+ 'Ż' => 'ż',
+ 'Ž' => 'ž',
+ 'Ɓ' => 'ɓ',
+ 'Ƃ' => 'ƃ',
+ 'Ƅ' => 'ƅ',
+ 'Ɔ' => 'ɔ',
+ 'Ƈ' => 'ƈ',
+ 'Ɖ' => 'ɖ',
+ 'Ɗ' => 'ɗ',
+ 'Ƌ' => 'ƌ',
+ 'Ǝ' => 'ǝ',
+ 'Ə' => 'ə',
+ 'Ɛ' => 'ɛ',
+ 'Ƒ' => 'ƒ',
+ 'Ɣ' => 'ɣ',
+ 'Ɩ' => 'ɩ',
+ 'Ɨ' => 'ɨ',
+ 'Ƙ' => 'ƙ',
+ 'Ɯ' => 'ɯ',
+ 'Ɲ' => 'ɲ',
+ 'Ɵ' => 'ɵ',
+ 'Ơ' => 'ơ',
+ 'Ƣ' => 'ƣ',
+ 'Ƥ' => 'ƥ',
+ 'Ʀ' => 'ʀ',
+ 'Ƨ' => 'ƨ',
+ 'Ʃ' => 'ʃ',
+ 'Ƭ' => 'ƭ',
+ 'Ʈ' => 'ʈ',
+ 'Ư' => 'ư',
+ 'Ʊ' => 'ʊ',
+ 'Ʋ' => 'ʋ',
+ 'Ƴ' => 'ƴ',
+ 'Ƶ' => 'ƶ',
+ 'Ʒ' => 'ʒ',
+ 'Ƹ' => 'ƹ',
+ 'Ƽ' => 'ƽ',
+ 'Dž' => 'dž',
+ 'Lj' => 'lj',
+ 'Nj' => 'nj',
+ 'Ǎ' => 'ǎ',
+ 'Ǐ' => 'ǐ',
+ 'Ǒ' => 'ǒ',
+ 'Ǔ' => 'ǔ',
+ 'Ǖ' => 'ǖ',
+ 'Ǘ' => 'ǘ',
+ 'Ǚ' => 'ǚ',
+ 'Ǜ' => 'ǜ',
+ 'Ǟ' => 'ǟ',
+ 'Ǡ' => 'ǡ',
+ 'Ǣ' => 'ǣ',
+ 'Ǥ' => 'ǥ',
+ 'Ǧ' => 'ǧ',
+ 'Ǩ' => 'ǩ',
+ 'Ǫ' => 'ǫ',
+ 'Ǭ' => 'ǭ',
+ 'Ǯ' => 'ǯ',
+ 'Dz' => 'dz',
+ 'Ǵ' => 'ǵ',
+ 'Ƕ' => 'ƕ',
+ 'Ƿ' => 'ƿ',
+ 'Ǹ' => 'ǹ',
+ 'Ǻ' => 'ǻ',
+ 'Ǽ' => 'ǽ',
+ 'Ǿ' => 'ǿ',
+ 'Ȁ' => 'ȁ',
+ 'Ȃ' => 'ȃ',
+ 'Ȅ' => 'ȅ',
+ 'Ȇ' => 'ȇ',
+ 'Ȉ' => 'ȉ',
+ 'Ȋ' => 'ȋ',
+ 'Ȍ' => 'ȍ',
+ 'Ȏ' => 'ȏ',
+ 'Ȑ' => 'ȑ',
+ 'Ȓ' => 'ȓ',
+ 'Ȕ' => 'ȕ',
+ 'Ȗ' => 'ȗ',
+ 'Ș' => 'ș',
+ 'Ț' => 'ț',
+ 'Ȝ' => 'ȝ',
+ 'Ȟ' => 'ȟ',
+ 'Ƞ' => 'ƞ',
+ 'Ȣ' => 'ȣ',
+ 'Ȥ' => 'ȥ',
+ 'Ȧ' => 'ȧ',
+ 'Ȩ' => 'ȩ',
+ 'Ȫ' => 'ȫ',
+ 'Ȭ' => 'ȭ',
+ 'Ȯ' => 'ȯ',
+ 'Ȱ' => 'ȱ',
+ 'Ȳ' => 'ȳ',
+ 'Ά' => 'ά',
+ 'Έ' => 'έ',
+ 'Ή' => 'ή',
+ 'Ί' => 'ί',
+ 'Ό' => 'ό',
+ 'Ύ' => 'ύ',
+ 'Ώ' => 'ώ',
+ 'Α' => 'α',
+ 'Β' => 'β',
+ 'Γ' => 'γ',
+ 'Δ' => 'δ',
+ 'Ε' => 'ε',
+ 'Ζ' => 'ζ',
+ 'Η' => 'η',
+ 'Θ' => 'θ',
+ 'Ι' => 'ι',
+ 'Κ' => 'κ',
+ 'Λ' => 'λ',
+ 'Μ' => 'μ',
+ 'Ν' => 'ν',
+ 'Ξ' => 'ξ',
+ 'Ο' => 'ο',
+ 'Π' => 'π',
+ 'Ρ' => 'ρ',
+ 'Σ' => 'σ',
+ 'Τ' => 'τ',
+ 'Υ' => 'υ',
+ 'Φ' => 'φ',
+ 'Χ' => 'χ',
+ 'Ψ' => 'ψ',
+ 'Ω' => 'ω',
+ 'Ϊ' => 'ϊ',
+ 'Ϋ' => 'ϋ',
+ 'Ϙ' => 'ϙ',
+ 'Ϛ' => 'ϛ',
+ 'Ϝ' => 'ϝ',
+ 'Ϟ' => 'ϟ',
+ 'Ϡ' => 'ϡ',
+ 'Ϣ' => 'ϣ',
+ 'Ϥ' => 'ϥ',
+ 'Ϧ' => 'ϧ',
+ 'Ϩ' => 'ϩ',
+ 'Ϫ' => 'ϫ',
+ 'Ϭ' => 'ϭ',
+ 'Ϯ' => 'ϯ',
+ 'Ѐ' => 'ѐ',
+ 'Ё' => 'ё',
+ 'Ђ' => 'ђ',
+ 'Ѓ' => 'ѓ',
+ 'Є' => 'є',
+ 'Ѕ' => 'ѕ',
+ 'І' => 'і',
+ 'Ї' => 'ї',
+ 'Ј' => 'ј',
+ 'Љ' => 'љ',
+ 'Њ' => 'њ',
+ 'Ћ' => 'ћ',
+ 'Ќ' => 'ќ',
+ 'Ѝ' => 'ѝ',
+ 'Ў' => 'ў',
+ 'Џ' => 'џ',
+ 'А' => 'а',
+ 'Б' => 'б',
+ 'В' => 'в',
+ 'Г' => 'г',
+ 'Д' => 'д',
+ 'Е' => 'е',
+ 'Ж' => 'ж',
+ 'З' => 'з',
+ 'И' => 'и',
+ 'Й' => 'й',
+ 'К' => 'к',
+ 'Л' => 'л',
+ 'М' => 'м',
+ 'Н' => 'н',
+ 'О' => 'о',
+ 'П' => 'п',
+ 'Р' => 'р',
+ 'С' => 'с',
+ 'Т' => 'т',
+ 'У' => 'у',
+ 'Ф' => 'ф',
+ 'Х' => 'х',
+ 'Ц' => 'ц',
+ 'Ч' => 'ч',
+ 'Ш' => 'ш',
+ 'Щ' => 'щ',
+ 'Ъ' => 'ъ',
+ 'Ы' => 'ы',
+ 'Ь' => 'ь',
+ 'Э' => 'э',
+ 'Ю' => 'ю',
+ 'Я' => 'я',
+ 'Ѡ' => 'ѡ',
+ 'Ѣ' => 'ѣ',
+ 'Ѥ' => 'ѥ',
+ 'Ѧ' => 'ѧ',
+ 'Ѩ' => 'ѩ',
+ 'Ѫ' => 'ѫ',
+ 'Ѭ' => 'ѭ',
+ 'Ѯ' => 'ѯ',
+ 'Ѱ' => 'ѱ',
+ 'Ѳ' => 'ѳ',
+ 'Ѵ' => 'ѵ',
+ 'Ѷ' => 'ѷ',
+ 'Ѹ' => 'ѹ',
+ 'Ѻ' => 'ѻ',
+ 'Ѽ' => 'ѽ',
+ 'Ѿ' => 'ѿ',
+ 'Ҁ' => 'ҁ',
+ 'Ҋ' => 'ҋ',
+ 'Ҍ' => 'ҍ',
+ 'Ҏ' => 'ҏ',
+ 'Ґ' => 'ґ',
+ 'Ғ' => 'ғ',
+ 'Ҕ' => 'ҕ',
+ 'Җ' => 'җ',
+ 'Ҙ' => 'ҙ',
+ 'Қ' => 'қ',
+ 'Ҝ' => 'ҝ',
+ 'Ҟ' => 'ҟ',
+ 'Ҡ' => 'ҡ',
+ 'Ң' => 'ң',
+ 'Ҥ' => 'ҥ',
+ 'Ҧ' => 'ҧ',
+ 'Ҩ' => 'ҩ',
+ 'Ҫ' => 'ҫ',
+ 'Ҭ' => 'ҭ',
+ 'Ү' => 'ү',
+ 'Ұ' => 'ұ',
+ 'Ҳ' => 'ҳ',
+ 'Ҵ' => 'ҵ',
+ 'Ҷ' => 'ҷ',
+ 'Ҹ' => 'ҹ',
+ 'Һ' => 'һ',
+ 'Ҽ' => 'ҽ',
+ 'Ҿ' => 'ҿ',
+ 'Ӂ' => 'ӂ',
+ 'Ӄ' => 'ӄ',
+ 'Ӆ' => 'ӆ',
+ 'Ӈ' => 'ӈ',
+ 'Ӊ' => 'ӊ',
+ 'Ӌ' => 'ӌ',
+ 'Ӎ' => 'ӎ',
+ 'Ӑ' => 'ӑ',
+ 'Ӓ' => 'ӓ',
+ 'Ӕ' => 'ӕ',
+ 'Ӗ' => 'ӗ',
+ 'Ә' => 'ә',
+ 'Ӛ' => 'ӛ',
+ 'Ӝ' => 'ӝ',
+ 'Ӟ' => 'ӟ',
+ 'Ӡ' => 'ӡ',
+ 'Ӣ' => 'ӣ',
+ 'Ӥ' => 'ӥ',
+ 'Ӧ' => 'ӧ',
+ 'Ө' => 'ө',
+ 'Ӫ' => 'ӫ',
+ 'Ӭ' => 'ӭ',
+ 'Ӯ' => 'ӯ',
+ 'Ӱ' => 'ӱ',
+ 'Ӳ' => 'ӳ',
+ 'Ӵ' => 'ӵ',
+ 'Ӹ' => 'ӹ',
+ 'Ԁ' => 'ԁ',
+ 'Ԃ' => 'ԃ',
+ 'Ԅ' => 'ԅ',
+ 'Ԇ' => 'ԇ',
+ 'Ԉ' => 'ԉ',
+ 'Ԋ' => 'ԋ',
+ 'Ԍ' => 'ԍ',
+ 'Ԏ' => 'ԏ',
+ 'Ա' => 'ա',
+ 'Բ' => 'բ',
+ 'Գ' => 'գ',
+ 'Դ' => 'դ',
+ 'Ե' => 'ե',
+ 'Զ' => 'զ',
+ 'Է' => 'է',
+ 'Ը' => 'ը',
+ 'Թ' => 'թ',
+ 'Ժ' => 'ժ',
+ 'Ի' => 'ի',
+ 'Լ' => 'լ',
+ 'Խ' => 'խ',
+ 'Ծ' => 'ծ',
+ 'Կ' => 'կ',
+ 'Հ' => 'հ',
+ 'Ձ' => 'ձ',
+ 'Ղ' => 'ղ',
+ 'Ճ' => 'ճ',
+ 'Մ' => 'մ',
+ 'Յ' => 'յ',
+ 'Ն' => 'ն',
+ 'Շ' => 'շ',
+ 'Ո' => 'ո',
+ 'Չ' => 'չ',
+ 'Պ' => 'պ',
+ 'Ջ' => 'ջ',
+ 'Ռ' => 'ռ',
+ 'Ս' => 'ս',
+ 'Վ' => 'վ',
+ 'Տ' => 'տ',
+ 'Ր' => 'ր',
+ 'Ց' => 'ց',
+ 'Ւ' => 'ւ',
+ 'Փ' => 'փ',
+ 'Ք' => 'ք',
+ 'Օ' => 'օ',
+ 'Ֆ' => 'ֆ',
+ 'Ḁ' => 'ḁ',
+ 'Ḃ' => 'ḃ',
+ 'Ḅ' => 'ḅ',
+ 'Ḇ' => 'ḇ',
+ 'Ḉ' => 'ḉ',
+ 'Ḋ' => 'ḋ',
+ 'Ḍ' => 'ḍ',
+ 'Ḏ' => 'ḏ',
+ 'Ḑ' => 'ḑ',
+ 'Ḓ' => 'ḓ',
+ 'Ḕ' => 'ḕ',
+ 'Ḗ' => 'ḗ',
+ 'Ḙ' => 'ḙ',
+ 'Ḛ' => 'ḛ',
+ 'Ḝ' => 'ḝ',
+ 'Ḟ' => 'ḟ',
+ 'Ḡ' => 'ḡ',
+ 'Ḣ' => 'ḣ',
+ 'Ḥ' => 'ḥ',
+ 'Ḧ' => 'ḧ',
+ 'Ḩ' => 'ḩ',
+ 'Ḫ' => 'ḫ',
+ 'Ḭ' => 'ḭ',
+ 'Ḯ' => 'ḯ',
+ 'Ḱ' => 'ḱ',
+ 'Ḳ' => 'ḳ',
+ 'Ḵ' => 'ḵ',
+ 'Ḷ' => 'ḷ',
+ 'Ḹ' => 'ḹ',
+ 'Ḻ' => 'ḻ',
+ 'Ḽ' => 'ḽ',
+ 'Ḿ' => 'ḿ',
+ 'Ṁ' => 'ṁ',
+ 'Ṃ' => 'ṃ',
+ 'Ṅ' => 'ṅ',
+ 'Ṇ' => 'ṇ',
+ 'Ṉ' => 'ṉ',
+ 'Ṋ' => 'ṋ',
+ 'Ṍ' => 'ṍ',
+ 'Ṏ' => 'ṏ',
+ 'Ṑ' => 'ṑ',
+ 'Ṓ' => 'ṓ',
+ 'Ṕ' => 'ṕ',
+ 'Ṗ' => 'ṗ',
+ 'Ṙ' => 'ṙ',
+ 'Ṛ' => 'ṛ',
+ 'Ṝ' => 'ṝ',
+ 'Ṟ' => 'ṟ',
+ 'Ṡ' => 'ṡ',
+ 'Ṣ' => 'ṣ',
+ 'Ṥ' => 'ṥ',
+ 'Ṧ' => 'ṧ',
+ 'Ṩ' => 'ṩ',
+ 'Ṫ' => 'ṫ',
+ 'Ṭ' => 'ṭ',
+ 'Ṯ' => 'ṯ',
+ 'Ṱ' => 'ṱ',
+ 'Ṳ' => 'ṳ',
+ 'Ṵ' => 'ṵ',
+ 'Ṷ' => 'ṷ',
+ 'Ṹ' => 'ṹ',
+ 'Ṻ' => 'ṻ',
+ 'Ṽ' => 'ṽ',
+ 'Ṿ' => 'ṿ',
+ 'Ẁ' => 'ẁ',
+ 'Ẃ' => 'ẃ',
+ 'Ẅ' => 'ẅ',
+ 'Ẇ' => 'ẇ',
+ 'Ẉ' => 'ẉ',
+ 'Ẋ' => 'ẋ',
+ 'Ẍ' => 'ẍ',
+ 'Ẏ' => 'ẏ',
+ 'Ẑ' => 'ẑ',
+ 'Ẓ' => 'ẓ',
+ 'Ẕ' => 'ẕ',
+ 'Ạ' => 'ạ',
+ 'Ả' => 'ả',
+ 'Ấ' => 'ấ',
+ 'Ầ' => 'ầ',
+ 'Ẩ' => 'ẩ',
+ 'Ẫ' => 'ẫ',
+ 'Ậ' => 'ậ',
+ 'Ắ' => 'ắ',
+ 'Ằ' => 'ằ',
+ 'Ẳ' => 'ẳ',
+ 'Ẵ' => 'ẵ',
+ 'Ặ' => 'ặ',
+ 'Ẹ' => 'ẹ',
+ 'Ẻ' => 'ẻ',
+ 'Ẽ' => 'ẽ',
+ 'Ế' => 'ế',
+ 'Ề' => 'ề',
+ 'Ể' => 'ể',
+ 'Ễ' => 'ễ',
+ 'Ệ' => 'ệ',
+ 'Ỉ' => 'ỉ',
+ 'Ị' => 'ị',
+ 'Ọ' => 'ọ',
+ 'Ỏ' => 'ỏ',
+ 'Ố' => 'ố',
+ 'Ồ' => 'ồ',
+ 'Ổ' => 'ổ',
+ 'Ỗ' => 'ỗ',
+ 'Ộ' => 'ộ',
+ 'Ớ' => 'ớ',
+ 'Ờ' => 'ờ',
+ 'Ở' => 'ở',
+ 'Ỡ' => 'ỡ',
+ 'Ợ' => 'ợ',
+ 'Ụ' => 'ụ',
+ 'Ủ' => 'ủ',
+ 'Ứ' => 'ứ',
+ 'Ừ' => 'ừ',
+ 'Ử' => 'ử',
+ 'Ữ' => 'ữ',
+ 'Ự' => 'ự',
+ 'Ỳ' => 'ỳ',
+ 'Ỵ' => 'ỵ',
+ 'Ỷ' => 'ỷ',
+ 'Ỹ' => 'ỹ',
+ 'Ἀ' => 'ἀ',
+ 'Ἁ' => 'ἁ',
+ 'Ἂ' => 'ἂ',
+ 'Ἃ' => 'ἃ',
+ 'Ἄ' => 'ἄ',
+ 'Ἅ' => 'ἅ',
+ 'Ἆ' => 'ἆ',
+ 'Ἇ' => 'ἇ',
+ 'Ἐ' => 'ἐ',
+ 'Ἑ' => 'ἑ',
+ 'Ἒ' => 'ἒ',
+ 'Ἓ' => 'ἓ',
+ 'Ἔ' => 'ἔ',
+ 'Ἕ' => 'ἕ',
+ 'Ἡ' => 'ἡ',
+ 'Ἢ' => 'ἢ',
+ 'Ἣ' => 'ἣ',
+ 'Ἤ' => 'ἤ',
+ 'Ἥ' => 'ἥ',
+ 'Ἦ' => 'ἦ',
+ 'Ἧ' => 'ἧ',
+ 'Ἰ' => 'ἰ',
+ 'Ἱ' => 'ἱ',
+ 'Ἲ' => 'ἲ',
+ 'Ἳ' => 'ἳ',
+ 'Ἴ' => 'ἴ',
+ 'Ἵ' => 'ἵ',
+ 'Ἶ' => 'ἶ',
+ 'Ἷ' => 'ἷ',
+ 'Ὀ' => 'ὀ',
+ 'Ὁ' => 'ὁ',
+ 'Ὂ' => 'ὂ',
+ 'Ὃ' => 'ὃ',
+ 'Ὄ' => 'ὄ',
+ 'Ὅ' => 'ὅ',
+ 'Ὑ' => 'ὑ',
+ 'Ὓ' => 'ὓ',
+ 'Ὕ' => 'ὕ',
+ 'Ὗ' => 'ὗ',
+ 'Ὡ' => 'ὡ',
+ 'Ὢ' => 'ὢ',
+ 'Ὣ' => 'ὣ',
+ 'Ὤ' => 'ὤ',
+ 'Ὥ' => 'ὥ',
+ 'Ὦ' => 'ὦ',
+ 'Ὧ' => 'ὧ',
+ 'ᾈ' => 'ᾀ',
+ 'ᾉ' => 'ᾁ',
+ 'ᾊ' => 'ᾂ',
+ 'ᾋ' => 'ᾃ',
+ 'ᾌ' => 'ᾄ',
+ 'ᾍ' => 'ᾅ',
+ 'ᾎ' => 'ᾆ',
+ 'ᾏ' => 'ᾇ',
+ 'ᾘ' => 'ᾐ',
+ 'ᾙ' => 'ᾑ',
+ 'ᾚ' => 'ᾒ',
+ 'ᾛ' => 'ᾓ',
+ 'ᾜ' => 'ᾔ',
+ 'ᾝ' => 'ᾕ',
+ 'ᾞ' => 'ᾖ',
+ 'ᾟ' => 'ᾗ',
+ 'ᾩ' => 'ᾡ',
+ 'ᾪ' => 'ᾢ',
+ 'ᾫ' => 'ᾣ',
+ 'ᾬ' => 'ᾤ',
+ 'ᾭ' => 'ᾥ',
+ 'ᾮ' => 'ᾦ',
+ 'ᾯ' => 'ᾧ',
+ 'Ᾰ' => 'ᾰ',
+ 'Ᾱ' => 'ᾱ',
+ 'Ὰ' => 'ὰ',
+ 'ᾼ' => 'ᾳ',
+ 'Ὲ' => 'ὲ',
+ 'Ὴ' => 'ὴ',
+ 'ῌ' => 'ῃ',
+ 'Ῐ' => 'ῐ',
+ 'Ῑ' => 'ῑ',
+ 'Ὶ' => 'ὶ',
+ 'Ῡ' => 'ῡ',
+ 'Ὺ' => 'ὺ',
+ 'Ῥ' => 'ῥ',
+ 'Ὸ' => 'ὸ',
+ 'Ὼ' => 'ὼ',
+ 'ῼ' => 'ῳ',
+ 'A' => 'a',
+ 'B' => 'b',
+ 'C' => 'c',
+ 'D' => 'd',
+ 'E' => 'e',
+ 'F' => 'f',
+ 'G' => 'g',
+ 'H' => 'h',
+ 'I' => 'i',
+ 'J' => 'j',
+ 'K' => 'k',
+ 'L' => 'l',
+ 'M' => 'm',
+ 'N' => 'n',
+ 'O' => 'o',
+ 'P' => 'p',
+ 'Q' => 'q',
+ 'R' => 'r',
+ 'S' => 's',
+ 'T' => 't',
+ 'U' => 'u',
+ 'V' => 'v',
+ 'W' => 'w',
+ 'X' => 'x',
+ 'Y' => 'y',
+ 'Z' => 'z',
+];
diff --git a/platform/www/inc/Utf8/tables/loweraccents.php b/platform/www/inc/Utf8/tables/loweraccents.php
new file mode 100644
index 0000000..cc3ec8e
--- /dev/null
+++ b/platform/www/inc/Utf8/tables/loweraccents.php
@@ -0,0 +1,116 @@
+<?php
+/**
+ * UTF-8 lookup table for lower case accented letters
+ *
+ * This lookuptable defines replacements for accented characters from the ASCII-7
+ * range. This are lower case letters only.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @see \dokuwiki\Utf8\Clean::deaccent()
+ */
+return [
+ 'á' => 'a',
+ 'à' => 'a',
+ 'ă' => 'a',
+ 'â' => 'a',
+ 'å' => 'a',
+ 'ä' => 'ae',
+ 'ã' => 'a',
+ 'ą' => 'a',
+ 'ā' => 'a',
+ 'æ' => 'ae',
+ 'ḃ' => 'b',
+ 'ć' => 'c',
+ 'ĉ' => 'c',
+ 'č' => 'c',
+ 'ċ' => 'c',
+ 'ç' => 'c',
+ 'ď' => 'd',
+ 'ḋ' => 'd',
+ 'đ' => 'd',
+ 'ð' => 'dh',
+ 'é' => 'e',
+ 'è' => 'e',
+ 'ĕ' => 'e',
+ 'ê' => 'e',
+ 'ě' => 'e',
+ 'ë' => 'e',
+ 'ė' => 'e',
+ 'ę' => 'e',
+ 'ē' => 'e',
+ 'ḟ' => 'f',
+ 'ƒ' => 'f',
+ 'ğ' => 'g',
+ 'ĝ' => 'g',
+ 'ġ' => 'g',
+ 'ģ' => 'g',
+ 'ĥ' => 'h',
+ 'ħ' => 'h',
+ 'í' => 'i',
+ 'ì' => 'i',
+ 'î' => 'i',
+ 'ï' => 'i',
+ 'ĩ' => 'i',
+ 'į' => 'i',
+ 'ī' => 'i',
+ 'ĵ' => 'j',
+ 'ķ' => 'k',
+ 'ĺ' => 'l',
+ 'ľ' => 'l',
+ 'ļ' => 'l',
+ 'ł' => 'l',
+ 'ṁ' => 'm',
+ 'ń' => 'n',
+ 'ň' => 'n',
+ 'ñ' => 'n',
+ 'ņ' => 'n',
+ 'ó' => 'o',
+ 'ò' => 'o',
+ 'ô' => 'o',
+ 'ö' => 'oe',
+ 'ő' => 'o',
+ 'õ' => 'o',
+ 'ø' => 'o',
+ 'ō' => 'o',
+ 'ơ' => 'o',
+ 'ṗ' => 'p',
+ 'ŕ' => 'r',
+ 'ř' => 'r',
+ 'ŗ' => 'r',
+ 'ś' => 's',
+ 'ŝ' => 's',
+ 'š' => 's',
+ 'ṡ' => 's',
+ 'ş' => 's',
+ 'ș' => 's',
+ 'ß' => 'ss',
+ 'ť' => 't',
+ 'ṫ' => 't',
+ 'ţ' => 't',
+ 'ț' => 't',
+ 'ŧ' => 't',
+ 'ú' => 'u',
+ 'ù' => 'u',
+ 'ŭ' => 'u',
+ 'û' => 'u',
+ 'ů' => 'u',
+ 'ü' => 'ue',
+ 'ű' => 'u',
+ 'ũ' => 'u',
+ 'ų' => 'u',
+ 'ū' => 'u',
+ 'ư' => 'u',
+ 'ẃ' => 'w',
+ 'ẁ' => 'w',
+ 'ŵ' => 'w',
+ 'ẅ' => 'w',
+ 'ý' => 'y',
+ 'ỳ' => 'y',
+ 'ŷ' => 'y',
+ 'ÿ' => 'y',
+ 'ź' => 'z',
+ 'ž' => 'z',
+ 'ż' => 'z',
+ 'þ' => 'th',
+ 'µ' => 'u',
+];
diff --git a/platform/www/inc/Utf8/tables/romanization.php b/platform/www/inc/Utf8/tables/romanization.php
new file mode 100644
index 0000000..e757b9c
--- /dev/null
+++ b/platform/www/inc/Utf8/tables/romanization.php
@@ -0,0 +1,1458 @@
+<?php
+/**
+ * Romanization lookup table
+ *
+ * This lookup tables provides a way to transform strings written in a language
+ * different from the ones based upon latin letters into plain ASCII.
+ *
+ * Please note: this is not a scientific transliteration table. It only works
+ * oneway from nonlatin to ASCII and it works by simple character replacement
+ * only. Specialities of each language are not supported.
+ *
+ * @todo some keys are used multiple times
+ * @todo remove or integrate commented pairs
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Vitaly Blokhin <vitinfo@vitn.com>
+ * @author Bisqwit <bisqwit@iki.fi>
+ * @author Arthit Suriyawongkul <arthit@gmail.com>
+ * @author Denis Scheither <amorphis@uni-bremen.de>
+ * @author Eivind Morland <eivind.morland@gmail.com>
+ * @link http://www.uconv.com/translit.htm
+ * @link http://kanjidict.stc.cx/hiragana.php?src=2
+ * @link http://www.translatum.gr/converter/greek-transliteration.htm
+ * @link http://en.wikipedia.org/wiki/Royal_Thai_General_System_of_Transcription
+ * @link http://www.btranslations.com/resources/romanization/korean.asp
+ */
+return [
+ // scandinavian - differs from what we do in deaccent
+ 'å' => 'a',
+ 'Å' => 'A',
+ 'ä' => 'a',
+ 'Ä' => 'A',
+ 'ö' => 'o',
+ 'Ö' => 'O',
+
+ //russian cyrillic
+ 'а' => 'a',
+ 'А' => 'A',
+ 'б' => 'b',
+ 'Б' => 'B',
+ 'в' => 'v',
+ 'В' => 'V',
+ 'г' => 'g',
+ 'Г' => 'G',
+ 'д' => 'd',
+ 'Д' => 'D',
+ 'е' => 'e',
+ 'Е' => 'E',
+ 'ё' => 'jo',
+ 'Ё' => 'Jo',
+ 'ж' => 'zh',
+ 'Ж' => 'Zh',
+ 'з' => 'z',
+ 'З' => 'Z',
+ 'и' => 'i',
+ 'И' => 'I',
+ 'й' => 'j',
+ 'Й' => 'J',
+ 'к' => 'k',
+ 'К' => 'K',
+ 'л' => 'l',
+ 'Л' => 'L',
+ 'м' => 'm',
+ 'М' => 'M',
+ 'н' => 'n',
+ 'Н' => 'N',
+ 'о' => 'o',
+ 'О' => 'O',
+ 'п' => 'p',
+ 'П' => 'P',
+ 'р' => 'r',
+ 'Р' => 'R',
+ 'с' => 's',
+ 'С' => 'S',
+ 'т' => 't',
+ 'Т' => 'T',
+ 'у' => 'u',
+ 'У' => 'U',
+ 'ф' => 'f',
+ 'Ф' => 'F',
+ 'х' => 'x',
+ 'Х' => 'X',
+ 'ц' => 'c',
+ 'Ц' => 'C',
+ 'ч' => 'ch',
+ 'Ч' => 'Ch',
+ 'ш' => 'sh',
+ 'Ш' => 'Sh',
+ 'щ' => 'sch',
+ 'Щ' => 'Sch',
+ 'ъ' => '',
+ 'Ъ' => '',
+ 'ы' => 'y',
+ 'Ы' => 'Y',
+ 'ь' => '',
+ 'Ь' => '',
+ 'э' => 'eh',
+ 'Э' => 'Eh',
+ 'ю' => 'ju',
+ 'Ю' => 'Ju',
+ 'я' => 'ja',
+ 'Я' => 'Ja',
+
+ // Ukrainian cyrillic
+ 'Ґ' => 'Gh',
+ 'ґ' => 'gh',
+ 'Є' => 'Je',
+ 'є' => 'je',
+ 'І' => 'I',
+ 'і' => 'i',
+ 'Ї' => 'Ji',
+ 'ї' => 'ji',
+
+ // Georgian
+ 'ა' => 'a',
+ 'ბ' => 'b',
+ 'გ' => 'g',
+ 'დ' => 'd',
+ 'ე' => 'e',
+ 'ვ' => 'v',
+ 'ზ' => 'z',
+ 'თ' => 'th',
+ 'ი' => 'i',
+ 'კ' => 'p',
+ 'ლ' => 'l',
+ 'მ' => 'm',
+ 'ნ' => 'n',
+ 'ო' => 'o',
+ 'პ' => 'p',
+ 'ჟ' => 'zh',
+ 'რ' => 'r',
+ 'ს' => 's',
+ 'ტ' => 't',
+ 'უ' => 'u',
+ 'ფ' => 'ph',
+ 'ქ' => 'kh',
+ 'ღ' => 'gh',
+ 'ყ' => 'q',
+ 'შ' => 'sh',
+ 'ჩ' => 'ch',
+ 'ც' => 'c',
+ 'ძ' => 'dh',
+ 'წ' => 'w',
+ 'ჭ' => 'j',
+ 'ხ' => 'x',
+ 'ჯ' => 'jh',
+ 'ჰ' => 'xh',
+
+ //Sanskrit
+ 'अ' => 'a',
+ 'आ' => 'ah',
+ 'इ' => 'i',
+ 'ई' => 'ih',
+ 'उ' => 'u',
+ 'ऊ' => 'uh',
+ 'ऋ' => 'ry',
+ 'ॠ' => 'ryh',
+ 'ऌ' => 'ly',
+ 'ॡ' => 'lyh',
+ 'ए' => 'e',
+ 'ऐ' => 'ay',
+ 'ओ' => 'o',
+ 'औ' => 'aw',
+ 'अं' => 'amh',
+ 'अः' => 'aq',
+ 'क' => 'k',
+ 'ख' => 'kh',
+ 'ग' => 'g',
+ 'घ' => 'gh',
+ 'ङ' => 'nh',
+ 'च' => 'c',
+ 'छ' => 'ch',
+ 'ज' => 'j',
+ 'झ' => 'jh',
+ 'ञ' => 'ny',
+ 'ट' => 'tq',
+ 'ठ' => 'tqh',
+ 'ड' => 'dq',
+ 'ढ' => 'dqh',
+ 'ण' => 'nq',
+ 'त' => 't',
+ 'थ' => 'th',
+ 'द' => 'd',
+ 'ध' => 'dh',
+ 'न' => 'n',
+ 'प' => 'p',
+ 'फ' => 'ph',
+ 'ब' => 'b',
+ 'भ' => 'bh',
+ 'म' => 'm',
+ 'य' => 'z',
+ 'र' => 'r',
+ 'ल' => 'l',
+ 'व' => 'v',
+ 'श' => 'sh',
+ 'ष' => 'sqh',
+ 'स' => 's',
+ 'ह' => 'x',
+
+ //Sanskrit diacritics
+ 'Ā' => 'A',
+ 'Ī' => 'I',
+ 'Ū' => 'U',
+ 'Ṛ' => 'R',
+ 'Ṝ' => 'R',
+ 'Ṅ' => 'N',
+ 'Ñ' => 'N',
+ 'Ṭ' => 'T',
+ 'Ḍ' => 'D',
+ 'Ṇ' => 'N',
+ 'Ś' => 'S',
+ 'Ṣ' => 'S',
+ 'Ṁ' => 'M',
+ 'Ṃ' => 'M',
+ 'Ḥ' => 'H',
+ 'Ḷ' => 'L',
+ 'Ḹ' => 'L',
+ 'ā' => 'a',
+ 'ī' => 'i',
+ 'ū' => 'u',
+ 'ṛ' => 'r',
+ 'ṝ' => 'r',
+ 'ṅ' => 'n',
+ 'ñ' => 'n',
+ 'ṭ' => 't',
+ 'ḍ' => 'd',
+ 'ṇ' => 'n',
+ 'ś' => 's',
+ 'ṣ' => 's',
+ 'ṁ' => 'm',
+ 'ṃ' => 'm',
+ 'ḥ' => 'h',
+ 'ḷ' => 'l',
+ 'ḹ' => 'l',
+
+ //Hebrew
+ 'א' => 'a',
+ 'ב' => 'b',
+ 'ג' => 'g',
+ 'ד' => 'd',
+ 'ה' => 'h',
+ 'ו' => 'v',
+ 'ז' => 'z',
+ 'ח' => 'kh',
+ 'ט' => 'th',
+ 'י' => 'y',
+ 'ך' => 'h',
+ 'כ' => 'k',
+ 'ל' => 'l',
+ 'ם' => 'm',
+ 'מ' => 'm',
+ 'ן' => 'n',
+ 'נ' => 'n',
+ 'ס' => 's',
+ 'ע' => 'ah',
+ 'ף' => 'f',
+ 'פ' => 'p',
+ 'ץ' => 'c',
+ 'צ' => 'c',
+ 'ק' => 'q',
+ 'ר' => 'r',
+ 'ש' => 'sh',
+ 'ת' => 't',
+
+ //Arabic
+ 'ا' => 'a',
+ 'ب' => 'b',
+ 'ت' => 't',
+ 'ث' => 'th',
+ 'ج' => 'g',
+ 'ح' => 'xh',
+ 'خ' => 'x',
+ 'د' => 'd',
+ 'ذ' => 'dh',
+ 'ر' => 'r',
+ 'ز' => 'z',
+ 'س' => 's',
+ 'ش' => 'sh',
+ 'ص' => 's\'',
+ 'ض' => 'd\'',
+ 'ط' => 't\'',
+ 'ظ' => 'z\'',
+ 'ع' => 'y',
+ 'غ' => 'gh',
+ 'ف' => 'f',
+ 'ق' => 'q',
+ 'ك' => 'k',
+ 'ل' => 'l',
+ 'م' => 'm',
+ 'ن' => 'n',
+ 'ه' => 'x\'',
+ 'و' => 'u',
+ 'ي' => 'i',
+
+ // Japanese characters (last update: 2008-05-09)
+
+ // Japanese hiragana
+
+ // 3 character syllables, っ doubles the consonant after
+ 'っちゃ' => 'ccha',
+ 'っちぇ' => 'cche',
+ 'っちょ' => 'ccho',
+ 'っちゅ' => 'cchu',
+ 'っびゃ' => 'bbya',
+ 'っびぇ' => 'bbye',
+ 'っびぃ' => 'bbyi',
+ 'っびょ' => 'bbyo',
+ 'っびゅ' => 'bbyu',
+ 'っぴゃ' => 'ppya',
+ 'っぴぇ' => 'ppye',
+ 'っぴぃ' => 'ppyi',
+ 'っぴょ' => 'ppyo',
+ 'っぴゅ' => 'ppyu',
+ 'っちゃ' => 'ccha',
+ 'っちぇ' => 'cche',
+ 'っち' => 'cchi',
+ 'っちょ' => 'ccho',
+ 'っちゅ' => 'cchu',
+ // 'っひゃ'=>'hya',
+ // 'っひぇ'=>'hye',
+ // 'っひぃ'=>'hyi',
+ // 'っひょ'=>'hyo',
+ // 'っひゅ'=>'hyu',
+ 'っきゃ' => 'kkya',
+ 'っきぇ' => 'kkye',
+ 'っきぃ' => 'kkyi',
+ 'っきょ' => 'kkyo',
+ 'っきゅ' => 'kkyu',
+ 'っぎゃ' => 'ggya',
+ 'っぎぇ' => 'ggye',
+ 'っぎぃ' => 'ggyi',
+ 'っぎょ' => 'ggyo',
+ 'っぎゅ' => 'ggyu',
+ 'っみゃ' => 'mmya',
+ 'っみぇ' => 'mmye',
+ 'っみぃ' => 'mmyi',
+ 'っみょ' => 'mmyo',
+ 'っみゅ' => 'mmyu',
+ 'っにゃ' => 'nnya',
+ 'っにぇ' => 'nnye',
+ 'っにぃ' => 'nnyi',
+ 'っにょ' => 'nnyo',
+ 'っにゅ' => 'nnyu',
+ 'っりゃ' => 'rrya',
+ 'っりぇ' => 'rrye',
+ 'っりぃ' => 'rryi',
+ 'っりょ' => 'rryo',
+ 'っりゅ' => 'rryu',
+ 'っしゃ' => 'ssha',
+ 'っしぇ' => 'sshe',
+ 'っし' => 'sshi',
+ 'っしょ' => 'ssho',
+ 'っしゅ' => 'sshu',
+
+ // seperate hiragana 'n' ('n' + 'i' != 'ni', normally we would write "kon'nichi wa" but the
+ // apostrophe would be converted to _ anyway)
+ 'んあ' => 'n_a',
+ 'んえ' => 'n_e',
+ 'んい' => 'n_i',
+ 'んお' => 'n_o',
+ 'んう' => 'n_u',
+ 'んや' => 'n_ya',
+ 'んよ' => 'n_yo',
+ 'んゆ' => 'n_yu',
+
+ // 2 character syllables - normal
+ 'ふぁ' => 'fa',
+ 'ふぇ' => 'fe',
+ 'ふぃ' => 'fi',
+ 'ふぉ' => 'fo',
+ 'ちゃ' => 'cha',
+ 'ちぇ' => 'che',
+ 'ち' => 'chi',
+ 'ちょ' => 'cho',
+ 'ちゅ' => 'chu',
+ 'ひゃ' => 'hya',
+ 'ひぇ' => 'hye',
+ 'ひぃ' => 'hyi',
+ 'ひょ' => 'hyo',
+ 'ひゅ' => 'hyu',
+ 'びゃ' => 'bya',
+ 'びぇ' => 'bye',
+ 'びぃ' => 'byi',
+ 'びょ' => 'byo',
+ 'びゅ' => 'byu',
+ 'ぴゃ' => 'pya',
+ 'ぴぇ' => 'pye',
+ 'ぴぃ' => 'pyi',
+ 'ぴょ' => 'pyo',
+ 'ぴゅ' => 'pyu',
+ 'きゃ' => 'kya',
+ 'きぇ' => 'kye',
+ 'きぃ' => 'kyi',
+ 'きょ' => 'kyo',
+ 'きゅ' => 'kyu',
+ 'ぎゃ' => 'gya',
+ 'ぎぇ' => 'gye',
+ 'ぎぃ' => 'gyi',
+ 'ぎょ' => 'gyo',
+ 'ぎゅ' => 'gyu',
+ 'みゃ' => 'mya',
+ 'みぇ' => 'mye',
+ 'みぃ' => 'myi',
+ 'みょ' => 'myo',
+ 'みゅ' => 'myu',
+ 'にゃ' => 'nya',
+ 'にぇ' => 'nye',
+ 'にぃ' => 'nyi',
+ 'にょ' => 'nyo',
+ 'にゅ' => 'nyu',
+ 'りゃ' => 'rya',
+ 'りぇ' => 'rye',
+ 'りぃ' => 'ryi',
+ 'りょ' => 'ryo',
+ 'りゅ' => 'ryu',
+ 'しゃ' => 'sha',
+ 'しぇ' => 'she',
+ 'し' => 'shi',
+ 'しょ' => 'sho',
+ 'しゅ' => 'shu',
+ 'じゃ' => 'ja',
+ 'じぇ' => 'je',
+ 'じょ' => 'jo',
+ 'じゅ' => 'ju',
+ 'うぇ' => 'we',
+ 'うぃ' => 'wi',
+ 'いぇ' => 'ye',
+
+ // 2 character syllables, っ doubles the consonant after
+ 'っば' => 'bba',
+ 'っべ' => 'bbe',
+ 'っび' => 'bbi',
+ 'っぼ' => 'bbo',
+ 'っぶ' => 'bbu',
+ 'っぱ' => 'ppa',
+ 'っぺ' => 'ppe',
+ 'っぴ' => 'ppi',
+ 'っぽ' => 'ppo',
+ 'っぷ' => 'ppu',
+ 'った' => 'tta',
+ 'って' => 'tte',
+ 'っち' => 'cchi',
+ 'っと' => 'tto',
+ 'っつ' => 'ttsu',
+ 'っだ' => 'dda',
+ 'っで' => 'dde',
+ 'っぢ' => 'ddi',
+ 'っど' => 'ddo',
+ 'っづ' => 'ddu',
+ 'っが' => 'gga',
+ 'っげ' => 'gge',
+ 'っぎ' => 'ggi',
+ 'っご' => 'ggo',
+ 'っぐ' => 'ggu',
+ 'っか' => 'kka',
+ 'っけ' => 'kke',
+ 'っき' => 'kki',
+ 'っこ' => 'kko',
+ 'っく' => 'kku',
+ 'っま' => 'mma',
+ 'っめ' => 'mme',
+ 'っみ' => 'mmi',
+ 'っも' => 'mmo',
+ 'っむ' => 'mmu',
+ 'っな' => 'nna',
+ 'っね' => 'nne',
+ 'っに' => 'nni',
+ 'っの' => 'nno',
+ 'っぬ' => 'nnu',
+ 'っら' => 'rra',
+ 'っれ' => 'rre',
+ 'っり' => 'rri',
+ 'っろ' => 'rro',
+ 'っる' => 'rru',
+ 'っさ' => 'ssa',
+ 'っせ' => 'sse',
+ 'っし' => 'sshi',
+ 'っそ' => 'sso',
+ 'っす' => 'ssu',
+ 'っざ' => 'zza',
+ 'っぜ' => 'zze',
+ 'っじ' => 'jji',
+ 'っぞ' => 'zzo',
+ 'っず' => 'zzu',
+
+ // 1 character syllabels
+ 'あ' => 'a',
+ 'え' => 'e',
+ 'い' => 'i',
+ 'お' => 'o',
+ 'う' => 'u',
+ 'ん' => 'n',
+ 'は' => 'ha',
+ 'へ' => 'he',
+ 'ひ' => 'hi',
+ 'ほ' => 'ho',
+ 'ふ' => 'fu',
+ 'ば' => 'ba',
+ 'べ' => 'be',
+ 'び' => 'bi',
+ 'ぼ' => 'bo',
+ 'ぶ' => 'bu',
+ 'ぱ' => 'pa',
+ 'ぺ' => 'pe',
+ 'ぴ' => 'pi',
+ 'ぽ' => 'po',
+ 'ぷ' => 'pu',
+ 'た' => 'ta',
+ 'て' => 'te',
+ 'ち' => 'chi',
+ 'と' => 'to',
+ 'つ' => 'tsu',
+ 'だ' => 'da',
+ 'で' => 'de',
+ 'ぢ' => 'di',
+ 'ど' => 'do',
+ 'づ' => 'du',
+ 'が' => 'ga',
+ 'げ' => 'ge',
+ 'ぎ' => 'gi',
+ 'ご' => 'go',
+ 'ぐ' => 'gu',
+ 'か' => 'ka',
+ 'け' => 'ke',
+ 'き' => 'ki',
+ 'こ' => 'ko',
+ 'く' => 'ku',
+ 'ま' => 'ma',
+ 'め' => 'me',
+ 'み' => 'mi',
+ 'も' => 'mo',
+ 'む' => 'mu',
+ 'な' => 'na',
+ 'ね' => 'ne',
+ 'に' => 'ni',
+ 'の' => 'no',
+ 'ぬ' => 'nu',
+ 'ら' => 'ra',
+ 'れ' => 're',
+ 'り' => 'ri',
+ 'ろ' => 'ro',
+ 'る' => 'ru',
+ 'さ' => 'sa',
+ 'せ' => 'se',
+ 'し' => 'shi',
+ 'そ' => 'so',
+ 'す' => 'su',
+ 'わ' => 'wa',
+ 'を' => 'wo',
+ 'ざ' => 'za',
+ 'ぜ' => 'ze',
+ 'じ' => 'ji',
+ 'ぞ' => 'zo',
+ 'ず' => 'zu',
+ 'や' => 'ya',
+ 'よ' => 'yo',
+ 'ゆ' => 'yu',
+ // old characters
+ 'ゑ' => 'we',
+ 'ゐ' => 'wi',
+
+ // convert what's left (probably only kicks in when something's missing above)
+ // 'ぁ'=>'a','ぇ'=>'e','ぃ'=>'i','ぉ'=>'o','ぅ'=>'u',
+ // 'ゃ'=>'ya','ょ'=>'yo','ゅ'=>'yu',
+
+ // never seen one of those (disabled for the moment)
+ // 'ヴぁ'=>'va','ヴぇ'=>'ve','ヴぃ'=>'vi','ヴぉ'=>'vo','ヴ'=>'vu',
+ // 'でゃ'=>'dha','でぇ'=>'dhe','でぃ'=>'dhi','でょ'=>'dho','でゅ'=>'dhu',
+ // 'どぁ'=>'dwa','どぇ'=>'dwe','どぃ'=>'dwi','どぉ'=>'dwo','どぅ'=>'dwu',
+ // 'ぢゃ'=>'dya','ぢぇ'=>'dye','ぢぃ'=>'dyi','ぢょ'=>'dyo','ぢゅ'=>'dyu',
+ // 'ふぁ'=>'fwa','ふぇ'=>'fwe','ふぃ'=>'fwi','ふぉ'=>'fwo','ふぅ'=>'fwu',
+ // 'ふゃ'=>'fya','ふぇ'=>'fye','ふぃ'=>'fyi','ふょ'=>'fyo','ふゅ'=>'fyu',
+ // 'すぁ'=>'swa','すぇ'=>'swe','すぃ'=>'swi','すぉ'=>'swo','すぅ'=>'swu',
+ // 'てゃ'=>'tha','てぇ'=>'the','てぃ'=>'thi','てょ'=>'tho','てゅ'=>'thu',
+ // 'つゃ'=>'tsa','つぇ'=>'tse','つぃ'=>'tsi','つょ'=>'tso','つ'=>'tsu',
+ // 'とぁ'=>'twa','とぇ'=>'twe','とぃ'=>'twi','とぉ'=>'two','とぅ'=>'twu',
+ // 'ヴゃ'=>'vya','ヴぇ'=>'vye','ヴぃ'=>'vyi','ヴょ'=>'vyo','ヴゅ'=>'vyu',
+ // 'うぁ'=>'wha','うぇ'=>'whe','うぃ'=>'whi','うぉ'=>'who','うぅ'=>'whu',
+ // 'じゃ'=>'zha','じぇ'=>'zhe','じぃ'=>'zhi','じょ'=>'zho','じゅ'=>'zhu',
+ // 'じゃ'=>'zya','じぇ'=>'zye','じぃ'=>'zyi','じょ'=>'zyo','じゅ'=>'zyu',
+
+ // 'spare' characters from other romanization systems
+ // 'だ'=>'da','で'=>'de','ぢ'=>'di','ど'=>'do','づ'=>'du',
+ // 'ら'=>'la','れ'=>'le','り'=>'li','ろ'=>'lo','る'=>'lu',
+ // 'さ'=>'sa','せ'=>'se','し'=>'si','そ'=>'so','す'=>'su',
+ // 'ちゃ'=>'cya','ちぇ'=>'cye','ちぃ'=>'cyi','ちょ'=>'cyo','ちゅ'=>'cyu',
+ //'じゃ'=>'jya','じぇ'=>'jye','じぃ'=>'jyi','じょ'=>'jyo','じゅ'=>'jyu',
+ //'りゃ'=>'lya','りぇ'=>'lye','りぃ'=>'lyi','りょ'=>'lyo','りゅ'=>'lyu',
+ //'しゃ'=>'sya','しぇ'=>'sye','しぃ'=>'syi','しょ'=>'syo','しゅ'=>'syu',
+ //'ちゃ'=>'tya','ちぇ'=>'tye','ちぃ'=>'tyi','ちょ'=>'tyo','ちゅ'=>'tyu',
+ //'し'=>'ci',,い'=>'yi','ぢ'=>'dzi',
+ //'っじゃ'=>'jja','っじぇ'=>'jje','っじ'=>'jji','っじょ'=>'jjo','っじゅ'=>'jju',
+
+
+ // Japanese katakana
+
+ // 4 character syllables: ッ doubles the consonant after, ー doubles the vowel before
+ // (usualy written with macron, but we don't want that in our URLs)
+ 'ッビャー' => 'bbyaa',
+ 'ッビェー' => 'bbyee',
+ 'ッビィー' => 'bbyii',
+ 'ッビョー' => 'bbyoo',
+ 'ッビュー' => 'bbyuu',
+ 'ッピャー' => 'ppyaa',
+ 'ッピェー' => 'ppyee',
+ 'ッピィー' => 'ppyii',
+ 'ッピョー' => 'ppyoo',
+ 'ッピュー' => 'ppyuu',
+ 'ッキャー' => 'kkyaa',
+ 'ッキェー' => 'kkyee',
+ 'ッキィー' => 'kkyii',
+ 'ッキョー' => 'kkyoo',
+ 'ッキュー' => 'kkyuu',
+ 'ッギャー' => 'ggyaa',
+ 'ッギェー' => 'ggyee',
+ 'ッギィー' => 'ggyii',
+ 'ッギョー' => 'ggyoo',
+ 'ッギュー' => 'ggyuu',
+ 'ッミャー' => 'mmyaa',
+ 'ッミェー' => 'mmyee',
+ 'ッミィー' => 'mmyii',
+ 'ッミョー' => 'mmyoo',
+ 'ッミュー' => 'mmyuu',
+ 'ッニャー' => 'nnyaa',
+ 'ッニェー' => 'nnyee',
+ 'ッニィー' => 'nnyii',
+ 'ッニョー' => 'nnyoo',
+ 'ッニュー' => 'nnyuu',
+ 'ッリャー' => 'rryaa',
+ 'ッリェー' => 'rryee',
+ 'ッリィー' => 'rryii',
+ 'ッリョー' => 'rryoo',
+ 'ッリュー' => 'rryuu',
+ 'ッシャー' => 'sshaa',
+ 'ッシェー' => 'sshee',
+ 'ッシー' => 'sshii',
+ 'ッショー' => 'sshoo',
+ 'ッシュー' => 'sshuu',
+ 'ッチャー' => 'cchaa',
+ 'ッチェー' => 'cchee',
+ 'ッチー' => 'cchii',
+ 'ッチョー' => 'cchoo',
+ 'ッチュー' => 'cchuu',
+ 'ッティー' => 'ttii',
+ 'ッヂィー' => 'ddii',
+
+ // 3 character syllables - doubled vowels
+ 'ファー' => 'faa',
+ 'フェー' => 'fee',
+ 'フィー' => 'fii',
+ 'フォー' => 'foo',
+ 'フャー' => 'fyaa',
+ 'フェー' => 'fyee',
+ 'フィー' => 'fyii',
+ 'フョー' => 'fyoo',
+ 'フュー' => 'fyuu',
+ 'ヒャー' => 'hyaa',
+ 'ヒェー' => 'hyee',
+ 'ヒィー' => 'hyii',
+ 'ヒョー' => 'hyoo',
+ 'ヒュー' => 'hyuu',
+ 'ビャー' => 'byaa',
+ 'ビェー' => 'byee',
+ 'ビィー' => 'byii',
+ 'ビョー' => 'byoo',
+ 'ビュー' => 'byuu',
+ 'ピャー' => 'pyaa',
+ 'ピェー' => 'pyee',
+ 'ピィー' => 'pyii',
+ 'ピョー' => 'pyoo',
+ 'ピュー' => 'pyuu',
+ 'キャー' => 'kyaa',
+ 'キェー' => 'kyee',
+ 'キィー' => 'kyii',
+ 'キョー' => 'kyoo',
+ 'キュー' => 'kyuu',
+ 'ギャー' => 'gyaa',
+ 'ギェー' => 'gyee',
+ 'ギィー' => 'gyii',
+ 'ギョー' => 'gyoo',
+ 'ギュー' => 'gyuu',
+ 'ミャー' => 'myaa',
+ 'ミェー' => 'myee',
+ 'ミィー' => 'myii',
+ 'ミョー' => 'myoo',
+ 'ミュー' => 'myuu',
+ 'ニャー' => 'nyaa',
+ 'ニェー' => 'nyee',
+ 'ニィー' => 'nyii',
+ 'ニョー' => 'nyoo',
+ 'ニュー' => 'nyuu',
+ 'リャー' => 'ryaa',
+ 'リェー' => 'ryee',
+ 'リィー' => 'ryii',
+ 'リョー' => 'ryoo',
+ 'リュー' => 'ryuu',
+ 'シャー' => 'shaa',
+ 'シェー' => 'shee',
+ 'シー' => 'shii',
+ 'ショー' => 'shoo',
+ 'シュー' => 'shuu',
+ 'ジャー' => 'jaa',
+ 'ジェー' => 'jee',
+ 'ジー' => 'jii',
+ 'ジョー' => 'joo',
+ 'ジュー' => 'juu',
+ 'スァー' => 'swaa',
+ 'スェー' => 'swee',
+ 'スィー' => 'swii',
+ 'スォー' => 'swoo',
+ 'スゥー' => 'swuu',
+ 'デァー' => 'daa',
+ 'デェー' => 'dee',
+ 'ディー' => 'dii',
+ 'デォー' => 'doo',
+ 'デゥー' => 'duu',
+ 'チャー' => 'chaa',
+ 'チェー' => 'chee',
+ 'チー' => 'chii',
+ 'チョー' => 'choo',
+ 'チュー' => 'chuu',
+ 'ヂャー' => 'dyaa',
+ 'ヂェー' => 'dyee',
+ 'ヂィー' => 'dyii',
+ 'ヂョー' => 'dyoo',
+ 'ヂュー' => 'dyuu',
+ 'ツャー' => 'tsaa',
+ 'ツェー' => 'tsee',
+ 'ツィー' => 'tsii',
+ 'ツョー' => 'tsoo',
+ 'ツー' => 'tsuu',
+ 'トァー' => 'twaa',
+ 'トェー' => 'twee',
+ 'トィー' => 'twii',
+ 'トォー' => 'twoo',
+ 'トゥー' => 'twuu',
+ 'ドァー' => 'dwaa',
+ 'ドェー' => 'dwee',
+ 'ドィー' => 'dwii',
+ 'ドォー' => 'dwoo',
+ 'ドゥー' => 'dwuu',
+ 'ウァー' => 'whaa',
+ 'ウェー' => 'whee',
+ 'ウィー' => 'whii',
+ 'ウォー' => 'whoo',
+ 'ウゥー' => 'whuu',
+ 'ヴャー' => 'vyaa',
+ 'ヴェー' => 'vyee',
+ 'ヴィー' => 'vyii',
+ 'ヴョー' => 'vyoo',
+ 'ヴュー' => 'vyuu',
+ 'ヴァー' => 'vaa',
+ 'ヴェー' => 'vee',
+ 'ヴィー' => 'vii',
+ 'ヴォー' => 'voo',
+ 'ヴー' => 'vuu',
+ 'ウェー' => 'wee',
+ 'ウィー' => 'wii',
+ 'イェー' => 'yee',
+ 'ティー' => 'tii',
+ 'ヂィー' => 'dii',
+
+ // 3 character syllables - doubled consonants
+ 'ッビャ' => 'bbya',
+ 'ッビェ' => 'bbye',
+ 'ッビィ' => 'bbyi',
+ 'ッビョ' => 'bbyo',
+ 'ッビュ' => 'bbyu',
+ 'ッピャ' => 'ppya',
+ 'ッピェ' => 'ppye',
+ 'ッピィ' => 'ppyi',
+ 'ッピョ' => 'ppyo',
+ 'ッピュ' => 'ppyu',
+ 'ッキャ' => 'kkya',
+ 'ッキェ' => 'kkye',
+ 'ッキィ' => 'kkyi',
+ 'ッキョ' => 'kkyo',
+ 'ッキュ' => 'kkyu',
+ 'ッギャ' => 'ggya',
+ 'ッギェ' => 'ggye',
+ 'ッギィ' => 'ggyi',
+ 'ッギョ' => 'ggyo',
+ 'ッギュ' => 'ggyu',
+ 'ッミャ' => 'mmya',
+ 'ッミェ' => 'mmye',
+ 'ッミィ' => 'mmyi',
+ 'ッミョ' => 'mmyo',
+ 'ッミュ' => 'mmyu',
+ 'ッニャ' => 'nnya',
+ 'ッニェ' => 'nnye',
+ 'ッニィ' => 'nnyi',
+ 'ッニョ' => 'nnyo',
+ 'ッニュ' => 'nnyu',
+ 'ッリャ' => 'rrya',
+ 'ッリェ' => 'rrye',
+ 'ッリィ' => 'rryi',
+ 'ッリョ' => 'rryo',
+ 'ッリュ' => 'rryu',
+ 'ッシャ' => 'ssha',
+ 'ッシェ' => 'sshe',
+ 'ッシ' => 'sshi',
+ 'ッショ' => 'ssho',
+ 'ッシュ' => 'sshu',
+ 'ッチャ' => 'ccha',
+ 'ッチェ' => 'cche',
+ 'ッチ' => 'cchi',
+ 'ッチョ' => 'ccho',
+ 'ッチュ' => 'cchu',
+ 'ッティ' => 'tti',
+ 'ッヂィ' => 'ddi',
+
+ // 3 character syllables - doubled vowel and consonants
+ 'ッバー' => 'bbaa',
+ 'ッベー' => 'bbee',
+ 'ッビー' => 'bbii',
+ 'ッボー' => 'bboo',
+ 'ッブー' => 'bbuu',
+ 'ッパー' => 'ppaa',
+ 'ッペー' => 'ppee',
+ 'ッピー' => 'ppii',
+ 'ッポー' => 'ppoo',
+ 'ップー' => 'ppuu',
+ 'ッケー' => 'kkee',
+ 'ッキー' => 'kkii',
+ 'ッコー' => 'kkoo',
+ 'ックー' => 'kkuu',
+ 'ッカー' => 'kkaa',
+ 'ッガー' => 'ggaa',
+ 'ッゲー' => 'ggee',
+ 'ッギー' => 'ggii',
+ 'ッゴー' => 'ggoo',
+ 'ッグー' => 'gguu',
+ 'ッマー' => 'maa',
+ 'ッメー' => 'mee',
+ 'ッミー' => 'mii',
+ 'ッモー' => 'moo',
+ 'ッムー' => 'muu',
+ 'ッナー' => 'nnaa',
+ 'ッネー' => 'nnee',
+ 'ッニー' => 'nnii',
+ 'ッノー' => 'nnoo',
+ 'ッヌー' => 'nnuu',
+ 'ッラー' => 'rraa',
+ 'ッレー' => 'rree',
+ 'ッリー' => 'rrii',
+ 'ッロー' => 'rroo',
+ 'ッルー' => 'rruu',
+ 'ッサー' => 'ssaa',
+ 'ッセー' => 'ssee',
+ 'ッシー' => 'sshii',
+ 'ッソー' => 'ssoo',
+ 'ッスー' => 'ssuu',
+ 'ッザー' => 'zzaa',
+ 'ッゼー' => 'zzee',
+ 'ッジー' => 'jjii',
+ 'ッゾー' => 'zzoo',
+ 'ッズー' => 'zzuu',
+ 'ッター' => 'ttaa',
+ 'ッテー' => 'ttee',
+ 'ッチー' => 'chii',
+ 'ットー' => 'ttoo',
+ 'ッツー' => 'ttsuu',
+ 'ッダー' => 'ddaa',
+ 'ッデー' => 'ddee',
+ 'ッヂー' => 'ddii',
+ 'ッドー' => 'ddoo',
+ 'ッヅー' => 'dduu',
+
+ // 2 character syllables - normal
+ 'ファ' => 'fa',
+ 'フェ' => 'fe',
+ 'フィ' => 'fi',
+ 'フォ' => 'fo',
+ 'フゥ' => 'fu',
+ // 'フャ'=>'fya',
+ // 'フェ'=>'fye',
+ // 'フィ'=>'fyi',
+ // 'フョ'=>'fyo',
+ // 'フュ'=>'fyu',
+ 'フャ' => 'fa',
+ 'フェ' => 'fe',
+ 'フィ' => 'fi',
+ 'フョ' => 'fo',
+ 'フュ' => 'fu',
+ 'ヒャ' => 'hya',
+ 'ヒェ' => 'hye',
+ 'ヒィ' => 'hyi',
+ 'ヒョ' => 'hyo',
+ 'ヒュ' => 'hyu',
+ 'ビャ' => 'bya',
+ 'ビェ' => 'bye',
+ 'ビィ' => 'byi',
+ 'ビョ' => 'byo',
+ 'ビュ' => 'byu',
+ 'ピャ' => 'pya',
+ 'ピェ' => 'pye',
+ 'ピィ' => 'pyi',
+ 'ピョ' => 'pyo',
+ 'ピュ' => 'pyu',
+ 'キャ' => 'kya',
+ 'キェ' => 'kye',
+ 'キィ' => 'kyi',
+ 'キョ' => 'kyo',
+ 'キュ' => 'kyu',
+ 'ギャ' => 'gya',
+ 'ギェ' => 'gye',
+ 'ギィ' => 'gyi',
+ 'ギョ' => 'gyo',
+ 'ギュ' => 'gyu',
+ 'ミャ' => 'mya',
+ 'ミェ' => 'mye',
+ 'ミィ' => 'myi',
+ 'ミョ' => 'myo',
+ 'ミュ' => 'myu',
+ 'ニャ' => 'nya',
+ 'ニェ' => 'nye',
+ 'ニィ' => 'nyi',
+ 'ニョ' => 'nyo',
+ 'ニュ' => 'nyu',
+ 'リャ' => 'rya',
+ 'リェ' => 'rye',
+ 'リィ' => 'ryi',
+ 'リョ' => 'ryo',
+ 'リュ' => 'ryu',
+ 'シャ' => 'sha',
+ 'シェ' => 'she',
+ 'ショ' => 'sho',
+ 'シュ' => 'shu',
+ 'ジャ' => 'ja',
+ 'ジェ' => 'je',
+ 'ジョ' => 'jo',
+ 'ジュ' => 'ju',
+ 'スァ' => 'swa',
+ 'スェ' => 'swe',
+ 'スィ' => 'swi',
+ 'スォ' => 'swo',
+ 'スゥ' => 'swu',
+ 'デァ' => 'da',
+ 'デェ' => 'de',
+ 'ディ' => 'di',
+ 'デォ' => 'do',
+ 'デゥ' => 'du',
+ 'チャ' => 'cha',
+ 'チェ' => 'che',
+ 'チ' => 'chi',
+ 'チョ' => 'cho',
+ 'チュ' => 'chu',
+ // 'ヂャ'=>'dya',
+ // 'ヂェ'=>'dye',
+ // 'ヂィ'=>'dyi',
+ // 'ヂョ'=>'dyo',
+ // 'ヂュ'=>'dyu',
+ 'ツャ' => 'tsa',
+ 'ツェ' => 'tse',
+ 'ツィ' => 'tsi',
+ 'ツョ' => 'tso',
+ 'ツ' => 'tsu',
+ 'トァ' => 'twa',
+ 'トェ' => 'twe',
+ 'トィ' => 'twi',
+ 'トォ' => 'two',
+ 'トゥ' => 'twu',
+ 'ドァ' => 'dwa',
+ 'ドェ' => 'dwe',
+ 'ドィ' => 'dwi',
+ 'ドォ' => 'dwo',
+ 'ドゥ' => 'dwu',
+ 'ウァ' => 'wha',
+ 'ウェ' => 'whe',
+ 'ウィ' => 'whi',
+ 'ウォ' => 'who',
+ 'ウゥ' => 'whu',
+ 'ヴャ' => 'vya',
+ 'ヴェ' => 'vye',
+ 'ヴィ' => 'vyi',
+ 'ヴョ' => 'vyo',
+ 'ヴュ' => 'vyu',
+ 'ヴァ' => 'va',
+ 'ヴェ' => 've',
+ 'ヴィ' => 'vi',
+ 'ヴォ' => 'vo',
+ 'ヴ' => 'vu',
+ 'ウェ' => 'we',
+ 'ウィ' => 'wi',
+ 'イェ' => 'ye',
+ 'ティ' => 'ti',
+ 'ヂィ' => 'di',
+
+ // 2 character syllables - doubled vocal
+ 'アー' => 'aa',
+ 'エー' => 'ee',
+ 'イー' => 'ii',
+ 'オー' => 'oo',
+ 'ウー' => 'uu',
+ 'ダー' => 'daa',
+ 'デー' => 'dee',
+ 'ヂー' => 'dii',
+ 'ドー' => 'doo',
+ 'ヅー' => 'duu',
+ 'ハー' => 'haa',
+ 'ヘー' => 'hee',
+ 'ヒー' => 'hii',
+ 'ホー' => 'hoo',
+ 'フー' => 'fuu',
+ 'バー' => 'baa',
+ 'ベー' => 'bee',
+ 'ビー' => 'bii',
+ 'ボー' => 'boo',
+ 'ブー' => 'buu',
+ 'パー' => 'paa',
+ 'ペー' => 'pee',
+ 'ピー' => 'pii',
+ 'ポー' => 'poo',
+ 'プー' => 'puu',
+ 'ケー' => 'kee',
+ 'キー' => 'kii',
+ 'コー' => 'koo',
+ 'クー' => 'kuu',
+ 'カー' => 'kaa',
+ 'ガー' => 'gaa',
+ 'ゲー' => 'gee',
+ 'ギー' => 'gii',
+ 'ゴー' => 'goo',
+ 'グー' => 'guu',
+ 'マー' => 'maa',
+ 'メー' => 'mee',
+ 'ミー' => 'mii',
+ 'モー' => 'moo',
+ 'ムー' => 'muu',
+ 'ナー' => 'naa',
+ 'ネー' => 'nee',
+ 'ニー' => 'nii',
+ 'ノー' => 'noo',
+ 'ヌー' => 'nuu',
+ 'ラー' => 'raa',
+ 'レー' => 'ree',
+ 'リー' => 'rii',
+ 'ロー' => 'roo',
+ 'ルー' => 'ruu',
+ 'サー' => 'saa',
+ 'セー' => 'see',
+ 'シー' => 'shii',
+ 'ソー' => 'soo',
+ 'スー' => 'suu',
+ 'ザー' => 'zaa',
+ 'ゼー' => 'zee',
+ 'ジー' => 'jii',
+ 'ゾー' => 'zoo',
+ 'ズー' => 'zuu',
+ 'ター' => 'taa',
+ 'テー' => 'tee',
+ 'チー' => 'chii',
+ 'トー' => 'too',
+ 'ツー' => 'tsuu',
+ 'ワー' => 'waa',
+ 'ヲー' => 'woo',
+ 'ヤー' => 'yaa',
+ 'ヨー' => 'yoo',
+ 'ユー' => 'yuu',
+ 'ヵー' => 'kaa',
+ 'ヶー' => 'kee',
+ // old characters
+ 'ヱー' => 'wee',
+ 'ヰー' => 'wii',
+
+ // seperate katakana 'n'
+ 'ンア' => 'n_a',
+ 'ンエ' => 'n_e',
+ 'ンイ' => 'n_i',
+ 'ンオ' => 'n_o',
+ 'ンウ' => 'n_u',
+ 'ンヤ' => 'n_ya',
+ 'ンヨ' => 'n_yo',
+ 'ンユ' => 'n_yu',
+
+ // 2 character syllables - doubled consonants
+ 'ッバ' => 'bba',
+ 'ッベ' => 'bbe',
+ 'ッビ' => 'bbi',
+ 'ッボ' => 'bbo',
+ 'ッブ' => 'bbu',
+ 'ッパ' => 'ppa',
+ 'ッペ' => 'ppe',
+ 'ッピ' => 'ppi',
+ 'ッポ' => 'ppo',
+ 'ップ' => 'ppu',
+ 'ッケ' => 'kke',
+ 'ッキ' => 'kki',
+ 'ッコ' => 'kko',
+ 'ック' => 'kku',
+ 'ッカ' => 'kka',
+ 'ッガ' => 'gga',
+ 'ッゲ' => 'gge',
+ 'ッギ' => 'ggi',
+ 'ッゴ' => 'ggo',
+ 'ッグ' => 'ggu',
+ 'ッマ' => 'ma',
+ 'ッメ' => 'me',
+ 'ッミ' => 'mi',
+ 'ッモ' => 'mo',
+ 'ッム' => 'mu',
+ 'ッナ' => 'nna',
+ 'ッネ' => 'nne',
+ 'ッニ' => 'nni',
+ 'ッノ' => 'nno',
+ 'ッヌ' => 'nnu',
+ 'ッラ' => 'rra',
+ 'ッレ' => 'rre',
+ 'ッリ' => 'rri',
+ 'ッロ' => 'rro',
+ 'ッル' => 'rru',
+ 'ッサ' => 'ssa',
+ 'ッセ' => 'sse',
+ 'ッシ' => 'sshi',
+ 'ッソ' => 'sso',
+ 'ッス' => 'ssu',
+ 'ッザ' => 'zza',
+ 'ッゼ' => 'zze',
+ 'ッジ' => 'jji',
+ 'ッゾ' => 'zzo',
+ 'ッズ' => 'zzu',
+ 'ッタ' => 'tta',
+ 'ッテ' => 'tte',
+ 'ッチ' => 'cchi',
+ 'ット' => 'tto',
+ 'ッツ' => 'ttsu',
+ 'ッダ' => 'dda',
+ 'ッデ' => 'dde',
+ 'ッヂ' => 'ddi',
+ 'ッド' => 'ddo',
+ 'ッヅ' => 'ddu',
+
+ // 1 character syllables
+ 'ア' => 'a',
+ 'エ' => 'e',
+ 'イ' => 'i',
+ 'オ' => 'o',
+ 'ウ' => 'u',
+ 'ン' => 'n',
+ 'ハ' => 'ha',
+ 'ヘ' => 'he',
+ 'ヒ' => 'hi',
+ 'ホ' => 'ho',
+ 'フ' => 'fu',
+ 'バ' => 'ba',
+ 'ベ' => 'be',
+ 'ビ' => 'bi',
+ 'ボ' => 'bo',
+ 'ブ' => 'bu',
+ 'パ' => 'pa',
+ 'ペ' => 'pe',
+ 'ピ' => 'pi',
+ 'ポ' => 'po',
+ 'プ' => 'pu',
+ 'ケ' => 'ke',
+ 'キ' => 'ki',
+ 'コ' => 'ko',
+ 'ク' => 'ku',
+ 'カ' => 'ka',
+ 'ガ' => 'ga',
+ 'ゲ' => 'ge',
+ 'ギ' => 'gi',
+ 'ゴ' => 'go',
+ 'グ' => 'gu',
+ 'マ' => 'ma',
+ 'メ' => 'me',
+ 'ミ' => 'mi',
+ 'モ' => 'mo',
+ 'ム' => 'mu',
+ 'ナ' => 'na',
+ 'ネ' => 'ne',
+ 'ニ' => 'ni',
+ 'ノ' => 'no',
+ 'ヌ' => 'nu',
+ 'ラ' => 'ra',
+ 'レ' => 're',
+ 'リ' => 'ri',
+ 'ロ' => 'ro',
+ 'ル' => 'ru',
+ 'サ' => 'sa',
+ 'セ' => 'se',
+ 'シ' => 'shi',
+ 'ソ' => 'so',
+ 'ス' => 'su',
+ 'ザ' => 'za',
+ 'ゼ' => 'ze',
+ 'ジ' => 'ji',
+ 'ゾ' => 'zo',
+ 'ズ' => 'zu',
+ 'タ' => 'ta',
+ 'テ' => 'te',
+ 'チ' => 'chi',
+ 'ト' => 'to',
+ 'ツ' => 'tsu',
+ 'ダ' => 'da',
+ 'デ' => 'de',
+ 'ヂ' => 'di',
+ 'ド' => 'do',
+ 'ヅ' => 'du',
+ 'ワ' => 'wa',
+ 'ヲ' => 'wo',
+ 'ヤ' => 'ya',
+ 'ヨ' => 'yo',
+ 'ユ' => 'yu',
+ 'ヵ' => 'ka',
+ 'ヶ' => 'ke',
+ // old characters
+ 'ヱ' => 'we',
+ 'ヰ' => 'wi',
+
+ // convert what's left (probably only kicks in when something's missing above)
+ 'ァ' => 'a',
+ 'ェ' => 'e',
+ 'ィ' => 'i',
+ 'ォ' => 'o',
+ 'ゥ' => 'u',
+ 'ャ' => 'ya',
+ 'ョ' => 'yo',
+ 'ュ' => 'yu',
+
+ // special characters
+ '・' => '_',
+ '、' => '_',
+ 'ー' => '_',
+ // when used with hiragana (seldom), this character would not be converted otherwise
+
+ // 'ラ'=>'la',
+ // 'レ'=>'le',
+ // 'リ'=>'li',
+ // 'ロ'=>'lo',
+ // 'ル'=>'lu',
+ // 'チャ'=>'cya',
+ // 'チェ'=>'cye',
+ // 'チィ'=>'cyi',
+ // 'チョ'=>'cyo',
+ // 'チュ'=>'cyu',
+ // 'デャ'=>'dha',
+ // 'デェ'=>'dhe',
+ // 'ディ'=>'dhi',
+ // 'デョ'=>'dho',
+ // 'デュ'=>'dhu',
+ // 'リャ'=>'lya',
+ // 'リェ'=>'lye',
+ // 'リィ'=>'lyi',
+ // 'リョ'=>'lyo',
+ // 'リュ'=>'lyu',
+ // 'テャ'=>'tha',
+ // 'テェ'=>'the',
+ // 'ティ'=>'thi',
+ // 'テョ'=>'tho',
+ // 'テュ'=>'thu',
+ // 'ファ'=>'fwa',
+ // 'フェ'=>'fwe',
+ // 'フィ'=>'fwi',
+ // 'フォ'=>'fwo',
+ // 'フゥ'=>'fwu',
+ // 'チャ'=>'tya',
+ // 'チェ'=>'tye',
+ // 'チィ'=>'tyi',
+ // 'チョ'=>'tyo',
+ // 'チュ'=>'tyu',
+ // 'ジャ'=>'jya',
+ // 'ジェ'=>'jye',
+ // 'ジィ'=>'jyi',
+ // 'ジョ'=>'jyo',
+ // 'ジュ'=>'jyu',
+ // 'ジャ'=>'zha',
+ // 'ジェ'=>'zhe',
+ // 'ジィ'=>'zhi',
+ // 'ジョ'=>'zho',
+ // 'ジュ'=>'zhu',
+ // 'ジャ'=>'zya',
+ // 'ジェ'=>'zye',
+ // 'ジィ'=>'zyi',
+ // 'ジョ'=>'zyo',
+ // 'ジュ'=>'zyu',
+ // 'シャ'=>'sya',
+ // 'シェ'=>'sye',
+ // 'シィ'=>'syi',
+ // 'ショ'=>'syo',
+ // 'シュ'=>'syu',
+ // 'シ'=>'ci',
+ // 'フ'=>'hu',
+ // 'シ'=>'si',
+ // 'チ'=>'ti',
+ // 'ツ'=>'tu',
+ // 'イ'=>'yi',
+ // 'ヂ'=>'dzi',
+
+ // "Greeklish"
+ 'Γ' => 'G',
+ 'Δ' => 'E',
+ 'Θ' => 'Th',
+ 'Λ' => 'L',
+ 'Ξ' => 'X',
+ 'Π' => 'P',
+ 'Σ' => 'S',
+ 'Φ' => 'F',
+ 'Ψ' => 'Ps',
+ 'γ' => 'g',
+ 'δ' => 'e',
+ 'θ' => 'th',
+ 'λ' => 'l',
+ 'ξ' => 'x',
+ 'π' => 'p',
+ 'σ' => 's',
+ 'φ' => 'f',
+ 'ψ' => 'ps',
+
+ // Thai
+ 'ก' => 'k',
+ 'ข' => 'kh',
+ 'ฃ' => 'kh',
+ 'ค' => 'kh',
+ 'ฅ' => 'kh',
+ 'ฆ' => 'kh',
+ 'ง' => 'ng',
+ 'จ' => 'ch',
+ 'ฉ' => 'ch',
+ 'ช' => 'ch',
+ 'ซ' => 's',
+ 'ฌ' => 'ch',
+ 'ญ' => 'y',
+ 'ฎ' => 'd',
+ 'ฏ' => 't',
+ 'ฐ' => 'th',
+ 'ฑ' => 'd',
+ 'ฒ' => 'th',
+ 'ณ' => 'n',
+ 'ด' => 'd',
+ 'ต' => 't',
+ 'ถ' => 'th',
+ 'ท' => 'th',
+ 'ธ' => 'th',
+ 'น' => 'n',
+ 'บ' => 'b',
+ 'ป' => 'p',
+ 'ผ' => 'ph',
+ 'ฝ' => 'f',
+ 'พ' => 'ph',
+ 'ฟ' => 'f',
+ 'ภ' => 'ph',
+ 'ม' => 'm',
+ 'ย' => 'y',
+ 'ร' => 'r',
+ 'ฤ' => 'rue',
+ 'ฤๅ' => 'rue',
+ 'ล' => 'l',
+ 'ฦ' => 'lue',
+ 'ฦๅ' => 'lue',
+ 'ว' => 'w',
+ 'ศ' => 's',
+ 'ษ' => 's',
+ 'ส' => 's',
+ 'ห' => 'h',
+ 'ฬ' => 'l',
+ 'ฮ' => 'h',
+ 'ะ' => 'a',
+ 'ั' => 'a',
+ 'รร' => 'a',
+ 'า' => 'a',
+ 'ๅ' => 'a',
+ 'ำ' => 'am',
+ 'ํา' => 'am',
+ 'ิ' => 'i',
+ 'ี' => 'i',
+ 'ึ' => 'ue',
+ 'ี' => 'ue',
+ 'ุ' => 'u',
+ 'ู' => 'u',
+ 'เ' => 'e',
+ 'แ' => 'ae',
+ 'โ' => 'o',
+ 'อ' => 'o',
+ 'ียะ' => 'ia',
+ 'ีย' => 'ia',
+ 'ือะ' => 'uea',
+ 'ือ' => 'uea',
+ 'ัวะ' => 'ua',
+ 'ัว' => 'ua',
+ 'ใ' => 'ai',
+ 'ไ' => 'ai',
+ 'ัย' => 'ai',
+ 'าย' => 'ai',
+ 'าว' => 'ao',
+ 'ุย' => 'ui',
+ 'อย' => 'oi',
+ 'ือย' => 'ueai',
+ 'วย' => 'uai',
+ 'ิว' => 'io',
+ '็ว' => 'eo',
+ 'ียว' => 'iao',
+ '่' => '',
+ '้' => '',
+ '๊' => '',
+ '๋' => '',
+ '็' => '',
+ '์' => '',
+ '๎' => '',
+ 'ํ' => '',
+ 'ฺ' => '',
+ 'ๆ' => '2',
+ '๏' => 'o',
+ 'ฯ' => '-',
+ '๚' => '-',
+ '๛' => '-',
+ '๐' => '0',
+ '๑' => '1',
+ '๒' => '2',
+ '๓' => '3',
+ '๔' => '4',
+ '๕' => '5',
+ '๖' => '6',
+ '๗' => '7',
+ '๘' => '8',
+ '๙' => '9',
+
+ // Korean
+ 'ㄱ' => 'k', 'ㅋ' => 'kh',
+ 'ㄲ' => 'kk',
+ 'ㄷ' => 't',
+ 'ㅌ' => 'th',
+ 'ㄸ' => 'tt',
+ 'ㅂ' => 'p',
+ 'ㅍ' => 'ph',
+ 'ㅃ' => 'pp',
+ 'ㅈ' => 'c',
+ 'ㅊ' => 'ch',
+ 'ㅉ' => 'cc',
+ 'ㅅ' => 's',
+ 'ㅆ' => 'ss',
+ 'ㅎ' => 'h',
+ 'ㅇ' => 'ng',
+ 'ㄴ' => 'n',
+ 'ㄹ' => 'l',
+ 'ㅁ' => 'm',
+ 'ㅏ' => 'a',
+ 'ㅓ' => 'e',
+ 'ㅗ' => 'o',
+ 'ㅜ' => 'wu',
+ 'ㅡ' => 'u',
+ 'ㅣ' => 'i',
+ 'ㅐ' => 'ay',
+ 'ㅔ' => 'ey',
+ 'ㅚ' => 'oy',
+ 'ㅘ' => 'wa',
+ 'ㅝ' => 'we',
+ 'ㅟ' => 'wi',
+ 'ㅙ' => 'way',
+ 'ㅞ' => 'wey',
+ 'ㅢ' => 'uy',
+ 'ㅑ' => 'ya',
+ 'ㅕ' => 'ye',
+ 'ㅛ' => 'oy',
+ 'ㅠ' => 'yu',
+ 'ㅒ' => 'yay',
+ 'ㅖ' => 'yey',
+];
diff --git a/platform/www/inc/Utf8/tables/specials.php b/platform/www/inc/Utf8/tables/specials.php
new file mode 100644
index 0000000..f6243bc
--- /dev/null
+++ b/platform/www/inc/Utf8/tables/specials.php
@@ -0,0 +1,615 @@
+<?php
+/**
+ * UTF-8 array of common special characters
+ *
+ * This array should contain all special characters (not a letter or digit)
+ * defined in the various local charsets - it's not a complete list of non-alphanum
+ * characters in UTF-8. It's not perfect but should match most cases of special
+ * chars.
+ *
+ * The controlchars 0x00 to 0x19 are _not_ included in this array. The space 0x20 is!
+ * These chars are _not_ in the array either: _ (0x5f), : 0x3a, . 0x2e, - 0x2d, * 0x2a
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @see \dokuwiki\Utf8\Clean::stripspecials()
+ */
+return [
+ 0x1a, // 
+ 0x1b, // 
+ 0x1c, // 
+ 0x1d, // 
+ 0x1e, // 
+ 0x1f, // 
+ 0x20, // <space>
+ 0x21, // !
+ 0x22, // "
+ 0x23, // #
+ 0x24, // $
+ 0x25, // %
+ 0x26, // &
+ 0x27, // '
+ 0x28, // (
+ 0x29, // )
+ 0x2b, // +
+ 0x2c, // ,
+ 0x2f, // /
+ 0x3b, // ;
+ 0x3c, // <
+ 0x3d, // =
+ 0x3e, // >
+ 0x3f, // ?
+ 0x40, // @
+ 0x5b, // [
+ 0x5c, // \
+ 0x5d, // ]
+ 0x5e, // ^
+ 0x60, // `
+ 0x7b, // {
+ 0x7c, // |
+ 0x7d, // }
+ 0x7e, // ~
+ 0x7f, // 
+ 0x80, // €
+ 0x81, // 
+ 0x82, // ‚
+ 0x83, // ƒ
+ 0x84, // „
+ 0x85, // …
+ 0x86, // †
+ 0x87, // ‡
+ 0x88, // ˆ
+ 0x89, // ‰
+ 0x8a, // Š
+ 0x8b, // ‹
+ 0x8c, // Œ
+ 0x8d, // 
+ 0x8e, // Ž
+ 0x8f, // 
+ 0x90, // 
+ 0x91, // ‘
+ 0x92, // ’
+ 0x93, // “
+ 0x94, // ”
+ 0x95, // •
+ 0x96, // –
+ 0x97, // —
+ 0x98, // ˜
+ 0x99, // ™
+ 0x9a, // š
+ 0x9b, // ›
+ 0x9c, // œ
+ 0x9d, // 
+ 0x9e, // ž
+ 0x9f, // Ÿ
+ 0xa0, //  
+ 0xa1, // ¡
+ 0xa2, // ¢
+ 0xa3, // £
+ 0xa4, // ¤
+ 0xa5, // ¥
+ 0xa6, // ¦
+ 0xa7, // §
+ 0xa8, // ¨
+ 0xa9, // ©
+ 0xaa, // ª
+ 0xab, // «
+ 0xac, // ¬
+ 0xad, // ­
+ 0xae, // ®
+ 0xaf, // ¯
+ 0xb0, // °
+ 0xb1, // ±
+ 0xb2, // ²
+ 0xb3, // ³
+ 0xb4, // ´
+ 0xb5, // µ
+ 0xb6, // ¶
+ 0xb7, // ·
+ 0xb8, // ¸
+ 0xb9, // ¹
+ 0xba, // º
+ 0xbb, // »
+ 0xbc, // ¼
+ 0xbd, // ½
+ 0xbe, // ¾
+ 0xbf, // ¿
+ 0xd7, // ×
+ 0xf7, // ÷
+ 0x2c7, // ˇ
+ 0x2d8, // ˘
+ 0x2d9, // ˙
+ 0x2da, // ˚
+ 0x2db, // ˛
+ 0x2dc, // ˜
+ 0x2dd, // ˝
+ 0x300, // ̀
+ 0x301, // ́
+ 0x303, // ̃
+ 0x309, // ̉
+ 0x323, // ̣
+ 0x384, // ΄
+ 0x385, // ΅
+ 0x387, // ·
+ 0x5b0, // ְ
+ 0x5b1, // ֱ
+ 0x5b2, // ֲ
+ 0x5b3, // ֳ
+ 0x5b4, // ִ
+ 0x5b5, // ֵ
+ 0x5b6, // ֶ
+ 0x5b7, // ַ
+ 0x5b8, // ָ
+ 0x5b9, // ֹ
+ 0x5bb, // ֻ
+ 0x5bc, // ּ
+ 0x5bd, // ֽ
+ 0x5be, // ־
+ 0x5bf, // ֿ
+ 0x5c0, // ׀
+ 0x5c1, // ׁ
+ 0x5c2, // ׂ
+ 0x5c3, // ׃
+ 0x5f3, // ׳
+ 0x5f4, // ״
+ 0x60c, // ،
+ 0x61b, // ؛
+ 0x61f, // ؟
+ 0x640, // ـ
+ 0x64b, // ً
+ 0x64c, // ٌ
+ 0x64d, // ٍ
+ 0x64e, // َ
+ 0x64f, // ُ
+ 0x650, // ِ
+ 0x651, // ّ
+ 0x652, // ْ
+ 0x66a, // ٪
+ 0xe3f, // ฿
+ 0x200c, // ‌
+ 0x200d, // ‍
+ 0x200e, // ‎
+ 0x200f, // ‏
+ 0x2013, // –
+ 0x2014, // —
+ 0x2015, // ―
+ 0x2017, // ‗
+ 0x2018, // ‘
+ 0x2019, // ’
+ 0x201a, // ‚
+ 0x201c, // “
+ 0x201d, // ”
+ 0x201e, // „
+ 0x2020, // †
+ 0x2021, // ‡
+ 0x2022, // •
+ 0x2026, // …
+ 0x2030, // ‰
+ 0x2032, // ′
+ 0x2033, // ″
+ 0x2039, // ‹
+ 0x203a, // ›
+ 0x2044, // ⁄
+ 0x20a7, // ₧
+ 0x20aa, // ₪
+ 0x20ab, // ₫
+ 0x20ac, // €
+ 0x2116, // №
+ 0x2118, // ℘
+ 0x2122, // ™
+ 0x2126, // Ω
+ 0x2135, // ℵ
+ 0x2190, // ←
+ 0x2191, // ↑
+ 0x2192, // →
+ 0x2193, // ↓
+ 0x2194, // ↔
+ 0x2195, // ↕
+ 0x21b5, // ↵
+ 0x21d0, // ⇐
+ 0x21d1, // ⇑
+ 0x21d2, // ⇒
+ 0x21d3, // ⇓
+ 0x21d4, // ⇔
+ 0x2200, // ∀
+ 0x2202, // ∂
+ 0x2203, // ∃
+ 0x2205, // ∅
+ 0x2206, // ∆
+ 0x2207, // ∇
+ 0x2208, // ∈
+ 0x2209, // ∉
+ 0x220b, // ∋
+ 0x220f, // ∏
+ 0x2211, // ∑
+ 0x2212, // −
+ 0x2215, // ∕
+ 0x2217, // ∗
+ 0x2219, // ∙
+ 0x221a, // √
+ 0x221d, // ∝
+ 0x221e, // ∞
+ 0x2220, // ∠
+ 0x2227, // ∧
+ 0x2228, // ∨
+ 0x2229, // ∩
+ 0x222a, // ∪
+ 0x222b, // ∫
+ 0x2234, // ∴
+ 0x223c, // ∼
+ 0x2245, // ≅
+ 0x2248, // ≈
+ 0x2260, // ≠
+ 0x2261, // ≡
+ 0x2264, // ≤
+ 0x2265, // ≥
+ 0x2282, // ⊂
+ 0x2283, // ⊃
+ 0x2284, // ⊄
+ 0x2286, // ⊆
+ 0x2287, // ⊇
+ 0x2295, // ⊕
+ 0x2297, // ⊗
+ 0x22a5, // ⊥
+ 0x22c5, // ⋅
+ 0x2310, // ⌐
+ 0x2320, // ⌠
+ 0x2321, // ⌡
+ 0x2329, // 〈
+ 0x232a, // 〉
+ 0x2469, // ⑩
+ 0x2500, // ─
+ 0x2502, // │
+ 0x250c, // ┌
+ 0x2510, // ┐
+ 0x2514, // └
+ 0x2518, // ┘
+ 0x251c, // ├
+ 0x2524, // ┤
+ 0x252c, // ┬
+ 0x2534, // ┴
+ 0x253c, // ┼
+ 0x2550, // ═
+ 0x2551, // ║
+ 0x2552, // ╒
+ 0x2553, // ╓
+ 0x2554, // ╔
+ 0x2555, // ╕
+ 0x2556, // ╖
+ 0x2557, // ╗
+ 0x2558, // ╘
+ 0x2559, // ╙
+ 0x255a, // ╚
+ 0x255b, // ╛
+ 0x255c, // ╜
+ 0x255d, // ╝
+ 0x255e, // ╞
+ 0x255f, // ╟
+ 0x2560, // ╠
+ 0x2561, // ╡
+ 0x2562, // ╢
+ 0x2563, // ╣
+ 0x2564, // ╤
+ 0x2565, // ╥
+ 0x2566, // ╦
+ 0x2567, // ╧
+ 0x2568, // ╨
+ 0x2569, // ╩
+ 0x256a, // ╪
+ 0x256b, // ╫
+ 0x256c, // ╬
+ 0x2580, // ▀
+ 0x2584, // ▄
+ 0x2588, // █
+ 0x258c, // ▌
+ 0x2590, // ▐
+ 0x2591, // ░
+ 0x2592, // ▒
+ 0x2593, // ▓
+ 0x25a0, // ■
+ 0x25b2, // ▲
+ 0x25bc, // ▼
+ 0x25c6, // ◆
+ 0x25ca, // ◊
+ 0x25cf, // ●
+ 0x25d7, // ◗
+ 0x2605, // ★
+ 0x260e, // ☎
+ 0x261b, // ☛
+ 0x261e, // ☞
+ 0x2660, // ♠
+ 0x2663, // ♣
+ 0x2665, // ♥
+ 0x2666, // ♦
+ 0x2701, // ✁
+ 0x2702, // ✂
+ 0x2703, // ✃
+ 0x2704, // ✄
+ 0x2706, // ✆
+ 0x2707, // ✇
+ 0x2708, // ✈
+ 0x2709, // ✉
+ 0x270c, // ✌
+ 0x270d, // ✍
+ 0x270e, // ✎
+ 0x270f, // ✏
+ 0x2710, // ✐
+ 0x2711, // ✑
+ 0x2712, // ✒
+ 0x2713, // ✓
+ 0x2714, // ✔
+ 0x2715, // ✕
+ 0x2716, // ✖
+ 0x2717, // ✗
+ 0x2718, // ✘
+ 0x2719, // ✙
+ 0x271a, // ✚
+ 0x271b, // ✛
+ 0x271c, // ✜
+ 0x271d, // ✝
+ 0x271e, // ✞
+ 0x271f, // ✟
+ 0x2720, // ✠
+ 0x2721, // ✡
+ 0x2722, // ✢
+ 0x2723, // ✣
+ 0x2724, // ✤
+ 0x2725, // ✥
+ 0x2726, // ✦
+ 0x2727, // ✧
+ 0x2729, // ✩
+ 0x272a, // ✪
+ 0x272b, // ✫
+ 0x272c, // ✬
+ 0x272d, // ✭
+ 0x272e, // ✮
+ 0x272f, // ✯
+ 0x2730, // ✰
+ 0x2731, // ✱
+ 0x2732, // ✲
+ 0x2733, // ✳
+ 0x2734, // ✴
+ 0x2735, // ✵
+ 0x2736, // ✶
+ 0x2737, // ✷
+ 0x2738, // ✸
+ 0x2739, // ✹
+ 0x273a, // ✺
+ 0x273b, // ✻
+ 0x273c, // ✼
+ 0x273d, // ✽
+ 0x273e, // ✾
+ 0x273f, // ✿
+ 0x2740, // ❀
+ 0x2741, // ❁
+ 0x2742, // ❂
+ 0x2743, // ❃
+ 0x2744, // ❄
+ 0x2745, // ❅
+ 0x2746, // ❆
+ 0x2747, // ❇
+ 0x2748, // ❈
+ 0x2749, // ❉
+ 0x274a, // ❊
+ 0x274b, // ❋
+ 0x274d, // ❍
+ 0x274f, // ❏
+ 0x2750, // ❐
+ 0x2751, // ❑
+ 0x2752, // ❒
+ 0x2756, // ❖
+ 0x2758, // ❘
+ 0x2759, // ❙
+ 0x275a, // ❚
+ 0x275b, // ❛
+ 0x275c, // ❜
+ 0x275d, // ❝
+ 0x275e, // ❞
+ 0x2761, // ❡
+ 0x2762, // ❢
+ 0x2763, // ❣
+ 0x2764, // ❤
+ 0x2765, // ❥
+ 0x2766, // ❦
+ 0x2767, // ❧
+ 0x277f, // ❿
+ 0x2789, // ➉
+ 0x2793, // ➓
+ 0x2794, // ➔
+ 0x2798, // ➘
+ 0x2799, // ➙
+ 0x279a, // ➚
+ 0x279b, // ➛
+ 0x279c, // ➜
+ 0x279d, // ➝
+ 0x279e, // ➞
+ 0x279f, // ➟
+ 0x27a0, // ➠
+ 0x27a1, // ➡
+ 0x27a2, // ➢
+ 0x27a3, // ➣
+ 0x27a4, // ➤
+ 0x27a5, // ➥
+ 0x27a6, // ➦
+ 0x27a7, // ➧
+ 0x27a8, // ➨
+ 0x27a9, // ➩
+ 0x27aa, // ➪
+ 0x27ab, // ➫
+ 0x27ac, // ➬
+ 0x27ad, // ➭
+ 0x27ae, // ➮
+ 0x27af, // ➯
+ 0x27b1, // ➱
+ 0x27b2, // ➲
+ 0x27b3, // ➳
+ 0x27b4, // ➴
+ 0x27b5, // ➵
+ 0x27b6, // ➶
+ 0x27b7, // ➷
+ 0x27b8, // ➸
+ 0x27b9, // ➹
+ 0x27ba, // ➺
+ 0x27bb, // ➻
+ 0x27bc, // ➼
+ 0x27bd, // ➽
+ 0x27be, // ➾
+ 0x3000, //  
+ 0x3001, // 、
+ 0x3002, // 。
+ 0x3003, // 〃
+ 0x3008, // 〈
+ 0x3009, // 〉
+ 0x300a, // 《
+ 0x300b, // 》
+ 0x300c, // 「
+ 0x300d, // 」
+ 0x300e, // 『
+ 0x300f, // 』
+ 0x3010, // 【
+ 0x3011, // 】
+ 0x3012, // 〒
+ 0x3014, // 〔
+ 0x3015, // 〕
+ 0x3016, // 〖
+ 0x3017, // 〗
+ 0x3018, // 〘
+ 0x3019, // 〙
+ 0x301a, // 〚
+ 0x301b, // 〛
+ 0x3036, // 〶
+ 0xf6d9, // 
+ 0xf6da, // 
+ 0xf6db, // 
+ 0xf8d7, // 
+ 0xf8d8, // 
+ 0xf8d9, // 
+ 0xf8da, // 
+ 0xf8db, // 
+ 0xf8dc, // 
+ 0xf8dd, // 
+ 0xf8de, // 
+ 0xf8df, // 
+ 0xf8e0, // 
+ 0xf8e1, // 
+ 0xf8e2, // 
+ 0xf8e3, // 
+ 0xf8e4, // 
+ 0xf8e5, // 
+ 0xf8e6, // 
+ 0xf8e7, // 
+ 0xf8e8, // 
+ 0xf8e9, // 
+ 0xf8ea, // 
+ 0xf8eb, // 
+ 0xf8ec, // 
+ 0xf8ed, // 
+ 0xf8ee, // 
+ 0xf8ef, // 
+ 0xf8f0, // 
+ 0xf8f1, // 
+ 0xf8f2, // 
+ 0xf8f3, // 
+ 0xf8f4, // 
+ 0xf8f5, // 
+ 0xf8f6, // 
+ 0xf8f7, // 
+ 0xf8f8, // 
+ 0xf8f9, // 
+ 0xf8fa, // 
+ 0xf8fb, // 
+ 0xf8fc, // 
+ 0xf8fd, // 
+ 0xf8fe, // 
+ 0xfe7c, // ﹼ
+ 0xfe7d, // ﹽ
+ 0xff01, // !
+ 0xff02, // "
+ 0xff03, // #
+ 0xff04, // $
+ 0xff05, // %
+ 0xff06, // &
+ 0xff07, // '
+ 0xff08, // (
+ 0xff09, // )
+ 0xff09, // )
+ 0xff0a, // *
+ 0xff0b, // +
+ 0xff0c, // ,
+ 0xff0d, // -
+ 0xff0e, // .
+ 0xff0f, // /
+ 0xff1a, // :
+ 0xff1b, // ;
+ 0xff1c, // <
+ 0xff1d, // =
+ 0xff1e, // >
+ 0xff1f, // ?
+ 0xff20, // @
+ 0xff3b, // [
+ 0xff3c, // \
+ 0xff3d, // ]
+ 0xff3e, // ^
+ 0xff40, // `
+ 0xff5b, // {
+ 0xff5c, // |
+ 0xff5d, // }
+ 0xff5e, // ~
+ 0xff5f, // ⦅
+ 0xff60, // ⦆
+ 0xff61, // 。
+ 0xff62, // 「
+ 0xff63, // 」
+ 0xff64, // 、
+ 0xff65, // ・
+ 0xffe0, // ¢
+ 0xffe1, // £
+ 0xffe2, // ¬
+ 0xffe3, //  ̄
+ 0xffe4, // ¦
+ 0xffe5, // ¥
+ 0xffe6, // ₩
+ 0xffe8, // │
+ 0xffe9, // ←
+ 0xffea, // ↑
+ 0xffeb, // →
+ 0xffec, // ↓
+ 0xffed, // ■
+ 0xffee, // ○
+ 0x1d6fc, // 𝛼
+ 0x1d6fd, // 𝛽
+ 0x1d6fe, // 𝛾
+ 0x1d6ff, // 𝛿
+ 0x1d700, // 𝜀
+ 0x1d701, // 𝜁
+ 0x1d702, // 𝜂
+ 0x1d703, // 𝜃
+ 0x1d704, // 𝜄
+ 0x1d705, // 𝜅
+ 0x1d706, // 𝜆
+ 0x1d707, // 𝜇
+ 0x1d708, // 𝜈
+ 0x1d709, // 𝜉
+ 0x1d70a, // 𝜊
+ 0x1d70b, // 𝜋
+ 0x1d70c, // 𝜌
+ 0x1d70d, // 𝜍
+ 0x1d70e, // 𝜎
+ 0x1d70f, // 𝜏
+ 0x1d710, // 𝜐
+ 0x1d711, // 𝜑
+ 0x1d712, // 𝜒
+ 0x1d713, // 𝜓
+ 0x1d714, // 𝜔
+ 0x1d715, // 𝜕
+ 0x1d716, // 𝜖
+ 0x1d717, // 𝜗
+ 0x1d718, // 𝜘
+ 0x1d719, // 𝜙
+ 0x1d71a, // 𝜚
+ 0x1d71b, // 𝜛
+ 0xc2a0, // 슠
+ 0xe28087, //
+ 0xe280af, //
+ 0xe281a0, //
+ 0xefbbbf, //
+];
diff --git a/platform/www/inc/Utf8/tables/upperaccents.php b/platform/www/inc/Utf8/tables/upperaccents.php
new file mode 100644
index 0000000..e6e48de
--- /dev/null
+++ b/platform/www/inc/Utf8/tables/upperaccents.php
@@ -0,0 +1,114 @@
+<?php
+/**
+ * UTF-8 lookup table for upper case accented letters
+ *
+ * This lookuptable defines replacements for accented characters from the ASCII-7
+ * range. This are upper case letters only.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @see \dokuwiki\Utf8\Clean::deaccent()
+ */
+return [
+ 'Á' => 'A',
+ 'À' => 'A',
+ 'Ă' => 'A',
+ 'Â' => 'A',
+ 'Å' => 'A',
+ 'Ä' => 'Ae',
+ 'Ã' => 'A',
+ 'Ą' => 'A',
+ 'Ā' => 'A',
+ 'Æ' => 'Ae',
+ 'Ḃ' => 'B',
+ 'Ć' => 'C',
+ 'Ĉ' => 'C',
+ 'Č' => 'C',
+ 'Ċ' => 'C',
+ 'Ç' => 'C',
+ 'Ď' => 'D',
+ 'Ḋ' => 'D',
+ 'Đ' => 'D',
+ 'Ð' => 'Dh',
+ 'É' => 'E',
+ 'È' => 'E',
+ 'Ĕ' => 'E',
+ 'Ê' => 'E',
+ 'Ě' => 'E',
+ 'Ë' => 'E',
+ 'Ė' => 'E',
+ 'Ę' => 'E',
+ 'Ē' => 'E',
+ 'Ḟ' => 'F',
+ 'Ƒ' => 'F',
+ 'Ğ' => 'G',
+ 'Ĝ' => 'G',
+ 'Ġ' => 'G',
+ 'Ģ' => 'G',
+ 'Ĥ' => 'H',
+ 'Ħ' => 'H',
+ 'Í' => 'I',
+ 'Ì' => 'I',
+ 'Î' => 'I',
+ 'Ï' => 'I',
+ 'Ĩ' => 'I',
+ 'Į' => 'I',
+ 'Ī' => 'I',
+ 'Ĵ' => 'J',
+ 'Ķ' => 'K',
+ 'Ĺ' => 'L',
+ 'Ľ' => 'L',
+ 'Ļ' => 'L',
+ 'Ł' => 'L',
+ 'Ṁ' => 'M',
+ 'Ń' => 'N',
+ 'Ň' => 'N',
+ 'Ñ' => 'N',
+ 'Ņ' => 'N',
+ 'Ó' => 'O',
+ 'Ò' => 'O',
+ 'Ô' => 'O',
+ 'Ö' => 'Oe',
+ 'Ő' => 'O',
+ 'Õ' => 'O',
+ 'Ø' => 'O',
+ 'Ō' => 'O',
+ 'Ơ' => 'O',
+ 'Ṗ' => 'P',
+ 'Ŕ' => 'R',
+ 'Ř' => 'R',
+ 'Ŗ' => 'R',
+ 'Ś' => 'S',
+ 'Ŝ' => 'S',
+ 'Š' => 'S',
+ 'Ṡ' => 'S',
+ 'Ş' => 'S',
+ 'Ș' => 'S',
+ 'Ť' => 'T',
+ 'Ṫ' => 'T',
+ 'Ţ' => 'T',
+ 'Ț' => 'T',
+ 'Ŧ' => 'T',
+ 'Ú' => 'U',
+ 'Ù' => 'U',
+ 'Ŭ' => 'U',
+ 'Û' => 'U',
+ 'Ů' => 'U',
+ 'Ü' => 'Ue',
+ 'Ű' => 'U',
+ 'Ũ' => 'U',
+ 'Ų' => 'U',
+ 'Ū' => 'U',
+ 'Ư' => 'U',
+ 'Ẃ' => 'W',
+ 'Ẁ' => 'W',
+ 'Ŵ' => 'W',
+ 'Ẅ' => 'W',
+ 'Ý' => 'Y',
+ 'Ỳ' => 'Y',
+ 'Ŷ' => 'Y',
+ 'Ÿ' => 'Y',
+ 'Ź' => 'Z',
+ 'Ž' => 'Z',
+ 'Ż' => 'Z',
+ 'Þ' => 'Th',
+];
diff --git a/platform/www/inc/actions.php b/platform/www/inc/actions.php
new file mode 100644
index 0000000..4ea529d
--- /dev/null
+++ b/platform/www/inc/actions.php
@@ -0,0 +1,64 @@
+<?php
+/**
+ * DokuWiki Actions
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+use dokuwiki\Extension\Event;
+
+/**
+ * All action processing starts here
+ */
+function act_dispatch(){
+ // always initialize on first dispatch (test request may dispatch mutliple times on one request)
+ $router = \dokuwiki\ActionRouter::getInstance(true);
+
+ $headers = array('Content-Type: text/html; charset=utf-8');
+ Event::createAndTrigger('ACTION_HEADERS_SEND',$headers,'act_sendheaders');
+
+ // clear internal variables
+ unset($router);
+ unset($headers);
+ // make all globals available to the template
+ extract($GLOBALS);
+
+ include(template('main.php'));
+ // output for the commands is now handled in inc/templates.php
+ // in function tpl_content()
+}
+
+/**
+ * Send the given headers using header()
+ *
+ * @param array $headers The headers that shall be sent
+ */
+function act_sendheaders($headers) {
+ foreach ($headers as $hdr) header($hdr);
+}
+
+/**
+ * Sanitize the action command
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array|string $act
+ * @return string
+ */
+function act_clean($act){
+ // check if the action was given as array key
+ if(is_array($act)){
+ list($act) = array_keys($act);
+ }
+
+ //remove all bad chars
+ $act = strtolower($act);
+ $act = preg_replace('/[^1-9a-z_]+/','',$act);
+
+ if($act == 'export_html') $act = 'export_xhtml';
+ if($act == 'export_htmlbody') $act = 'export_xhtmlbody';
+
+ if($act === '') $act = 'show';
+ return $act;
+}
diff --git a/platform/www/inc/auth.php b/platform/www/inc/auth.php
new file mode 100644
index 0000000..96ae7c2
--- /dev/null
+++ b/platform/www/inc/auth.php
@@ -0,0 +1,1279 @@
+<?php
+/**
+ * Authentication library
+ *
+ * Including this file will automatically try to login
+ * a user by calling auth_login()
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+use dokuwiki\Extension\AuthPlugin;
+use dokuwiki\Extension\Event;
+use dokuwiki\Extension\PluginController;
+use dokuwiki\PassHash;
+use dokuwiki\Subscriptions\RegistrationSubscriptionSender;
+
+/**
+ * Initialize the auth system.
+ *
+ * This function is automatically called at the end of init.php
+ *
+ * This used to be the main() of the auth.php
+ *
+ * @todo backend loading maybe should be handled by the class autoloader
+ * @todo maybe split into multiple functions at the XXX marked positions
+ * @triggers AUTH_LOGIN_CHECK
+ * @return bool
+ */
+function auth_setup() {
+ global $conf;
+ /* @var AuthPlugin $auth */
+ global $auth;
+ /* @var Input $INPUT */
+ global $INPUT;
+ global $AUTH_ACL;
+ global $lang;
+ /* @var PluginController $plugin_controller */
+ global $plugin_controller;
+ $AUTH_ACL = array();
+
+ if(!$conf['useacl']) return false;
+
+ // try to load auth backend from plugins
+ foreach ($plugin_controller->getList('auth') as $plugin) {
+ if ($conf['authtype'] === $plugin) {
+ $auth = $plugin_controller->load('auth', $plugin);
+ break;
+ }
+ }
+
+ if(!isset($auth) || !$auth){
+ msg($lang['authtempfail'], -1);
+ return false;
+ }
+
+ if ($auth->success == false) {
+ // degrade to unauthenticated user
+ unset($auth);
+ auth_logoff();
+ msg($lang['authtempfail'], -1);
+ return false;
+ }
+
+ // do the login either by cookie or provided credentials XXX
+ $INPUT->set('http_credentials', false);
+ if(!$conf['rememberme']) $INPUT->set('r', false);
+
+ // handle renamed HTTP_AUTHORIZATION variable (can happen when a fix like
+ // the one presented at
+ // http://www.besthostratings.com/articles/http-auth-php-cgi.html is used
+ // for enabling HTTP authentication with CGI/SuExec)
+ if(isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']))
+ $_SERVER['HTTP_AUTHORIZATION'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
+ // streamline HTTP auth credentials (IIS/rewrite -> mod_php)
+ if(isset($_SERVER['HTTP_AUTHORIZATION'])) {
+ list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) =
+ explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
+ }
+
+ // if no credentials were given try to use HTTP auth (for SSO)
+ if(!$INPUT->str('u') && empty($_COOKIE[DOKU_COOKIE]) && !empty($_SERVER['PHP_AUTH_USER'])) {
+ $INPUT->set('u', $_SERVER['PHP_AUTH_USER']);
+ $INPUT->set('p', $_SERVER['PHP_AUTH_PW']);
+ $INPUT->set('http_credentials', true);
+ }
+
+ // apply cleaning (auth specific user names, remove control chars)
+ if (true === $auth->success) {
+ $INPUT->set('u', $auth->cleanUser(stripctl($INPUT->str('u'))));
+ $INPUT->set('p', stripctl($INPUT->str('p')));
+ }
+
+ $ok = null;
+ if (!is_null($auth) && $auth->canDo('external')) {
+ $ok = $auth->trustExternal($INPUT->str('u'), $INPUT->str('p'), $INPUT->bool('r'));
+ }
+
+ if ($ok === null) {
+ // external trust mechanism not in place, or returns no result,
+ // then attempt auth_login
+ $evdata = array(
+ 'user' => $INPUT->str('u'),
+ 'password' => $INPUT->str('p'),
+ 'sticky' => $INPUT->bool('r'),
+ 'silent' => $INPUT->bool('http_credentials')
+ );
+ Event::createAndTrigger('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper');
+ }
+
+ //load ACL into a global array XXX
+ $AUTH_ACL = auth_loadACL();
+
+ return true;
+}
+
+/**
+ * Loads the ACL setup and handle user wildcards
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @return array
+ */
+function auth_loadACL() {
+ global $config_cascade;
+ global $USERINFO;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ if(!is_readable($config_cascade['acl']['default'])) return array();
+
+ $acl = file($config_cascade['acl']['default']);
+
+ $out = array();
+ foreach($acl as $line) {
+ $line = trim($line);
+ if(empty($line) || ($line[0] == '#')) continue; // skip blank lines & comments
+ list($id,$rest) = preg_split('/[ \t]+/',$line,2);
+
+ // substitute user wildcard first (its 1:1)
+ if(strstr($line, '%USER%')){
+ // if user is not logged in, this ACL line is meaningless - skip it
+ if (!$INPUT->server->has('REMOTE_USER')) continue;
+
+ $id = str_replace('%USER%',cleanID($INPUT->server->str('REMOTE_USER')),$id);
+ $rest = str_replace('%USER%',auth_nameencode($INPUT->server->str('REMOTE_USER')),$rest);
+ }
+
+ // substitute group wildcard (its 1:m)
+ if(strstr($line, '%GROUP%')){
+ // if user is not logged in, grps is empty, no output will be added (i.e. skipped)
+ if(isset($USERINFO['grps'])){
+ foreach((array) $USERINFO['grps'] as $grp){
+ $nid = str_replace('%GROUP%',cleanID($grp),$id);
+ $nrest = str_replace('%GROUP%','@'.auth_nameencode($grp),$rest);
+ $out[] = "$nid\t$nrest";
+ }
+ }
+ } else {
+ $out[] = "$id\t$rest";
+ }
+ }
+
+ return $out;
+}
+
+/**
+ * Event hook callback for AUTH_LOGIN_CHECK
+ *
+ * @param array $evdata
+ * @return bool
+ */
+function auth_login_wrapper($evdata) {
+ return auth_login(
+ $evdata['user'],
+ $evdata['password'],
+ $evdata['sticky'],
+ $evdata['silent']
+ );
+}
+
+/**
+ * This tries to login the user based on the sent auth credentials
+ *
+ * The authentication works like this: if a username was given
+ * a new login is assumed and user/password are checked. If they
+ * are correct the password is encrypted with blowfish and stored
+ * together with the username in a cookie - the same info is stored
+ * in the session, too. Additonally a browserID is stored in the
+ * session.
+ *
+ * If no username was given the cookie is checked: if the username,
+ * crypted password and browserID match between session and cookie
+ * no further testing is done and the user is accepted
+ *
+ * If a cookie was found but no session info was availabe the
+ * blowfish encrypted password from the cookie is decrypted and
+ * together with username rechecked by calling this function again.
+ *
+ * On a successful login $_SERVER[REMOTE_USER] and $USERINFO
+ * are set.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $user Username
+ * @param string $pass Cleartext Password
+ * @param bool $sticky Cookie should not expire
+ * @param bool $silent Don't show error on bad auth
+ * @return bool true on successful auth
+ */
+function auth_login($user, $pass, $sticky = false, $silent = false) {
+ global $USERINFO;
+ global $conf;
+ global $lang;
+ /* @var AuthPlugin $auth */
+ global $auth;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ $sticky ? $sticky = true : $sticky = false; //sanity check
+
+ if(!$auth) return false;
+
+ if(!empty($user)) {
+ //usual login
+ if(!empty($pass) && $auth->checkPass($user, $pass)) {
+ // make logininfo globally available
+ $INPUT->server->set('REMOTE_USER', $user);
+ $secret = auth_cookiesalt(!$sticky, true); //bind non-sticky to session
+ auth_setCookie($user, auth_encrypt($pass, $secret), $sticky);
+ return true;
+ } else {
+ //invalid credentials - log off
+ if(!$silent) {
+ http_status(403, 'Login failed');
+ msg($lang['badlogin'], -1);
+ }
+ auth_logoff();
+ return false;
+ }
+ } else {
+ // read cookie information
+ list($user, $sticky, $pass) = auth_getCookie();
+ if($user && $pass) {
+ // we got a cookie - see if we can trust it
+
+ // get session info
+ $session = $_SESSION[DOKU_COOKIE]['auth'];
+ if(isset($session) &&
+ $auth->useSessionCache($user) &&
+ ($session['time'] >= time() - $conf['auth_security_timeout']) &&
+ ($session['user'] == $user) &&
+ ($session['pass'] == sha1($pass)) && //still crypted
+ ($session['buid'] == auth_browseruid())
+ ) {
+
+ // he has session, cookie and browser right - let him in
+ $INPUT->server->set('REMOTE_USER', $user);
+ $USERINFO = $session['info']; //FIXME move all references to session
+ return true;
+ }
+ // no we don't trust it yet - recheck pass but silent
+ $secret = auth_cookiesalt(!$sticky, true); //bind non-sticky to session
+ $pass = auth_decrypt($pass, $secret);
+ return auth_login($user, $pass, $sticky, true);
+ }
+ }
+ //just to be sure
+ auth_logoff(true);
+ return false;
+}
+
+/**
+ * Builds a pseudo UID from browser and IP data
+ *
+ * This is neither unique nor unfakable - still it adds some
+ * security. Using the first part of the IP makes sure
+ * proxy farms like AOLs are still okay.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @return string a MD5 sum of various browser headers
+ */
+function auth_browseruid() {
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ $ip = clientIP(true);
+ $uid = '';
+ $uid .= $INPUT->server->str('HTTP_USER_AGENT');
+ $uid .= $INPUT->server->str('HTTP_ACCEPT_CHARSET');
+ $uid .= substr($ip, 0, strpos($ip, '.'));
+ $uid = strtolower($uid);
+ return md5($uid);
+}
+
+/**
+ * Creates a random key to encrypt the password in cookies
+ *
+ * This function tries to read the password for encrypting
+ * cookies from $conf['metadir'].'/_htcookiesalt'
+ * if no such file is found a random key is created and
+ * and stored in this file.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param bool $addsession if true, the sessionid is added to the salt
+ * @param bool $secure if security is more important than keeping the old value
+ * @return string
+ */
+function auth_cookiesalt($addsession = false, $secure = false) {
+ if (defined('SIMPLE_TEST')) {
+ return 'test';
+ }
+ global $conf;
+ $file = $conf['metadir'].'/_htcookiesalt';
+ if ($secure || !file_exists($file)) {
+ $file = $conf['metadir'].'/_htcookiesalt2';
+ }
+ $salt = io_readFile($file);
+ if(empty($salt)) {
+ $salt = bin2hex(auth_randombytes(64));
+ io_saveFile($file, $salt);
+ }
+ if($addsession) {
+ $salt .= session_id();
+ }
+ return $salt;
+}
+
+/**
+ * Return cryptographically secure random bytes.
+ *
+ * @author Niklas Keller <me@kelunik.com>
+ *
+ * @param int $length number of bytes
+ * @return string cryptographically secure random bytes
+ */
+function auth_randombytes($length) {
+ return random_bytes($length);
+}
+
+/**
+ * Cryptographically secure random number generator.
+ *
+ * @author Niklas Keller <me@kelunik.com>
+ *
+ * @param int $min
+ * @param int $max
+ * @return int
+ */
+function auth_random($min, $max) {
+ return random_int($min, $max);
+}
+
+/**
+ * Encrypt data using the given secret using AES
+ *
+ * The mode is CBC with a random initialization vector, the key is derived
+ * using pbkdf2.
+ *
+ * @param string $data The data that shall be encrypted
+ * @param string $secret The secret/password that shall be used
+ * @return string The ciphertext
+ */
+function auth_encrypt($data, $secret) {
+ $iv = auth_randombytes(16);
+ $cipher = new \phpseclib\Crypt\AES();
+ $cipher->setPassword($secret);
+
+ /*
+ this uses the encrypted IV as IV as suggested in
+ http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf, Appendix C
+ for unique but necessarily random IVs. The resulting ciphertext is
+ compatible to ciphertext that was created using a "normal" IV.
+ */
+ return $cipher->encrypt($iv.$data);
+}
+
+/**
+ * Decrypt the given AES ciphertext
+ *
+ * The mode is CBC, the key is derived using pbkdf2
+ *
+ * @param string $ciphertext The encrypted data
+ * @param string $secret The secret/password that shall be used
+ * @return string The decrypted data
+ */
+function auth_decrypt($ciphertext, $secret) {
+ $iv = substr($ciphertext, 0, 16);
+ $cipher = new \phpseclib\Crypt\AES();
+ $cipher->setPassword($secret);
+ $cipher->setIV($iv);
+
+ return $cipher->decrypt(substr($ciphertext, 16));
+}
+
+/**
+ * Log out the current user
+ *
+ * This clears all authentication data and thus log the user
+ * off. It also clears session data.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param bool $keepbc - when true, the breadcrumb data is not cleared
+ */
+function auth_logoff($keepbc = false) {
+ global $conf;
+ global $USERINFO;
+ /* @var AuthPlugin $auth */
+ global $auth;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ // make sure the session is writable (it usually is)
+ @session_start();
+
+ if(isset($_SESSION[DOKU_COOKIE]['auth']['user']))
+ unset($_SESSION[DOKU_COOKIE]['auth']['user']);
+ if(isset($_SESSION[DOKU_COOKIE]['auth']['pass']))
+ unset($_SESSION[DOKU_COOKIE]['auth']['pass']);
+ if(isset($_SESSION[DOKU_COOKIE]['auth']['info']))
+ unset($_SESSION[DOKU_COOKIE]['auth']['info']);
+ if(!$keepbc && isset($_SESSION[DOKU_COOKIE]['bc']))
+ unset($_SESSION[DOKU_COOKIE]['bc']);
+ $INPUT->server->remove('REMOTE_USER');
+ $USERINFO = null; //FIXME
+
+ $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
+ setcookie(DOKU_COOKIE, '', time() - 600000, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
+
+ if($auth) $auth->logOff();
+}
+
+/**
+ * Check if a user is a manager
+ *
+ * Should usually be called without any parameters to check the current
+ * user.
+ *
+ * The info is available through $INFO['ismanager'], too
+ *
+ * @param string $user Username
+ * @param array $groups List of groups the user is in
+ * @param bool $adminonly when true checks if user is admin
+ * @param bool $recache set to true to refresh the cache
+ * @return bool
+ * @see auth_isadmin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function auth_ismanager($user = null, $groups = null, $adminonly = false, $recache=false) {
+ global $conf;
+ global $USERINFO;
+ /* @var AuthPlugin $auth */
+ global $auth;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+
+ if(!$auth) return false;
+ if(is_null($user)) {
+ if(!$INPUT->server->has('REMOTE_USER')) {
+ return false;
+ } else {
+ $user = $INPUT->server->str('REMOTE_USER');
+ }
+ }
+ if(is_null($groups)) {
+ $groups = $USERINFO ? (array) $USERINFO['grps'] : array();
+ }
+
+ // prefer cached result
+ static $cache = [];
+ $cachekey = serialize([$user, $adminonly, $groups]);
+ if (!isset($cache[$cachekey]) || $recache) {
+ // check superuser match
+ $ok = auth_isMember($conf['superuser'], $user, $groups);
+
+ // check managers
+ if (!$ok && !$adminonly) {
+ $ok = auth_isMember($conf['manager'], $user, $groups);
+ }
+
+ $cache[$cachekey] = $ok;
+ }
+
+ return $cache[$cachekey];
+}
+
+/**
+ * Check if a user is admin
+ *
+ * Alias to auth_ismanager with adminonly=true
+ *
+ * The info is available through $INFO['isadmin'], too
+ *
+ * @param string $user Username
+ * @param array $groups List of groups the user is in
+ * @param bool $recache set to true to refresh the cache
+ * @return bool
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @see auth_ismanager()
+ *
+ */
+function auth_isadmin($user = null, $groups = null, $recache=false) {
+ return auth_ismanager($user, $groups, true, $recache);
+}
+
+/**
+ * Match a user and his groups against a comma separated list of
+ * users and groups to determine membership status
+ *
+ * Note: all input should NOT be nameencoded.
+ *
+ * @param string $memberlist commaseparated list of allowed users and groups
+ * @param string $user user to match against
+ * @param array $groups groups the user is member of
+ * @return bool true for membership acknowledged
+ */
+function auth_isMember($memberlist, $user, array $groups) {
+ /* @var AuthPlugin $auth */
+ global $auth;
+ if(!$auth) return false;
+
+ // clean user and groups
+ if(!$auth->isCaseSensitive()) {
+ $user = \dokuwiki\Utf8\PhpString::strtolower($user);
+ $groups = array_map('utf8_strtolower', $groups);
+ }
+ $user = $auth->cleanUser($user);
+ $groups = array_map(array($auth, 'cleanGroup'), $groups);
+
+ // extract the memberlist
+ $members = explode(',', $memberlist);
+ $members = array_map('trim', $members);
+ $members = array_unique($members);
+ $members = array_filter($members);
+
+ // compare cleaned values
+ foreach($members as $member) {
+ if($member == '@ALL' ) return true;
+ if(!$auth->isCaseSensitive()) $member = \dokuwiki\Utf8\PhpString::strtolower($member);
+ if($member[0] == '@') {
+ $member = $auth->cleanGroup(substr($member, 1));
+ if(in_array($member, $groups)) return true;
+ } else {
+ $member = $auth->cleanUser($member);
+ if($member == $user) return true;
+ }
+ }
+
+ // still here? not a member!
+ return false;
+}
+
+/**
+ * Convinience function for auth_aclcheck()
+ *
+ * This checks the permissions for the current user
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id page ID (needs to be resolved and cleaned)
+ * @return int permission level
+ */
+function auth_quickaclcheck($id) {
+ global $conf;
+ global $USERINFO;
+ /* @var Input $INPUT */
+ global $INPUT;
+ # if no ACL is used always return upload rights
+ if(!$conf['useacl']) return AUTH_UPLOAD;
+ return auth_aclcheck($id, $INPUT->server->str('REMOTE_USER'), is_array($USERINFO) ? $USERINFO['grps'] : array());
+}
+
+/**
+ * Returns the maximum rights a user has for the given ID or its namespace
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @triggers AUTH_ACL_CHECK
+ * @param string $id page ID (needs to be resolved and cleaned)
+ * @param string $user Username
+ * @param array|null $groups Array of groups the user is in
+ * @return int permission level
+ */
+function auth_aclcheck($id, $user, $groups) {
+ $data = array(
+ 'id' => $id,
+ 'user' => $user,
+ 'groups' => $groups
+ );
+
+ return Event::createAndTrigger('AUTH_ACL_CHECK', $data, 'auth_aclcheck_cb');
+}
+
+/**
+ * default ACL check method
+ *
+ * DO NOT CALL DIRECTLY, use auth_aclcheck() instead
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $data event data
+ * @return int permission level
+ */
+function auth_aclcheck_cb($data) {
+ $id =& $data['id'];
+ $user =& $data['user'];
+ $groups =& $data['groups'];
+
+ global $conf;
+ global $AUTH_ACL;
+ /* @var AuthPlugin $auth */
+ global $auth;
+
+ // if no ACL is used always return upload rights
+ if(!$conf['useacl']) return AUTH_UPLOAD;
+ if(!$auth) return AUTH_NONE;
+
+ //make sure groups is an array
+ if(!is_array($groups)) $groups = array();
+
+ //if user is superuser or in superusergroup return 255 (acl_admin)
+ if(auth_isadmin($user, $groups)) {
+ return AUTH_ADMIN;
+ }
+
+ if(!$auth->isCaseSensitive()) {
+ $user = \dokuwiki\Utf8\PhpString::strtolower($user);
+ $groups = array_map('utf8_strtolower', $groups);
+ }
+ $user = auth_nameencode($auth->cleanUser($user));
+ $groups = array_map(array($auth, 'cleanGroup'), (array) $groups);
+
+ //prepend groups with @ and nameencode
+ foreach($groups as &$group) {
+ $group = '@'.auth_nameencode($group);
+ }
+
+ $ns = getNS($id);
+ $perm = -1;
+
+ //add ALL group
+ $groups[] = '@ALL';
+
+ //add User
+ if($user) $groups[] = $user;
+
+ //check exact match first
+ $matches = preg_grep('/^'.preg_quote($id, '/').'[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL);
+ if(count($matches)) {
+ foreach($matches as $match) {
+ $match = preg_replace('/#.*$/', '', $match); //ignore comments
+ $acl = preg_split('/[ \t]+/', $match);
+ if(!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
+ $acl[1] = \dokuwiki\Utf8\PhpString::strtolower($acl[1]);
+ }
+ if(!in_array($acl[1], $groups)) {
+ continue;
+ }
+ if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
+ if($acl[2] > $perm) {
+ $perm = $acl[2];
+ }
+ }
+ if($perm > -1) {
+ //we had a match - return it
+ return (int) $perm;
+ }
+ }
+
+ //still here? do the namespace checks
+ if($ns) {
+ $path = $ns.':*';
+ } else {
+ $path = '*'; //root document
+ }
+
+ do {
+ $matches = preg_grep('/^'.preg_quote($path, '/').'[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL);
+ if(count($matches)) {
+ foreach($matches as $match) {
+ $match = preg_replace('/#.*$/', '', $match); //ignore comments
+ $acl = preg_split('/[ \t]+/', $match);
+ if(!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
+ $acl[1] = \dokuwiki\Utf8\PhpString::strtolower($acl[1]);
+ }
+ if(!in_array($acl[1], $groups)) {
+ continue;
+ }
+ if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
+ if($acl[2] > $perm) {
+ $perm = $acl[2];
+ }
+ }
+ //we had a match - return it
+ if($perm != -1) {
+ return (int) $perm;
+ }
+ }
+ //get next higher namespace
+ $ns = getNS($ns);
+
+ if($path != '*') {
+ $path = $ns.':*';
+ if($path == ':*') $path = '*';
+ } else {
+ //we did this already
+ //looks like there is something wrong with the ACL
+ //break here
+ msg('No ACL setup yet! Denying access to everyone.');
+ return AUTH_NONE;
+ }
+ } while(1); //this should never loop endless
+ return AUTH_NONE;
+}
+
+/**
+ * Encode ASCII special chars
+ *
+ * Some auth backends allow special chars in their user and groupnames
+ * The special chars are encoded with this function. Only ASCII chars
+ * are encoded UTF-8 multibyte are left as is (different from usual
+ * urlencoding!).
+ *
+ * Decoding can be done with rawurldecode
+ *
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ * @see rawurldecode()
+ *
+ * @param string $name
+ * @param bool $skip_group
+ * @return string
+ */
+function auth_nameencode($name, $skip_group = false) {
+ global $cache_authname;
+ $cache =& $cache_authname;
+ $name = (string) $name;
+
+ // never encode wildcard FS#1955
+ if($name == '%USER%') return $name;
+ if($name == '%GROUP%') return $name;
+
+ if(!isset($cache[$name][$skip_group])) {
+ if($skip_group && $name[0] == '@') {
+ $cache[$name][$skip_group] = '@'.preg_replace_callback(
+ '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/',
+ 'auth_nameencode_callback', substr($name, 1)
+ );
+ } else {
+ $cache[$name][$skip_group] = preg_replace_callback(
+ '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/',
+ 'auth_nameencode_callback', $name
+ );
+ }
+ }
+
+ return $cache[$name][$skip_group];
+}
+
+/**
+ * callback encodes the matches
+ *
+ * @param array $matches first complete match, next matching subpatterms
+ * @return string
+ */
+function auth_nameencode_callback($matches) {
+ return '%'.dechex(ord(substr($matches[1],-1)));
+}
+
+/**
+ * Create a pronouncable password
+ *
+ * The $foruser variable might be used by plugins to run additional password
+ * policy checks, but is not used by the default implementation
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @link http://www.phpbuilder.com/annotate/message.php3?id=1014451
+ * @triggers AUTH_PASSWORD_GENERATE
+ *
+ * @param string $foruser username for which the password is generated
+ * @return string pronouncable password
+ */
+function auth_pwgen($foruser = '') {
+ $data = array(
+ 'password' => '',
+ 'foruser' => $foruser
+ );
+
+ $evt = new Event('AUTH_PASSWORD_GENERATE', $data);
+ if($evt->advise_before(true)) {
+ $c = 'bcdfghjklmnprstvwz'; //consonants except hard to speak ones
+ $v = 'aeiou'; //vowels
+ $a = $c.$v; //both
+ $s = '!$%&?+*~#-_:.;,'; // specials
+
+ //use thre syllables...
+ for($i = 0; $i < 3; $i++) {
+ $data['password'] .= $c[auth_random(0, strlen($c) - 1)];
+ $data['password'] .= $v[auth_random(0, strlen($v) - 1)];
+ $data['password'] .= $a[auth_random(0, strlen($a) - 1)];
+ }
+ //... and add a nice number and special
+ $data['password'] .= $s[auth_random(0, strlen($s) - 1)].auth_random(10, 99);
+ }
+ $evt->advise_after();
+
+ return $data['password'];
+}
+
+/**
+ * Sends a password to the given user
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $user Login name of the user
+ * @param string $password The new password in clear text
+ * @return bool true on success
+ */
+function auth_sendPassword($user, $password) {
+ global $lang;
+ /* @var AuthPlugin $auth */
+ global $auth;
+ if(!$auth) return false;
+
+ $user = $auth->cleanUser($user);
+ $userinfo = $auth->getUserData($user, $requireGroups = false);
+
+ if(!$userinfo['mail']) return false;
+
+ $text = rawLocale('password');
+ $trep = array(
+ 'FULLNAME' => $userinfo['name'],
+ 'LOGIN' => $user,
+ 'PASSWORD' => $password
+ );
+
+ $mail = new Mailer();
+ $mail->to($mail->getCleanName($userinfo['name']).' <'.$userinfo['mail'].'>');
+ $mail->subject($lang['regpwmail']);
+ $mail->setBody($text, $trep);
+ return $mail->send();
+}
+
+/**
+ * Register a new user
+ *
+ * This registers a new user - Data is read directly from $_POST
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @return bool true on success, false on any error
+ */
+function register() {
+ global $lang;
+ global $conf;
+ /* @var \dokuwiki\Extension\AuthPlugin $auth */
+ global $auth;
+ global $INPUT;
+
+ if(!$INPUT->post->bool('save')) return false;
+ if(!actionOK('register')) return false;
+
+ // gather input
+ $login = trim($auth->cleanUser($INPUT->post->str('login')));
+ $fullname = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('fullname')));
+ $email = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('email')));
+ $pass = $INPUT->post->str('pass');
+ $passchk = $INPUT->post->str('passchk');
+
+ if(empty($login) || empty($fullname) || empty($email)) {
+ msg($lang['regmissing'], -1);
+ return false;
+ }
+
+ if($conf['autopasswd']) {
+ $pass = auth_pwgen($login); // automatically generate password
+ } elseif(empty($pass) || empty($passchk)) {
+ msg($lang['regmissing'], -1); // complain about missing passwords
+ return false;
+ } elseif($pass != $passchk) {
+ msg($lang['regbadpass'], -1); // complain about misspelled passwords
+ return false;
+ }
+
+ //check mail
+ if(!mail_isvalid($email)) {
+ msg($lang['regbadmail'], -1);
+ return false;
+ }
+
+ //okay try to create the user
+ if(!$auth->triggerUserMod('create', array($login, $pass, $fullname, $email))) {
+ msg($lang['regfail'], -1);
+ return false;
+ }
+
+ // send notification about the new user
+ $subscription = new RegistrationSubscriptionSender();
+ $subscription->sendRegister($login, $fullname, $email);
+
+ // are we done?
+ if(!$conf['autopasswd']) {
+ msg($lang['regsuccess2'], 1);
+ return true;
+ }
+
+ // autogenerated password? then send password to user
+ if(auth_sendPassword($login, $pass)) {
+ msg($lang['regsuccess'], 1);
+ return true;
+ } else {
+ msg($lang['regmailfail'], -1);
+ return false;
+ }
+}
+
+/**
+ * Update user profile
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+function updateprofile() {
+ global $conf;
+ global $lang;
+ /* @var AuthPlugin $auth */
+ global $auth;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ if(!$INPUT->post->bool('save')) return false;
+ if(!checkSecurityToken()) return false;
+
+ if(!actionOK('profile')) {
+ msg($lang['profna'], -1);
+ return false;
+ }
+
+ $changes = array();
+ $changes['pass'] = $INPUT->post->str('newpass');
+ $changes['name'] = $INPUT->post->str('fullname');
+ $changes['mail'] = $INPUT->post->str('email');
+
+ // check misspelled passwords
+ if($changes['pass'] != $INPUT->post->str('passchk')) {
+ msg($lang['regbadpass'], -1);
+ return false;
+ }
+
+ // clean fullname and email
+ $changes['name'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['name']));
+ $changes['mail'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['mail']));
+
+ // no empty name and email (except the backend doesn't support them)
+ if((empty($changes['name']) && $auth->canDo('modName')) ||
+ (empty($changes['mail']) && $auth->canDo('modMail'))
+ ) {
+ msg($lang['profnoempty'], -1);
+ return false;
+ }
+ if(!mail_isvalid($changes['mail']) && $auth->canDo('modMail')) {
+ msg($lang['regbadmail'], -1);
+ return false;
+ }
+
+ $changes = array_filter($changes);
+
+ // check for unavailable capabilities
+ if(!$auth->canDo('modName')) unset($changes['name']);
+ if(!$auth->canDo('modMail')) unset($changes['mail']);
+ if(!$auth->canDo('modPass')) unset($changes['pass']);
+
+ // anything to do?
+ if(!count($changes)) {
+ msg($lang['profnochange'], -1);
+ return false;
+ }
+
+ if($conf['profileconfirm']) {
+ if(!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) {
+ msg($lang['badpassconfirm'], -1);
+ return false;
+ }
+ }
+
+ if(!$auth->triggerUserMod('modify', array($INPUT->server->str('REMOTE_USER'), &$changes))) {
+ msg($lang['proffail'], -1);
+ return false;
+ }
+
+ if($changes['pass']) {
+ // update cookie and session with the changed data
+ list( /*user*/, $sticky, /*pass*/) = auth_getCookie();
+ $pass = auth_encrypt($changes['pass'], auth_cookiesalt(!$sticky, true));
+ auth_setCookie($INPUT->server->str('REMOTE_USER'), $pass, (bool) $sticky);
+ } else {
+ // make sure the session is writable
+ @session_start();
+ // invalidate session cache
+ $_SESSION[DOKU_COOKIE]['auth']['time'] = 0;
+ session_write_close();
+ }
+
+ return true;
+}
+
+/**
+ * Delete the current logged-in user
+ *
+ * @return bool true on success, false on any error
+ */
+function auth_deleteprofile(){
+ global $conf;
+ global $lang;
+ /* @var \dokuwiki\Extension\AuthPlugin $auth */
+ global $auth;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ if(!$INPUT->post->bool('delete')) return false;
+ if(!checkSecurityToken()) return false;
+
+ // action prevented or auth module disallows
+ if(!actionOK('profile_delete') || !$auth->canDo('delUser')) {
+ msg($lang['profnodelete'], -1);
+ return false;
+ }
+
+ if(!$INPUT->post->bool('confirm_delete')){
+ msg($lang['profconfdeletemissing'], -1);
+ return false;
+ }
+
+ if($conf['profileconfirm']) {
+ if(!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) {
+ msg($lang['badpassconfirm'], -1);
+ return false;
+ }
+ }
+
+ $deleted = array();
+ $deleted[] = $INPUT->server->str('REMOTE_USER');
+ if($auth->triggerUserMod('delete', array($deleted))) {
+ // force and immediate logout including removing the sticky cookie
+ auth_logoff();
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Send a new password
+ *
+ * This function handles both phases of the password reset:
+ *
+ * - handling the first request of password reset
+ * - validating the password reset auth token
+ *
+ * @author Benoit Chesneau <benoit@bchesneau.info>
+ * @author Chris Smith <chris@jalakai.co.uk>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @return bool true on success, false on any error
+ */
+function act_resendpwd() {
+ global $lang;
+ global $conf;
+ /* @var AuthPlugin $auth */
+ global $auth;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ if(!actionOK('resendpwd')) {
+ msg($lang['resendna'], -1);
+ return false;
+ }
+
+ $token = preg_replace('/[^a-f0-9]+/', '', $INPUT->str('pwauth'));
+
+ if($token) {
+ // we're in token phase - get user info from token
+
+ $tfile = $conf['cachedir'].'/'.$token[0].'/'.$token.'.pwauth';
+ if(!file_exists($tfile)) {
+ msg($lang['resendpwdbadauth'], -1);
+ $INPUT->remove('pwauth');
+ return false;
+ }
+ // token is only valid for 3 days
+ if((time() - filemtime($tfile)) > (3 * 60 * 60 * 24)) {
+ msg($lang['resendpwdbadauth'], -1);
+ $INPUT->remove('pwauth');
+ @unlink($tfile);
+ return false;
+ }
+
+ $user = io_readfile($tfile);
+ $userinfo = $auth->getUserData($user, $requireGroups = false);
+ if(!$userinfo['mail']) {
+ msg($lang['resendpwdnouser'], -1);
+ return false;
+ }
+
+ if(!$conf['autopasswd']) { // we let the user choose a password
+ $pass = $INPUT->str('pass');
+
+ // password given correctly?
+ if(!$pass) return false;
+ if($pass != $INPUT->str('passchk')) {
+ msg($lang['regbadpass'], -1);
+ return false;
+ }
+
+ // change it
+ if(!$auth->triggerUserMod('modify', array($user, array('pass' => $pass)))) {
+ msg($lang['proffail'], -1);
+ return false;
+ }
+
+ } else { // autogenerate the password and send by mail
+
+ $pass = auth_pwgen($user);
+ if(!$auth->triggerUserMod('modify', array($user, array('pass' => $pass)))) {
+ msg($lang['proffail'], -1);
+ return false;
+ }
+
+ if(auth_sendPassword($user, $pass)) {
+ msg($lang['resendpwdsuccess'], 1);
+ } else {
+ msg($lang['regmailfail'], -1);
+ }
+ }
+
+ @unlink($tfile);
+ return true;
+
+ } else {
+ // we're in request phase
+
+ if(!$INPUT->post->bool('save')) return false;
+
+ if(!$INPUT->post->str('login')) {
+ msg($lang['resendpwdmissing'], -1);
+ return false;
+ } else {
+ $user = trim($auth->cleanUser($INPUT->post->str('login')));
+ }
+
+ $userinfo = $auth->getUserData($user, $requireGroups = false);
+ if(!$userinfo['mail']) {
+ msg($lang['resendpwdnouser'], -1);
+ return false;
+ }
+
+ // generate auth token
+ $token = md5(auth_randombytes(16)); // random secret
+ $tfile = $conf['cachedir'].'/'.$token[0].'/'.$token.'.pwauth';
+ $url = wl('', array('do'=> 'resendpwd', 'pwauth'=> $token), true, '&');
+
+ io_saveFile($tfile, $user);
+
+ $text = rawLocale('pwconfirm');
+ $trep = array(
+ 'FULLNAME' => $userinfo['name'],
+ 'LOGIN' => $user,
+ 'CONFIRM' => $url
+ );
+
+ $mail = new Mailer();
+ $mail->to($userinfo['name'].' <'.$userinfo['mail'].'>');
+ $mail->subject($lang['regpwmail']);
+ $mail->setBody($text, $trep);
+ if($mail->send()) {
+ msg($lang['resendpwdconfirm'], 1);
+ } else {
+ msg($lang['regmailfail'], -1);
+ }
+ return true;
+ }
+ // never reached
+}
+
+/**
+ * Encrypts a password using the given method and salt
+ *
+ * If the selected method needs a salt and none was given, a random one
+ * is chosen.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $clear The clear text password
+ * @param string $method The hashing method
+ * @param string $salt A salt, null for random
+ * @return string The crypted password
+ */
+function auth_cryptPassword($clear, $method = '', $salt = null) {
+ global $conf;
+ if(empty($method)) $method = $conf['passcrypt'];
+
+ $pass = new PassHash();
+ $call = 'hash_'.$method;
+
+ if(!method_exists($pass, $call)) {
+ msg("Unsupported crypt method $method", -1);
+ return false;
+ }
+
+ return $pass->$call($clear, $salt);
+}
+
+/**
+ * Verifies a cleartext password against a crypted hash
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $clear The clear text password
+ * @param string $crypt The hash to compare with
+ * @return bool true if both match
+ */
+function auth_verifyPassword($clear, $crypt) {
+ $pass = new PassHash();
+ return $pass->verify_hash($clear, $crypt);
+}
+
+/**
+ * Set the authentication cookie and add user identification data to the session
+ *
+ * @param string $user username
+ * @param string $pass encrypted password
+ * @param bool $sticky whether or not the cookie will last beyond the session
+ * @return bool
+ */
+function auth_setCookie($user, $pass, $sticky) {
+ global $conf;
+ /* @var AuthPlugin $auth */
+ global $auth;
+ global $USERINFO;
+
+ if(!$auth) return false;
+ $USERINFO = $auth->getUserData($user);
+
+ // set cookie
+ $cookie = base64_encode($user).'|'.((int) $sticky).'|'.base64_encode($pass);
+ $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
+ $time = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year
+ setcookie(DOKU_COOKIE, $cookie, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
+
+ // set session
+ $_SESSION[DOKU_COOKIE]['auth']['user'] = $user;
+ $_SESSION[DOKU_COOKIE]['auth']['pass'] = sha1($pass);
+ $_SESSION[DOKU_COOKIE]['auth']['buid'] = auth_browseruid();
+ $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
+ $_SESSION[DOKU_COOKIE]['auth']['time'] = time();
+
+ return true;
+}
+
+/**
+ * Returns the user, (encrypted) password and sticky bit from cookie
+ *
+ * @returns array
+ */
+function auth_getCookie() {
+ if(!isset($_COOKIE[DOKU_COOKIE])) {
+ return array(null, null, null);
+ }
+ list($user, $sticky, $pass) = explode('|', $_COOKIE[DOKU_COOKIE], 3);
+ $sticky = (bool) $sticky;
+ $pass = base64_decode($pass);
+ $user = base64_decode($user);
+ return array($user, $sticky, $pass);
+}
+
+//Setup VIM: ex: et ts=2 :
diff --git a/platform/www/inc/cache.php b/platform/www/inc/cache.php
new file mode 100644
index 0000000..b5793c2
--- /dev/null
+++ b/platform/www/inc/cache.php
@@ -0,0 +1,57 @@
+<?php
+// phpcs:ignoreFile
+use dokuwiki\Cache\CacheParser;
+use dokuwiki\Cache\CacheInstructions;
+use dokuwiki\Cache\CacheRenderer;
+use dokuwiki\Debug\DebugHelper;
+
+/**
+ * @deprecated since 2019-02-02 use \dokuwiki\Cache\Cache instead!
+ */
+class cache extends \dokuwiki\Cache\Cache
+{
+ public function __construct($key, $ext)
+ {
+ DebugHelper::dbgDeprecatedFunction(dokuwiki\Cache\Cache::class);
+ parent::__construct($key, $ext);
+ }
+}
+
+/**
+ * @deprecated since 2019-02-02 use \dokuwiki\Cache\CacheParser instead!
+ */
+class cache_parser extends \dokuwiki\Cache\CacheParser
+{
+
+ public function __construct($id, $file, $mode)
+ {
+ DebugHelper::dbgDeprecatedFunction(CacheParser::class);
+ parent::__construct($id, $file, $mode);
+ }
+
+}
+
+/**
+ * @deprecated since 2019-02-02 use \dokuwiki\Cache\CacheRenderer instead!
+ */
+class cache_renderer extends \dokuwiki\Cache\CacheRenderer
+{
+
+ public function __construct($id, $file, $mode)
+ {
+ DebugHelper::dbgDeprecatedFunction(CacheRenderer::class);
+ parent::__construct($id, $file, $mode);
+ }
+}
+
+/**
+ * @deprecated since 2019-02-02 use \dokuwiki\Cache\CacheInstructions instead!
+ */
+class cache_instructions extends \dokuwiki\Cache\CacheInstructions
+{
+ public function __construct($id, $file)
+ {
+ DebugHelper::dbgDeprecatedFunction(CacheInstructions::class);
+ parent::__construct($id, $file);
+ }
+}
diff --git a/platform/www/inc/changelog.php b/platform/www/inc/changelog.php
new file mode 100644
index 0000000..f02572e
--- /dev/null
+++ b/platform/www/inc/changelog.php
@@ -0,0 +1,403 @@
+<?php
+/**
+ * Changelog handling functions
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+/**
+ * parses a changelog line into it's components
+ *
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ *
+ * @param string $line changelog line
+ * @return array|bool parsed line or false
+ */
+function parseChangelogLine($line) {
+ $line = rtrim($line, "\n");
+ $tmp = explode("\t", $line);
+ if ($tmp!==false && count($tmp)>1) {
+ $info = array();
+ $info['date'] = (int)$tmp[0]; // unix timestamp
+ $info['ip'] = $tmp[1]; // IPv4 address (127.0.0.1)
+ $info['type'] = $tmp[2]; // log line type
+ $info['id'] = $tmp[3]; // page id
+ $info['user'] = $tmp[4]; // user name
+ $info['sum'] = $tmp[5]; // edit summary (or action reason)
+ $info['extra'] = $tmp[6]; // extra data (varies by line type)
+ if(isset($tmp[7]) && $tmp[7] !== '') { //last item has line-end||
+ $info['sizechange'] = (int) $tmp[7];
+ } else {
+ $info['sizechange'] = null;
+ }
+ return $info;
+ } else {
+ return false;
+ }
+}
+
+/**
+ * Add's an entry to the changelog and saves the metadata for the page
+ *
+ * @param int $date Timestamp of the change
+ * @param String $id Name of the affected page
+ * @param String $type Type of the change see DOKU_CHANGE_TYPE_*
+ * @param String $summary Summary of the change
+ * @param mixed $extra In case of a revert the revision (timestmp) of the reverted page
+ * @param array $flags Additional flags in a key value array.
+ * Available flags:
+ * - ExternalEdit - mark as an external edit.
+ * @param null|int $sizechange Change of filesize
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Esther Brunner <wikidesign@gmail.com>
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ */
+function addLogEntry($date, $id, $type=DOKU_CHANGE_TYPE_EDIT, $summary='', $extra='', $flags=null, $sizechange = null){
+ global $conf, $INFO;
+ /** @var Input $INPUT */
+ global $INPUT;
+
+ // check for special flags as keys
+ if (!is_array($flags)) { $flags = array(); }
+ $flagExternalEdit = isset($flags['ExternalEdit']);
+
+ $id = cleanid($id);
+ $file = wikiFN($id);
+ $created = @filectime($file);
+ $minor = ($type===DOKU_CHANGE_TYPE_MINOR_EDIT);
+ $wasRemoved = ($type===DOKU_CHANGE_TYPE_DELETE);
+
+ if(!$date) $date = time(); //use current time if none supplied
+ $remote = (!$flagExternalEdit)?clientIP(true):'127.0.0.1';
+ $user = (!$flagExternalEdit)?$INPUT->server->str('REMOTE_USER'):'';
+ if($sizechange === null) {
+ $sizechange = '';
+ } else {
+ $sizechange = (int) $sizechange;
+ }
+
+ $strip = array("\t", "\n");
+ $logline = array(
+ 'date' => $date,
+ 'ip' => $remote,
+ 'type' => str_replace($strip, '', $type),
+ 'id' => $id,
+ 'user' => $user,
+ 'sum' => \dokuwiki\Utf8\PhpString::substr(str_replace($strip, '', $summary), 0, 255),
+ 'extra' => str_replace($strip, '', $extra),
+ 'sizechange' => $sizechange
+ );
+
+ $wasCreated = ($type===DOKU_CHANGE_TYPE_CREATE);
+ $wasReverted = ($type===DOKU_CHANGE_TYPE_REVERT);
+ // update metadata
+ if (!$wasRemoved) {
+ $oldmeta = p_read_metadata($id);
+ $meta = array();
+ if (
+ $wasCreated && (
+ empty($oldmeta['persistent']['date']['created']) ||
+ $oldmeta['persistent']['date']['created'] === $created
+ )
+ ){
+ // newly created
+ $meta['date']['created'] = $created;
+ if ($user){
+ $meta['creator'] = isset($INFO) ? $INFO['userinfo']['name'] : null;
+ $meta['user'] = $user;
+ }
+ } elseif (($wasCreated || $wasReverted) && !empty($oldmeta['persistent']['date']['created'])) {
+ // re-created / restored
+ $meta['date']['created'] = $oldmeta['persistent']['date']['created'];
+ $meta['date']['modified'] = $created; // use the files ctime here
+ $meta['creator'] = $oldmeta['persistent']['creator'];
+ if ($user) $meta['contributor'][$user] = isset($INFO) ? $INFO['userinfo']['name'] : null;
+ } elseif (!$minor) { // non-minor modification
+ $meta['date']['modified'] = $date;
+ if ($user) $meta['contributor'][$user] = isset($INFO) ? $INFO['userinfo']['name'] : null;
+ }
+ $meta['last_change'] = $logline;
+ p_set_metadata($id, $meta);
+ }
+
+ // add changelog lines
+ $logline = implode("\t", $logline)."\n";
+ io_saveFile(metaFN($id,'.changes'),$logline,true); //page changelog
+ io_saveFile($conf['changelog'],$logline,true); //global changelog cache
+}
+
+/**
+ * Add's an entry to the media changelog
+ *
+ * @author Michael Hamann <michael@content-space.de>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Esther Brunner <wikidesign@gmail.com>
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ *
+ * @param int $date Timestamp of the change
+ * @param String $id Name of the affected page
+ * @param String $type Type of the change see DOKU_CHANGE_TYPE_*
+ * @param String $summary Summary of the change
+ * @param mixed $extra In case of a revert the revision (timestmp) of the reverted page
+ * @param array $flags Additional flags in a key value array.
+ * Available flags:
+ * - (none, so far)
+ * @param null|int $sizechange Change of filesize
+ */
+function addMediaLogEntry(
+ $date,
+ $id,
+ $type=DOKU_CHANGE_TYPE_EDIT,
+ $summary='',
+ $extra='',
+ $flags=null,
+ $sizechange = null)
+{
+ global $conf;
+ /** @var Input $INPUT */
+ global $INPUT;
+
+ $id = cleanid($id);
+
+ if(!$date) $date = time(); //use current time if none supplied
+ $remote = clientIP(true);
+ $user = $INPUT->server->str('REMOTE_USER');
+ if($sizechange === null) {
+ $sizechange = '';
+ } else {
+ $sizechange = (int) $sizechange;
+ }
+
+ $strip = array("\t", "\n");
+ $logline = array(
+ 'date' => $date,
+ 'ip' => $remote,
+ 'type' => str_replace($strip, '', $type),
+ 'id' => $id,
+ 'user' => $user,
+ 'sum' => \dokuwiki\Utf8\PhpString::substr(str_replace($strip, '', $summary), 0, 255),
+ 'extra' => str_replace($strip, '', $extra),
+ 'sizechange' => $sizechange
+ );
+
+ // add changelog lines
+ $logline = implode("\t", $logline)."\n";
+ io_saveFile($conf['media_changelog'],$logline,true); //global media changelog cache
+ io_saveFile(mediaMetaFN($id,'.changes'),$logline,true); //media file's changelog
+}
+
+/**
+ * returns an array of recently changed files using the
+ * changelog
+ *
+ * The following constants can be used to control which changes are
+ * included. Add them together as needed.
+ *
+ * RECENTS_SKIP_DELETED - don't include deleted pages
+ * RECENTS_SKIP_MINORS - don't include minor changes
+ * RECENTS_ONLY_CREATION - only include new created pages and media
+ * RECENTS_SKIP_SUBSPACES - don't include subspaces
+ * RECENTS_MEDIA_CHANGES - return media changes instead of page changes
+ * RECENTS_MEDIA_PAGES_MIXED - return both media changes and page changes
+ *
+ * @param int $first number of first entry returned (for paginating
+ * @param int $num return $num entries
+ * @param string $ns restrict to given namespace
+ * @param int $flags see above
+ * @return array recently changed files
+ *
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ */
+function getRecents($first,$num,$ns='',$flags=0){
+ global $conf;
+ $recent = array();
+ $count = 0;
+
+ if(!$num)
+ return $recent;
+
+ // read all recent changes. (kept short)
+ if ($flags & RECENTS_MEDIA_CHANGES) {
+ $lines = @file($conf['media_changelog']) ?: [];
+ } else {
+ $lines = @file($conf['changelog']) ?: [];
+ }
+ if (!is_array($lines)) {
+ $lines = array();
+ }
+ $lines_position = count($lines)-1;
+ $media_lines_position = 0;
+ $media_lines = array();
+
+ if ($flags & RECENTS_MEDIA_PAGES_MIXED) {
+ $media_lines = @file($conf['media_changelog']) ?: [];
+ if (!is_array($media_lines)) {
+ $media_lines = array();
+ }
+ $media_lines_position = count($media_lines)-1;
+ }
+
+ $seen = array(); // caches seen lines, _handleRecent() skips them
+
+ // handle lines
+ while ($lines_position >= 0 || (($flags & RECENTS_MEDIA_PAGES_MIXED) && $media_lines_position >=0)) {
+ if (empty($rec) && $lines_position >= 0) {
+ $rec = _handleRecent(@$lines[$lines_position], $ns, $flags, $seen);
+ if (!$rec) {
+ $lines_position --;
+ continue;
+ }
+ }
+ if (($flags & RECENTS_MEDIA_PAGES_MIXED) && empty($media_rec) && $media_lines_position >= 0) {
+ $media_rec = _handleRecent(
+ @$media_lines[$media_lines_position],
+ $ns,
+ $flags | RECENTS_MEDIA_CHANGES,
+ $seen
+ );
+ if (!$media_rec) {
+ $media_lines_position --;
+ continue;
+ }
+ }
+ if (($flags & RECENTS_MEDIA_PAGES_MIXED) && @$media_rec['date'] >= @$rec['date']) {
+ $media_lines_position--;
+ $x = $media_rec;
+ $x['media'] = true;
+ $media_rec = false;
+ } else {
+ $lines_position--;
+ $x = $rec;
+ if ($flags & RECENTS_MEDIA_CHANGES) $x['media'] = true;
+ $rec = false;
+ }
+ if(--$first >= 0) continue; // skip first entries
+ $recent[] = $x;
+ $count++;
+ // break when we have enough entries
+ if($count >= $num){ break; }
+ }
+ return $recent;
+}
+
+/**
+ * returns an array of files changed since a given time using the
+ * changelog
+ *
+ * The following constants can be used to control which changes are
+ * included. Add them together as needed.
+ *
+ * RECENTS_SKIP_DELETED - don't include deleted pages
+ * RECENTS_SKIP_MINORS - don't include minor changes
+ * RECENTS_ONLY_CREATION - only include new created pages and media
+ * RECENTS_SKIP_SUBSPACES - don't include subspaces
+ * RECENTS_MEDIA_CHANGES - return media changes instead of page changes
+ *
+ * @param int $from date of the oldest entry to return
+ * @param int $to date of the newest entry to return (for pagination, optional)
+ * @param string $ns restrict to given namespace (optional)
+ * @param int $flags see above (optional)
+ * @return array of files
+ *
+ * @author Michael Hamann <michael@content-space.de>
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ */
+function getRecentsSince($from,$to=null,$ns='',$flags=0){
+ global $conf;
+ $recent = array();
+
+ if($to && $to < $from)
+ return $recent;
+
+ // read all recent changes. (kept short)
+ if ($flags & RECENTS_MEDIA_CHANGES) {
+ $lines = @file($conf['media_changelog']);
+ } else {
+ $lines = @file($conf['changelog']);
+ }
+ if(!$lines) return $recent;
+
+ // we start searching at the end of the list
+ $lines = array_reverse($lines);
+
+ // handle lines
+ $seen = array(); // caches seen lines, _handleRecent() skips them
+
+ foreach($lines as $line){
+ $rec = _handleRecent($line, $ns, $flags, $seen);
+ if($rec !== false) {
+ if ($rec['date'] >= $from) {
+ if (!$to || $rec['date'] <= $to) {
+ $recent[] = $rec;
+ }
+ } else {
+ break;
+ }
+ }
+ }
+
+ return array_reverse($recent);
+}
+
+/**
+ * Internal function used by getRecents
+ *
+ * don't call directly
+ *
+ * @see getRecents()
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ *
+ * @param string $line changelog line
+ * @param string $ns restrict to given namespace
+ * @param int $flags flags to control which changes are included
+ * @param array $seen listing of seen pages
+ * @return array|bool false or array with info about a change
+ */
+function _handleRecent($line,$ns,$flags,&$seen){
+ if(empty($line)) return false; //skip empty lines
+
+ // split the line into parts
+ $recent = parseChangelogLine($line);
+ if ($recent===false) { return false; }
+
+ // skip seen ones
+ if(isset($seen[$recent['id']])) return false;
+
+ // skip changes, of only new items are requested
+ if($recent['type']!==DOKU_CHANGE_TYPE_CREATE && ($flags & RECENTS_ONLY_CREATION)) return false;
+
+ // skip minors
+ if($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false;
+
+ // remember in seen to skip additional sights
+ $seen[$recent['id']] = 1;
+
+ // check if it's a hidden page
+ if(isHiddenPage($recent['id'])) return false;
+
+ // filter namespace
+ if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false;
+
+ // exclude subnamespaces
+ if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false;
+
+ // check ACL
+ if ($flags & RECENTS_MEDIA_CHANGES) {
+ $recent['perms'] = auth_quickaclcheck(getNS($recent['id']).':*');
+ } else {
+ $recent['perms'] = auth_quickaclcheck($recent['id']);
+ }
+ if ($recent['perms'] < AUTH_READ) return false;
+
+ // check existance
+ if($flags & RECENTS_SKIP_DELETED){
+ $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id']));
+ if(!file_exists($fn)) return false;
+ }
+
+ return $recent;
+}
diff --git a/platform/www/inc/cli.php b/platform/www/inc/cli.php
new file mode 100644
index 0000000..cb4dabf
--- /dev/null
+++ b/platform/www/inc/cli.php
@@ -0,0 +1,656 @@
+<?php
+
+/**
+ * Class DokuCLI
+ *
+ * All DokuWiki commandline scripts should inherit from this class and implement the abstract methods.
+ *
+ * @deprecated 2017-11-10
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+abstract class DokuCLI {
+ /** @var string the executed script itself */
+ protected $bin;
+ /** @var DokuCLI_Options the option parser */
+ protected $options;
+ /** @var DokuCLI_Colors */
+ public $colors;
+
+ /**
+ * constructor
+ *
+ * Initialize the arguments, set up helper classes and set up the CLI environment
+ */
+ public function __construct() {
+ set_exception_handler(array($this, 'fatal'));
+
+ $this->options = new DokuCLI_Options();
+ $this->colors = new DokuCLI_Colors();
+
+ dbg_deprecated('use \splitbrain\phpcli\CLI instead');
+ $this->error('DokuCLI is deprecated, use \splitbrain\phpcli\CLI instead.');
+ }
+
+ /**
+ * Register options and arguments on the given $options object
+ *
+ * @param DokuCLI_Options $options
+ * @return void
+ */
+ abstract protected function setup(DokuCLI_Options $options);
+
+ /**
+ * Your main program
+ *
+ * Arguments and options have been parsed when this is run
+ *
+ * @param DokuCLI_Options $options
+ * @return void
+ */
+ abstract protected function main(DokuCLI_Options $options);
+
+ /**
+ * Execute the CLI program
+ *
+ * Executes the setup() routine, adds default options, initiate the options parsing and argument checking
+ * and finally executes main()
+ */
+ public function run() {
+ if('cli' != php_sapi_name()) throw new DokuCLI_Exception('This has to be run from the command line');
+
+ // setup
+ $this->setup($this->options);
+ $this->options->registerOption(
+ 'no-colors',
+ 'Do not use any colors in output. Useful when piping output to other tools or files.'
+ );
+ $this->options->registerOption(
+ 'help',
+ 'Display this help screen and exit immediately.',
+ 'h'
+ );
+
+ // parse
+ $this->options->parseOptions();
+
+ // handle defaults
+ if($this->options->getOpt('no-colors')) {
+ $this->colors->disable();
+ }
+ if($this->options->getOpt('help')) {
+ echo $this->options->help();
+ exit(0);
+ }
+
+ // check arguments
+ $this->options->checkArguments();
+
+ // execute
+ $this->main($this->options);
+
+ exit(0);
+ }
+
+ /**
+ * Exits the program on a fatal error
+ *
+ * @param Exception|string $error either an exception or an error message
+ */
+ public function fatal($error) {
+ $code = 0;
+ if(is_object($error) && is_a($error, 'Exception')) {
+ /** @var Exception $error */
+ $code = $error->getCode();
+ $error = $error->getMessage();
+ }
+ if(!$code) $code = DokuCLI_Exception::E_ANY;
+
+ $this->error($error);
+ exit($code);
+ }
+
+ /**
+ * Print an error message
+ *
+ * @param string $string
+ */
+ public function error($string) {
+ $this->colors->ptln("E: $string", 'red', STDERR);
+ }
+
+ /**
+ * Print a success message
+ *
+ * @param string $string
+ */
+ public function success($string) {
+ $this->colors->ptln("S: $string", 'green', STDERR);
+ }
+
+ /**
+ * Print an info message
+ *
+ * @param string $string
+ */
+ public function info($string) {
+ $this->colors->ptln("I: $string", 'cyan', STDERR);
+ }
+
+}
+
+/**
+ * Class DokuCLI_Colors
+ *
+ * Handles color output on (Linux) terminals
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+class DokuCLI_Colors {
+ /** @var array known color names */
+ protected $colors = array(
+ 'reset' => "\33[0m",
+ 'black' => "\33[0;30m",
+ 'darkgray' => "\33[1;30m",
+ 'blue' => "\33[0;34m",
+ 'lightblue' => "\33[1;34m",
+ 'green' => "\33[0;32m",
+ 'lightgreen' => "\33[1;32m",
+ 'cyan' => "\33[0;36m",
+ 'lightcyan' => "\33[1;36m",
+ 'red' => "\33[0;31m",
+ 'lightred' => "\33[1;31m",
+ 'purple' => "\33[0;35m",
+ 'lightpurple' => "\33[1;35m",
+ 'brown' => "\33[0;33m",
+ 'yellow' => "\33[1;33m",
+ 'lightgray' => "\33[0;37m",
+ 'white' => "\33[1;37m",
+ );
+
+ /** @var bool should colors be used? */
+ protected $enabled = true;
+
+ /**
+ * Constructor
+ *
+ * Tries to disable colors for non-terminals
+ */
+ public function __construct() {
+ if(function_exists('posix_isatty') && !posix_isatty(STDOUT)) {
+ $this->enabled = false;
+ return;
+ }
+ if(!getenv('TERM')) {
+ $this->enabled = false;
+ return;
+ }
+ }
+
+ /**
+ * enable color output
+ */
+ public function enable() {
+ $this->enabled = true;
+ }
+
+ /**
+ * disable color output
+ */
+ public function disable() {
+ $this->enabled = false;
+ }
+
+ /**
+ * Convenience function to print a line in a given color
+ *
+ * @param string $line
+ * @param string $color
+ * @param resource $channel
+ */
+ public function ptln($line, $color, $channel = STDOUT) {
+ $this->set($color);
+ fwrite($channel, rtrim($line)."\n");
+ $this->reset();
+ }
+
+ /**
+ * Set the given color for consecutive output
+ *
+ * @param string $color one of the supported color names
+ * @throws DokuCLI_Exception
+ */
+ public function set($color) {
+ if(!$this->enabled) return;
+ if(!isset($this->colors[$color])) throw new DokuCLI_Exception("No such color $color");
+ echo $this->colors[$color];
+ }
+
+ /**
+ * reset the terminal color
+ */
+ public function reset() {
+ $this->set('reset');
+ }
+}
+
+/**
+ * Class DokuCLI_Options
+ *
+ * Parses command line options passed to the CLI script. Allows CLI scripts to easily register all accepted options and
+ * commands and even generates a help text from this setup.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+class DokuCLI_Options {
+ /** @var array keeps the list of options to parse */
+ protected $setup;
+
+ /** @var array store parsed options */
+ protected $options = array();
+
+ /** @var string current parsed command if any */
+ protected $command = '';
+
+ /** @var array passed non-option arguments */
+ public $args = array();
+
+ /** @var string the executed script */
+ protected $bin;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ $this->setup = array(
+ '' => array(
+ 'opts' => array(),
+ 'args' => array(),
+ 'help' => ''
+ )
+ ); // default command
+
+ $this->args = $this->readPHPArgv();
+ $this->bin = basename(array_shift($this->args));
+
+ $this->options = array();
+ }
+
+ /**
+ * Sets the help text for the tool itself
+ *
+ * @param string $help
+ */
+ public function setHelp($help) {
+ $this->setup['']['help'] = $help;
+ }
+
+ /**
+ * Register the names of arguments for help generation and number checking
+ *
+ * This has to be called in the order arguments are expected
+ *
+ * @param string $arg argument name (just for help)
+ * @param string $help help text
+ * @param bool $required is this a required argument
+ * @param string $command if theses apply to a sub command only
+ * @throws DokuCLI_Exception
+ */
+ public function registerArgument($arg, $help, $required = true, $command = '') {
+ if(!isset($this->setup[$command])) throw new DokuCLI_Exception("Command $command not registered");
+
+ $this->setup[$command]['args'][] = array(
+ 'name' => $arg,
+ 'help' => $help,
+ 'required' => $required
+ );
+ }
+
+ /**
+ * This registers a sub command
+ *
+ * Sub commands have their own options and use their own function (not main()).
+ *
+ * @param string $command
+ * @param string $help
+ * @throws DokuCLI_Exception
+ */
+ public function registerCommand($command, $help) {
+ if(isset($this->setup[$command])) throw new DokuCLI_Exception("Command $command already registered");
+
+ $this->setup[$command] = array(
+ 'opts' => array(),
+ 'args' => array(),
+ 'help' => $help
+ );
+
+ }
+
+ /**
+ * Register an option for option parsing and help generation
+ *
+ * @param string $long multi character option (specified with --)
+ * @param string $help help text for this option
+ * @param string|null $short one character option (specified with -)
+ * @param bool|string $needsarg does this option require an argument? give it a name here
+ * @param string $command what command does this option apply to
+ * @throws DokuCLI_Exception
+ */
+ public function registerOption($long, $help, $short = null, $needsarg = false, $command = '') {
+ if(!isset($this->setup[$command])) throw new DokuCLI_Exception("Command $command not registered");
+
+ $this->setup[$command]['opts'][$long] = array(
+ 'needsarg' => $needsarg,
+ 'help' => $help,
+ 'short' => $short
+ );
+
+ if($short) {
+ if(strlen($short) > 1) throw new DokuCLI_Exception("Short options should be exactly one ASCII character");
+
+ $this->setup[$command]['short'][$short] = $long;
+ }
+ }
+
+ /**
+ * Checks the actual number of arguments against the required number
+ *
+ * Throws an exception if arguments are missing. Called from parseOptions()
+ *
+ * @throws DokuCLI_Exception
+ */
+ public function checkArguments() {
+ $argc = count($this->args);
+
+ $req = 0;
+ foreach($this->setup[$this->command]['args'] as $arg) {
+ if(!$arg['required']) break; // last required arguments seen
+ $req++;
+ }
+
+ if($req > $argc) throw new DokuCLI_Exception("Not enough arguments", DokuCLI_Exception::E_OPT_ARG_REQUIRED);
+ }
+
+ /**
+ * Parses the given arguments for known options and command
+ *
+ * The given $args array should NOT contain the executed file as first item anymore! The $args
+ * array is stripped from any options and possible command. All found otions can be accessed via the
+ * getOpt() function
+ *
+ * Note that command options will overwrite any global options with the same name
+ *
+ * @throws DokuCLI_Exception
+ */
+ public function parseOptions() {
+ $non_opts = array();
+
+ $argc = count($this->args);
+ for($i = 0; $i < $argc; $i++) {
+ $arg = $this->args[$i];
+
+ // The special element '--' means explicit end of options. Treat the rest of the arguments as non-options
+ // and end the loop.
+ if($arg == '--') {
+ $non_opts = array_merge($non_opts, array_slice($this->args, $i + 1));
+ break;
+ }
+
+ // '-' is stdin - a normal argument
+ if($arg == '-') {
+ $non_opts = array_merge($non_opts, array_slice($this->args, $i));
+ break;
+ }
+
+ // first non-option
+ if($arg[0] != '-') {
+ $non_opts = array_merge($non_opts, array_slice($this->args, $i));
+ break;
+ }
+
+ // long option
+ if(strlen($arg) > 1 && $arg[1] == '-') {
+ list($opt, $val) = explode('=', substr($arg, 2), 2);
+
+ if(!isset($this->setup[$this->command]['opts'][$opt])) {
+ throw new DokuCLI_Exception("No such option $arg", DokuCLI_Exception::E_UNKNOWN_OPT);
+ }
+
+ // argument required?
+ if($this->setup[$this->command]['opts'][$opt]['needsarg']) {
+ if(is_null($val) && $i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) {
+ $val = $this->args[++$i];
+ }
+ if(is_null($val)) {
+ throw new DokuCLI_Exception("Option $arg requires an argument", DokuCLI_Exception::E_OPT_ARG_REQUIRED);
+ }
+ $this->options[$opt] = $val;
+ } else {
+ $this->options[$opt] = true;
+ }
+
+ continue;
+ }
+
+ // short option
+ $opt = substr($arg, 1);
+ if(!isset($this->setup[$this->command]['short'][$opt])) {
+ throw new DokuCLI_Exception("No such option $arg", DokuCLI_Exception::E_UNKNOWN_OPT);
+ } else {
+ $opt = $this->setup[$this->command]['short'][$opt]; // store it under long name
+ }
+
+ // argument required?
+ if($this->setup[$this->command]['opts'][$opt]['needsarg']) {
+ $val = null;
+ if($i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) {
+ $val = $this->args[++$i];
+ }
+ if(is_null($val)) {
+ throw new DokuCLI_Exception("Option $arg requires an argument", DokuCLI_Exception::E_OPT_ARG_REQUIRED);
+ }
+ $this->options[$opt] = $val;
+ } else {
+ $this->options[$opt] = true;
+ }
+ }
+
+ // parsing is now done, update args array
+ $this->args = $non_opts;
+
+ // if not done yet, check if first argument is a command and reexecute argument parsing if it is
+ if(!$this->command && $this->args && isset($this->setup[$this->args[0]])) {
+ // it is a command!
+ $this->command = array_shift($this->args);
+ $this->parseOptions(); // second pass
+ }
+ }
+
+ /**
+ * Get the value of the given option
+ *
+ * Please note that all options are accessed by their long option names regardless of how they were
+ * specified on commandline.
+ *
+ * Can only be used after parseOptions() has been run
+ *
+ * @param string $option
+ * @param bool|string $default what to return if the option was not set
+ * @return bool|string
+ */
+ public function getOpt($option, $default = false) {
+ if(isset($this->options[$option])) return $this->options[$option];
+ return $default;
+ }
+
+ /**
+ * Return the found command if any
+ *
+ * @return string
+ */
+ public function getCmd() {
+ return $this->command;
+ }
+
+ /**
+ * Builds a help screen from the available options. You may want to call it from -h or on error
+ *
+ * @return string
+ */
+ public function help() {
+ $text = '';
+
+ $hascommands = (count($this->setup) > 1);
+ foreach($this->setup as $command => $config) {
+ $hasopts = (bool) $this->setup[$command]['opts'];
+ $hasargs = (bool) $this->setup[$command]['args'];
+
+ if(!$command) {
+ $text .= 'USAGE: '.$this->bin;
+ } else {
+ $text .= "\n$command";
+ }
+
+ if($hasopts) $text .= ' <OPTIONS>';
+
+ foreach($this->setup[$command]['args'] as $arg) {
+ if($arg['required']) {
+ $text .= ' <'.$arg['name'].'>';
+ } else {
+ $text .= ' [<'.$arg['name'].'>]';
+ }
+ }
+ $text .= "\n";
+
+ if($this->setup[$command]['help']) {
+ $text .= "\n";
+ $text .= $this->tableFormat(
+ array(2, 72),
+ array('', $this->setup[$command]['help']."\n")
+ );
+ }
+
+ if($hasopts) {
+ $text .= "\n OPTIONS\n\n";
+ foreach($this->setup[$command]['opts'] as $long => $opt) {
+
+ $name = '';
+ if($opt['short']) {
+ $name .= '-'.$opt['short'];
+ if($opt['needsarg']) $name .= ' <'.$opt['needsarg'].'>';
+ $name .= ', ';
+ }
+ $name .= "--$long";
+ if($opt['needsarg']) $name .= ' <'.$opt['needsarg'].'>';
+
+ $text .= $this->tableFormat(
+ array(2, 20, 52),
+ array('', $name, $opt['help'])
+ );
+ $text .= "\n";
+ }
+ }
+
+ if($hasargs) {
+ $text .= "\n";
+ foreach($this->setup[$command]['args'] as $arg) {
+ $name = '<'.$arg['name'].'>';
+
+ $text .= $this->tableFormat(
+ array(2, 20, 52),
+ array('', $name, $arg['help'])
+ );
+ }
+ }
+
+ if($command == '' && $hascommands) {
+ $text .= "\nThis tool accepts a command as first parameter as outlined below:\n";
+ }
+ }
+
+ return $text;
+ }
+
+ /**
+ * Safely read the $argv PHP array across different PHP configurations.
+ * Will take care on register_globals and register_argc_argv ini directives
+ *
+ * @throws DokuCLI_Exception
+ * @return array the $argv PHP array or PEAR error if not registered
+ */
+ private function readPHPArgv() {
+ global $argv;
+ if(!is_array($argv)) {
+ if(!@is_array($_SERVER['argv'])) {
+ if(!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) {
+ throw new DokuCLI_Exception(
+ "Could not read cmd args (register_argc_argv=Off?)",
+ DOKU_CLI_OPTS_ARG_READ
+ );
+ }
+ return $GLOBALS['HTTP_SERVER_VARS']['argv'];
+ }
+ return $_SERVER['argv'];
+ }
+ return $argv;
+ }
+
+ /**
+ * Displays text in multiple word wrapped columns
+ *
+ * @param int[] $widths list of column widths (in characters)
+ * @param string[] $texts list of texts for each column
+ * @return string
+ */
+ private function tableFormat($widths, $texts) {
+ $wrapped = array();
+ $maxlen = 0;
+
+ foreach($widths as $col => $width) {
+ $wrapped[$col] = explode("\n", wordwrap($texts[$col], $width - 1, "\n", true)); // -1 char border
+ $len = count($wrapped[$col]);
+ if($len > $maxlen) $maxlen = $len;
+
+ }
+
+ $out = '';
+ for($i = 0; $i < $maxlen; $i++) {
+ foreach($widths as $col => $width) {
+ if(isset($wrapped[$col][$i])) {
+ $val = $wrapped[$col][$i];
+ } else {
+ $val = '';
+ }
+ $out .= sprintf('%-'.$width.'s', $val);
+ }
+ $out .= "\n";
+ }
+ return $out;
+ }
+}
+
+/**
+ * Class DokuCLI_Exception
+ *
+ * The code is used as exit code for the CLI tool. This should probably be extended. Many cases just fall back to the
+ * E_ANY code.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+class DokuCLI_Exception extends Exception {
+ const E_ANY = -1; // no error code specified
+ const E_UNKNOWN_OPT = 1; //Unrecognized option
+ const E_OPT_ARG_REQUIRED = 2; //Option requires argument
+ const E_OPT_ARG_DENIED = 3; //Option not allowed argument
+ const E_OPT_ABIGUOUS = 4; //Option abiguous
+ const E_ARG_READ = 5; //Could not read argv
+
+ /**
+ * @param string $message The Exception message to throw.
+ * @param int $code The Exception code
+ * @param Exception $previous The previous exception used for the exception chaining.
+ */
+ public function __construct($message = "", $code = 0, Exception $previous = null) {
+ if(!$code) $code = DokuCLI_Exception::E_ANY;
+ parent::__construct($message, $code, $previous);
+ }
+}
diff --git a/platform/www/inc/common.php b/platform/www/inc/common.php
new file mode 100644
index 0000000..2910358
--- /dev/null
+++ b/platform/www/inc/common.php
@@ -0,0 +1,2132 @@
+<?php
+/**
+ * Common DokuWiki functions
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+use dokuwiki\Cache\CacheInstructions;
+use dokuwiki\Cache\CacheRenderer;
+use dokuwiki\ChangeLog\PageChangeLog;
+use dokuwiki\Subscriptions\PageSubscriptionSender;
+use dokuwiki\Subscriptions\SubscriberManager;
+use dokuwiki\Extension\AuthPlugin;
+use dokuwiki\Extension\Event;
+
+/**
+ * Wrapper around htmlspecialchars()
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @see htmlspecialchars()
+ *
+ * @param string $string the string being converted
+ * @return string converted string
+ */
+function hsc($string) {
+ return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
+}
+
+/**
+ * Checks if the given input is blank
+ *
+ * This is similar to empty() but will return false for "0".
+ *
+ * Please note: when you pass uninitialized variables, they will implicitly be created
+ * with a NULL value without warning.
+ *
+ * To avoid this it's recommended to guard the call with isset like this:
+ *
+ * (isset($foo) && !blank($foo))
+ * (!isset($foo) || blank($foo))
+ *
+ * @param $in
+ * @param bool $trim Consider a string of whitespace to be blank
+ * @return bool
+ */
+function blank(&$in, $trim = false) {
+ if(is_null($in)) return true;
+ if(is_array($in)) return empty($in);
+ if($in === "\0") return true;
+ if($trim && trim($in) === '') return true;
+ if(strlen($in) > 0) return false;
+ return empty($in);
+}
+
+/**
+ * print a newline terminated string
+ *
+ * You can give an indention as optional parameter
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $string line of text
+ * @param int $indent number of spaces indention
+ */
+function ptln($string, $indent = 0) {
+ echo str_repeat(' ', $indent)."$string\n";
+}
+
+/**
+ * strips control characters (<32) from the given string
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $string being stripped
+ * @return string
+ */
+function stripctl($string) {
+ return preg_replace('/[\x00-\x1F]+/s', '', $string);
+}
+
+/**
+ * Return a secret token to be used for CSRF attack prevention
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @link http://en.wikipedia.org/wiki/Cross-site_request_forgery
+ * @link http://christ1an.blogspot.com/2007/04/preventing-csrf-efficiently.html
+ *
+ * @return string
+ */
+function getSecurityToken() {
+ /** @var Input $INPUT */
+ global $INPUT;
+
+ $user = $INPUT->server->str('REMOTE_USER');
+ $session = session_id();
+
+ // CSRF checks are only for logged in users - do not generate for anonymous
+ if(trim($user) == '' || trim($session) == '') return '';
+ return \dokuwiki\PassHash::hmac('md5', $session.$user, auth_cookiesalt());
+}
+
+/**
+ * Check the secret CSRF token
+ *
+ * @param null|string $token security token or null to read it from request variable
+ * @return bool success if the token matched
+ */
+function checkSecurityToken($token = null) {
+ /** @var Input $INPUT */
+ global $INPUT;
+ if(!$INPUT->server->str('REMOTE_USER')) return true; // no logged in user, no need for a check
+
+ if(is_null($token)) $token = $INPUT->str('sectok');
+ if(getSecurityToken() != $token) {
+ msg('Security Token did not match. Possible CSRF attack.', -1);
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Print a hidden form field with a secret CSRF token
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param bool $print if true print the field, otherwise html of the field is returned
+ * @return string html of hidden form field
+ */
+function formSecurityToken($print = true) {
+ $ret = '<div class="no"><input type="hidden" name="sectok" value="'.getSecurityToken().'" /></div>'."\n";
+ if($print) echo $ret;
+ return $ret;
+}
+
+/**
+ * Determine basic information for a request of $id
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param string $id pageid
+ * @param bool $htmlClient add info about whether is mobile browser
+ * @return array with info for a request of $id
+ *
+ */
+function basicinfo($id, $htmlClient=true){
+ global $USERINFO;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ // set info about manager/admin status.
+ $info = array();
+ $info['isadmin'] = false;
+ $info['ismanager'] = false;
+ if($INPUT->server->has('REMOTE_USER')) {
+ $info['userinfo'] = $USERINFO;
+ $info['perm'] = auth_quickaclcheck($id);
+ $info['client'] = $INPUT->server->str('REMOTE_USER');
+
+ if($info['perm'] == AUTH_ADMIN) {
+ $info['isadmin'] = true;
+ $info['ismanager'] = true;
+ } elseif(auth_ismanager()) {
+ $info['ismanager'] = true;
+ }
+
+ // if some outside auth were used only REMOTE_USER is set
+ if(!$info['userinfo']['name']) {
+ $info['userinfo']['name'] = $INPUT->server->str('REMOTE_USER');
+ }
+
+ } else {
+ $info['perm'] = auth_aclcheck($id, '', null);
+ $info['client'] = clientIP(true);
+ }
+
+ $info['namespace'] = getNS($id);
+
+ // mobile detection
+ if ($htmlClient) {
+ $info['ismobile'] = clientismobile();
+ }
+
+ return $info;
+ }
+
+/**
+ * Return info about the current document as associative
+ * array.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @return array with info about current document
+ */
+function pageinfo() {
+ global $ID;
+ global $REV;
+ global $RANGE;
+ global $lang;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ $info = basicinfo($ID);
+
+ // include ID & REV not redundant, as some parts of DokuWiki may temporarily change $ID, e.g. p_wiki_xhtml
+ // FIXME ... perhaps it would be better to ensure the temporary changes weren't necessary
+ $info['id'] = $ID;
+ $info['rev'] = $REV;
+
+ $subManager = new SubscriberManager();
+ $info['subscribed'] = $subManager->userSubscription();
+
+ $info['locked'] = checklock($ID);
+ $info['filepath'] = wikiFN($ID);
+ $info['exists'] = file_exists($info['filepath']);
+ $info['currentrev'] = @filemtime($info['filepath']);
+ if($REV) {
+ //check if current revision was meant
+ if($info['exists'] && ($info['currentrev'] == $REV)) {
+ $REV = '';
+ } elseif($RANGE) {
+ //section editing does not work with old revisions!
+ $REV = '';
+ $RANGE = '';
+ msg($lang['nosecedit'], 0);
+ } else {
+ //really use old revision
+ $info['filepath'] = wikiFN($ID, $REV);
+ $info['exists'] = file_exists($info['filepath']);
+ }
+ }
+ $info['rev'] = $REV;
+ if($info['exists']) {
+ $info['writable'] = (is_writable($info['filepath']) &&
+ ($info['perm'] >= AUTH_EDIT));
+ } else {
+ $info['writable'] = ($info['perm'] >= AUTH_CREATE);
+ }
+ $info['editable'] = ($info['writable'] && empty($info['locked']));
+ $info['lastmod'] = @filemtime($info['filepath']);
+
+ //load page meta data
+ $info['meta'] = p_get_metadata($ID);
+
+ //who's the editor
+ $pagelog = new PageChangeLog($ID, 1024);
+ if($REV) {
+ $revinfo = $pagelog->getRevisionInfo($REV);
+ } else {
+ if(!empty($info['meta']['last_change']) && is_array($info['meta']['last_change'])) {
+ $revinfo = $info['meta']['last_change'];
+ } else {
+ $revinfo = $pagelog->getRevisionInfo($info['lastmod']);
+ // cache most recent changelog line in metadata if missing and still valid
+ if($revinfo !== false) {
+ $info['meta']['last_change'] = $revinfo;
+ p_set_metadata($ID, array('last_change' => $revinfo));
+ }
+ }
+ }
+ //and check for an external edit
+ if($revinfo !== false && $revinfo['date'] != $info['lastmod']) {
+ // cached changelog line no longer valid
+ $revinfo = false;
+ $info['meta']['last_change'] = $revinfo;
+ p_set_metadata($ID, array('last_change' => $revinfo));
+ }
+
+ if($revinfo !== false){
+ $info['ip'] = $revinfo['ip'];
+ $info['user'] = $revinfo['user'];
+ $info['sum'] = $revinfo['sum'];
+ // See also $INFO['meta']['last_change'] which is the most recent log line for page $ID.
+ // Use $INFO['meta']['last_change']['type']===DOKU_CHANGE_TYPE_MINOR_EDIT in place of $info['minor'].
+
+ if($revinfo['user']) {
+ $info['editor'] = $revinfo['user'];
+ } else {
+ $info['editor'] = $revinfo['ip'];
+ }
+ }else{
+ $info['ip'] = null;
+ $info['user'] = null;
+ $info['sum'] = null;
+ $info['editor'] = null;
+ }
+
+ // draft
+ $draft = new \dokuwiki\Draft($ID, $info['client']);
+ if ($draft->isDraftAvailable()) {
+ $info['draft'] = $draft->getDraftFilename();
+ }
+
+ return $info;
+}
+
+/**
+ * Initialize and/or fill global $JSINFO with some basic info to be given to javascript
+ */
+function jsinfo() {
+ global $JSINFO, $ID, $INFO, $ACT;
+
+ if (!is_array($JSINFO)) {
+ $JSINFO = [];
+ }
+ //export minimal info to JS, plugins can add more
+ $JSINFO['id'] = $ID;
+ $JSINFO['namespace'] = isset($INFO) ? (string) $INFO['namespace'] : '';
+ $JSINFO['ACT'] = act_clean($ACT);
+ $JSINFO['useHeadingNavigation'] = (int) useHeading('navigation');
+ $JSINFO['useHeadingContent'] = (int) useHeading('content');
+}
+
+/**
+ * Return information about the current media item as an associative array.
+ *
+ * @return array with info about current media item
+ */
+function mediainfo(){
+ global $NS;
+ global $IMG;
+
+ $info = basicinfo("$NS:*");
+ $info['image'] = $IMG;
+
+ return $info;
+}
+
+/**
+ * Build an string of URL parameters
+ *
+ * @author Andreas Gohr
+ *
+ * @param array $params array with key-value pairs
+ * @param string $sep series of pairs are separated by this character
+ * @return string query string
+ */
+function buildURLparams($params, $sep = '&amp;') {
+ $url = '';
+ $amp = false;
+ foreach($params as $key => $val) {
+ if($amp) $url .= $sep;
+
+ $url .= rawurlencode($key).'=';
+ $url .= rawurlencode((string) $val);
+ $amp = true;
+ }
+ return $url;
+}
+
+/**
+ * Build an string of html tag attributes
+ *
+ * Skips keys starting with '_', values get HTML encoded
+ *
+ * @author Andreas Gohr
+ *
+ * @param array $params array with (attribute name-attribute value) pairs
+ * @param bool $skipEmptyStrings skip empty string values?
+ * @return string
+ */
+function buildAttributes($params, $skipEmptyStrings = false) {
+ $url = '';
+ $white = false;
+ foreach($params as $key => $val) {
+ if($key[0] == '_') continue;
+ if($val === '' && $skipEmptyStrings) continue;
+ if($white) $url .= ' ';
+
+ $url .= $key.'="';
+ $url .= htmlspecialchars($val);
+ $url .= '"';
+ $white = true;
+ }
+ return $url;
+}
+
+/**
+ * This builds the breadcrumb trail and returns it as array
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @return string[] with the data: array(pageid=>name, ... )
+ */
+function breadcrumbs() {
+ // we prepare the breadcrumbs early for quick session closing
+ static $crumbs = null;
+ if($crumbs != null) return $crumbs;
+
+ global $ID;
+ global $ACT;
+ global $conf;
+ global $INFO;
+
+ //first visit?
+ $crumbs = isset($_SESSION[DOKU_COOKIE]['bc']) ? $_SESSION[DOKU_COOKIE]['bc'] : array();
+ //we only save on show and existing visible readable wiki documents
+ $file = wikiFN($ID);
+ if($ACT != 'show' || $INFO['perm'] < AUTH_READ || isHiddenPage($ID) || !file_exists($file)) {
+ $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
+ return $crumbs;
+ }
+
+ // page names
+ $name = noNSorNS($ID);
+ if(useHeading('navigation')) {
+ // get page title
+ $title = p_get_first_heading($ID, METADATA_RENDER_USING_SIMPLE_CACHE);
+ if($title) {
+ $name = $title;
+ }
+ }
+
+ //remove ID from array
+ if(isset($crumbs[$ID])) {
+ unset($crumbs[$ID]);
+ }
+
+ //add to array
+ $crumbs[$ID] = $name;
+ //reduce size
+ while(count($crumbs) > $conf['breadcrumbs']) {
+ array_shift($crumbs);
+ }
+ //save to session
+ $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
+ return $crumbs;
+}
+
+/**
+ * Filter for page IDs
+ *
+ * This is run on a ID before it is outputted somewhere
+ * currently used to replace the colon with something else
+ * on Windows (non-IIS) systems and to have proper URL encoding
+ *
+ * See discussions at https://github.com/splitbrain/dokuwiki/pull/84 and
+ * https://github.com/splitbrain/dokuwiki/pull/173 why we use a whitelist of
+ * unaffected servers instead of blacklisting affected servers here.
+ *
+ * Urlencoding is ommitted when the second parameter is false
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id pageid being filtered
+ * @param bool $ue apply urlencoding?
+ * @return string
+ */
+function idfilter($id, $ue = true) {
+ global $conf;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ if($conf['useslash'] && $conf['userewrite']) {
+ $id = strtr($id, ':', '/');
+ } elseif(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' &&
+ $conf['userewrite'] &&
+ strpos($INPUT->server->str('SERVER_SOFTWARE'), 'Microsoft-IIS') === false
+ ) {
+ $id = strtr($id, ':', ';');
+ }
+ if($ue) {
+ $id = rawurlencode($id);
+ $id = str_replace('%3A', ':', $id); //keep as colon
+ $id = str_replace('%3B', ';', $id); //keep as semicolon
+ $id = str_replace('%2F', '/', $id); //keep as slash
+ }
+ return $id;
+}
+
+/**
+ * This builds a link to a wikipage
+ *
+ * It handles URL rewriting and adds additional parameters
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id page id, defaults to start page
+ * @param string|array $urlParameters URL parameters, associative array recommended
+ * @param bool $absolute request an absolute URL instead of relative
+ * @param string $separator parameter separator
+ * @return string
+ */
+function wl($id = '', $urlParameters = '', $absolute = false, $separator = '&amp;') {
+ global $conf;
+ if(is_array($urlParameters)) {
+ if(isset($urlParameters['rev']) && !$urlParameters['rev']) unset($urlParameters['rev']);
+ if(isset($urlParameters['at']) && $conf['date_at_format']) {
+ $urlParameters['at'] = date($conf['date_at_format'], $urlParameters['at']);
+ }
+ $urlParameters = buildURLparams($urlParameters, $separator);
+ } else {
+ $urlParameters = str_replace(',', $separator, $urlParameters);
+ }
+ if($id === '') {
+ $id = $conf['start'];
+ }
+ $id = idfilter($id);
+ if($absolute) {
+ $xlink = DOKU_URL;
+ } else {
+ $xlink = DOKU_BASE;
+ }
+
+ if($conf['userewrite'] == 2) {
+ $xlink .= DOKU_SCRIPT.'/'.$id;
+ if($urlParameters) $xlink .= '?'.$urlParameters;
+ } elseif($conf['userewrite']) {
+ $xlink .= $id;
+ if($urlParameters) $xlink .= '?'.$urlParameters;
+ } elseif($id !== '') {
+ $xlink .= DOKU_SCRIPT.'?id='.$id;
+ if($urlParameters) $xlink .= $separator.$urlParameters;
+ } else {
+ $xlink .= DOKU_SCRIPT;
+ if($urlParameters) $xlink .= '?'.$urlParameters;
+ }
+
+ return $xlink;
+}
+
+/**
+ * This builds a link to an alternate page format
+ *
+ * Handles URL rewriting if enabled. Follows the style of wl().
+ *
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ * @param string $id page id, defaults to start page
+ * @param string $format the export renderer to use
+ * @param string|array $urlParameters URL parameters, associative array recommended
+ * @param bool $abs request an absolute URL instead of relative
+ * @param string $sep parameter separator
+ * @return string
+ */
+function exportlink($id = '', $format = 'raw', $urlParameters = '', $abs = false, $sep = '&amp;') {
+ global $conf;
+ if(is_array($urlParameters)) {
+ $urlParameters = buildURLparams($urlParameters, $sep);
+ } else {
+ $urlParameters = str_replace(',', $sep, $urlParameters);
+ }
+
+ $format = rawurlencode($format);
+ $id = idfilter($id);
+ if($abs) {
+ $xlink = DOKU_URL;
+ } else {
+ $xlink = DOKU_BASE;
+ }
+
+ if($conf['userewrite'] == 2) {
+ $xlink .= DOKU_SCRIPT.'/'.$id.'?do=export_'.$format;
+ if($urlParameters) $xlink .= $sep.$urlParameters;
+ } elseif($conf['userewrite'] == 1) {
+ $xlink .= '_export/'.$format.'/'.$id;
+ if($urlParameters) $xlink .= '?'.$urlParameters;
+ } else {
+ $xlink .= DOKU_SCRIPT.'?do=export_'.$format.$sep.'id='.$id;
+ if($urlParameters) $xlink .= $sep.$urlParameters;
+ }
+
+ return $xlink;
+}
+
+/**
+ * Build a link to a media file
+ *
+ * Will return a link to the detail page if $direct is false
+ *
+ * The $more parameter should always be given as array, the function then
+ * will strip default parameters to produce even cleaner URLs
+ *
+ * @param string $id the media file id or URL
+ * @param mixed $more string or array with additional parameters
+ * @param bool $direct link to detail page if false
+ * @param string $sep URL parameter separator
+ * @param bool $abs Create an absolute URL
+ * @return string
+ */
+function ml($id = '', $more = '', $direct = true, $sep = '&amp;', $abs = false) {
+ global $conf;
+ $isexternalimage = media_isexternal($id);
+ if(!$isexternalimage) {
+ $id = cleanID($id);
+ }
+
+ if(is_array($more)) {
+ // add token for resized images
+ if(!empty($more['w']) || !empty($more['h']) || $isexternalimage){
+ $more['tok'] = media_get_token($id,$more['w'],$more['h']);
+ }
+ // strip defaults for shorter URLs
+ if(isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']);
+ if(empty($more['w'])) unset($more['w']);
+ if(empty($more['h'])) unset($more['h']);
+ if(isset($more['id']) && $direct) unset($more['id']);
+ if(isset($more['rev']) && !$more['rev']) unset($more['rev']);
+ $more = buildURLparams($more, $sep);
+ } else {
+ $matches = array();
+ if (preg_match_all('/\b(w|h)=(\d*)\b/',$more,$matches,PREG_SET_ORDER) || $isexternalimage){
+ $resize = array('w'=>0, 'h'=>0);
+ foreach ($matches as $match){
+ $resize[$match[1]] = $match[2];
+ }
+ $more .= $more === '' ? '' : $sep;
+ $more .= 'tok='.media_get_token($id,$resize['w'],$resize['h']);
+ }
+ $more = str_replace('cache=cache', '', $more); //skip default
+ $more = str_replace(',,', ',', $more);
+ $more = str_replace(',', $sep, $more);
+ }
+
+ if($abs) {
+ $xlink = DOKU_URL;
+ } else {
+ $xlink = DOKU_BASE;
+ }
+
+ // external URLs are always direct without rewriting
+ if($isexternalimage) {
+ $xlink .= 'lib/exe/fetch.php';
+ $xlink .= '?'.$more;
+ $xlink .= $sep.'media='.rawurlencode($id);
+ return $xlink;
+ }
+
+ $id = idfilter($id);
+
+ // decide on scriptname
+ if($direct) {
+ if($conf['userewrite'] == 1) {
+ $script = '_media';
+ } else {
+ $script = 'lib/exe/fetch.php';
+ }
+ } else {
+ if($conf['userewrite'] == 1) {
+ $script = '_detail';
+ } else {
+ $script = 'lib/exe/detail.php';
+ }
+ }
+
+ // build URL based on rewrite mode
+ if($conf['userewrite']) {
+ $xlink .= $script.'/'.$id;
+ if($more) $xlink .= '?'.$more;
+ } else {
+ if($more) {
+ $xlink .= $script.'?'.$more;
+ $xlink .= $sep.'media='.$id;
+ } else {
+ $xlink .= $script.'?media='.$id;
+ }
+ }
+
+ return $xlink;
+}
+
+/**
+ * Returns the URL to the DokuWiki base script
+ *
+ * Consider using wl() instead, unless you absoutely need the doku.php endpoint
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @return string
+ */
+function script() {
+ return DOKU_BASE.DOKU_SCRIPT;
+}
+
+/**
+ * Spamcheck against wordlist
+ *
+ * Checks the wikitext against a list of blocked expressions
+ * returns true if the text contains any bad words
+ *
+ * Triggers COMMON_WORDBLOCK_BLOCKED
+ *
+ * Action Plugins can use this event to inspect the blocked data
+ * and gain information about the user who was blocked.
+ *
+ * Event data:
+ * data['matches'] - array of matches
+ * data['userinfo'] - information about the blocked user
+ * [ip] - ip address
+ * [user] - username (if logged in)
+ * [mail] - mail address (if logged in)
+ * [name] - real name (if logged in)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Michael Klier <chi@chimeric.de>
+ *
+ * @param string $text - optional text to check, if not given the globals are used
+ * @return bool - true if a spam word was found
+ */
+function checkwordblock($text = '') {
+ global $TEXT;
+ global $PRE;
+ global $SUF;
+ global $SUM;
+ global $conf;
+ global $INFO;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ if(!$conf['usewordblock']) return false;
+
+ if(!$text) $text = "$PRE $TEXT $SUF $SUM";
+
+ // we prepare the text a tiny bit to prevent spammers circumventing URL checks
+ // phpcs:disable Generic.Files.LineLength.TooLong
+ $text = preg_replace(
+ '!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i',
+ '\1http://\2 \2\3',
+ $text
+ );
+ // phpcs:enable
+
+ $wordblocks = getWordblocks();
+ // how many lines to read at once (to work around some PCRE limits)
+ if(version_compare(phpversion(), '4.3.0', '<')) {
+ // old versions of PCRE define a maximum of parenthesises even if no
+ // backreferences are used - the maximum is 99
+ // this is very bad performancewise and may even be too high still
+ $chunksize = 40;
+ } else {
+ // read file in chunks of 200 - this should work around the
+ // MAX_PATTERN_SIZE in modern PCRE
+ $chunksize = 200;
+ }
+ while($blocks = array_splice($wordblocks, 0, $chunksize)) {
+ $re = array();
+ // build regexp from blocks
+ foreach($blocks as $block) {
+ $block = preg_replace('/#.*$/', '', $block);
+ $block = trim($block);
+ if(empty($block)) continue;
+ $re[] = $block;
+ }
+ if(count($re) && preg_match('#('.join('|', $re).')#si', $text, $matches)) {
+ // prepare event data
+ $data = array();
+ $data['matches'] = $matches;
+ $data['userinfo']['ip'] = $INPUT->server->str('REMOTE_ADDR');
+ if($INPUT->server->str('REMOTE_USER')) {
+ $data['userinfo']['user'] = $INPUT->server->str('REMOTE_USER');
+ $data['userinfo']['name'] = $INFO['userinfo']['name'];
+ $data['userinfo']['mail'] = $INFO['userinfo']['mail'];
+ }
+ $callback = function () {
+ return true;
+ };
+ return Event::createAndTrigger('COMMON_WORDBLOCK_BLOCKED', $data, $callback, true);
+ }
+ }
+ return false;
+}
+
+/**
+ * Return the IP of the client
+ *
+ * Honours X-Forwarded-For and X-Real-IP Proxy Headers
+ *
+ * It returns a comma separated list of IPs if the above mentioned
+ * headers are set. If the single parameter is set, it tries to return
+ * a routable public address, prefering the ones suplied in the X
+ * headers
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param boolean $single If set only a single IP is returned
+ * @return string
+ */
+function clientIP($single = false) {
+ /* @var Input $INPUT */
+ global $INPUT, $conf;
+
+ $ip = array();
+ $ip[] = $INPUT->server->str('REMOTE_ADDR');
+ if($INPUT->server->str('HTTP_X_FORWARDED_FOR')) {
+ $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_FORWARDED_FOR'))));
+ }
+ if($INPUT->server->str('HTTP_X_REAL_IP')) {
+ $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_REAL_IP'))));
+ }
+
+ // some IPv4/v6 regexps borrowed from Feyd
+ // see: http://forums.devnetwork.net/viewtopic.php?f=38&t=53479
+ $dec_octet = '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|[0-9])';
+ $hex_digit = '[A-Fa-f0-9]';
+ $h16 = "{$hex_digit}{1,4}";
+ $IPv4Address = "$dec_octet\\.$dec_octet\\.$dec_octet\\.$dec_octet";
+ $ls32 = "(?:$h16:$h16|$IPv4Address)";
+ $IPv6Address =
+ "(?:(?:{$IPv4Address})|(?:".
+ "(?:$h16:){6}$ls32".
+ "|::(?:$h16:){5}$ls32".
+ "|(?:$h16)?::(?:$h16:){4}$ls32".
+ "|(?:(?:$h16:){0,1}$h16)?::(?:$h16:){3}$ls32".
+ "|(?:(?:$h16:){0,2}$h16)?::(?:$h16:){2}$ls32".
+ "|(?:(?:$h16:){0,3}$h16)?::(?:$h16:){1}$ls32".
+ "|(?:(?:$h16:){0,4}$h16)?::$ls32".
+ "|(?:(?:$h16:){0,5}$h16)?::$h16".
+ "|(?:(?:$h16:){0,6}$h16)?::".
+ ")(?:\\/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))?)";
+
+ // remove any non-IP stuff
+ $cnt = count($ip);
+ $match = array();
+ for($i = 0; $i < $cnt; $i++) {
+ if(preg_match("/^$IPv4Address$/", $ip[$i], $match) || preg_match("/^$IPv6Address$/", $ip[$i], $match)) {
+ $ip[$i] = $match[0];
+ } else {
+ $ip[$i] = '';
+ }
+ if(empty($ip[$i])) unset($ip[$i]);
+ }
+ $ip = array_values(array_unique($ip));
+ if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP
+
+ if(!$single) return join(',', $ip);
+
+ // skip trusted local addresses
+ foreach($ip as $i) {
+ if(!empty($conf['trustedproxy']) && preg_match('/'.$conf['trustedproxy'].'/', $i)) {
+ continue;
+ } else {
+ return $i;
+ }
+ }
+
+ // still here? just use the last address
+ // this case all ips in the list are trusted
+ return $ip[count($ip)-1];
+}
+
+/**
+ * Check if the browser is on a mobile device
+ *
+ * Adapted from the example code at url below
+ *
+ * @link http://www.brainhandles.com/2007/10/15/detecting-mobile-browsers/#code
+ *
+ * @deprecated 2018-04-27 you probably want media queries instead anyway
+ * @return bool if true, client is mobile browser; otherwise false
+ */
+function clientismobile() {
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ if($INPUT->server->has('HTTP_X_WAP_PROFILE')) return true;
+
+ if(preg_match('/wap\.|\.wap/i', $INPUT->server->str('HTTP_ACCEPT'))) return true;
+
+ if(!$INPUT->server->has('HTTP_USER_AGENT')) return false;
+
+ $uamatches = join(
+ '|',
+ [
+ 'midp', 'j2me', 'avantg', 'docomo', 'novarra', 'palmos', 'palmsource', '240x320', 'opwv',
+ 'chtml', 'pda', 'windows ce', 'mmp\/', 'blackberry', 'mib\/', 'symbian', 'wireless', 'nokia',
+ 'hand', 'mobi', 'phone', 'cdm', 'up\.b', 'audio', 'SIE\-', 'SEC\-', 'samsung', 'HTC', 'mot\-',
+ 'mitsu', 'sagem', 'sony', 'alcatel', 'lg', 'erics', 'vx', 'NEC', 'philips', 'mmm', 'xx',
+ 'panasonic', 'sharp', 'wap', 'sch', 'rover', 'pocket', 'benq', 'java', 'pt', 'pg', 'vox',
+ 'amoi', 'bird', 'compal', 'kg', 'voda', 'sany', 'kdd', 'dbt', 'sendo', 'sgh', 'gradi', 'jb',
+ '\d\d\di', 'moto'
+ ]
+ );
+
+ if(preg_match("/$uamatches/i", $INPUT->server->str('HTTP_USER_AGENT'))) return true;
+
+ return false;
+}
+
+/**
+ * check if a given link is interwiki link
+ *
+ * @param string $link the link, e.g. "wiki>page"
+ * @return bool
+ */
+function link_isinterwiki($link){
+ if (preg_match('/^[a-zA-Z0-9\.]+>/u',$link)) return true;
+ return false;
+}
+
+/**
+ * Convert one or more comma separated IPs to hostnames
+ *
+ * If $conf['dnslookups'] is disabled it simply returns the input string
+ *
+ * @author Glen Harris <astfgl@iamnota.org>
+ *
+ * @param string $ips comma separated list of IP addresses
+ * @return string a comma separated list of hostnames
+ */
+function gethostsbyaddrs($ips) {
+ global $conf;
+ if(!$conf['dnslookups']) return $ips;
+
+ $hosts = array();
+ $ips = explode(',', $ips);
+
+ if(is_array($ips)) {
+ foreach($ips as $ip) {
+ $hosts[] = gethostbyaddr(trim($ip));
+ }
+ return join(',', $hosts);
+ } else {
+ return gethostbyaddr(trim($ips));
+ }
+}
+
+/**
+ * Checks if a given page is currently locked.
+ *
+ * removes stale lockfiles
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id page id
+ * @return bool page is locked?
+ */
+function checklock($id) {
+ global $conf;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ $lock = wikiLockFN($id);
+
+ //no lockfile
+ if(!file_exists($lock)) return false;
+
+ //lockfile expired
+ if((time() - filemtime($lock)) > $conf['locktime']) {
+ @unlink($lock);
+ return false;
+ }
+
+ //my own lock
+ @list($ip, $session) = explode("\n", io_readFile($lock));
+ if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || (session_id() && $session == session_id())) {
+ return false;
+ }
+
+ return $ip;
+}
+
+/**
+ * Lock a page for editing
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id page id to lock
+ */
+function lock($id) {
+ global $conf;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ if($conf['locktime'] == 0) {
+ return;
+ }
+
+ $lock = wikiLockFN($id);
+ if($INPUT->server->str('REMOTE_USER')) {
+ io_saveFile($lock, $INPUT->server->str('REMOTE_USER'));
+ } else {
+ io_saveFile($lock, clientIP()."\n".session_id());
+ }
+}
+
+/**
+ * Unlock a page if it was locked by the user
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id page id to unlock
+ * @return bool true if a lock was removed
+ */
+function unlock($id) {
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ $lock = wikiLockFN($id);
+ if(file_exists($lock)) {
+ @list($ip, $session) = explode("\n", io_readFile($lock));
+ if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || $session == session_id()) {
+ @unlink($lock);
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * convert line ending to unix format
+ *
+ * also makes sure the given text is valid UTF-8
+ *
+ * @see formText() for 2crlf conversion
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $text
+ * @return string
+ */
+function cleanText($text) {
+ $text = preg_replace("/(\015\012)|(\015)/", "\012", $text);
+
+ // if the text is not valid UTF-8 we simply assume latin1
+ // this won't break any worse than it breaks with the wrong encoding
+ // but might actually fix the problem in many cases
+ if(!\dokuwiki\Utf8\Clean::isUtf8($text)) $text = utf8_encode($text);
+
+ return $text;
+}
+
+/**
+ * Prepares text for print in Webforms by encoding special chars.
+ * It also converts line endings to Windows format which is
+ * pseudo standard for webforms.
+ *
+ * @see cleanText() for 2unix conversion
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $text
+ * @return string
+ */
+function formText($text) {
+ $text = str_replace("\012", "\015\012", $text);
+ return htmlspecialchars($text);
+}
+
+/**
+ * Returns the specified local text in raw format
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id page id
+ * @param string $ext extension of file being read, default 'txt'
+ * @return string
+ */
+function rawLocale($id, $ext = 'txt') {
+ return io_readFile(localeFN($id, $ext));
+}
+
+/**
+ * Returns the raw WikiText
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id page id
+ * @param string|int $rev timestamp when a revision of wikitext is desired
+ * @return string
+ */
+function rawWiki($id, $rev = '') {
+ return io_readWikiPage(wikiFN($id, $rev), $id, $rev);
+}
+
+/**
+ * Returns the pagetemplate contents for the ID's namespace
+ *
+ * @triggers COMMON_PAGETPL_LOAD
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id the id of the page to be created
+ * @return string parsed pagetemplate content
+ */
+function pageTemplate($id) {
+ global $conf;
+
+ if(is_array($id)) $id = $id[0];
+
+ // prepare initial event data
+ $data = array(
+ 'id' => $id, // the id of the page to be created
+ 'tpl' => '', // the text used as template
+ 'tplfile' => '', // the file above text was/should be loaded from
+ 'doreplace' => true // should wildcard replacements be done on the text?
+ );
+
+ $evt = new Event('COMMON_PAGETPL_LOAD', $data);
+ if($evt->advise_before(true)) {
+ // the before event might have loaded the content already
+ if(empty($data['tpl'])) {
+ // if the before event did not set a template file, try to find one
+ if(empty($data['tplfile'])) {
+ $path = dirname(wikiFN($id));
+ if(file_exists($path.'/_template.txt')) {
+ $data['tplfile'] = $path.'/_template.txt';
+ } else {
+ // search upper namespaces for templates
+ $len = strlen(rtrim($conf['datadir'], '/'));
+ while(strlen($path) >= $len) {
+ if(file_exists($path.'/__template.txt')) {
+ $data['tplfile'] = $path.'/__template.txt';
+ break;
+ }
+ $path = substr($path, 0, strrpos($path, '/'));
+ }
+ }
+ }
+ // load the content
+ $data['tpl'] = io_readFile($data['tplfile']);
+ }
+ if($data['doreplace']) parsePageTemplate($data);
+ }
+ $evt->advise_after();
+ unset($evt);
+
+ return $data['tpl'];
+}
+
+/**
+ * Performs common page template replacements
+ * This works on data from COMMON_PAGETPL_LOAD
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $data array with event data
+ * @return string
+ */
+function parsePageTemplate(&$data) {
+ /**
+ * @var string $id the id of the page to be created
+ * @var string $tpl the text used as template
+ * @var string $tplfile the file above text was/should be loaded from
+ * @var bool $doreplace should wildcard replacements be done on the text?
+ */
+ extract($data);
+
+ global $USERINFO;
+ global $conf;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ // replace placeholders
+ $file = noNS($id);
+ $page = strtr($file, $conf['sepchar'], ' ');
+
+ $tpl = str_replace(
+ array(
+ '@ID@',
+ '@NS@',
+ '@CURNS@',
+ '@!CURNS@',
+ '@!!CURNS@',
+ '@!CURNS!@',
+ '@FILE@',
+ '@!FILE@',
+ '@!FILE!@',
+ '@PAGE@',
+ '@!PAGE@',
+ '@!!PAGE@',
+ '@!PAGE!@',
+ '@USER@',
+ '@NAME@',
+ '@MAIL@',
+ '@DATE@',
+ ),
+ array(
+ $id,
+ getNS($id),
+ curNS($id),
+ \dokuwiki\Utf8\PhpString::ucfirst(curNS($id)),
+ \dokuwiki\Utf8\PhpString::ucwords(curNS($id)),
+ \dokuwiki\Utf8\PhpString::strtoupper(curNS($id)),
+ $file,
+ \dokuwiki\Utf8\PhpString::ucfirst($file),
+ \dokuwiki\Utf8\PhpString::strtoupper($file),
+ $page,
+ \dokuwiki\Utf8\PhpString::ucfirst($page),
+ \dokuwiki\Utf8\PhpString::ucwords($page),
+ \dokuwiki\Utf8\PhpString::strtoupper($page),
+ $INPUT->server->str('REMOTE_USER'),
+ $USERINFO ? $USERINFO['name'] : '',
+ $USERINFO ? $USERINFO['mail'] : '',
+ $conf['dformat'],
+ ), $tpl
+ );
+
+ // we need the callback to work around strftime's char limit
+ $tpl = preg_replace_callback(
+ '/%./',
+ function ($m) {
+ return strftime($m[0]);
+ },
+ $tpl
+ );
+ $data['tpl'] = $tpl;
+ return $tpl;
+}
+
+/**
+ * Returns the raw Wiki Text in three slices.
+ *
+ * The range parameter needs to have the form "from-to"
+ * and gives the range of the section in bytes - no
+ * UTF-8 awareness is needed.
+ * The returned order is prefix, section and suffix.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $range in form "from-to"
+ * @param string $id page id
+ * @param string $rev optional, the revision timestamp
+ * @return string[] with three slices
+ */
+function rawWikiSlices($range, $id, $rev = '') {
+ $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
+
+ // Parse range
+ list($from, $to) = explode('-', $range, 2);
+ // Make range zero-based, use defaults if marker is missing
+ $from = !$from ? 0 : ($from - 1);
+ $to = !$to ? strlen($text) : ($to - 1);
+
+ $slices = array();
+ $slices[0] = substr($text, 0, $from);
+ $slices[1] = substr($text, $from, $to - $from);
+ $slices[2] = substr($text, $to);
+ return $slices;
+}
+
+/**
+ * Joins wiki text slices
+ *
+ * function to join the text slices.
+ * When the pretty parameter is set to true it adds additional empty
+ * lines between sections if needed (used on saving).
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $pre prefix
+ * @param string $text text in the middle
+ * @param string $suf suffix
+ * @param bool $pretty add additional empty lines between sections
+ * @return string
+ */
+function con($pre, $text, $suf, $pretty = false) {
+ if($pretty) {
+ if($pre !== '' && substr($pre, -1) !== "\n" &&
+ substr($text, 0, 1) !== "\n"
+ ) {
+ $pre .= "\n";
+ }
+ if($suf !== '' && substr($text, -1) !== "\n" &&
+ substr($suf, 0, 1) !== "\n"
+ ) {
+ $text .= "\n";
+ }
+ }
+
+ return $pre.$text.$suf;
+}
+
+/**
+ * Checks if the current page version is newer than the last entry in the page's
+ * changelog. If so, we assume it has been an external edit and we create an
+ * attic copy and add a proper changelog line.
+ *
+ * This check is only executed when the page is about to be saved again from the
+ * wiki, triggered in @see saveWikiText()
+ *
+ * @param string $id the page ID
+ */
+function detectExternalEdit($id) {
+ global $lang;
+
+ $fileLastMod = wikiFN($id);
+ $lastMod = @filemtime($fileLastMod); // from page
+ $pagelog = new PageChangeLog($id, 1024);
+ $lastRev = $pagelog->getRevisions(-1, 1); // from changelog
+ $lastRev = (int) (empty($lastRev) ? 0 : $lastRev[0]);
+
+ if(!file_exists(wikiFN($id, $lastMod)) && file_exists($fileLastMod) && $lastMod >= $lastRev) {
+ // add old revision to the attic if missing
+ saveOldRevision($id);
+ // add a changelog entry if this edit came from outside dokuwiki
+ if($lastMod > $lastRev) {
+ $fileLastRev = wikiFN($id, $lastRev);
+ $revinfo = $pagelog->getRevisionInfo($lastRev);
+ if(empty($lastRev) || !file_exists($fileLastRev) || $revinfo['type'] == DOKU_CHANGE_TYPE_DELETE) {
+ $filesize_old = 0;
+ } else {
+ $filesize_old = io_getSizeFile($fileLastRev);
+ }
+ $filesize_new = filesize($fileLastMod);
+ $sizechange = $filesize_new - $filesize_old;
+
+ addLogEntry(
+ $lastMod,
+ $id,
+ DOKU_CHANGE_TYPE_EDIT,
+ $lang['external_edit'],
+ '',
+ array('ExternalEdit' => true),
+ $sizechange
+ );
+ // remove soon to be stale instructions
+ $cache = new CacheInstructions($id, $fileLastMod);
+ $cache->removeCache();
+ }
+ }
+}
+
+/**
+ * Saves a wikitext by calling io_writeWikiPage.
+ * Also directs changelog and attic updates.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ *
+ * @param string $id page id
+ * @param string $text wikitext being saved
+ * @param string $summary summary of text update
+ * @param bool $minor mark this saved version as minor update
+ */
+function saveWikiText($id, $text, $summary, $minor = false) {
+ /* Note to developers:
+ This code is subtle and delicate. Test the behavior of
+ the attic and changelog with dokuwiki and external edits
+ after any changes. External edits change the wiki page
+ directly without using php or dokuwiki.
+ */
+ global $conf;
+ global $lang;
+ global $REV;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ // prepare data for event
+ $svdta = array();
+ $svdta['id'] = $id;
+ $svdta['file'] = wikiFN($id);
+ $svdta['revertFrom'] = $REV;
+ $svdta['oldRevision'] = @filemtime($svdta['file']);
+ $svdta['newRevision'] = 0;
+ $svdta['newContent'] = $text;
+ $svdta['oldContent'] = rawWiki($id);
+ $svdta['summary'] = $summary;
+ $svdta['contentChanged'] = ($svdta['newContent'] != $svdta['oldContent']);
+ $svdta['changeInfo'] = '';
+ $svdta['changeType'] = DOKU_CHANGE_TYPE_EDIT;
+ $svdta['sizechange'] = null;
+
+ // select changelog line type
+ if($REV) {
+ $svdta['changeType'] = DOKU_CHANGE_TYPE_REVERT;
+ $svdta['changeInfo'] = $REV;
+ } else if(!file_exists($svdta['file'])) {
+ $svdta['changeType'] = DOKU_CHANGE_TYPE_CREATE;
+ } else if(trim($text) == '') {
+ // empty or whitespace only content deletes
+ $svdta['changeType'] = DOKU_CHANGE_TYPE_DELETE;
+ // autoset summary on deletion
+ if(blank($svdta['summary'])) {
+ $svdta['summary'] = $lang['deleted'];
+ }
+ } else if($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER')) {
+ //minor edits only for logged in users
+ $svdta['changeType'] = DOKU_CHANGE_TYPE_MINOR_EDIT;
+ }
+
+ $event = new Event('COMMON_WIKIPAGE_SAVE', $svdta);
+ if(!$event->advise_before()) return;
+
+ // if the content has not been changed, no save happens (plugins may override this)
+ if(!$svdta['contentChanged']) return;
+
+ detectExternalEdit($id);
+
+ if(
+ $svdta['changeType'] == DOKU_CHANGE_TYPE_CREATE ||
+ ($svdta['changeType'] == DOKU_CHANGE_TYPE_REVERT && !file_exists($svdta['file']))
+ ) {
+ $filesize_old = 0;
+ } else {
+ $filesize_old = filesize($svdta['file']);
+ }
+ if($svdta['changeType'] == DOKU_CHANGE_TYPE_DELETE) {
+ // Send "update" event with empty data, so plugins can react to page deletion
+ $data = array(array($svdta['file'], '', false), getNS($id), noNS($id), false);
+ Event::createAndTrigger('IO_WIKIPAGE_WRITE', $data);
+ // pre-save deleted revision
+ @touch($svdta['file']);
+ clearstatcache();
+ $svdta['newRevision'] = saveOldRevision($id);
+ // remove empty file
+ @unlink($svdta['file']);
+ $filesize_new = 0;
+ // don't remove old meta info as it should be saved, plugins can use
+ // IO_WIKIPAGE_WRITE for removing their metadata...
+ // purge non-persistant meta data
+ p_purge_metadata($id);
+ // remove empty namespaces
+ io_sweepNS($id, 'datadir');
+ io_sweepNS($id, 'mediadir');
+ } else {
+ // save file (namespace dir is created in io_writeWikiPage)
+ io_writeWikiPage($svdta['file'], $svdta['newContent'], $id);
+ // pre-save the revision, to keep the attic in sync
+ $svdta['newRevision'] = saveOldRevision($id);
+ $filesize_new = filesize($svdta['file']);
+ }
+ $svdta['sizechange'] = $filesize_new - $filesize_old;
+
+ $event->advise_after();
+
+ addLogEntry(
+ $svdta['newRevision'],
+ $svdta['id'],
+ $svdta['changeType'],
+ $svdta['summary'],
+ $svdta['changeInfo'],
+ null,
+ $svdta['sizechange']
+ );
+
+ // send notify mails
+ notify($svdta['id'], 'admin', $svdta['oldRevision'], $svdta['summary'], $minor, $svdta['newRevision']);
+ notify($svdta['id'], 'subscribers', $svdta['oldRevision'], $svdta['summary'], $minor, $svdta['newRevision']);
+
+ // update the purgefile (timestamp of the last time anything within the wiki was changed)
+ io_saveFile($conf['cachedir'].'/purgefile', time());
+
+ // if useheading is enabled, purge the cache of all linking pages
+ if(useHeading('content')) {
+ $pages = ft_backlinks($id, true);
+ foreach($pages as $page) {
+ $cache = new CacheRenderer($page, wikiFN($page), 'xhtml');
+ $cache->removeCache();
+ }
+ }
+}
+
+/**
+ * moves the current version to the attic and returns its
+ * revision date
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id page id
+ * @return int|string revision timestamp
+ */
+function saveOldRevision($id) {
+ $oldf = wikiFN($id);
+ if(!file_exists($oldf)) return '';
+ $date = filemtime($oldf);
+ $newf = wikiFN($id, $date);
+ io_writeWikiPage($newf, rawWiki($id), $id, $date);
+ return $date;
+}
+
+/**
+ * Sends a notify mail on page change or registration
+ *
+ * @param string $id The changed page
+ * @param string $who Who to notify (admin|subscribers|register)
+ * @param int|string $rev Old page revision
+ * @param string $summary What changed
+ * @param boolean $minor Is this a minor edit?
+ * @param string[] $replace Additional string substitutions, @KEY@ to be replaced by value
+ * @param int|string $current_rev New page revision
+ * @return bool
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = array(), $current_rev = false) {
+ global $conf;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ // decide if there is something to do, eg. whom to mail
+ if($who == 'admin') {
+ if(empty($conf['notify'])) return false; //notify enabled?
+ $tpl = 'mailtext';
+ $to = $conf['notify'];
+ } elseif($who == 'subscribers') {
+ if(!actionOK('subscribe')) return false; //subscribers enabled?
+ if($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors
+ $data = array('id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace);
+ Event::createAndTrigger(
+ 'COMMON_NOTIFY_ADDRESSLIST', $data,
+ array(new SubscriberManager(), 'notifyAddresses')
+ );
+ $to = $data['addresslist'];
+ if(empty($to)) return false;
+ $tpl = 'subscr_single';
+ } else {
+ return false; //just to be safe
+ }
+
+ // prepare content
+ $subscription = new PageSubscriptionSender();
+ return $subscription->sendPageDiff($to, $tpl, $id, $rev, $summary, $current_rev);
+}
+
+/**
+ * extracts the query from a search engine referrer
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Todd Augsburger <todd@rollerorgans.com>
+ *
+ * @return array|string
+ */
+function getGoogleQuery() {
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ if(!$INPUT->server->has('HTTP_REFERER')) {
+ return '';
+ }
+ $url = parse_url($INPUT->server->str('HTTP_REFERER'));
+
+ // only handle common SEs
+ if(!preg_match('/(google|bing|yahoo|ask|duckduckgo|babylon|aol|yandex)/',$url['host'])) return '';
+
+ $query = array();
+ parse_str($url['query'], $query);
+
+ $q = '';
+ if(isset($query['q'])){
+ $q = $query['q'];
+ }elseif(isset($query['p'])){
+ $q = $query['p'];
+ }elseif(isset($query['query'])){
+ $q = $query['query'];
+ }
+ $q = trim($q);
+
+ if(!$q) return '';
+ // ignore if query includes a full URL
+ if(strpos($q, '//') !== false) return '';
+ $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY);
+ return $q;
+}
+
+/**
+ * Return the human readable size of a file
+ *
+ * @param int $size A file size
+ * @param int $dec A number of decimal places
+ * @return string human readable size
+ *
+ * @author Martin Benjamin <b.martin@cybernet.ch>
+ * @author Aidan Lister <aidan@php.net>
+ * @version 1.0.0
+ */
+function filesize_h($size, $dec = 1) {
+ $sizes = array('B', 'KB', 'MB', 'GB');
+ $count = count($sizes);
+ $i = 0;
+
+ while($size >= 1024 && ($i < $count - 1)) {
+ $size /= 1024;
+ $i++;
+ }
+
+ return round($size, $dec)."\xC2\xA0".$sizes[$i]; //non-breaking space
+}
+
+/**
+ * Return the given timestamp as human readable, fuzzy age
+ *
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ *
+ * @param int $dt timestamp
+ * @return string
+ */
+function datetime_h($dt) {
+ global $lang;
+
+ $ago = time() - $dt;
+ if($ago > 24 * 60 * 60 * 30 * 12 * 2) {
+ return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12)));
+ }
+ if($ago > 24 * 60 * 60 * 30 * 2) {
+ return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30)));
+ }
+ if($ago > 24 * 60 * 60 * 7 * 2) {
+ return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7)));
+ }
+ if($ago > 24 * 60 * 60 * 2) {
+ return sprintf($lang['days'], round($ago / (24 * 60 * 60)));
+ }
+ if($ago > 60 * 60 * 2) {
+ return sprintf($lang['hours'], round($ago / (60 * 60)));
+ }
+ if($ago > 60 * 2) {
+ return sprintf($lang['minutes'], round($ago / (60)));
+ }
+ return sprintf($lang['seconds'], $ago);
+}
+
+/**
+ * Wraps around strftime but provides support for fuzzy dates
+ *
+ * The format default to $conf['dformat']. It is passed to
+ * strftime - %f can be used to get the value from datetime_h()
+ *
+ * @see datetime_h
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ *
+ * @param int|null $dt timestamp when given, null will take current timestamp
+ * @param string $format empty default to $conf['dformat'], or provide format as recognized by strftime()
+ * @return string
+ */
+function dformat($dt = null, $format = '') {
+ global $conf;
+
+ if(is_null($dt)) $dt = time();
+ $dt = (int) $dt;
+ if(!$format) $format = $conf['dformat'];
+
+ $format = str_replace('%f', datetime_h($dt), $format);
+ return strftime($format, $dt);
+}
+
+/**
+ * Formats a timestamp as ISO 8601 date
+ *
+ * @author <ungu at terong dot com>
+ * @link http://php.net/manual/en/function.date.php#54072
+ *
+ * @param int $int_date current date in UNIX timestamp
+ * @return string
+ */
+function date_iso8601($int_date) {
+ $date_mod = date('Y-m-d\TH:i:s', $int_date);
+ $pre_timezone = date('O', $int_date);
+ $time_zone = substr($pre_timezone, 0, 3).":".substr($pre_timezone, 3, 2);
+ $date_mod .= $time_zone;
+ return $date_mod;
+}
+
+/**
+ * return an obfuscated email address in line with $conf['mailguard'] setting
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ *
+ * @param string $email email address
+ * @return string
+ */
+function obfuscate($email) {
+ global $conf;
+
+ switch($conf['mailguard']) {
+ case 'visible' :
+ $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
+ return strtr($email, $obfuscate);
+
+ case 'hex' :
+ return \dokuwiki\Utf8\Conversion::toHtml($email, true);
+
+ case 'none' :
+ default :
+ return $email;
+ }
+}
+
+/**
+ * Removes quoting backslashes
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $string
+ * @param string $char backslashed character
+ * @return string
+ */
+function unslash($string, $char = "'") {
+ return str_replace('\\'.$char, $char, $string);
+}
+
+/**
+ * Convert php.ini shorthands to byte
+ *
+ * On 32 bit systems values >= 2GB will fail!
+ *
+ * -1 (infinite size) will be reported as -1
+ *
+ * @link https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
+ * @param string $value PHP size shorthand
+ * @return int
+ */
+function php_to_byte($value) {
+ switch (strtoupper(substr($value,-1))) {
+ case 'G':
+ $ret = intval(substr($value, 0, -1)) * 1024 * 1024 * 1024;
+ break;
+ case 'M':
+ $ret = intval(substr($value, 0, -1)) * 1024 * 1024;
+ break;
+ case 'K':
+ $ret = intval(substr($value, 0, -1)) * 1024;
+ break;
+ default:
+ $ret = intval($value);
+ break;
+ }
+ return $ret;
+}
+
+/**
+ * Wrapper around preg_quote adding the default delimiter
+ *
+ * @param string $string
+ * @return string
+ */
+function preg_quote_cb($string) {
+ return preg_quote($string, '/');
+}
+
+/**
+ * Shorten a given string by removing data from the middle
+ *
+ * You can give the string in two parts, the first part $keep
+ * will never be shortened. The second part $short will be cut
+ * in the middle to shorten but only if at least $min chars are
+ * left to display it. Otherwise it will be left off.
+ *
+ * @param string $keep the part to keep
+ * @param string $short the part to shorten
+ * @param int $max maximum chars you want for the whole string
+ * @param int $min minimum number of chars to have left for middle shortening
+ * @param string $char the shortening character to use
+ * @return string
+ */
+function shorten($keep, $short, $max, $min = 9, $char = '…') {
+ $max = $max - \dokuwiki\Utf8\PhpString::strlen($keep);
+ if($max < $min) return $keep;
+ $len = \dokuwiki\Utf8\PhpString::strlen($short);
+ if($len <= $max) return $keep.$short;
+ $half = floor($max / 2);
+ return $keep .
+ \dokuwiki\Utf8\PhpString::substr($short, 0, $half - 1) .
+ $char .
+ \dokuwiki\Utf8\PhpString::substr($short, $len - $half);
+}
+
+/**
+ * Return the users real name or e-mail address for use
+ * in page footer and recent changes pages
+ *
+ * @param string|null $username or null when currently logged-in user should be used
+ * @param bool $textonly true returns only plain text, true allows returning html
+ * @return string html or plain text(not escaped) of formatted user name
+ *
+ * @author Andy Webber <dokuwiki AT andywebber DOT com>
+ */
+function editorinfo($username, $textonly = false) {
+ return userlink($username, $textonly);
+}
+
+/**
+ * Returns users realname w/o link
+ *
+ * @param string|null $username or null when currently logged-in user should be used
+ * @param bool $textonly true returns only plain text, true allows returning html
+ * @return string html or plain text(not escaped) of formatted user name
+ *
+ * @triggers COMMON_USER_LINK
+ */
+function userlink($username = null, $textonly = false) {
+ global $conf, $INFO;
+ /** @var AuthPlugin $auth */
+ global $auth;
+ /** @var Input $INPUT */
+ global $INPUT;
+
+ // prepare initial event data
+ $data = array(
+ 'username' => $username, // the unique user name
+ 'name' => '',
+ 'link' => array( //setting 'link' to false disables linking
+ 'target' => '',
+ 'pre' => '',
+ 'suf' => '',
+ 'style' => '',
+ 'more' => '',
+ 'url' => '',
+ 'title' => '',
+ 'class' => ''
+ ),
+ 'userlink' => '', // formatted user name as will be returned
+ 'textonly' => $textonly
+ );
+ if($username === null) {
+ $data['username'] = $username = $INPUT->server->str('REMOTE_USER');
+ if($textonly){
+ $data['name'] = $INFO['userinfo']['name']. ' (' . $INPUT->server->str('REMOTE_USER') . ')';
+ }else {
+ $data['name'] = '<bdi>' . hsc($INFO['userinfo']['name']) . '</bdi> '.
+ '(<bdi>' . hsc($INPUT->server->str('REMOTE_USER')) . '</bdi>)';
+ }
+ }
+
+ $evt = new Event('COMMON_USER_LINK', $data);
+ if($evt->advise_before(true)) {
+ if(empty($data['name'])) {
+ if($auth) $info = $auth->getUserData($username);
+ if($conf['showuseras'] != 'loginname' && isset($info) && $info) {
+ switch($conf['showuseras']) {
+ case 'username':
+ case 'username_link':
+ $data['name'] = $textonly ? $info['name'] : hsc($info['name']);
+ break;
+ case 'email':
+ case 'email_link':
+ $data['name'] = obfuscate($info['mail']);
+ break;
+ }
+ } else {
+ $data['name'] = $textonly ? $data['username'] : hsc($data['username']);
+ }
+ }
+
+ /** @var Doku_Renderer_xhtml $xhtml_renderer */
+ static $xhtml_renderer = null;
+
+ if(!$data['textonly'] && empty($data['link']['url'])) {
+
+ if(in_array($conf['showuseras'], array('email_link', 'username_link'))) {
+ if(!isset($info)) {
+ if($auth) $info = $auth->getUserData($username);
+ }
+ if(isset($info) && $info) {
+ if($conf['showuseras'] == 'email_link') {
+ $data['link']['url'] = 'mailto:' . obfuscate($info['mail']);
+ } else {
+ if(is_null($xhtml_renderer)) {
+ $xhtml_renderer = p_get_renderer('xhtml');
+ }
+ if(empty($xhtml_renderer->interwiki)) {
+ $xhtml_renderer->interwiki = getInterwiki();
+ }
+ $shortcut = 'user';
+ $exists = null;
+ $data['link']['url'] = $xhtml_renderer->_resolveInterWiki($shortcut, $username, $exists);
+ $data['link']['class'] .= ' interwiki iw_user';
+ if($exists !== null) {
+ if($exists) {
+ $data['link']['class'] .= ' wikilink1';
+ } else {
+ $data['link']['class'] .= ' wikilink2';
+ $data['link']['rel'] = 'nofollow';
+ }
+ }
+ }
+ } else {
+ $data['textonly'] = true;
+ }
+
+ } else {
+ $data['textonly'] = true;
+ }
+ }
+
+ if($data['textonly']) {
+ $data['userlink'] = $data['name'];
+ } else {
+ $data['link']['name'] = $data['name'];
+ if(is_null($xhtml_renderer)) {
+ $xhtml_renderer = p_get_renderer('xhtml');
+ }
+ $data['userlink'] = $xhtml_renderer->_formatLink($data['link']);
+ }
+ }
+ $evt->advise_after();
+ unset($evt);
+
+ return $data['userlink'];
+}
+
+/**
+ * Returns the path to a image file for the currently chosen license.
+ * When no image exists, returns an empty string
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $type - type of image 'badge' or 'button'
+ * @return string
+ */
+function license_img($type) {
+ global $license;
+ global $conf;
+ if(!$conf['license']) return '';
+ if(!is_array($license[$conf['license']])) return '';
+ $try = array();
+ $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png';
+ $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif';
+ if(substr($conf['license'], 0, 3) == 'cc-') {
+ $try[] = 'lib/images/license/'.$type.'/cc.png';
+ }
+ foreach($try as $src) {
+ if(file_exists(DOKU_INC.$src)) return $src;
+ }
+ return '';
+}
+
+/**
+ * Checks if the given amount of memory is available
+ *
+ * If the memory_get_usage() function is not available the
+ * function just assumes $bytes of already allocated memory
+ *
+ * @author Filip Oscadal <webmaster@illusionsoftworks.cz>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param int $mem Size of memory you want to allocate in bytes
+ * @param int $bytes already allocated memory (see above)
+ * @return bool
+ */
+function is_mem_available($mem, $bytes = 1048576) {
+ $limit = trim(ini_get('memory_limit'));
+ if(empty($limit)) return true; // no limit set!
+ if($limit == -1) return true; // unlimited
+
+ // parse limit to bytes
+ $limit = php_to_byte($limit);
+
+ // get used memory if possible
+ if(function_exists('memory_get_usage')) {
+ $used = memory_get_usage();
+ } else {
+ $used = $bytes;
+ }
+
+ if($used + $mem > $limit) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Send a HTTP redirect to the browser
+ *
+ * Works arround Microsoft IIS cookie sending bug. Exits the script.
+ *
+ * @link http://support.microsoft.com/kb/q176113/
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $url url being directed to
+ */
+function send_redirect($url) {
+ $url = stripctl($url); // defend against HTTP Response Splitting
+
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ //are there any undisplayed messages? keep them in session for display
+ global $MSG;
+ if(isset($MSG) && count($MSG) && !defined('NOSESSION')) {
+ //reopen session, store data and close session again
+ @session_start();
+ $_SESSION[DOKU_COOKIE]['msg'] = $MSG;
+ }
+
+ // always close the session
+ session_write_close();
+
+ // check if running on IIS < 6 with CGI-PHP
+ if($INPUT->server->has('SERVER_SOFTWARE') && $INPUT->server->has('GATEWAY_INTERFACE') &&
+ (strpos($INPUT->server->str('GATEWAY_INTERFACE'), 'CGI') !== false) &&
+ (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($INPUT->server->str('SERVER_SOFTWARE')), $matches)) &&
+ $matches[1] < 6
+ ) {
+ header('Refresh: 0;url='.$url);
+ } else {
+ header('Location: '.$url);
+ }
+
+ // no exits during unit tests
+ if(defined('DOKU_UNITTEST')) {
+ // pass info about the redirect back to the test suite
+ $testRequest = TestRequest::getRunning();
+ if($testRequest !== null) {
+ $testRequest->addData('send_redirect', $url);
+ }
+ return;
+ }
+
+ exit;
+}
+
+/**
+ * Validate a value using a set of valid values
+ *
+ * This function checks whether a specified value is set and in the array
+ * $valid_values. If not, the function returns a default value or, if no
+ * default is specified, throws an exception.
+ *
+ * @param string $param The name of the parameter
+ * @param array $valid_values A set of valid values; Optionally a default may
+ * be marked by the key “default”.
+ * @param array $array The array containing the value (typically $_POST
+ * or $_GET)
+ * @param string $exc The text of the raised exception
+ *
+ * @throws Exception
+ * @return mixed
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+function valid_input_set($param, $valid_values, $array, $exc = '') {
+ if(isset($array[$param]) && in_array($array[$param], $valid_values)) {
+ return $array[$param];
+ } elseif(isset($valid_values['default'])) {
+ return $valid_values['default'];
+ } else {
+ throw new Exception($exc);
+ }
+}
+
+/**
+ * Read a preference from the DokuWiki cookie
+ * (remembering both keys & values are urlencoded)
+ *
+ * @param string $pref preference key
+ * @param mixed $default value returned when preference not found
+ * @return string preference value
+ */
+function get_doku_pref($pref, $default) {
+ $enc_pref = urlencode($pref);
+ if(isset($_COOKIE['DOKU_PREFS']) && strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) {
+ $parts = explode('#', $_COOKIE['DOKU_PREFS']);
+ $cnt = count($parts);
+
+ // due to #2721 there might be duplicate entries,
+ // so we read from the end
+ for($i = $cnt-2; $i >= 0; $i -= 2) {
+ if($parts[$i] == $enc_pref) {
+ return urldecode($parts[$i + 1]);
+ }
+ }
+ }
+ return $default;
+}
+
+/**
+ * Add a preference to the DokuWiki cookie
+ * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded)
+ * Remove it by setting $val to false
+ *
+ * @param string $pref preference key
+ * @param string $val preference value
+ */
+function set_doku_pref($pref, $val) {
+ global $conf;
+ $orig = get_doku_pref($pref, false);
+ $cookieVal = '';
+
+ if($orig !== false && ($orig !== $val)) {
+ $parts = explode('#', $_COOKIE['DOKU_PREFS']);
+ $cnt = count($parts);
+ // urlencode $pref for the comparison
+ $enc_pref = rawurlencode($pref);
+ $seen = false;
+ for ($i = 0; $i < $cnt; $i += 2) {
+ if ($parts[$i] == $enc_pref) {
+ if (!$seen){
+ if ($val !== false) {
+ $parts[$i + 1] = rawurlencode($val);
+ } else {
+ unset($parts[$i]);
+ unset($parts[$i + 1]);
+ }
+ $seen = true;
+ } else {
+ // no break because we want to remove duplicate entries
+ unset($parts[$i]);
+ unset($parts[$i + 1]);
+ }
+ }
+ }
+ $cookieVal = implode('#', $parts);
+ } else if ($orig === false && $val !== false) {
+ $cookieVal = ($_COOKIE['DOKU_PREFS'] ? $_COOKIE['DOKU_PREFS'] . '#' : '') .
+ rawurlencode($pref) . '#' . rawurlencode($val);
+ }
+
+ $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
+ if(defined('DOKU_UNITTEST')) {
+ $_COOKIE['DOKU_PREFS'] = $cookieVal;
+ }else{
+ setcookie('DOKU_PREFS', $cookieVal, time()+365*24*3600, $cookieDir, '', ($conf['securecookie'] && is_ssl()));
+ }
+}
+
+/**
+ * Strips source mapping declarations from given text #601
+ *
+ * @param string &$text reference to the CSS or JavaScript code to clean
+ */
+function stripsourcemaps(&$text){
+ $text = preg_replace('/^(\/\/|\/\*)[@#]\s+sourceMappingURL=.*?(\*\/)?$/im', '\\1\\2', $text);
+}
+
+/**
+ * Returns the contents of a given SVG file for embedding
+ *
+ * Inlining SVGs saves on HTTP requests and more importantly allows for styling them through
+ * CSS. However it should used with small SVGs only. The $maxsize setting ensures only small
+ * files are embedded.
+ *
+ * This strips unneeded headers, comments and newline. The result is not a vaild standalone SVG!
+ *
+ * @param string $file full path to the SVG file
+ * @param int $maxsize maximum allowed size for the SVG to be embedded
+ * @return string|false the SVG content, false if the file couldn't be loaded
+ */
+function inlineSVG($file, $maxsize = 2048) {
+ $file = trim($file);
+ if($file === '') return false;
+ if(!file_exists($file)) return false;
+ if(filesize($file) > $maxsize) return false;
+ if(!is_readable($file)) return false;
+ $content = file_get_contents($file);
+ $content = preg_replace('/<!--.*?(-->)/s','', $content); // comments
+ $content = preg_replace('/<\?xml .*?\?>/i', '', $content); // xml header
+ $content = preg_replace('/<!DOCTYPE .*?>/i', '', $content); // doc type
+ $content = preg_replace('/>\s+</s', '><', $content); // newlines between tags
+ $content = trim($content);
+ if(substr($content, 0, 5) !== '<svg ') return false;
+ return $content;
+}
+
+//Setup VIM: ex: et ts=2 :
diff --git a/platform/www/inc/compatibility.php b/platform/www/inc/compatibility.php
new file mode 100644
index 0000000..445f245
--- /dev/null
+++ b/platform/www/inc/compatibility.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * compatibility functions
+ *
+ * This file contains a few functions that might be missing from the PHP build
+ */
+
+if(!function_exists('ctype_space')) {
+ /**
+ * Check for whitespace character(s)
+ *
+ * @see ctype_space
+ * @param string $text
+ * @return bool
+ */
+ function ctype_space($text) {
+ if(!is_string($text)) return false; #FIXME original treats between -128 and 255 inclusive as ASCII chars
+ if(trim($text) === '') return true;
+ return false;
+ }
+}
+
+if(!function_exists('ctype_digit')) {
+ /**
+ * Check for numeric character(s)
+ *
+ * @see ctype_digit
+ * @param string $text
+ * @return bool
+ */
+ function ctype_digit($text) {
+ if(!is_string($text)) return false; #FIXME original treats between -128 and 255 inclusive as ASCII chars
+ if(preg_match('/^\d+$/', $text)) return true;
+ return false;
+ }
+}
+
+if(!function_exists('gzopen') && function_exists('gzopen64')) {
+ /**
+ * work around for PHP compiled against certain zlib versions #865
+ *
+ * @link http://stackoverflow.com/questions/23417519/php-zlib-gzopen-not-exists
+ *
+ * @param string $filename
+ * @param string $mode
+ * @param int $use_include_path
+ * @return mixed
+ */
+ function gzopen($filename, $mode, $use_include_path = 0) {
+ return gzopen64($filename, $mode, $use_include_path);
+ }
+}
+
+if(!function_exists('gzseek') && function_exists('gzseek64')) {
+ /**
+ * work around for PHP compiled against certain zlib versions #865
+ *
+ * @link http://stackoverflow.com/questions/23417519/php-zlib-gzopen-not-exists
+ *
+ * @param resource $zp
+ * @param int $offset
+ * @param int $whence
+ * @return int
+ */
+ function gzseek($zp, $offset, $whence = SEEK_SET) {
+ return gzseek64($zp, $offset, $whence);
+ }
+}
+
+if(!function_exists('gztell') && function_exists('gztell64')) {
+ /**
+ * work around for PHP compiled against certain zlib versions #865
+ *
+ * @link http://stackoverflow.com/questions/23417519/php-zlib-gzopen-not-exists
+ *
+ * @param resource $zp
+ * @return int
+ */
+ function gztell($zp) {
+ return gztell64($zp);
+ }
+}
+
diff --git a/platform/www/inc/config_cascade.php b/platform/www/inc/config_cascade.php
new file mode 100644
index 0000000..61d099c
--- /dev/null
+++ b/platform/www/inc/config_cascade.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * The default config cascade
+ *
+ * This array configures the default locations of various files in the
+ * DokuWiki directory hierarchy. It can be overriden in inc/preload.php
+ */
+$config_cascade = array_merge(
+ array(
+ 'main' => array(
+ 'default' => array(DOKU_CONF . 'dokuwiki.php'),
+ 'local' => array(DOKU_CONF . 'local.php'),
+ 'protected' => array(DOKU_CONF . 'local.protected.php'),
+ ),
+ 'acronyms' => array(
+ 'default' => array(DOKU_CONF . 'acronyms.conf'),
+ 'local' => array(DOKU_CONF . 'acronyms.local.conf'),
+ ),
+ 'entities' => array(
+ 'default' => array(DOKU_CONF . 'entities.conf'),
+ 'local' => array(DOKU_CONF . 'entities.local.conf'),
+ ),
+ 'interwiki' => array(
+ 'default' => array(DOKU_CONF . 'interwiki.conf'),
+ 'local' => array(DOKU_CONF . 'interwiki.local.conf'),
+ ),
+ 'license' => array(
+ 'default' => array(DOKU_CONF . 'license.php'),
+ 'local' => array(DOKU_CONF . 'license.local.php'),
+ ),
+ 'manifest' => array(
+ 'default' => array(DOKU_CONF . 'manifest.json'),
+ 'local' => array(DOKU_CONF . 'manifest.local.json'),
+ ),
+ 'mediameta' => array(
+ 'default' => array(DOKU_CONF . 'mediameta.php'),
+ 'local' => array(DOKU_CONF . 'mediameta.local.php'),
+ ),
+ 'mime' => array(
+ 'default' => array(DOKU_CONF . 'mime.conf'),
+ 'local' => array(DOKU_CONF . 'mime.local.conf'),
+ ),
+ 'scheme' => array(
+ 'default' => array(DOKU_CONF . 'scheme.conf'),
+ 'local' => array(DOKU_CONF . 'scheme.local.conf'),
+ ),
+ 'smileys' => array(
+ 'default' => array(DOKU_CONF . 'smileys.conf'),
+ 'local' => array(DOKU_CONF . 'smileys.local.conf'),
+ ),
+ 'wordblock' => array(
+ 'default' => array(DOKU_CONF . 'wordblock.conf'),
+ 'local' => array(DOKU_CONF . 'wordblock.local.conf'),
+ ),
+ 'userstyle' => array(
+ 'screen' => array(DOKU_CONF . 'userstyle.css', DOKU_CONF . 'userstyle.less'),
+ 'print' => array(DOKU_CONF . 'userprint.css', DOKU_CONF . 'userprint.less'),
+ 'feed' => array(DOKU_CONF . 'userfeed.css', DOKU_CONF . 'userfeed.less'),
+ 'all' => array(DOKU_CONF . 'userall.css', DOKU_CONF . 'userall.less')
+ ),
+ 'userscript' => array(
+ 'default' => array(DOKU_CONF . 'userscript.js')
+ ),
+ 'styleini' => array(
+ 'default' => array(DOKU_INC . 'lib/tpl/%TEMPLATE%/' . 'style.ini'),
+ 'local' => array(DOKU_CONF . 'tpl/%TEMPLATE%/' . 'style.ini')
+ ),
+ 'acl' => array(
+ 'default' => DOKU_CONF . 'acl.auth.php',
+ ),
+ 'plainauth.users' => array(
+ 'default' => DOKU_CONF . 'users.auth.php',
+ 'protected' => '' // not used by default
+ ),
+ 'plugins' => array(
+ 'default' => array(DOKU_CONF . 'plugins.php'),
+ 'local' => array(DOKU_CONF . 'plugins.local.php'),
+ 'protected' => array(
+ DOKU_CONF . 'plugins.required.php',
+ DOKU_CONF . 'plugins.protected.php',
+ ),
+ ),
+ 'lang' => array(
+ 'core' => array(DOKU_CONF . 'lang/'),
+ 'plugin' => array(DOKU_CONF . 'plugin_lang/'),
+ 'template' => array(DOKU_CONF . 'template_lang/')
+ )
+ ),
+ $config_cascade
+);
+
diff --git a/platform/www/inc/confutils.php b/platform/www/inc/confutils.php
new file mode 100644
index 0000000..31724d2
--- /dev/null
+++ b/platform/www/inc/confutils.php
@@ -0,0 +1,474 @@
+<?php
+/**
+ * Utilities for collecting data from config files
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ */
+
+/*
+ * line prefix used to negate single value config items
+ * (scheme.conf & stopwords.conf), e.g.
+ * !gopher
+ */
+
+use dokuwiki\Extension\AuthPlugin;
+use dokuwiki\Extension\Event;
+const DOKU_CONF_NEGATION = '!';
+
+/**
+ * Returns the (known) extension and mimetype of a given filename
+ *
+ * If $knownonly is true (the default), then only known extensions
+ * are returned.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $file file name
+ * @param bool $knownonly
+ * @return array with extension, mimetype and if it should be downloaded
+ */
+function mimetype($file, $knownonly=true){
+ $mtypes = getMimeTypes(); // known mimetypes
+ $ext = strrpos($file, '.');
+ if ($ext === false) {
+ return array(false, false, false);
+ }
+ $ext = strtolower(substr($file, $ext + 1));
+ if (!isset($mtypes[$ext])){
+ if ($knownonly) {
+ return array(false, false, false);
+ } else {
+ return array($ext, 'application/octet-stream', true);
+ }
+ }
+ if($mtypes[$ext][0] == '!'){
+ return array($ext, substr($mtypes[$ext],1), true);
+ }else{
+ return array($ext, $mtypes[$ext], false);
+ }
+}
+
+/**
+ * returns a hash of mimetypes
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function getMimeTypes() {
+ static $mime = null;
+ if ( !$mime ) {
+ $mime = retrieveConfig('mime','confToHash');
+ $mime = array_filter($mime);
+ }
+ return $mime;
+}
+
+/**
+ * returns a hash of acronyms
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ */
+function getAcronyms() {
+ static $acronyms = null;
+ if ( !$acronyms ) {
+ $acronyms = retrieveConfig('acronyms','confToHash');
+ $acronyms = array_filter($acronyms, 'strlen');
+ }
+ return $acronyms;
+}
+
+/**
+ * returns a hash of smileys
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ */
+function getSmileys() {
+ static $smileys = null;
+ if ( !$smileys ) {
+ $smileys = retrieveConfig('smileys','confToHash');
+ $smileys = array_filter($smileys, 'strlen');
+ }
+ return $smileys;
+}
+
+/**
+ * returns a hash of entities
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ */
+function getEntities() {
+ static $entities = null;
+ if ( !$entities ) {
+ $entities = retrieveConfig('entities','confToHash');
+ $entities = array_filter($entities, 'strlen');
+ }
+ return $entities;
+}
+
+/**
+ * returns a hash of interwikilinks
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ */
+function getInterwiki() {
+ static $wikis = null;
+ if ( !$wikis ) {
+ $wikis = retrieveConfig('interwiki','confToHash',array(true));
+ $wikis = array_filter($wikis, 'strlen');
+
+ //add sepecial case 'this'
+ $wikis['this'] = DOKU_URL.'{NAME}';
+ }
+ return $wikis;
+}
+
+/**
+ * Returns the jquery script URLs for the versions defined in lib/scripts/jquery/versions
+ *
+ * @trigger CONFUTIL_CDN_SELECT
+ * @return array
+ */
+function getCdnUrls() {
+ global $conf;
+
+ // load version info
+ $versions = array();
+ $lines = file(DOKU_INC . 'lib/scripts/jquery/versions');
+ foreach($lines as $line) {
+ $line = trim(preg_replace('/#.*$/', '', $line));
+ if($line === '') continue;
+ list($key, $val) = explode('=', $line, 2);
+ $key = trim($key);
+ $val = trim($val);
+ $versions[$key] = $val;
+ }
+
+ $src = array();
+ $data = array(
+ 'versions' => $versions,
+ 'src' => &$src
+ );
+ $event = new Event('CONFUTIL_CDN_SELECT', $data);
+ if($event->advise_before()) {
+ if(!$conf['jquerycdn']) {
+ $jqmod = md5(join('-', $versions));
+ $src[] = DOKU_BASE . 'lib/exe/jquery.php' . '?tseed=' . $jqmod;
+ } elseif($conf['jquerycdn'] == 'jquery') {
+ $src[] = sprintf('https://code.jquery.com/jquery-%s.min.js', $versions['JQ_VERSION']);
+ $src[] = sprintf('https://code.jquery.com/ui/%s/jquery-ui.min.js', $versions['JQUI_VERSION']);
+ } elseif($conf['jquerycdn'] == 'cdnjs') {
+ $src[] = sprintf(
+ 'https://cdnjs.cloudflare.com/ajax/libs/jquery/%s/jquery.min.js',
+ $versions['JQ_VERSION']
+ );
+ $src[] = sprintf(
+ 'https://cdnjs.cloudflare.com/ajax/libs/jqueryui/%s/jquery-ui.min.js',
+ $versions['JQUI_VERSION']
+ );
+ }
+ }
+ $event->advise_after();
+
+ return $src;
+}
+
+/**
+ * returns array of wordblock patterns
+ *
+ */
+function getWordblocks() {
+ static $wordblocks = null;
+ if ( !$wordblocks ) {
+ $wordblocks = retrieveConfig('wordblock','file',null,'array_merge_with_removal');
+ }
+ return $wordblocks;
+}
+
+/**
+ * Gets the list of configured schemes
+ *
+ * @return array the schemes
+ */
+function getSchemes() {
+ static $schemes = null;
+ if ( !$schemes ) {
+ $schemes = retrieveConfig('scheme','file',null,'array_merge_with_removal');
+ $schemes = array_map('trim', $schemes);
+ $schemes = preg_replace('/^#.*/', '', $schemes);
+ $schemes = array_filter($schemes);
+ }
+ return $schemes;
+}
+
+/**
+ * Builds a hash from an array of lines
+ *
+ * If $lower is set to true all hash keys are converted to
+ * lower case.
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Gina Haeussge <gina@foosel.net>
+ *
+ * @param array $lines
+ * @param bool $lower
+ *
+ * @return array
+ */
+function linesToHash($lines, $lower = false) {
+ $conf = array();
+ // remove BOM
+ if(isset($lines[0]) && substr($lines[0], 0, 3) == pack('CCC', 0xef, 0xbb, 0xbf))
+ $lines[0] = substr($lines[0], 3);
+ foreach($lines as $line) {
+ //ignore comments (except escaped ones)
+ $line = preg_replace('/(?<![&\\\\])#.*$/', '', $line);
+ $line = str_replace('\\#', '#', $line);
+ $line = trim($line);
+ if($line === '') continue;
+ $line = preg_split('/\s+/', $line, 2);
+ $line = array_pad($line, 2, '');
+ // Build the associative array
+ if($lower) {
+ $conf[strtolower($line[0])] = $line[1];
+ } else {
+ $conf[$line[0]] = $line[1];
+ }
+ }
+
+ return $conf;
+}
+
+/**
+ * Builds a hash from a configfile
+ *
+ * If $lower is set to true all hash keys are converted to
+ * lower case.
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Gina Haeussge <gina@foosel.net>
+ *
+ * @param string $file
+ * @param bool $lower
+ *
+ * @return array
+ */
+function confToHash($file,$lower=false) {
+ $conf = array();
+ $lines = @file( $file );
+ if ( !$lines ) return $conf;
+
+ return linesToHash($lines, $lower);
+}
+
+/**
+ * Read a json config file into an array
+ *
+ * @param string $file
+ * @return array
+ */
+function jsonToArray($file)
+{
+ $json = file_get_contents($file);
+
+ $conf = json_decode($json, true);
+
+ if ($conf === null) {
+ return [];
+ }
+
+ return $conf;
+}
+
+/**
+ * Retrieve the requested configuration information
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param string $type the configuration settings to be read, must correspond to a key/array in $config_cascade
+ * @param callback $fn the function used to process the configuration file into an array
+ * @param array $params optional additional params to pass to the callback
+ * @param callback $combine the function used to combine arrays of values read from different configuration files;
+ * the function takes two parameters,
+ * $combined - the already read & merged configuration values
+ * $new - array of config values from the config cascade file being currently processed
+ * and returns an array of the merged configuration values.
+ * @return array configuration values
+ */
+function retrieveConfig($type,$fn,$params=null,$combine='array_merge') {
+ global $config_cascade;
+
+ if(!is_array($params)) $params = array();
+
+ $combined = array();
+ if (!is_array($config_cascade[$type])) trigger_error('Missing config cascade for "'.$type.'"',E_USER_WARNING);
+ foreach (array('default','local','protected') as $config_group) {
+ if (empty($config_cascade[$type][$config_group])) continue;
+ foreach ($config_cascade[$type][$config_group] as $file) {
+ if (file_exists($file)) {
+ $config = call_user_func_array($fn,array_merge(array($file),$params));
+ $combined = $combine($combined, $config);
+ }
+ }
+ }
+
+ return $combined;
+}
+
+/**
+ * Include the requested configuration information
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param string $type the configuration settings to be read, must correspond to a key/array in $config_cascade
+ * @return array list of files, default before local before protected
+ */
+function getConfigFiles($type) {
+ global $config_cascade;
+ $files = array();
+
+ if (!is_array($config_cascade[$type])) trigger_error('Missing config cascade for "'.$type.'"',E_USER_WARNING);
+ foreach (array('default','local','protected') as $config_group) {
+ if (empty($config_cascade[$type][$config_group])) continue;
+ $files = array_merge($files, $config_cascade[$type][$config_group]);
+ }
+
+ return $files;
+}
+
+/**
+ * check if the given action was disabled in config
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $action
+ * @returns boolean true if enabled, false if disabled
+ */
+function actionOK($action){
+ static $disabled = null;
+ if(is_null($disabled) || defined('SIMPLE_TEST')){
+ global $conf;
+ /** @var AuthPlugin $auth */
+ global $auth;
+
+ // prepare disabled actions array and handle legacy options
+ $disabled = explode(',',$conf['disableactions']);
+ $disabled = array_map('trim',$disabled);
+ if((isset($conf['openregister']) && !$conf['openregister']) || is_null($auth) || !$auth->canDo('addUser')) {
+ $disabled[] = 'register';
+ }
+ if((isset($conf['resendpasswd']) && !$conf['resendpasswd']) || is_null($auth) || !$auth->canDo('modPass')) {
+ $disabled[] = 'resendpwd';
+ }
+ if((isset($conf['subscribers']) && !$conf['subscribers']) || is_null($auth)) {
+ $disabled[] = 'subscribe';
+ }
+ if (is_null($auth) || !$auth->canDo('Profile')) {
+ $disabled[] = 'profile';
+ }
+ if (is_null($auth) || !$auth->canDo('delUser')) {
+ $disabled[] = 'profile_delete';
+ }
+ if (is_null($auth)) {
+ $disabled[] = 'login';
+ }
+ if (is_null($auth) || !$auth->canDo('logout')) {
+ $disabled[] = 'logout';
+ }
+ $disabled = array_unique($disabled);
+ }
+
+ return !in_array($action,$disabled);
+}
+
+/**
+ * check if headings should be used as link text for the specified link type
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param string $linktype 'content'|'navigation', content applies to links in wiki text
+ * navigation applies to all other links
+ * @return boolean true if headings should be used for $linktype, false otherwise
+ */
+function useHeading($linktype) {
+ static $useHeading = null;
+ if(defined('DOKU_UNITTEST')) $useHeading = null; // don't cache during unit tests
+
+ if (is_null($useHeading)) {
+ global $conf;
+
+ if (!empty($conf['useheading'])) {
+ switch ($conf['useheading']) {
+ case 'content':
+ $useHeading['content'] = true;
+ break;
+
+ case 'navigation':
+ $useHeading['navigation'] = true;
+ break;
+ default:
+ $useHeading['content'] = true;
+ $useHeading['navigation'] = true;
+ }
+ } else {
+ $useHeading = array();
+ }
+ }
+
+ return (!empty($useHeading[$linktype]));
+}
+
+/**
+ * obscure config data so information isn't plain text
+ *
+ * @param string $str data to be encoded
+ * @param string $code encoding method, values: plain, base64, uuencode.
+ * @return string the encoded value
+ */
+function conf_encodeString($str,$code) {
+ switch ($code) {
+ case 'base64' : return '<b>'.base64_encode($str);
+ case 'uuencode' : return '<u>'.convert_uuencode($str);
+ case 'plain':
+ default:
+ return $str;
+ }
+}
+/**
+ * return obscured data as plain text
+ *
+ * @param string $str encoded data
+ * @return string plain text
+ */
+function conf_decodeString($str) {
+ switch (substr($str,0,3)) {
+ case '<b>' : return base64_decode(substr($str,3));
+ case '<u>' : return convert_uudecode(substr($str,3));
+ default: // not encoded (or unknown)
+ return $str;
+ }
+}
+
+/**
+ * array combination function to remove negated values (prefixed by !)
+ *
+ * @param array $current
+ * @param array $new
+ *
+ * @return array the combined array, numeric keys reset
+ */
+function array_merge_with_removal($current, $new) {
+ foreach ($new as $val) {
+ if (substr($val,0,1) == DOKU_CONF_NEGATION) {
+ $idx = array_search(trim(substr($val,1)),$current);
+ if ($idx !== false) {
+ unset($current[$idx]);
+ }
+ } else {
+ $current[] = trim($val);
+ }
+ }
+
+ return array_slice($current,0);
+}
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/inc/defines.php b/platform/www/inc/defines.php
new file mode 100644
index 0000000..d864f71
--- /dev/null
+++ b/platform/www/inc/defines.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Set up globally available constants
+ */
+
+/**
+ * Auth Levels
+ * @file inc/auth.php
+ */
+define('AUTH_NONE', 0);
+define('AUTH_READ', 1);
+define('AUTH_EDIT', 2);
+define('AUTH_CREATE', 4);
+define('AUTH_UPLOAD', 8);
+define('AUTH_DELETE', 16);
+define('AUTH_ADMIN', 255);
+
+/**
+ * Message types
+ * @see msg()
+ */
+define('MSG_PUBLIC', 0);
+define('MSG_USERS_ONLY', 1);
+define('MSG_MANAGERS_ONLY', 2);
+define('MSG_ADMINS_ONLY', 4);
+
+/**
+ * Lexer constants
+ * @see \dokuwiki\Parsing\Lexer\Lexer
+ */
+define('DOKU_LEXER_ENTER', 1);
+define('DOKU_LEXER_MATCHED', 2);
+define('DOKU_LEXER_UNMATCHED', 3);
+define('DOKU_LEXER_EXIT', 4);
+define('DOKU_LEXER_SPECIAL', 5);
+
+/**
+ * Constants for known core changelog line types.
+ * @file inc/changelog.php
+ */
+define('DOKU_CHANGE_TYPE_CREATE', 'C');
+define('DOKU_CHANGE_TYPE_EDIT', 'E');
+define('DOKU_CHANGE_TYPE_MINOR_EDIT', 'e');
+define('DOKU_CHANGE_TYPE_DELETE', 'D');
+define('DOKU_CHANGE_TYPE_REVERT', 'R');
+
+/**
+ * Changelog filter constants
+ * @file inc/changelog.php
+ */
+define('RECENTS_SKIP_DELETED', 2);
+define('RECENTS_SKIP_MINORS', 4);
+define('RECENTS_SKIP_SUBSPACES', 8);
+define('RECENTS_MEDIA_CHANGES', 16);
+define('RECENTS_MEDIA_PAGES_MIXED', 32);
+define('RECENTS_ONLY_CREATION', 64);
+
+/**
+ * Media error types
+ * @file inc/media.php
+ */
+define('DOKU_MEDIA_DELETED', 1);
+define('DOKU_MEDIA_NOT_AUTH', 2);
+define('DOKU_MEDIA_INUSE', 4);
+define('DOKU_MEDIA_EMPTY_NS', 8);
diff --git a/platform/www/inc/deprecated.php b/platform/www/inc/deprecated.php
new file mode 100644
index 0000000..2050373
--- /dev/null
+++ b/platform/www/inc/deprecated.php
@@ -0,0 +1,570 @@
+<?php
+// phpcs:ignoreFile -- this file violates PSR2 by definition
+/**
+ * These classes and functions are deprecated and will be removed in future releases
+ */
+
+use dokuwiki\Debug\DebugHelper;
+use dokuwiki\Subscriptions\BulkSubscriptionSender;
+use dokuwiki\Subscriptions\MediaSubscriptionSender;
+use dokuwiki\Subscriptions\PageSubscriptionSender;
+use dokuwiki\Subscriptions\RegistrationSubscriptionSender;
+use dokuwiki\Subscriptions\SubscriberManager;
+
+/**
+ * @inheritdoc
+ * @deprecated 2018-05-07
+ */
+class RemoteAccessDeniedException extends \dokuwiki\Remote\AccessDeniedException
+{
+ /** @inheritdoc */
+ public function __construct($message = "", $code = 0, Throwable $previous = null)
+ {
+ dbg_deprecated(\dokuwiki\Remote\AccessDeniedException::class);
+ parent::__construct($message, $code, $previous);
+ }
+
+}
+
+/**
+ * @inheritdoc
+ * @deprecated 2018-05-07
+ */
+class RemoteException extends \dokuwiki\Remote\RemoteException
+{
+ /** @inheritdoc */
+ public function __construct($message = "", $code = 0, Throwable $previous = null)
+ {
+ dbg_deprecated(\dokuwiki\Remote\RemoteException::class);
+ parent::__construct($message, $code, $previous);
+ }
+
+}
+
+/**
+ * Escapes regex characters other than (, ) and /
+ *
+ * @param string $str
+ * @return string
+ * @deprecated 2018-05-04
+ */
+function Doku_Lexer_Escape($str)
+{
+ dbg_deprecated('\\dokuwiki\\Parsing\\Lexer\\Lexer::escape()');
+ return \dokuwiki\Parsing\Lexer\Lexer::escape($str);
+}
+
+/**
+ * @inheritdoc
+ * @deprecated 2018-06-01
+ */
+class setting extends \dokuwiki\plugin\config\core\Setting\Setting
+{
+ /** @inheritdoc */
+ public function __construct($key, array $params = null)
+ {
+ dbg_deprecated(\dokuwiki\plugin\config\core\Setting\Setting::class);
+ parent::__construct($key, $params);
+ }
+}
+
+/**
+ * @inheritdoc
+ * @deprecated 2018-06-01
+ */
+class setting_authtype extends \dokuwiki\plugin\config\core\Setting\SettingAuthtype
+{
+ /** @inheritdoc */
+ public function __construct($key, array $params = null)
+ {
+ dbg_deprecated(\dokuwiki\plugin\config\core\Setting\SettingAuthtype::class);
+ parent::__construct($key, $params);
+ }
+}
+
+/**
+ * @inheritdoc
+ * @deprecated 2018-06-01
+ */
+class setting_string extends \dokuwiki\plugin\config\core\Setting\SettingString
+{
+ /** @inheritdoc */
+ public function __construct($key, array $params = null)
+ {
+ dbg_deprecated(\dokuwiki\plugin\config\core\Setting\SettingString::class);
+ parent::__construct($key, $params);
+ }
+}
+
+/**
+ * @inheritdoc
+ * @deprecated 2018-06-15
+ */
+class PageChangelog extends \dokuwiki\ChangeLog\PageChangeLog
+{
+ /** @inheritdoc */
+ public function __construct($id, $chunk_size = 8192)
+ {
+ dbg_deprecated(\dokuwiki\ChangeLog\PageChangeLog::class);
+ parent::__construct($id, $chunk_size);
+ }
+}
+
+/**
+ * @inheritdoc
+ * @deprecated 2018-06-15
+ */
+class MediaChangelog extends \dokuwiki\ChangeLog\MediaChangeLog
+{
+ /** @inheritdoc */
+ public function __construct($id, $chunk_size = 8192)
+ {
+ dbg_deprecated(\dokuwiki\ChangeLog\MediaChangeLog::class);
+ parent::__construct($id, $chunk_size);
+ }
+}
+
+/** Behavior switch for JSON::decode() */
+define('JSON_LOOSE_TYPE', 16);
+
+/** Behavior switch for JSON::decode() */
+define('JSON_STRICT_TYPE', 0);
+
+/**
+ * Encode/Decode JSON
+ * @deprecated 2018-07-27
+ */
+class JSON
+{
+ protected $use = 0;
+
+ /**
+ * @param int $use JSON_*_TYPE flag
+ * @deprecated 2018-07-27
+ */
+ public function __construct($use = JSON_STRICT_TYPE)
+ {
+ $this->use = $use;
+ }
+
+ /**
+ * Encode given structure to JSON
+ *
+ * @param mixed $var
+ * @return string
+ * @deprecated 2018-07-27
+ */
+ public function encode($var)
+ {
+ dbg_deprecated('json_encode');
+ return json_encode($var);
+ }
+
+ /**
+ * Alias for encode()
+ * @param $var
+ * @return string
+ * @deprecated 2018-07-27
+ */
+ public function enc($var) {
+ return $this->encode($var);
+ }
+
+ /**
+ * Decode given string from JSON
+ *
+ * @param string $str
+ * @return mixed
+ * @deprecated 2018-07-27
+ */
+ public function decode($str)
+ {
+ dbg_deprecated('json_encode');
+ return json_decode($str, ($this->use == JSON_LOOSE_TYPE));
+ }
+
+ /**
+ * Alias for decode
+ *
+ * @param $str
+ * @return mixed
+ * @deprecated 2018-07-27
+ */
+ public function dec($str) {
+ return $this->decode($str);
+ }
+}
+
+/**
+ * @inheritdoc
+ * @deprecated 2019-02-19
+ */
+class Input extends \dokuwiki\Input\Input {
+ /**
+ * @inheritdoc
+ * @deprecated 2019-02-19
+ */
+ public function __construct()
+ {
+ dbg_deprecated(\dokuwiki\Input\Input::class);
+ parent::__construct();
+ }
+}
+
+/**
+ * @inheritdoc
+ * @deprecated 2019-02-19
+ */
+class PostInput extends \dokuwiki\Input\Post {
+ /**
+ * @inheritdoc
+ * @deprecated 2019-02-19
+ */
+ public function __construct()
+ {
+ dbg_deprecated(\dokuwiki\Input\Post::class);
+ parent::__construct();
+ }
+}
+
+/**
+ * @inheritdoc
+ * @deprecated 2019-02-19
+ */
+class GetInput extends \dokuwiki\Input\Get {
+ /**
+ * @inheritdoc
+ * @deprecated 2019-02-19
+ */
+ public function __construct()
+ {
+ dbg_deprecated(\dokuwiki\Input\Get::class);
+ parent::__construct();
+ }
+}
+
+/**
+ * @inheritdoc
+ * @deprecated 2019-02-19
+ */
+class ServerInput extends \dokuwiki\Input\Server {
+ /**
+ * @inheritdoc
+ * @deprecated 2019-02-19
+ */
+ public function __construct()
+ {
+ dbg_deprecated(\dokuwiki\Input\Server::class);
+ parent::__construct();
+ }
+}
+
+/**
+ * @inheritdoc
+ * @deprecated 2019-03-06
+ */
+class PassHash extends \dokuwiki\PassHash {
+ /**
+ * @inheritdoc
+ * @deprecated 2019-03-06
+ */
+ public function __construct()
+ {
+ dbg_deprecated(\dokuwiki\PassHash::class);
+ }
+}
+
+/**
+ * @deprecated since 2019-03-17 use \dokuwiki\HTTP\HTTPClientException instead!
+ */
+class HTTPClientException extends \dokuwiki\HTTP\HTTPClientException {
+
+ /**
+ * @inheritdoc
+ * @deprecated 2019-03-17
+ */
+ public function __construct($message = '', $code = 0, $previous = null)
+ {
+ DebugHelper::dbgDeprecatedFunction(dokuwiki\HTTP\HTTPClientException::class);
+ parent::__construct($message, $code, $previous);
+ }
+}
+
+/**
+ * @deprecated since 2019-03-17 use \dokuwiki\HTTP\HTTPClient instead!
+ */
+class HTTPClient extends \dokuwiki\HTTP\HTTPClient {
+
+ /**
+ * @inheritdoc
+ * @deprecated 2019-03-17
+ */
+ public function __construct()
+ {
+ DebugHelper::dbgDeprecatedFunction(dokuwiki\HTTP\HTTPClient::class);
+ parent::__construct();
+ }
+}
+
+/**
+ * @deprecated since 2019-03-17 use \dokuwiki\HTTP\DokuHTTPClient instead!
+ */
+class DokuHTTPClient extends \dokuwiki\HTTP\DokuHTTPClient
+{
+
+ /**
+ * @inheritdoc
+ * @deprecated 2019-03-17
+ */
+ public function __construct()
+ {
+ DebugHelper::dbgDeprecatedFunction(dokuwiki\HTTP\DokuHTTPClient::class);
+ parent::__construct();
+ }
+}
+
+/**
+ * function wrapper to process (create, trigger and destroy) an event
+ *
+ * @param string $name name for the event
+ * @param mixed $data event data
+ * @param callback $action (optional, default=NULL) default action, a php callback function
+ * @param bool $canPreventDefault (optional, default=true) can hooks prevent the default action
+ *
+ * @return mixed the event results value after all event processing is complete
+ * by default this is the return value of the default action however
+ * it can be set or modified by event handler hooks
+ * @deprecated 2018-06-15
+ */
+function trigger_event($name, &$data, $action=null, $canPreventDefault=true) {
+ dbg_deprecated('\dokuwiki\Extension\Event::createAndTrigger');
+ return \dokuwiki\Extension\Event::createAndTrigger($name, $data, $action, $canPreventDefault);
+}
+
+/**
+ * @inheritdoc
+ * @deprecated 2018-06-15
+ */
+class Doku_Plugin_Controller extends \dokuwiki\Extension\PluginController {
+ /** @inheritdoc */
+ public function __construct()
+ {
+ dbg_deprecated(\dokuwiki\Extension\PluginController::class);
+ parent::__construct();
+ }
+}
+
+
+/**
+ * Class for handling (email) subscriptions
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @deprecated 2019-04-22 Use the classes in the \dokuwiki\Subscriptions namespace instead!
+ */
+class Subscription {
+
+ /**
+ * Check if subscription system is enabled
+ *
+ * @return bool
+ *
+ * @deprecated 2019-04-20 \dokuwiki\Subscriptions\SubscriberManager::isenabled
+ */
+ public function isenabled() {
+ DebugHelper::dbgDeprecatedFunction('\dokuwiki\Subscriptions\SubscriberManager::isenabled');
+ $subscriberManager = new SubscriberManager();
+ return $subscriberManager->isenabled();
+ }
+
+ /**
+ * Recursively search for matching subscriptions
+ *
+ * This function searches all relevant subscription files for a page or
+ * namespace.
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ *
+ * @param string $page The target object’s (namespace or page) id
+ * @param string|array $user
+ * @param string|array $style
+ * @param string|array $data
+ * @return array
+ *
+ * @deprecated 2019-04-20 \dokuwiki\Subscriptions\SubscriberManager::subscribers
+ */
+ public function subscribers($page, $user = null, $style = null, $data = null) {
+ DebugHelper::dbgDeprecatedFunction('\dokuwiki\Subscriptions\SubscriberManager::subscribers');
+ $manager = new SubscriberManager();
+ return $manager->subscribers($page, $user, $style, $data);
+ }
+
+ /**
+ * Adds a new subscription for the given page or namespace
+ *
+ * This will automatically overwrite any existent subscription for the given user on this
+ * *exact* page or namespace. It will *not* modify any subscription that may exist in higher namespaces.
+ *
+ * @param string $id The target page or namespace, specified by id; Namespaces
+ * are identified by appending a colon.
+ * @param string $user
+ * @param string $style
+ * @param string $data
+ * @throws Exception when user or style is empty
+ * @return bool
+ *
+ * @deprecated 2019-04-20 \dokuwiki\Subscriptions\SubscriberManager::add
+ */
+ public function add($id, $user, $style, $data = '') {
+ DebugHelper::dbgDeprecatedFunction('\dokuwiki\Subscriptions\SubscriberManager::add');
+ $manager = new SubscriberManager();
+ return $manager->add($id, $user, $style, $data);
+ }
+
+ /**
+ * Removes a subscription for the given page or namespace
+ *
+ * This removes all subscriptions matching the given criteria on the given page or
+ * namespace. It will *not* modify any subscriptions that may exist in higher
+ * namespaces.
+ *
+ * @param string $id The target object’s (namespace or page) id
+ * @param string|array $user
+ * @param string|array $style
+ * @param string|array $data
+ * @return bool
+ *
+ * @deprecated 2019-04-20 \dokuwiki\Subscriptions\SubscriberManager::remove
+ */
+ public function remove($id, $user = null, $style = null, $data = null) {
+ DebugHelper::dbgDeprecatedFunction('\dokuwiki\Subscriptions\SubscriberManager::remove');
+ $manager = new SubscriberManager();
+ return $manager->remove($id, $user, $style, $data);
+ }
+
+ /**
+ * Get data for $INFO['subscribed']
+ *
+ * $INFO['subscribed'] is either false if no subscription for the current page
+ * and user is in effect. Else it contains an array of arrays with the fields
+ * “target”, “style”, and optionally “data”.
+ *
+ * @param string $id Page ID, defaults to global $ID
+ * @param string $user User, defaults to $_SERVER['REMOTE_USER']
+ * @return array|false
+ * @author Adrian Lang <lang@cosmocode.de>
+ *
+ * @deprecated 2019-04-20 \dokuwiki\Subscriptions\SubscriberManager::userSubscription
+ */
+ public function user_subscription($id = '', $user = '') {
+ DebugHelper::dbgDeprecatedFunction('\dokuwiki\Subscriptions\SubscriberManager::userSubscription');
+ $manager = new SubscriberManager();
+ return $manager->userSubscription($id, $user);
+ }
+
+ /**
+ * Send digest and list subscriptions
+ *
+ * This sends mails to all subscribers that have a subscription for namespaces above
+ * the given page if the needed $conf['subscribe_time'] has passed already.
+ *
+ * This function is called form lib/exe/indexer.php
+ *
+ * @param string $page
+ * @return int number of sent mails
+ *
+ * @deprecated 2019-04-20 \dokuwiki\Subscriptions\BulkSubscriptionSender::sendBulk
+ */
+ public function send_bulk($page) {
+ DebugHelper::dbgDeprecatedFunction('\dokuwiki\Subscriptions\BulkSubscriptionSender::sendBulk');
+ $subscriptionSender = new BulkSubscriptionSender();
+ return $subscriptionSender->sendBulk($page);
+ }
+
+ /**
+ * Send the diff for some page change
+ *
+ * @param string $subscriber_mail The target mail address
+ * @param string $template Mail template ('subscr_digest', 'subscr_single', 'mailtext', ...)
+ * @param string $id Page for which the notification is
+ * @param int|null $rev Old revision if any
+ * @param string $summary Change summary if any
+ * @return bool true if successfully sent
+ *
+ * @deprecated 2019-04-20 \dokuwiki\Subscriptions\PageSubscriptionSender::sendPageDiff
+ */
+ public function send_diff($subscriber_mail, $template, $id, $rev = null, $summary = '') {
+ DebugHelper::dbgDeprecatedFunction('\dokuwiki\Subscriptions\PageSubscriptionSender::sendPageDiff');
+ $subscriptionSender = new PageSubscriptionSender();
+ return $subscriptionSender->sendPageDiff($subscriber_mail, $template, $id, $rev, $summary);
+ }
+
+ /**
+ * Send the diff for some media change
+ *
+ * @fixme this should embed thumbnails of images in HTML version
+ *
+ * @param string $subscriber_mail The target mail address
+ * @param string $template Mail template ('uploadmail', ...)
+ * @param string $id Media file for which the notification is
+ * @param int|bool $rev Old revision if any
+ *
+ * @deprecated 2019-04-20 \dokuwiki\Subscriptions\MediaSubscriptionSender::sendMediaDiff
+ */
+ public function send_media_diff($subscriber_mail, $template, $id, $rev = false) {
+ DebugHelper::dbgDeprecatedFunction('\dokuwiki\Subscriptions\MediaSubscriptionSender::sendMediaDiff');
+ $subscriptionSender = new MediaSubscriptionSender();
+ return $subscriptionSender->sendMediaDiff($subscriber_mail, $template, $id, $rev);
+ }
+
+ /**
+ * Send a notify mail on new registration
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $login login name of the new user
+ * @param string $fullname full name of the new user
+ * @param string $email email address of the new user
+ * @return bool true if a mail was sent
+ *
+ * @deprecated 2019-04-20 \dokuwiki\Subscriptions\RegistrationSubscriptionSender::sendRegister
+ */
+ public function send_register($login, $fullname, $email) {
+ DebugHelper::dbgDeprecatedFunction('\dokuwiki\Subscriptions\RegistrationSubscriptionSender::sendRegister');
+ $subscriptionSender = new RegistrationSubscriptionSender();
+ return $subscriptionSender->sendRegister($login, $fullname, $email);
+ }
+
+
+ /**
+ * Default callback for COMMON_NOTIFY_ADDRESSLIST
+ *
+ * Aggregates all email addresses of user who have subscribed the given page with 'every' style
+ *
+ * @author Steven Danz <steven-danz@kc.rr.com>
+ * @author Adrian Lang <lang@cosmocode.de>
+ *
+ * @todo move the whole functionality into this class, trigger SUBSCRIPTION_NOTIFY_ADDRESSLIST instead,
+ * use an array for the addresses within it
+ *
+ * @param array &$data Containing the entries:
+ * - $id (the page id),
+ * - $self (whether the author should be notified,
+ * - $addresslist (current email address list)
+ * - $replacements (array of additional string substitutions, @KEY@ to be replaced by value)
+ *
+ * @deprecated 2019-04-20 \dokuwiki\Subscriptions\SubscriberManager::notifyAddresses
+ */
+ public function notifyaddresses(&$data) {
+ DebugHelper::dbgDeprecatedFunction('\dokuwiki\Subscriptions\SubscriberManager::notifyAddresses');
+ $manager = new SubscriberManager();
+ $manager->notifyAddresses($data);
+ }
+}
+
+/**
+ * @deprecated 2019-12-29 use \dokuwiki\Search\Indexer
+ */
+class Doku_Indexer extends \dokuwiki\Search\Indexer {};
diff --git a/platform/www/inc/farm.php b/platform/www/inc/farm.php
new file mode 100644
index 0000000..03aa0eb
--- /dev/null
+++ b/platform/www/inc/farm.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ * This overwrites DOKU_CONF. Each animal gets its own configuration and data directory.
+ * This can be used together with preload.php. See preload.php.dist for an example setup.
+ * For more information see http://www.dokuwiki.org/farms.
+ *
+ * The farm directory (constant DOKU_FARMDIR) can be any directory and needs to be set.
+ * Animals are direct subdirectories of the farm directory.
+ * There are two different approaches:
+ * * An .htaccess based setup can use any animal directory name:
+ * http://example.org/<path_to_farm>/subdir/ will need the subdirectory '$farm/subdir/'.
+ * * A virtual host based setup needs animal directory names which have to reflect
+ * the domain name: If an animal resides in http://www.example.org:8080/mysite/test/,
+ * directories that will match range from '$farm/8080.www.example.org.mysite.test/'
+ * to a simple '$farm/domain/'.
+ *
+ * @author Anika Henke <anika@selfthinker.org>
+ * @author Michael Klier <chi@chimeric.de>
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ * @author virtual host part of farm_confpath() based on conf_path() from Drupal.org's /includes/bootstrap.inc
+ * (see https://github.com/drupal/drupal/blob/7.x/includes/bootstrap.inc#L537)
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ */
+
+// DOKU_FARMDIR needs to be set in preload.php, the fallback is the same as DOKU_INC would be (if it was set already)
+if(!defined('DOKU_FARMDIR')) define('DOKU_FARMDIR', fullpath(dirname(__FILE__).'/../').'/');
+if(!defined('DOKU_CONF')) define('DOKU_CONF', farm_confpath(DOKU_FARMDIR));
+if(!defined('DOKU_FARM')) define('DOKU_FARM', false);
+
+
+/**
+ * Find the appropriate configuration directory.
+ *
+ * If the .htaccess based setup is used, the configuration directory can be
+ * any subdirectory of the farm directory.
+ *
+ * Otherwise try finding a matching configuration directory by stripping the
+ * website's hostname from left to right and pathname from right to left. The
+ * first configuration file found will be used; the remaining will ignored.
+ * If no configuration file is found, return the default confdir './conf'.
+ *
+ * @param string $farm
+ *
+ * @return string
+ */
+function farm_confpath($farm) {
+
+ // htaccess based or cli
+ // cli usage example: animal=your_animal bin/indexer.php
+ if(isset($_REQUEST['animal']) || ('cli' == php_sapi_name() && isset($_SERVER['animal']))) {
+ $mode = isset($_REQUEST['animal']) ? 'htaccess' : 'cli';
+ $animal = $mode == 'htaccess' ? $_REQUEST['animal'] : $_SERVER['animal'];
+ // check that $animal is a string and just a directory name and not a path
+ if (!is_string($animal) || strpbrk($animal, '\\/') !== false)
+ nice_die('Sorry! Invalid animal name!');
+ if(!is_dir($farm.'/'.$animal))
+ nice_die("Sorry! This Wiki doesn't exist!");
+ if(!defined('DOKU_FARM')) define('DOKU_FARM', $mode);
+ return $farm.'/'.$animal.'/conf/';
+ }
+
+ // virtual host based
+ $uri = explode('/', $_SERVER['SCRIPT_NAME'] ? $_SERVER['SCRIPT_NAME'] : $_SERVER['SCRIPT_FILENAME']);
+ $server = explode('.', implode('.', array_reverse(explode(':', rtrim($_SERVER['HTTP_HOST'], '.')))));
+ for ($i = count($uri) - 1; $i > 0; $i--) {
+ for ($j = count($server); $j > 0; $j--) {
+ $dir = implode('.', array_slice($server, -$j)) . implode('.', array_slice($uri, 0, $i));
+ if(is_dir("$farm/$dir/conf/")) {
+ if(!defined('DOKU_FARM')) define('DOKU_FARM', 'virtual');
+ return "$farm/$dir/conf/";
+ }
+ }
+ }
+
+ // default conf directory in farm
+ if(is_dir("$farm/default/conf/")) {
+ if(!defined('DOKU_FARM')) define('DOKU_FARM', 'default');
+ return "$farm/default/conf/";
+ }
+ // farmer
+ return DOKU_INC.'conf/';
+}
+
+/* Use default config files and local animal config files */
+$config_cascade = array(
+ 'main' => array(
+ 'default' => array(DOKU_INC.'conf/dokuwiki.php'),
+ 'local' => array(DOKU_CONF.'local.php'),
+ 'protected' => array(DOKU_CONF.'local.protected.php'),
+ ),
+ 'acronyms' => array(
+ 'default' => array(DOKU_INC.'conf/acronyms.conf'),
+ 'local' => array(DOKU_CONF.'acronyms.local.conf'),
+ ),
+ 'entities' => array(
+ 'default' => array(DOKU_INC.'conf/entities.conf'),
+ 'local' => array(DOKU_CONF.'entities.local.conf'),
+ ),
+ 'interwiki' => array(
+ 'default' => array(DOKU_INC.'conf/interwiki.conf'),
+ 'local' => array(DOKU_CONF.'interwiki.local.conf'),
+ ),
+ 'license' => array(
+ 'default' => array(DOKU_INC.'conf/license.php'),
+ 'local' => array(DOKU_CONF.'license.local.php'),
+ ),
+ 'mediameta' => array(
+ 'default' => array(DOKU_INC.'conf/mediameta.php'),
+ 'local' => array(DOKU_CONF.'mediameta.local.php'),
+ ),
+ 'mime' => array(
+ 'default' => array(DOKU_INC.'conf/mime.conf'),
+ 'local' => array(DOKU_CONF.'mime.local.conf'),
+ ),
+ 'scheme' => array(
+ 'default' => array(DOKU_INC.'conf/scheme.conf'),
+ 'local' => array(DOKU_CONF.'scheme.local.conf'),
+ ),
+ 'smileys' => array(
+ 'default' => array(DOKU_INC.'conf/smileys.conf'),
+ 'local' => array(DOKU_CONF.'smileys.local.conf'),
+ ),
+ 'wordblock' => array(
+ 'default' => array(DOKU_INC.'conf/wordblock.conf'),
+ 'local' => array(DOKU_CONF.'wordblock.local.conf'),
+ ),
+ 'acl' => array(
+ 'default' => DOKU_CONF.'acl.auth.php',
+ ),
+ 'plainauth.users' => array(
+ 'default' => DOKU_CONF.'users.auth.php',
+ ),
+ 'plugins' => array( // needed since Angua
+ 'default' => array(DOKU_INC.'conf/plugins.php'),
+ 'local' => array(DOKU_CONF.'plugins.local.php'),
+ 'protected' => array(
+ DOKU_INC.'conf/plugins.required.php',
+ DOKU_CONF.'plugins.protected.php',
+ ),
+ ),
+ 'userstyle' => array(
+ 'screen' => array(DOKU_CONF . 'userstyle.css', DOKU_CONF . 'userstyle.less'),
+ 'print' => array(DOKU_CONF . 'userprint.css', DOKU_CONF . 'userprint.less'),
+ 'feed' => array(DOKU_CONF . 'userfeed.css', DOKU_CONF . 'userfeed.less'),
+ 'all' => array(DOKU_CONF . 'userall.css', DOKU_CONF . 'userall.less')
+ ),
+ 'userscript' => array(
+ 'default' => array(DOKU_CONF . 'userscript.js')
+ ),
+);
diff --git a/platform/www/inc/fetch.functions.php b/platform/www/inc/fetch.functions.php
new file mode 100644
index 0000000..6367262
--- /dev/null
+++ b/platform/www/inc/fetch.functions.php
@@ -0,0 +1,196 @@
+<?php
+/**
+ * Functions used by lib/exe/fetch.php
+ * (not included by other parts of dokuwiki)
+ */
+
+/**
+ * Set headers and send the file to the client
+ *
+ * The $cache parameter influences how long files may be kept in caches, the $public parameter
+ * influences if this caching may happen in public proxis or in the browser cache only FS#2734
+ *
+ * This function will abort the current script when a 304 is sent or file sending is handled
+ * through x-sendfile
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ * @author Gerry Weissbach <dokuwiki@gammaproduction.de>
+ *
+ * @param string $file local file to send
+ * @param string $mime mime type of the file
+ * @param bool $dl set to true to force a browser download
+ * @param int $cache remaining cache time in seconds (-1 for $conf['cache'], 0 for no-cache)
+ * @param bool $public is this a public ressource or a private one?
+ * @param string $orig original file to send - the file name will be used for the Content-Disposition
+ */
+function sendFile($file, $mime, $dl, $cache, $public = false, $orig = null) {
+ global $conf;
+ // send mime headers
+ header("Content-Type: $mime");
+
+ // calculate cache times
+ if($cache == -1) {
+ $maxage = max($conf['cachetime'], 3600); // cachetime or one hour
+ $expires = time() + $maxage;
+ } else if($cache > 0) {
+ $maxage = $cache; // given time
+ $expires = time() + $maxage;
+ } else { // $cache == 0
+ $maxage = 0;
+ $expires = 0; // 1970-01-01
+ }
+
+ // smart http caching headers
+ if($maxage) {
+ if($public) {
+ // cache publically
+ header('Expires: '.gmdate("D, d M Y H:i:s", $expires).' GMT');
+ header('Cache-Control: public, proxy-revalidate, no-transform, max-age='.$maxage);
+ } else {
+ // cache in browser
+ header('Expires: '.gmdate("D, d M Y H:i:s", $expires).' GMT');
+ header('Cache-Control: private, no-transform, max-age='.$maxage);
+ }
+ } else {
+ // no cache at all
+ header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
+ header('Cache-Control: no-cache, no-transform');
+ }
+
+ //send important headers first, script stops here if '304 Not Modified' response
+ $fmtime = @filemtime($file);
+ http_conditionalRequest($fmtime);
+
+ // Use the current $file if is $orig is not set.
+ if ( $orig == null ) {
+ $orig = $file;
+ }
+
+ //download or display?
+ if ($dl) {
+ header('Content-Disposition: attachment;' . rfc2231_encode(
+ 'filename', \dokuwiki\Utf8\PhpString::basename($orig)) . ';'
+ );
+ } else {
+ header('Content-Disposition: inline;' . rfc2231_encode(
+ 'filename', \dokuwiki\Utf8\PhpString::basename($orig)) . ';'
+ );
+ }
+
+ //use x-sendfile header to pass the delivery to compatible webservers
+ http_sendfile($file);
+
+ // send file contents
+ $fp = @fopen($file, "rb");
+ if($fp) {
+ http_rangeRequest($fp, filesize($file), $mime);
+ } else {
+ http_status(500);
+ print "Could not read $file - bad permissions?";
+ }
+}
+
+/**
+ * Try an rfc2231 compatible encoding. This ensures correct
+ * interpretation of filenames outside of the ASCII set.
+ * This seems to be needed for file names with e.g. umlauts that
+ * would otherwise decode wrongly in IE.
+ *
+ * There is no additional checking, just the encoding and setting the key=value for usage in headers
+ *
+ * @author Gerry Weissbach <gerry.w@gammaproduction.de>
+ * @param string $name name of the field to be set in the header() call
+ * @param string $value value of the field to be set in the header() call
+ * @param string $charset used charset for the encoding of value
+ * @param string $lang language used.
+ * @return string in the format " name=value" for values WITHOUT special characters
+ * @return string in the format " name*=charset'lang'value" for values WITH special characters
+ */
+function rfc2231_encode($name, $value, $charset='utf-8', $lang='en') {
+ $internal = preg_replace_callback(
+ '/[\x00-\x20*\'%()<>@,;:\\\\"\/[\]?=\x80-\xFF]/',
+ function ($match) {
+ return rawurlencode($match[0]);
+ },
+ $value
+ );
+ if ( $value != $internal ) {
+ return ' '.$name.'*='.$charset."'".$lang."'".$internal;
+ } else {
+ return ' '.$name.'="'.$value.'"';
+ }
+}
+
+/**
+ * Check for media for preconditions and return correct status code
+ *
+ * READ: MEDIA, MIME, EXT, CACHE
+ * WRITE: MEDIA, FILE, array( STATUS, STATUSMESSAGE )
+ *
+ * @author Gerry Weissbach <gerry.w@gammaproduction.de>
+ *
+ * @param string $media reference to the media id
+ * @param string $file reference to the file variable
+ * @param string $rev
+ * @param int $width
+ * @param int $height
+ * @return array as array(STATUS, STATUSMESSAGE)
+ */
+function checkFileStatus(&$media, &$file, $rev = '', $width=0, $height=0) {
+ global $MIME, $EXT, $CACHE, $INPUT;
+
+ //media to local file
+ if(media_isexternal($media)) {
+ //check token for external image and additional for resized and cached images
+ if(media_get_token($media, $width, $height) !== $INPUT->str('tok')) {
+ return array(412, 'Precondition Failed');
+ }
+ //handle external images
+ if(strncmp($MIME, 'image/', 6) == 0) $file = media_get_from_URL($media, $EXT, $CACHE);
+ if(!$file) {
+ //download failed - redirect to original URL
+ return array(302, $media);
+ }
+ } else {
+ $media = cleanID($media);
+ if(empty($media)) {
+ return array(400, 'Bad request');
+ }
+ // check token for resized images
+ if (($width || $height) && media_get_token($media, $width, $height) !== $INPUT->str('tok')) {
+ return array(412, 'Precondition Failed');
+ }
+
+ //check permissions (namespace only)
+ if(auth_quickaclcheck(getNS($media).':X') < AUTH_READ) {
+ return array(403, 'Forbidden');
+ }
+ $file = mediaFN($media, $rev);
+ }
+
+ //check file existance
+ if(!file_exists($file)) {
+ return array(404, 'Not Found');
+ }
+
+ return array(200, null);
+}
+
+/**
+ * Returns the wanted cachetime in seconds
+ *
+ * Resolves named constants
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $cache
+ * @return int cachetime in seconds
+ */
+function calc_cache($cache) {
+ global $conf;
+
+ if(strtolower($cache) == 'nocache') return 0; //never cache
+ if(strtolower($cache) == 'recache') return $conf['cachetime']; //use standard cache
+ return -1; //cache endless
+}
diff --git a/platform/www/inc/form.php b/platform/www/inc/form.php
new file mode 100644
index 0000000..7a4d737
--- /dev/null
+++ b/platform/www/inc/form.php
@@ -0,0 +1,1105 @@
+<?php
+/**
+ * DokuWiki XHTML Form
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+
+// phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
+
+
+/**
+ * Class for creating simple HTML forms.
+ *
+ * The forms is built from a list of pseudo-tags (arrays with expected keys).
+ * Every pseudo-tag must have the key '_elem' set to the name of the element.
+ * When printed, the form class calls functions named 'form_$type' for each
+ * element it contains.
+ *
+ * Standard practice is for non-attribute keys in a pseudo-element to start
+ * with '_'. Other keys are HTML attributes that will be included in the element
+ * tag. That way, the element output functions can pass the pseudo-element
+ * directly to buildAttributes.
+ *
+ * See the form_make* functions later in this file.
+ *
+ * Please note that even though this class is technically deprecated (use dokuwiki\Form instead),
+ * it is still widely used in the core and the related form events. Until those have been rewritten,
+ * this will continue to be used
+ *
+ * @deprecated 2019-07-14
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+class Doku_Form {
+
+ // Form id attribute
+ public $params = array();
+
+ // Draw a border around form fields.
+ // Adds <fieldset></fieldset> around the elements
+ public $_infieldset = false;
+
+ // Hidden form fields.
+ public $_hidden = array();
+
+ // Array of pseudo-tags
+ public $_content = array();
+
+ /**
+ * Constructor
+ *
+ * Sets parameters and autoadds a security token. The old calling convention
+ * with up to four parameters is deprecated, instead the first parameter
+ * should be an array with parameters.
+ *
+ * @param mixed $params Parameters for the HTML form element; Using the deprecated
+ * calling convention this is the ID attribute of the form
+ * @param bool|string $action (optional, deprecated) submit URL, defaults to current page
+ * @param bool|string $method (optional, deprecated) 'POST' or 'GET', default is POST
+ * @param bool|string $enctype (optional, deprecated) Encoding type of the data
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ public function __construct($params, $action=false, $method=false, $enctype=false) {
+ if(!is_array($params)) {
+ $this->params = array('id' => $params);
+ if ($action !== false) $this->params['action'] = $action;
+ if ($method !== false) $this->params['method'] = strtolower($method);
+ if ($enctype !== false) $this->params['enctype'] = $enctype;
+ } else {
+ $this->params = $params;
+ }
+
+ if (!isset($this->params['method'])) {
+ $this->params['method'] = 'post';
+ } else {
+ $this->params['method'] = strtolower($this->params['method']);
+ }
+
+ if (!isset($this->params['action'])) {
+ $this->params['action'] = '';
+ }
+
+ $this->addHidden('sectok', getSecurityToken());
+ }
+
+ /**
+ * startFieldset
+ *
+ * Add <fieldset></fieldset> tags around fields.
+ * Usually results in a border drawn around the form.
+ *
+ * @param string $legend Label that will be printed with the border.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ public function startFieldset($legend) {
+ if ($this->_infieldset) {
+ $this->addElement(array('_elem'=>'closefieldset'));
+ }
+ $this->addElement(array('_elem'=>'openfieldset', '_legend'=>$legend));
+ $this->_infieldset = true;
+ }
+
+ /**
+ * endFieldset
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ public function endFieldset() {
+ if ($this->_infieldset) {
+ $this->addElement(array('_elem'=>'closefieldset'));
+ }
+ $this->_infieldset = false;
+ }
+
+ /**
+ * addHidden
+ *
+ * Adds a name/value pair as a hidden field.
+ * The value of the field (but not the name) will be passed to
+ * formText() before printing.
+ *
+ * @param string $name Field name.
+ * @param string $value Field value. If null, remove a previously added field.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ public function addHidden($name, $value) {
+ if (is_null($value))
+ unset($this->_hidden[$name]);
+ else
+ $this->_hidden[$name] = $value;
+ }
+
+ /**
+ * addElement
+ *
+ * Appends a content element to the form.
+ * The element can be either a pseudo-tag or string.
+ * If string, it is printed without escaping special chars. *
+ *
+ * @param string|array $elem Pseudo-tag or string to add to the form.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ public function addElement($elem) {
+ $this->_content[] = $elem;
+ }
+
+ /**
+ * insertElement
+ *
+ * Inserts a content element at a position.
+ *
+ * @param string $pos 0-based index where the element will be inserted.
+ * @param string|array $elem Pseudo-tag or string to add to the form.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ public function insertElement($pos, $elem) {
+ array_splice($this->_content, $pos, 0, array($elem));
+ }
+
+ /**
+ * replaceElement
+ *
+ * Replace with NULL to remove an element.
+ *
+ * @param int $pos 0-based index the element will be placed at.
+ * @param string|array $elem Pseudo-tag or string to add to the form.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ public function replaceElement($pos, $elem) {
+ $rep = array();
+ if (!is_null($elem)) $rep[] = $elem;
+ array_splice($this->_content, $pos, 1, $rep);
+ }
+
+ /**
+ * findElementByType
+ *
+ * Gets the position of the first of a type of element.
+ *
+ * @param string $type Element type to look for.
+ * @return int|false position of element if found, otherwise false
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ public function findElementByType($type) {
+ foreach ($this->_content as $pos=>$elem) {
+ if (is_array($elem) && $elem['_elem'] == $type)
+ return $pos;
+ }
+ return false;
+ }
+
+ /**
+ * findElementById
+ *
+ * Gets the position of the element with an ID attribute.
+ *
+ * @param string $id ID of the element to find.
+ * @return int|false position of element if found, otherwise false
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ public function findElementById($id) {
+ foreach ($this->_content as $pos=>$elem) {
+ if (is_array($elem) && isset($elem['id']) && $elem['id'] == $id)
+ return $pos;
+ }
+ return false;
+ }
+
+ /**
+ * findElementByAttribute
+ *
+ * Gets the position of the first element with a matching attribute value.
+ *
+ * @param string $name Attribute name.
+ * @param string $value Attribute value.
+ * @return int|false position of element if found, otherwise false
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ public function findElementByAttribute($name, $value) {
+ foreach ($this->_content as $pos=>$elem) {
+ if (is_array($elem) && isset($elem[$name]) && $elem[$name] == $value)
+ return $pos;
+ }
+ return false;
+ }
+
+ /**
+ * getElementAt
+ *
+ * Returns a reference to the element at a position.
+ * A position out-of-bounds will return either the
+ * first (underflow) or last (overflow) element.
+ *
+ * @param int $pos 0-based index
+ * @return array reference pseudo-element
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+ public function &getElementAt($pos) {
+ if ($pos < 0) $pos = count($this->_content) + $pos;
+ if ($pos < 0) $pos = 0;
+ if ($pos >= count($this->_content)) $pos = count($this->_content) - 1;
+ return $this->_content[$pos];
+ }
+
+ /**
+ * Return the assembled HTML for the form.
+ *
+ * Each element in the form will be passed to a function named
+ * 'form_$type'. The function should return the HTML to be printed.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @return string html of the form
+ */
+ public function getForm() {
+ global $lang;
+ $form = '';
+ $this->params['accept-charset'] = $lang['encoding'];
+ $form .= '<form ' . buildAttributes($this->params,false) . '><div class="no">' . DOKU_LF;
+ if (!empty($this->_hidden)) {
+ foreach ($this->_hidden as $name=>$value)
+ $form .= form_hidden(array('name'=>$name, 'value'=>$value));
+ }
+ foreach ($this->_content as $element) {
+ if (is_array($element)) {
+ $elem_type = $element['_elem'];
+ if (function_exists('form_'.$elem_type)) {
+ $form .= call_user_func('form_'.$elem_type, $element).DOKU_LF;
+ }
+ } else {
+ $form .= $element;
+ }
+ }
+ if ($this->_infieldset) $form .= form_closefieldset().DOKU_LF;
+ $form .= '</div></form>'.DOKU_LF;
+
+ return $form;
+ }
+
+ /**
+ * Print the assembled form
+ *
+ * wraps around getForm()
+ */
+ public function printForm(){
+ echo $this->getForm();
+ }
+
+ /**
+ * Add a radio set
+ *
+ * This function adds a set of radio buttons to the form. If $_POST[$name]
+ * is set, this radio is preselected, else the first radio button.
+ *
+ * @param string $name The HTML field name
+ * @param array $entries An array of entries $value => $caption
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+
+ public function addRadioSet($name, $entries) {
+ global $INPUT;
+ $value = (array_key_exists($INPUT->post->str($name), $entries)) ?
+ $INPUT->str($name) : key($entries);
+ foreach($entries as $val => $cap) {
+ $data = ($value === $val) ? array('checked' => 'checked') : array();
+ $this->addElement(form_makeRadioField($name, $val, $cap, '', '', $data));
+ }
+ }
+
+}
+
+/**
+ * form_makeTag
+ *
+ * Create a form element for a non-specific empty tag.
+ *
+ * @param string $tag Tag name.
+ * @param array $attrs Optional attributes.
+ * @return array pseudo-tag
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+function form_makeTag($tag, $attrs=array()) {
+ $elem = array('_elem'=>'tag', '_tag'=>$tag);
+ return array_merge($elem, $attrs);
+}
+
+/**
+ * form_makeOpenTag
+ *
+ * Create a form element for a non-specific opening tag.
+ * Remember to put a matching close tag after this as well.
+ *
+ * @param string $tag Tag name.
+ * @param array $attrs Optional attributes.
+ * @return array pseudo-tag
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+function form_makeOpenTag($tag, $attrs=array()) {
+ $elem = array('_elem'=>'opentag', '_tag'=>$tag);
+ return array_merge($elem, $attrs);
+}
+
+/**
+ * form_makeCloseTag
+ *
+ * Create a form element for a non-specific closing tag.
+ * Careless use of this will result in invalid XHTML.
+ *
+ * @param string $tag Tag name.
+ * @return array pseudo-tag
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+function form_makeCloseTag($tag) {
+ return array('_elem'=>'closetag', '_tag'=>$tag);
+}
+
+/**
+ * form_makeWikiText
+ *
+ * Create a form element for a textarea containing wiki text.
+ * Only one wikitext element is allowed on a page. It will have
+ * a name of 'wikitext' and id 'wiki__text'. The text will
+ * be passed to formText() before printing.
+ *
+ * @param string $text Text to fill the field with.
+ * @param array $attrs Optional attributes.
+ * @return array pseudo-tag
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+function form_makeWikiText($text, $attrs=array()) {
+ $elem = array('_elem'=>'wikitext', '_text'=>$text,
+ 'class'=>'edit', 'cols'=>'80', 'rows'=>'10');
+ return array_merge($elem, $attrs);
+}
+
+/**
+ * form_makeButton
+ *
+ * Create a form element for an action button.
+ * A title will automatically be generated using the value and
+ * accesskey attributes, unless you provide one.
+ *
+ * @param string $type Type attribute. 'submit' or 'cancel'
+ * @param string $act Wiki action of the button, will be used as the do= parameter
+ * @param string $value (optional) Displayed label. Uses $act if not provided.
+ * @param array $attrs Optional attributes.
+ * @return array pseudo-tag
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+function form_makeButton($type, $act, $value='', $attrs=array()) {
+ if ($value == '') $value = $act;
+ $elem = array('_elem'=>'button', 'type'=>$type, '_action'=>$act,
+ 'value'=>$value);
+ if (!empty($attrs['accesskey']) && empty($attrs['title'])) {
+ $attrs['title'] = $value . ' ['.strtoupper($attrs['accesskey']).']';
+ }
+ return array_merge($elem, $attrs);
+}
+
+/**
+ * form_makeField
+ *
+ * Create a form element for a labelled input element.
+ * The label text will be printed before the input.
+ *
+ * @param string $type Type attribute of input.
+ * @param string $name Name attribute of the input.
+ * @param string $value (optional) Default value.
+ * @param string $class Class attribute of the label. If this is 'block',
+ * then a line break will be added after the field.
+ * @param string $label Label that will be printed before the input.
+ * @param string $id ID attribute of the input. If set, the label will
+ * reference it with a 'for' attribute.
+ * @param array $attrs Optional attributes.
+ * @return array pseudo-tag
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+function form_makeField($type, $name, $value='', $label=null, $id='', $class='', $attrs=array()) {
+ if (is_null($label)) $label = $name;
+ $elem = array('_elem'=>'field', '_text'=>$label, '_class'=>$class,
+ 'type'=>$type, 'id'=>$id, 'name'=>$name, 'value'=>$value);
+ return array_merge($elem, $attrs);
+}
+
+/**
+ * form_makeFieldRight
+ *
+ * Create a form element for a labelled input element.
+ * The label text will be printed after the input.
+ *
+ * @see form_makeField
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param string $type
+ * @param string $name
+ * @param string $value
+ * @param null|string $label
+ * @param string $id
+ * @param string $class
+ * @param array $attrs
+ *
+ * @return array
+ */
+function form_makeFieldRight($type, $name, $value='', $label=null, $id='', $class='', $attrs=array()) {
+ if (is_null($label)) $label = $name;
+ $elem = array('_elem'=>'fieldright', '_text'=>$label, '_class'=>$class,
+ 'type'=>$type, 'id'=>$id, 'name'=>$name, 'value'=>$value);
+ return array_merge($elem, $attrs);
+}
+
+/**
+ * form_makeTextField
+ *
+ * Create a form element for a text input element with label.
+ *
+ * @see form_makeField
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param string $name
+ * @param string $value
+ * @param null|string $label
+ * @param string $id
+ * @param string $class
+ * @param array $attrs
+ *
+ * @return array
+ */
+function form_makeTextField($name, $value='', $label=null, $id='', $class='', $attrs=array()) {
+ if (is_null($label)) $label = $name;
+ $elem = array('_elem'=>'textfield', '_text'=>$label, '_class'=>$class,
+ 'id'=>$id, 'name'=>$name, 'value'=>$value, 'class'=>'edit');
+ return array_merge($elem, $attrs);
+}
+
+/**
+ * form_makePasswordField
+ *
+ * Create a form element for a password input element with label.
+ * Password elements have no default value, for obvious reasons.
+ *
+ * @see form_makeField
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param string $name
+ * @param null|string $label
+ * @param string $id
+ * @param string $class
+ * @param array $attrs
+ *
+ * @return array
+ */
+function form_makePasswordField($name, $label=null, $id='', $class='', $attrs=array()) {
+ if (is_null($label)) $label = $name;
+ $elem = array('_elem'=>'passwordfield', '_text'=>$label, '_class'=>$class,
+ 'id'=>$id, 'name'=>$name, 'class'=>'edit');
+ return array_merge($elem, $attrs);
+}
+
+/**
+ * form_makeFileField
+ *
+ * Create a form element for a file input element with label
+ *
+ * @see form_makeField
+ * @author Michael Klier <chi@chimeric.de>
+ *
+ * @param string $name
+ * @param null|string $label
+ * @param string $id
+ * @param string $class
+ * @param array $attrs
+ *
+ * @return array
+ */
+function form_makeFileField($name, $label=null, $id='', $class='', $attrs=array()) {
+ if (is_null($label)) $label = $name;
+ $elem = array('_elem'=>'filefield', '_text'=>$label, '_class'=>$class,
+ 'id'=>$id, 'name'=>$name, 'class'=>'edit');
+ return array_merge($elem, $attrs);
+}
+
+/**
+ * form_makeCheckboxField
+ *
+ * Create a form element for a checkbox input element with label.
+ * If $value is an array, a hidden field with the same name and the value
+ * $value[1] is constructed as well.
+ *
+ * @see form_makeFieldRight
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param string $name
+ * @param string $value
+ * @param null|string $label
+ * @param string $id
+ * @param string $class
+ * @param array $attrs
+ *
+ * @return array
+ */
+function form_makeCheckboxField($name, $value='1', $label=null, $id='', $class='', $attrs=array()) {
+ if (is_null($label)) $label = $name;
+ if (is_null($value) || $value=='') $value='0';
+ $elem = array('_elem'=>'checkboxfield', '_text'=>$label, '_class'=>$class,
+ 'id'=>$id, 'name'=>$name, 'value'=>$value);
+ return array_merge($elem, $attrs);
+}
+
+/**
+ * form_makeRadioField
+ *
+ * Create a form element for a radio button input element with label.
+ *
+ * @see form_makeFieldRight
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param string $name
+ * @param string $value
+ * @param null|string $label
+ * @param string $id
+ * @param string $class
+ * @param array $attrs
+ *
+ * @return array
+ */
+function form_makeRadioField($name, $value='1', $label=null, $id='', $class='', $attrs=array()) {
+ if (is_null($label)) $label = $name;
+ if (is_null($value) || $value=='') $value='0';
+ $elem = array('_elem'=>'radiofield', '_text'=>$label, '_class'=>$class,
+ 'id'=>$id, 'name'=>$name, 'value'=>$value);
+ return array_merge($elem, $attrs);
+}
+
+/**
+ * form_makeMenuField
+ *
+ * Create a form element for a drop-down menu with label.
+ * The list of values can be strings, arrays of (value,text),
+ * or an associative array with the values as keys and labels as values.
+ * An item is selected by supplying its value or integer index.
+ * If the list of values is an associative array, the selected item must be
+ * a string.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param string $name Name attribute of the input.
+ * @param string[]|array[] $values The list of values can be strings, arrays of (value,text),
+ * or an associative array with the values as keys and labels as values.
+ * @param string|int $selected default selected value, string or index number
+ * @param string $class Class attribute of the label. If this is 'block',
+ * then a line break will be added after the field.
+ * @param string $label Label that will be printed before the input.
+ * @param string $id ID attribute of the input. If set, the label will
+ * reference it with a 'for' attribute.
+ * @param array $attrs Optional attributes.
+ * @return array pseudo-tag
+ */
+function form_makeMenuField($name, $values, $selected='', $label=null, $id='', $class='', $attrs=array()) {
+ if (is_null($label)) $label = $name;
+ $options = array();
+ reset($values);
+ // FIXME: php doesn't know the difference between a string and an integer
+ if (is_string(key($values))) {
+ foreach ($values as $val=>$text) {
+ $options[] = array($val,$text, (!is_null($selected) && $val==$selected));
+ }
+ } else {
+ if (is_integer($selected)) $selected = $values[$selected];
+ foreach ($values as $val) {
+ if (is_array($val))
+ @list($val,$text) = $val;
+ else
+ $text = null;
+ $options[] = array($val,$text,$val===$selected);
+ }
+ }
+ $elem = array('_elem'=>'menufield', '_options'=>$options, '_text'=>$label, '_class'=>$class,
+ 'id'=>$id, 'name'=>$name);
+ return array_merge($elem, $attrs);
+}
+
+/**
+ * form_makeListboxField
+ *
+ * Create a form element for a list box with label.
+ * The list of values can be strings, arrays of (value,text),
+ * or an associative array with the values as keys and labels as values.
+ * Items are selected by supplying its value or an array of values.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param string $name Name attribute of the input.
+ * @param string[]|array[] $values The list of values can be strings, arrays of (value,text),
+ * or an associative array with the values as keys and labels as values.
+ * @param array|string $selected value or array of values of the items that need to be selected
+ * @param string $class Class attribute of the label. If this is 'block',
+ * then a line break will be added after the field.
+ * @param string $label Label that will be printed before the input.
+ * @param string $id ID attribute of the input. If set, the label will
+ * reference it with a 'for' attribute.
+ * @param array $attrs Optional attributes.
+ * @return array pseudo-tag
+ */
+function form_makeListboxField($name, $values, $selected='', $label=null, $id='', $class='', $attrs=array()) {
+ if (is_null($label)) $label = $name;
+ $options = array();
+ reset($values);
+ if (is_null($selected) || $selected == '') {
+ $selected = array();
+ } elseif (!is_array($selected)) {
+ $selected = array($selected);
+ }
+ // FIXME: php doesn't know the difference between a string and an integer
+ if (is_string(key($values))) {
+ foreach ($values as $val=>$text) {
+ $options[] = array($val,$text,in_array($val,$selected));
+ }
+ } else {
+ foreach ($values as $val) {
+ $disabled = false;
+ if (is_array($val)) {
+ @list($val,$text,$disabled) = $val;
+ } else {
+ $text = null;
+ }
+ $options[] = array($val,$text,in_array($val,$selected),$disabled);
+ }
+ }
+ $elem = array('_elem'=>'listboxfield', '_options'=>$options, '_text'=>$label, '_class'=>$class,
+ 'id'=>$id, 'name'=>$name);
+ return array_merge($elem, $attrs);
+}
+
+/**
+ * form_tag
+ *
+ * Print the HTML for a generic empty tag.
+ * Requires '_tag' key with name of the tag.
+ * Attributes are passed to buildAttributes()
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param array $attrs attributes
+ * @return string html of tag
+ */
+function form_tag($attrs) {
+ return '<'.$attrs['_tag'].' '.buildAttributes($attrs,true).'/>';
+}
+
+/**
+ * form_opentag
+ *
+ * Print the HTML for a generic opening tag.
+ * Requires '_tag' key with name of the tag.
+ * Attributes are passed to buildAttributes()
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param array $attrs attributes
+ * @return string html of tag
+ */
+function form_opentag($attrs) {
+ return '<'.$attrs['_tag'].' '.buildAttributes($attrs,true).'>';
+}
+
+/**
+ * form_closetag
+ *
+ * Print the HTML for a generic closing tag.
+ * Requires '_tag' key with name of the tag.
+ * There are no attributes.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param array $attrs attributes
+ * @return string html of tag
+ */
+function form_closetag($attrs) {
+ return '</'.$attrs['_tag'].'>';
+}
+
+/**
+ * form_openfieldset
+ *
+ * Print the HTML for an opening fieldset tag.
+ * Uses the '_legend' key.
+ * Attributes are passed to buildAttributes()
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param array $attrs attributes
+ * @return string html
+ */
+function form_openfieldset($attrs) {
+ $s = '<fieldset '.buildAttributes($attrs,true).'>';
+ if (!is_null($attrs['_legend'])) $s .= '<legend>'.$attrs['_legend'].'</legend>';
+ return $s;
+}
+
+/**
+ * form_closefieldset
+ *
+ * Print the HTML for a closing fieldset tag.
+ * There are no attributes.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @return string html
+ */
+function form_closefieldset() {
+ return '</fieldset>';
+}
+
+/**
+ * form_hidden
+ *
+ * Print the HTML for a hidden input element.
+ * Uses only 'name' and 'value' attributes.
+ * Value is passed to formText()
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param array $attrs attributes
+ * @return string html
+ */
+function form_hidden($attrs) {
+ return '<input type="hidden" name="'.$attrs['name'].'" value="'.formText($attrs['value']).'" />';
+}
+
+/**
+ * form_wikitext
+ *
+ * Print the HTML for the wiki textarea.
+ * Requires '_text' with default text of the field.
+ * Text will be passed to formText(), attributes to buildAttributes()
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param array $attrs attributes
+ * @return string html
+ */
+function form_wikitext($attrs) {
+ // mandatory attributes
+ unset($attrs['name']);
+ unset($attrs['id']);
+ return '<textarea name="wikitext" id="wiki__text" dir="auto" '
+ .buildAttributes($attrs,true).'>'.DOKU_LF
+ .formText($attrs['_text'])
+ .'</textarea>';
+}
+
+/**
+ * form_button
+ *
+ * Print the HTML for a form button.
+ * If '_action' is set, the button name will be "do[_action]".
+ * Other attributes are passed to buildAttributes()
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param array $attrs attributes
+ * @return string html
+ */
+function form_button($attrs) {
+ $p = (!empty($attrs['_action'])) ? 'name="do['.$attrs['_action'].']" ' : '';
+ $value = $attrs['value'];
+ unset($attrs['value']);
+ return '<button '.$p.buildAttributes($attrs,true).'>'.$value.'</button>';
+}
+
+/**
+ * form_field
+ *
+ * Print the HTML for a form input field.
+ * _class : class attribute used on the label tag
+ * _text : Text to display before the input. Not escaped.
+ * Other attributes are passed to buildAttributes() for the input tag.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param array $attrs attributes
+ * @return string html
+ */
+function form_field($attrs) {
+ $s = '<label';
+ if ($attrs['_class']) $s .= ' class="'.$attrs['_class'].'"';
+ if (!empty($attrs['id'])) $s .= ' for="'.$attrs['id'].'"';
+ $s .= '><span>'.$attrs['_text'].'</span>';
+ $s .= ' <input '.buildAttributes($attrs,true).' /></label>';
+ if (preg_match('/(^| )block($| )/', $attrs['_class']))
+ $s .= '<br />';
+ return $s;
+}
+
+/**
+ * form_fieldright
+ *
+ * Print the HTML for a form input field. (right-aligned)
+ * _class : class attribute used on the label tag
+ * _text : Text to display after the input. Not escaped.
+ * Other attributes are passed to buildAttributes() for the input tag.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param array $attrs attributes
+ * @return string html
+ */
+function form_fieldright($attrs) {
+ $s = '<label';
+ if ($attrs['_class']) $s .= ' class="'.$attrs['_class'].'"';
+ if (!empty($attrs['id'])) $s .= ' for="'.$attrs['id'].'"';
+ $s .= '><input '.buildAttributes($attrs,true).' />';
+ $s .= ' <span>'.$attrs['_text'].'</span></label>';
+ if (preg_match('/(^| )block($| )/', $attrs['_class']))
+ $s .= '<br />';
+ return $s;
+}
+
+/**
+ * form_textfield
+ *
+ * Print the HTML for a text input field.
+ * _class : class attribute used on the label tag
+ * _text : Text to display before the input. Not escaped.
+ * Other attributes are passed to buildAttributes() for the input tag.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param array $attrs attributes
+ * @return string html
+ */
+function form_textfield($attrs) {
+ // mandatory attributes
+ unset($attrs['type']);
+ $s = '<label';
+ if ($attrs['_class']) $s .= ' class="'.$attrs['_class'].'"';
+ if (!empty($attrs['id'])) $s .= ' for="'.$attrs['id'].'"';
+ $s .= '><span>'.$attrs['_text'].'</span> ';
+ $s .= '<input type="text" '.buildAttributes($attrs,true).' /></label>';
+ if (preg_match('/(^| )block($| )/', $attrs['_class']))
+ $s .= '<br />';
+ return $s;
+}
+
+/**
+ * form_passwordfield
+ *
+ * Print the HTML for a password input field.
+ * _class : class attribute used on the label tag
+ * _text : Text to display before the input. Not escaped.
+ * Other attributes are passed to buildAttributes() for the input tag.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param array $attrs attributes
+ * @return string html
+ */
+function form_passwordfield($attrs) {
+ // mandatory attributes
+ unset($attrs['type']);
+ $s = '<label';
+ if ($attrs['_class']) $s .= ' class="'.$attrs['_class'].'"';
+ if (!empty($attrs['id'])) $s .= ' for="'.$attrs['id'].'"';
+ $s .= '><span>'.$attrs['_text'].'</span> ';
+ $s .= '<input type="password" '.buildAttributes($attrs,true).' /></label>';
+ if (preg_match('/(^| )block($| )/', $attrs['_class']))
+ $s .= '<br />';
+ return $s;
+}
+
+/**
+ * form_filefield
+ *
+ * Print the HTML for a file input field.
+ * _class : class attribute used on the label tag
+ * _text : Text to display before the input. Not escaped
+ * _maxlength : Allowed size in byte
+ * _accept : Accepted mime-type
+ * Other attributes are passed to buildAttributes() for the input tag
+ *
+ * @author Michael Klier <chi@chimeric.de>
+ *
+ * @param array $attrs attributes
+ * @return string html
+ */
+function form_filefield($attrs) {
+ $s = '<label';
+ if ($attrs['_class']) $s .= ' class="'.$attrs['_class'].'"';
+ if (!empty($attrs['id'])) $s .= ' for="'.$attrs['id'].'"';
+ $s .= '><span>'.$attrs['_text'].'</span> ';
+ $s .= '<input type="file" '.buildAttributes($attrs,true);
+ if (!empty($attrs['_maxlength'])) $s .= ' maxlength="'.$attrs['_maxlength'].'"';
+ if (!empty($attrs['_accept'])) $s .= ' accept="'.$attrs['_accept'].'"';
+ $s .= ' /></label>';
+ if (preg_match('/(^| )block($| )/', $attrs['_class']))
+ $s .= '<br />';
+ return $s;
+}
+
+/**
+ * form_checkboxfield
+ *
+ * Print the HTML for a checkbox input field.
+ * _class : class attribute used on the label tag
+ * _text : Text to display after the input. Not escaped.
+ * Other attributes are passed to buildAttributes() for the input tag.
+ * If value is an array, a hidden field with the same name and the value
+ * $attrs['value'][1] is constructed as well.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param array $attrs attributes
+ * @return string html
+ */
+function form_checkboxfield($attrs) {
+ // mandatory attributes
+ unset($attrs['type']);
+ $s = '<label';
+ if ($attrs['_class']) $s .= ' class="'.$attrs['_class'].'"';
+ if (!empty($attrs['id'])) $s .= ' for="'.$attrs['id'].'"';
+ $s .= '>';
+ if (is_array($attrs['value'])) {
+ echo '<input type="hidden" name="' . hsc($attrs['name']) .'"'
+ . ' value="' . hsc($attrs['value'][1]) . '" />';
+ $attrs['value'] = $attrs['value'][0];
+ }
+ $s .= '<input type="checkbox" '.buildAttributes($attrs,true).' />';
+ $s .= ' <span>'.$attrs['_text'].'</span></label>';
+ if (preg_match('/(^| )block($| )/', $attrs['_class']))
+ $s .= '<br />';
+ return $s;
+}
+
+/**
+ * form_radiofield
+ *
+ * Print the HTML for a radio button input field.
+ * _class : class attribute used on the label tag
+ * _text : Text to display after the input. Not escaped.
+ * Other attributes are passed to buildAttributes() for the input tag.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param array $attrs attributes
+ * @return string html
+ */
+function form_radiofield($attrs) {
+ // mandatory attributes
+ unset($attrs['type']);
+ $s = '<label';
+ if ($attrs['_class']) $s .= ' class="'.$attrs['_class'].'"';
+ if (!empty($attrs['id'])) $s .= ' for="'.$attrs['id'].'"';
+ $s .= '><input type="radio" '.buildAttributes($attrs,true).' />';
+ $s .= ' <span>'.$attrs['_text'].'</span></label>';
+ if (preg_match('/(^| )block($| )/', $attrs['_class']))
+ $s .= '<br />';
+ return $s;
+}
+
+/**
+ * form_menufield
+ *
+ * Print the HTML for a drop-down menu.
+ * _options : Array of (value,text,selected) for the menu.
+ * Text can be omitted. Text and value are passed to formText()
+ * Only one item can be selected.
+ * _class : class attribute used on the label tag
+ * _text : Text to display before the menu. Not escaped.
+ * Other attributes are passed to buildAttributes() for the input tag.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param array $attrs attributes
+ * @return string html
+ */
+function form_menufield($attrs) {
+ $attrs['size'] = '1';
+ $s = '<label';
+ if ($attrs['_class']) $s .= ' class="'.$attrs['_class'].'"';
+ if (!empty($attrs['id'])) $s .= ' for="'.$attrs['id'].'"';
+ $s .= '><span>'.$attrs['_text'].'</span>';
+ $s .= ' <select '.buildAttributes($attrs,true).'>'.DOKU_LF;
+ if (!empty($attrs['_options'])) {
+ $selected = false;
+
+ $cnt = count($attrs['_options']);
+ for($n=0; $n < $cnt; $n++){
+ @list($value,$text,$select) = $attrs['_options'][$n];
+ $p = '';
+ if (!is_null($text))
+ $p .= ' value="'.formText($value).'"';
+ else
+ $text = $value;
+ if (!empty($select) && !$selected) {
+ $p .= ' selected="selected"';
+ $selected = true;
+ }
+ $s .= '<option'.$p.'>'.formText($text).'</option>';
+ }
+ } else {
+ $s .= '<option></option>';
+ }
+ $s .= DOKU_LF.'</select></label>';
+ if (preg_match('/(^| )block($| )/', $attrs['_class']))
+ $s .= '<br />';
+ return $s;
+}
+
+/**
+ * form_listboxfield
+ *
+ * Print the HTML for a list box.
+ * _options : Array of (value,text,selected) for the list.
+ * Text can be omitted. Text and value are passed to formText()
+ * _class : class attribute used on the label tag
+ * _text : Text to display before the menu. Not escaped.
+ * Other attributes are passed to buildAttributes() for the input tag.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param array $attrs attributes
+ * @return string html
+ */
+function form_listboxfield($attrs) {
+ $s = '<label';
+ if ($attrs['_class']) $s .= ' class="'.$attrs['_class'].'"';
+ if (!empty($attrs['id'])) $s .= ' for="'.$attrs['id'].'"';
+ $s .= '><span>'.$attrs['_text'].'</span> ';
+ $s .= '<select '.buildAttributes($attrs,true).'>'.DOKU_LF;
+ if (!empty($attrs['_options'])) {
+ foreach ($attrs['_options'] as $opt) {
+ @list($value,$text,$select,$disabled) = $opt;
+ $p = '';
+ if(is_null($text)) $text = $value;
+ $p .= ' value="'.formText($value).'"';
+ if (!empty($select)) $p .= ' selected="selected"';
+ if ($disabled) $p .= ' disabled="disabled"';
+ $s .= '<option'.$p.'>'.formText($text).'</option>';
+ }
+ } else {
+ $s .= '<option></option>';
+ }
+ $s .= DOKU_LF.'</select></label>';
+ if (preg_match('/(^| )block($| )/', $attrs['_class']))
+ $s .= '<br />';
+ return $s;
+}
diff --git a/platform/www/inc/fulltext.php b/platform/www/inc/fulltext.php
new file mode 100644
index 0000000..670f048
--- /dev/null
+++ b/platform/www/inc/fulltext.php
@@ -0,0 +1,933 @@
+<?php
+/**
+ * DokuWiki fulltextsearch functions using the index
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+use dokuwiki\Extension\Event;
+
+/**
+ * create snippets for the first few results only
+ */
+if(!defined('FT_SNIPPET_NUMBER')) define('FT_SNIPPET_NUMBER',15);
+
+/**
+ * The fulltext search
+ *
+ * Returns a list of matching documents for the given query
+ *
+ * refactored into ft_pageSearch(), _ft_pageSearch() and trigger_event()
+ *
+ * @param string $query
+ * @param array $highlight
+ * @param string $sort
+ * @param int|string $after only show results with mtime after this date, accepts timestap or strtotime arguments
+ * @param int|string $before only show results with mtime before this date, accepts timestap or strtotime arguments
+ *
+ * @return array
+ */
+function ft_pageSearch($query,&$highlight, $sort = null, $after = null, $before = null){
+
+ if ($sort === null) {
+ $sort = 'hits';
+ }
+ $data = [
+ 'query' => $query,
+ 'sort' => $sort,
+ 'after' => $after,
+ 'before' => $before
+ ];
+ $data['highlight'] =& $highlight;
+
+ return Event::createAndTrigger('SEARCH_QUERY_FULLPAGE', $data, '_ft_pageSearch');
+}
+
+/**
+ * Returns a list of matching documents for the given query
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Kazutaka Miyasaka <kazmiya@gmail.com>
+ *
+ * @param array $data event data
+ * @return array matching documents
+ */
+function _ft_pageSearch(&$data) {
+ $Indexer = idx_get_indexer();
+
+ // parse the given query
+ $q = ft_queryParser($Indexer, $data['query']);
+ $data['highlight'] = $q['highlight'];
+
+ if (empty($q['parsed_ary'])) return array();
+
+ // lookup all words found in the query
+ $lookup = $Indexer->lookup($q['words']);
+
+ // get all pages in this dokuwiki site (!: includes nonexistent pages)
+ $pages_all = array();
+ foreach ($Indexer->getPages() as $id) {
+ $pages_all[$id] = 0; // base: 0 hit
+ }
+
+ // process the query
+ $stack = array();
+ foreach ($q['parsed_ary'] as $token) {
+ switch (substr($token, 0, 3)) {
+ case 'W+:':
+ case 'W-:':
+ case 'W_:': // word
+ $word = substr($token, 3);
+ $stack[] = (array) $lookup[$word];
+ break;
+ case 'P+:':
+ case 'P-:': // phrase
+ $phrase = substr($token, 3);
+ // since phrases are always parsed as ((W1)(W2)...(P)),
+ // the end($stack) always points the pages that contain
+ // all words in this phrase
+ $pages = end($stack);
+ $pages_matched = array();
+ foreach(array_keys($pages) as $id){
+ $evdata = array(
+ 'id' => $id,
+ 'phrase' => $phrase,
+ 'text' => rawWiki($id)
+ );
+ $evt = new Event('FULLTEXT_PHRASE_MATCH',$evdata);
+ if ($evt->advise_before() && $evt->result !== true) {
+ $text = \dokuwiki\Utf8\PhpString::strtolower($evdata['text']);
+ if (strpos($text, $phrase) !== false) {
+ $evt->result = true;
+ }
+ }
+ $evt->advise_after();
+ if ($evt->result === true) {
+ $pages_matched[$id] = 0; // phrase: always 0 hit
+ }
+ }
+ $stack[] = $pages_matched;
+ break;
+ case 'N+:':
+ case 'N-:': // namespace
+ $ns = cleanID(substr($token, 3)) . ':';
+ $pages_matched = array();
+ foreach (array_keys($pages_all) as $id) {
+ if (strpos($id, $ns) === 0) {
+ $pages_matched[$id] = 0; // namespace: always 0 hit
+ }
+ }
+ $stack[] = $pages_matched;
+ break;
+ case 'AND': // and operation
+ list($pages1, $pages2) = array_splice($stack, -2);
+ $stack[] = ft_resultCombine(array($pages1, $pages2));
+ break;
+ case 'OR': // or operation
+ list($pages1, $pages2) = array_splice($stack, -2);
+ $stack[] = ft_resultUnite(array($pages1, $pages2));
+ break;
+ case 'NOT': // not operation (unary)
+ $pages = array_pop($stack);
+ $stack[] = ft_resultComplement(array($pages_all, $pages));
+ break;
+ }
+ }
+ $docs = array_pop($stack);
+
+ if (empty($docs)) return array();
+
+ // check: settings, acls, existence
+ foreach (array_keys($docs) as $id) {
+ if (isHiddenPage($id) || auth_quickaclcheck($id) < AUTH_READ || !page_exists($id, '', false)) {
+ unset($docs[$id]);
+ }
+ }
+
+ $docs = _ft_filterResultsByTime($docs, $data['after'], $data['before']);
+
+ if ($data['sort'] === 'mtime') {
+ uksort($docs, 'ft_pagemtimesorter');
+ } else {
+ // sort docs by count
+ arsort($docs);
+ }
+
+ return $docs;
+}
+
+/**
+ * Returns the backlinks for a given page
+ *
+ * Uses the metadata index.
+ *
+ * @param string $id The id for which links shall be returned
+ * @param bool $ignore_perms Ignore the fact that pages are hidden or read-protected
+ * @return array The pages that contain links to the given page
+ */
+function ft_backlinks($id, $ignore_perms = false){
+ $result = idx_get_indexer()->lookupKey('relation_references', $id);
+
+ if(!count($result)) return $result;
+
+ // check ACL permissions
+ foreach(array_keys($result) as $idx){
+ if(($ignore_perms !== true && (
+ isHiddenPage($result[$idx]) || auth_quickaclcheck($result[$idx]) < AUTH_READ
+ )) || !page_exists($result[$idx], '', false)){
+ unset($result[$idx]);
+ }
+ }
+
+ sort($result);
+ return $result;
+}
+
+/**
+ * Returns the pages that use a given media file
+ *
+ * Uses the relation media metadata property and the metadata index.
+ *
+ * Note that before 2013-07-31 the second parameter was the maximum number of results and
+ * permissions were ignored. That's why the parameter is now checked to be explicitely set
+ * to true (with type bool) in order to be compatible with older uses of the function.
+ *
+ * @param string $id The media id to look for
+ * @param bool $ignore_perms Ignore hidden pages and acls (optional, default: false)
+ * @return array A list of pages that use the given media file
+ */
+function ft_mediause($id, $ignore_perms = false){
+ $result = idx_get_indexer()->lookupKey('relation_media', $id);
+
+ if(!count($result)) return $result;
+
+ // check ACL permissions
+ foreach(array_keys($result) as $idx){
+ if(($ignore_perms !== true && (
+ isHiddenPage($result[$idx]) || auth_quickaclcheck($result[$idx]) < AUTH_READ
+ )) || !page_exists($result[$idx], '', false)){
+ unset($result[$idx]);
+ }
+ }
+
+ sort($result);
+ return $result;
+}
+
+
+/**
+ * Quicksearch for pagenames
+ *
+ * By default it only matches the pagename and ignores the
+ * namespace. This can be changed with the second parameter.
+ * The third parameter allows to search in titles as well.
+ *
+ * The function always returns titles as well
+ *
+ * @triggers SEARCH_QUERY_PAGELOOKUP
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Adrian Lang <lang@cosmocode.de>
+ *
+ * @param string $id page id
+ * @param bool $in_ns match against namespace as well?
+ * @param bool $in_title search in title?
+ * @param int|string $after only show results with mtime after this date, accepts timestap or strtotime arguments
+ * @param int|string $before only show results with mtime before this date, accepts timestap or strtotime arguments
+ *
+ * @return string[]
+ */
+function ft_pageLookup($id, $in_ns=false, $in_title=false, $after = null, $before = null){
+ $data = [
+ 'id' => $id,
+ 'in_ns' => $in_ns,
+ 'in_title' => $in_title,
+ 'after' => $after,
+ 'before' => $before
+ ];
+ $data['has_titles'] = true; // for plugin backward compatibility check
+ return Event::createAndTrigger('SEARCH_QUERY_PAGELOOKUP', $data, '_ft_pageLookup');
+}
+
+/**
+ * Returns list of pages as array(pageid => First Heading)
+ *
+ * @param array &$data event data
+ * @return string[]
+ */
+function _ft_pageLookup(&$data){
+ // split out original parameters
+ $id = $data['id'];
+ $Indexer = idx_get_indexer();
+ $parsedQuery = ft_queryParser($Indexer, $id);
+ if (count($parsedQuery['ns']) > 0) {
+ $ns = cleanID($parsedQuery['ns'][0]) . ':';
+ $id = implode(' ', $parsedQuery['highlight']);
+ }
+
+ $in_ns = $data['in_ns'];
+ $in_title = $data['in_title'];
+ $cleaned = cleanID($id);
+
+ $Indexer = idx_get_indexer();
+ $page_idx = $Indexer->getPages();
+
+ $pages = array();
+ if ($id !== '' && $cleaned !== '') {
+ foreach ($page_idx as $p_id) {
+ if ((strpos($in_ns ? $p_id : noNSorNS($p_id), $cleaned) !== false)) {
+ if (!isset($pages[$p_id]))
+ $pages[$p_id] = p_get_first_heading($p_id, METADATA_DONT_RENDER);
+ }
+ }
+ if ($in_title) {
+ foreach ($Indexer->lookupKey('title', $id, '_ft_pageLookupTitleCompare') as $p_id) {
+ if (!isset($pages[$p_id]))
+ $pages[$p_id] = p_get_first_heading($p_id, METADATA_DONT_RENDER);
+ }
+ }
+ }
+
+ if (isset($ns)) {
+ foreach (array_keys($pages) as $p_id) {
+ if (strpos($p_id, $ns) !== 0) {
+ unset($pages[$p_id]);
+ }
+ }
+ }
+
+ // discard hidden pages
+ // discard nonexistent pages
+ // check ACL permissions
+ foreach(array_keys($pages) as $idx){
+ if(!isVisiblePage($idx) || !page_exists($idx) ||
+ auth_quickaclcheck($idx) < AUTH_READ) {
+ unset($pages[$idx]);
+ }
+ }
+
+ $pages = _ft_filterResultsByTime($pages, $data['after'], $data['before']);
+
+ uksort($pages,'ft_pagesorter');
+ return $pages;
+}
+
+
+/**
+ * @param array $results search results in the form pageid => value
+ * @param int|string $after only returns results with mtime after this date, accepts timestap or strtotime arguments
+ * @param int|string $before only returns results with mtime after this date, accepts timestap or strtotime arguments
+ *
+ * @return array
+ */
+function _ft_filterResultsByTime(array $results, $after, $before) {
+ if ($after || $before) {
+ $after = is_int($after) ? $after : strtotime($after);
+ $before = is_int($before) ? $before : strtotime($before);
+
+ foreach ($results as $id => $value) {
+ $mTime = filemtime(wikiFN($id));
+ if ($after && $after > $mTime) {
+ unset($results[$id]);
+ continue;
+ }
+ if ($before && $before < $mTime) {
+ unset($results[$id]);
+ }
+ }
+ }
+
+ return $results;
+}
+
+/**
+ * Tiny helper function for comparing the searched title with the title
+ * from the search index. This function is a wrapper around stripos with
+ * adapted argument order and return value.
+ *
+ * @param string $search searched title
+ * @param string $title title from index
+ * @return bool
+ */
+function _ft_pageLookupTitleCompare($search, $title) {
+ return stripos($title, $search) !== false;
+}
+
+/**
+ * Sort pages based on their namespace level first, then on their string
+ * values. This makes higher hierarchy pages rank higher than lower hierarchy
+ * pages.
+ *
+ * @param string $a
+ * @param string $b
+ * @return int Returns < 0 if $a is less than $b; > 0 if $a is greater than $b, and 0 if they are equal.
+ */
+function ft_pagesorter($a, $b){
+ $ac = count(explode(':',$a));
+ $bc = count(explode(':',$b));
+ if($ac < $bc){
+ return -1;
+ }elseif($ac > $bc){
+ return 1;
+ }
+ return strcmp ($a,$b);
+}
+
+/**
+ * Sort pages by their mtime, from newest to oldest
+ *
+ * @param string $a
+ * @param string $b
+ *
+ * @return int Returns < 0 if $a is newer than $b, > 0 if $b is newer than $a and 0 if they are of the same age
+ */
+function ft_pagemtimesorter($a, $b) {
+ $mtimeA = filemtime(wikiFN($a));
+ $mtimeB = filemtime(wikiFN($b));
+ return $mtimeB - $mtimeA;
+}
+
+/**
+ * Creates a snippet extract
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @triggers FULLTEXT_SNIPPET_CREATE
+ *
+ * @param string $id page id
+ * @param array $highlight
+ * @return mixed
+ */
+function ft_snippet($id,$highlight){
+ $text = rawWiki($id);
+ $text = str_replace("\xC2\xAD",'',$text); // remove soft-hyphens
+ $evdata = array(
+ 'id' => $id,
+ 'text' => &$text,
+ 'highlight' => &$highlight,
+ 'snippet' => '',
+ );
+
+ $evt = new Event('FULLTEXT_SNIPPET_CREATE',$evdata);
+ if ($evt->advise_before()) {
+ $match = array();
+ $snippets = array();
+ $utf8_offset = $offset = $end = 0;
+ $len = \dokuwiki\Utf8\PhpString::strlen($text);
+
+ // build a regexp from the phrases to highlight
+ $re1 = '(' .
+ join(
+ '|',
+ array_map(
+ 'ft_snippet_re_preprocess',
+ array_map(
+ 'preg_quote_cb',
+ array_filter((array) $highlight)
+ )
+ )
+ ) .
+ ')';
+ $re2 = "$re1.{0,75}(?!\\1)$re1";
+ $re3 = "$re1.{0,45}(?!\\1)$re1.{0,45}(?!\\1)(?!\\2)$re1";
+
+ for ($cnt=4; $cnt--;) {
+ if (0) {
+ } else if (preg_match('/'.$re3.'/iu',$text,$match,PREG_OFFSET_CAPTURE,$offset)) {
+ } else if (preg_match('/'.$re2.'/iu',$text,$match,PREG_OFFSET_CAPTURE,$offset)) {
+ } else if (preg_match('/'.$re1.'/iu',$text,$match,PREG_OFFSET_CAPTURE,$offset)) {
+ } else {
+ break;
+ }
+
+ list($str,$idx) = $match[0];
+
+ // convert $idx (a byte offset) into a utf8 character offset
+ $utf8_idx = \dokuwiki\Utf8\PhpString::strlen(substr($text,0,$idx));
+ $utf8_len = \dokuwiki\Utf8\PhpString::strlen($str);
+
+ // establish context, 100 bytes surrounding the match string
+ // first look to see if we can go 100 either side,
+ // then drop to 50 adding any excess if the other side can't go to 50,
+ $pre = min($utf8_idx-$utf8_offset,100);
+ $post = min($len-$utf8_idx-$utf8_len,100);
+
+ if ($pre>50 && $post>50) {
+ $pre = $post = 50;
+ } else if ($pre>50) {
+ $pre = min($pre,100-$post);
+ } else if ($post>50) {
+ $post = min($post, 100-$pre);
+ } else if ($offset == 0) {
+ // both are less than 50, means the context is the whole string
+ // make it so and break out of this loop - there is no need for the
+ // complex snippet calculations
+ $snippets = array($text);
+ break;
+ }
+
+ // establish context start and end points, try to append to previous
+ // context if possible
+ $start = $utf8_idx - $pre;
+ $append = ($start < $end) ? $end : false; // still the end of the previous context snippet
+ $end = $utf8_idx + $utf8_len + $post; // now set it to the end of this context
+
+ if ($append) {
+ $snippets[count($snippets)-1] .= \dokuwiki\Utf8\PhpString::substr($text,$append,$end-$append);
+ } else {
+ $snippets[] = \dokuwiki\Utf8\PhpString::substr($text,$start,$end-$start);
+ }
+
+ // set $offset for next match attempt
+ // continue matching after the current match
+ // if the current match is not the longest possible match starting at the current offset
+ // this prevents further matching of this snippet but for possible matches of length
+ // smaller than match length + context (at least 50 characters) this match is part of the context
+ $utf8_offset = $utf8_idx + $utf8_len;
+ $offset = $idx + strlen(\dokuwiki\Utf8\PhpString::substr($text,$utf8_idx,$utf8_len));
+ $offset = \dokuwiki\Utf8\Clean::correctIdx($text,$offset);
+ }
+
+ $m = "\1";
+ $snippets = preg_replace('/'.$re1.'/iu',$m.'$1'.$m,$snippets);
+ $snippet = preg_replace(
+ '/' . $m . '([^' . $m . ']*?)' . $m . '/iu',
+ '<strong class="search_hit">$1</strong>',
+ hsc(join('... ', $snippets))
+ );
+
+ $evdata['snippet'] = $snippet;
+ }
+ $evt->advise_after();
+ unset($evt);
+
+ return $evdata['snippet'];
+}
+
+/**
+ * Wraps a search term in regex boundary checks.
+ *
+ * @param string $term
+ * @return string
+ */
+function ft_snippet_re_preprocess($term) {
+ // do not process asian terms where word boundaries are not explicit
+ if(\dokuwiki\Utf8\Asian::isAsianWords($term)) return $term;
+
+ if (UTF8_PROPERTYSUPPORT) {
+ // unicode word boundaries
+ // see http://stackoverflow.com/a/2449017/172068
+ $BL = '(?<!\pL)';
+ $BR = '(?!\pL)';
+ } else {
+ // not as correct as above, but at least won't break
+ $BL = '\b';
+ $BR = '\b';
+ }
+
+ if(substr($term,0,2) == '\\*'){
+ $term = substr($term,2);
+ }else{
+ $term = $BL.$term;
+ }
+
+ if(substr($term,-2,2) == '\\*'){
+ $term = substr($term,0,-2);
+ }else{
+ $term = $term.$BR;
+ }
+
+ if($term == $BL || $term == $BR || $term == $BL.$BR) $term = '';
+ return $term;
+}
+
+/**
+ * Combine found documents and sum up their scores
+ *
+ * This function is used to combine searched words with a logical
+ * AND. Only documents available in all arrays are returned.
+ *
+ * based upon PEAR's PHP_Compat function for array_intersect_key()
+ *
+ * @param array $args An array of page arrays
+ * @return array
+ */
+function ft_resultCombine($args){
+ $array_count = count($args);
+ if($array_count == 1){
+ return $args[0];
+ }
+
+ $result = array();
+ if ($array_count > 1) {
+ foreach ($args[0] as $key => $value) {
+ $result[$key] = $value;
+ for ($i = 1; $i !== $array_count; $i++) {
+ if (!isset($args[$i][$key])) {
+ unset($result[$key]);
+ break;
+ }
+ $result[$key] += $args[$i][$key];
+ }
+ }
+ }
+ return $result;
+}
+
+/**
+ * Unites found documents and sum up their scores
+ *
+ * based upon ft_resultCombine() function
+ *
+ * @param array $args An array of page arrays
+ * @return array
+ *
+ * @author Kazutaka Miyasaka <kazmiya@gmail.com>
+ */
+function ft_resultUnite($args) {
+ $array_count = count($args);
+ if ($array_count === 1) {
+ return $args[0];
+ }
+
+ $result = $args[0];
+ for ($i = 1; $i !== $array_count; $i++) {
+ foreach (array_keys($args[$i]) as $id) {
+ $result[$id] += $args[$i][$id];
+ }
+ }
+ return $result;
+}
+
+/**
+ * Computes the difference of documents using page id for comparison
+ *
+ * nearly identical to PHP5's array_diff_key()
+ *
+ * @param array $args An array of page arrays
+ * @return array
+ *
+ * @author Kazutaka Miyasaka <kazmiya@gmail.com>
+ */
+function ft_resultComplement($args) {
+ $array_count = count($args);
+ if ($array_count === 1) {
+ return $args[0];
+ }
+
+ $result = $args[0];
+ foreach (array_keys($result) as $id) {
+ for ($i = 1; $i !== $array_count; $i++) {
+ if (isset($args[$i][$id])) unset($result[$id]);
+ }
+ }
+ return $result;
+}
+
+/**
+ * Parses a search query and builds an array of search formulas
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Kazutaka Miyasaka <kazmiya@gmail.com>
+ *
+ * @param dokuwiki\Search\Indexer $Indexer
+ * @param string $query search query
+ * @return array of search formulas
+ */
+function ft_queryParser($Indexer, $query){
+ /**
+ * parse a search query and transform it into intermediate representation
+ *
+ * in a search query, you can use the following expressions:
+ *
+ * words:
+ * include
+ * -exclude
+ * phrases:
+ * "phrase to be included"
+ * -"phrase you want to exclude"
+ * namespaces:
+ * @include:namespace (or ns:include:namespace)
+ * ^exclude:namespace (or -ns:exclude:namespace)
+ * groups:
+ * ()
+ * -()
+ * operators:
+ * and ('and' is the default operator: you can always omit this)
+ * or (or pipe symbol '|', lower precedence than 'and')
+ *
+ * e.g. a query [ aa "bb cc" @dd:ee ] means "search pages which contain
+ * a word 'aa', a phrase 'bb cc' and are within a namespace 'dd:ee'".
+ * this query is equivalent to [ -(-aa or -"bb cc" or -ns:dd:ee) ]
+ * as long as you don't mind hit counts.
+ *
+ * intermediate representation consists of the following parts:
+ *
+ * ( ) - group
+ * AND - logical and
+ * OR - logical or
+ * NOT - logical not
+ * W+:, W-:, W_: - word (underscore: no need to highlight)
+ * P+:, P-: - phrase (minus sign: logically in NOT group)
+ * N+:, N-: - namespace
+ */
+ $parsed_query = '';
+ $parens_level = 0;
+ $terms = preg_split('/(-?".*?")/u', \dokuwiki\Utf8\PhpString::strtolower($query),
+ -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+
+ foreach ($terms as $term) {
+ $parsed = '';
+ if (preg_match('/^(-?)"(.+)"$/u', $term, $matches)) {
+ // phrase-include and phrase-exclude
+ $not = $matches[1] ? 'NOT' : '';
+ $parsed = $not.ft_termParser($Indexer, $matches[2], false, true);
+ } else {
+ // fix incomplete phrase
+ $term = str_replace('"', ' ', $term);
+
+ // fix parentheses
+ $term = str_replace(')' , ' ) ', $term);
+ $term = str_replace('(' , ' ( ', $term);
+ $term = str_replace('- (', ' -(', $term);
+
+ // treat pipe symbols as 'OR' operators
+ $term = str_replace('|', ' or ', $term);
+
+ // treat ideographic spaces (U+3000) as search term separators
+ // FIXME: some more separators?
+ $term = preg_replace('/[ \x{3000}]+/u', ' ', $term);
+ $term = trim($term);
+ if ($term === '') continue;
+
+ $tokens = explode(' ', $term);
+ foreach ($tokens as $token) {
+ if ($token === '(') {
+ // parenthesis-include-open
+ $parsed .= '(';
+ ++$parens_level;
+ } elseif ($token === '-(') {
+ // parenthesis-exclude-open
+ $parsed .= 'NOT(';
+ ++$parens_level;
+ } elseif ($token === ')') {
+ // parenthesis-any-close
+ if ($parens_level === 0) continue;
+ $parsed .= ')';
+ $parens_level--;
+ } elseif ($token === 'and') {
+ // logical-and (do nothing)
+ } elseif ($token === 'or') {
+ // logical-or
+ $parsed .= 'OR';
+ } elseif (preg_match('/^(?:\^|-ns:)(.+)$/u', $token, $matches)) {
+ // namespace-exclude
+ $parsed .= 'NOT(N+:'.$matches[1].')';
+ } elseif (preg_match('/^(?:@|ns:)(.+)$/u', $token, $matches)) {
+ // namespace-include
+ $parsed .= '(N+:'.$matches[1].')';
+ } elseif (preg_match('/^-(.+)$/', $token, $matches)) {
+ // word-exclude
+ $parsed .= 'NOT('.ft_termParser($Indexer, $matches[1]).')';
+ } else {
+ // word-include
+ $parsed .= ft_termParser($Indexer, $token);
+ }
+ }
+ }
+ $parsed_query .= $parsed;
+ }
+
+ // cleanup (very sensitive)
+ $parsed_query .= str_repeat(')', $parens_level);
+ do {
+ $parsed_query_old = $parsed_query;
+ $parsed_query = preg_replace('/(NOT)?\(\)/u', '', $parsed_query);
+ } while ($parsed_query !== $parsed_query_old);
+ $parsed_query = preg_replace('/(NOT|OR)+\)/u', ')' , $parsed_query);
+ $parsed_query = preg_replace('/(OR)+/u' , 'OR' , $parsed_query);
+ $parsed_query = preg_replace('/\(OR/u' , '(' , $parsed_query);
+ $parsed_query = preg_replace('/^OR|OR$/u' , '' , $parsed_query);
+ $parsed_query = preg_replace('/\)(NOT)?\(/u' , ')AND$1(', $parsed_query);
+
+ // adjustment: make highlightings right
+ $parens_level = 0;
+ $notgrp_levels = array();
+ $parsed_query_new = '';
+ $tokens = preg_split('/(NOT\(|[()])/u', $parsed_query, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+ foreach ($tokens as $token) {
+ if ($token === 'NOT(') {
+ $notgrp_levels[] = ++$parens_level;
+ } elseif ($token === '(') {
+ ++$parens_level;
+ } elseif ($token === ')') {
+ if ($parens_level-- === end($notgrp_levels)) array_pop($notgrp_levels);
+ } elseif (count($notgrp_levels) % 2 === 1) {
+ // turn highlight-flag off if terms are logically in "NOT" group
+ $token = preg_replace('/([WPN])\+\:/u', '$1-:', $token);
+ }
+ $parsed_query_new .= $token;
+ }
+ $parsed_query = $parsed_query_new;
+
+ /**
+ * convert infix notation string into postfix (Reverse Polish notation) array
+ * by Shunting-yard algorithm
+ *
+ * see: http://en.wikipedia.org/wiki/Reverse_Polish_notation
+ * see: http://en.wikipedia.org/wiki/Shunting-yard_algorithm
+ */
+ $parsed_ary = array();
+ $ope_stack = array();
+ $ope_precedence = array(')' => 1, 'OR' => 2, 'AND' => 3, 'NOT' => 4, '(' => 5);
+ $ope_regex = '/([()]|OR|AND|NOT)/u';
+
+ $tokens = preg_split($ope_regex, $parsed_query, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+ foreach ($tokens as $token) {
+ if (preg_match($ope_regex, $token)) {
+ // operator
+ $last_ope = end($ope_stack);
+ while ($last_ope !== false && $ope_precedence[$token] <= $ope_precedence[$last_ope] && $last_ope != '(') {
+ $parsed_ary[] = array_pop($ope_stack);
+ $last_ope = end($ope_stack);
+ }
+ if ($token == ')') {
+ array_pop($ope_stack); // this array_pop always deletes '('
+ } else {
+ $ope_stack[] = $token;
+ }
+ } else {
+ // operand
+ $token_decoded = str_replace(array('OP', 'CP'), array('(', ')'), $token);
+ $parsed_ary[] = $token_decoded;
+ }
+ }
+ $parsed_ary = array_values(array_merge($parsed_ary, array_reverse($ope_stack)));
+
+ // cleanup: each double "NOT" in RPN array actually does nothing
+ $parsed_ary_count = count($parsed_ary);
+ for ($i = 1; $i < $parsed_ary_count; ++$i) {
+ if ($parsed_ary[$i] === 'NOT' && $parsed_ary[$i - 1] === 'NOT') {
+ unset($parsed_ary[$i], $parsed_ary[$i - 1]);
+ }
+ }
+ $parsed_ary = array_values($parsed_ary);
+
+ // build return value
+ $q = array();
+ $q['query'] = $query;
+ $q['parsed_str'] = $parsed_query;
+ $q['parsed_ary'] = $parsed_ary;
+
+ foreach ($q['parsed_ary'] as $token) {
+ if ($token[2] !== ':') continue;
+ $body = substr($token, 3);
+
+ switch (substr($token, 0, 3)) {
+ case 'N+:':
+ $q['ns'][] = $body; // for backward compatibility
+ break;
+ case 'N-:':
+ $q['notns'][] = $body; // for backward compatibility
+ break;
+ case 'W_:':
+ $q['words'][] = $body;
+ break;
+ case 'W-:':
+ $q['words'][] = $body;
+ $q['not'][] = $body; // for backward compatibility
+ break;
+ case 'W+:':
+ $q['words'][] = $body;
+ $q['highlight'][] = $body;
+ $q['and'][] = $body; // for backward compatibility
+ break;
+ case 'P-:':
+ $q['phrases'][] = $body;
+ break;
+ case 'P+:':
+ $q['phrases'][] = $body;
+ $q['highlight'][] = $body;
+ break;
+ }
+ }
+ foreach (array('words', 'phrases', 'highlight', 'ns', 'notns', 'and', 'not') as $key) {
+ $q[$key] = empty($q[$key]) ? array() : array_values(array_unique($q[$key]));
+ }
+
+ return $q;
+}
+
+/**
+ * Transforms given search term into intermediate representation
+ *
+ * This function is used in ft_queryParser() and not for general purpose use.
+ *
+ * @author Kazutaka Miyasaka <kazmiya@gmail.com>
+ *
+ * @param dokuwiki\Search\Indexer $Indexer
+ * @param string $term
+ * @param bool $consider_asian
+ * @param bool $phrase_mode
+ * @return string
+ */
+function ft_termParser($Indexer, $term, $consider_asian = true, $phrase_mode = false) {
+ $parsed = '';
+ if ($consider_asian) {
+ // successive asian characters need to be searched as a phrase
+ $words = \dokuwiki\Utf8\Asian::splitAsianWords($term);
+ foreach ($words as $word) {
+ $phrase_mode = $phrase_mode ? true : \dokuwiki\Utf8\Asian::isAsianWords($word);
+ $parsed .= ft_termParser($Indexer, $word, false, $phrase_mode);
+ }
+ } else {
+ $term_noparen = str_replace(array('(', ')'), ' ', $term);
+ $words = $Indexer->tokenizer($term_noparen, true);
+
+ // W_: no need to highlight
+ if (empty($words)) {
+ $parsed = '()'; // important: do not remove
+ } elseif ($words[0] === $term) {
+ $parsed = '(W+:'.$words[0].')';
+ } elseif ($phrase_mode) {
+ $term_encoded = str_replace(array('(', ')'), array('OP', 'CP'), $term);
+ $parsed = '((W_:'.implode(')(W_:', $words).')(P+:'.$term_encoded.'))';
+ } else {
+ $parsed = '((W+:'.implode(')(W+:', $words).'))';
+ }
+ }
+ return $parsed;
+}
+
+/**
+ * Recreate a search query string based on parsed parts, doesn't support negated phrases and `OR` searches
+ *
+ * @param array $and
+ * @param array $not
+ * @param array $phrases
+ * @param array $ns
+ * @param array $notns
+ *
+ * @return string
+ */
+function ft_queryUnparser_simple(array $and, array $not, array $phrases, array $ns, array $notns) {
+ $query = implode(' ', $and);
+ if (!empty($not)) {
+ $query .= ' -' . implode(' -', $not);
+ }
+
+ if (!empty($phrases)) {
+ $query .= ' "' . implode('" "', $phrases) . '"';
+ }
+
+ if (!empty($ns)) {
+ $query .= ' @' . implode(' @', $ns);
+ }
+
+ if (!empty($notns)) {
+ $query .= ' ^' . implode(' ^', $notns);
+ }
+
+ return $query;
+}
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/inc/html.php b/platform/www/inc/html.php
new file mode 100644
index 0000000..1f494d4
--- /dev/null
+++ b/platform/www/inc/html.php
@@ -0,0 +1,2380 @@
+<?php
+/**
+ * HTML output functions
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+use dokuwiki\ChangeLog\MediaChangeLog;
+use dokuwiki\ChangeLog\PageChangeLog;
+use dokuwiki\Extension\AuthPlugin;
+use dokuwiki\Extension\Event;
+
+if (!defined('SEC_EDIT_PATTERN')) {
+ define('SEC_EDIT_PATTERN', '#<!-- EDIT({.*?}) -->#');
+}
+
+
+/**
+ * Convenience function to quickly build a wikilink
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $id id of the target page
+ * @param string $name the name of the link, i.e. the text that is displayed
+ * @param string|array $search search string(s) that shall be highlighted in the target page
+ * @return string the HTML code of the link
+ */
+function html_wikilink($id,$name=null,$search=''){
+ /** @var Doku_Renderer_xhtml $xhtml_renderer */
+ static $xhtml_renderer = null;
+ if(is_null($xhtml_renderer)){
+ $xhtml_renderer = p_get_renderer('xhtml');
+ }
+
+ return $xhtml_renderer->internallink($id,$name,$search,true,'navigation');
+}
+
+/**
+ * The loginform
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param bool $svg Whether to show svg icons in the register and resendpwd links or not
+ */
+function html_login($svg = false){
+ global $lang;
+ global $conf;
+ global $ID;
+ global $INPUT;
+
+ print p_locale_xhtml('login');
+ print '<div class="centeralign">'.NL;
+ $form = new Doku_Form(array('id' => 'dw__login', 'action'=>wl($ID)));
+ $form->startFieldset($lang['btn_login']);
+ $form->addHidden('id', $ID);
+ $form->addHidden('do', 'login');
+ $form->addElement(form_makeTextField(
+ 'u',
+ ((!$INPUT->bool('http_credentials')) ? $INPUT->str('u') : ''),
+ $lang['user'],
+ 'focus__this',
+ 'block')
+ );
+ $form->addElement(form_makePasswordField('p', $lang['pass'], '', 'block'));
+ if($conf['rememberme']) {
+ $form->addElement(form_makeCheckboxField('r', '1', $lang['remember'], 'remember__me', 'simple'));
+ }
+ $form->addElement(form_makeButton('submit', '', $lang['btn_login']));
+ $form->endFieldset();
+
+ if(actionOK('register')){
+ $registerLink = (new \dokuwiki\Menu\Item\Register())->asHtmlLink('', $svg);
+ $form->addElement('<p>'.$lang['reghere'].': '. $registerLink .'</p>');
+ }
+
+ if (actionOK('resendpwd')) {
+ $resendPwLink = (new \dokuwiki\Menu\Item\Resendpwd())->asHtmlLink('', $svg);
+ $form->addElement('<p>'.$lang['pwdforget'].': '. $resendPwLink .'</p>');
+ }
+
+ html_form('login', $form);
+ print '</div>'.NL;
+}
+
+
+/**
+ * Denied page content
+ *
+ * @return string html
+ */
+function html_denied() {
+ print p_locale_xhtml('denied');
+
+ if(empty($_SERVER['REMOTE_USER']) && actionOK('login')){
+ html_login();
+ }
+}
+
+/**
+ * inserts section edit buttons if wanted or removes the markers
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $text
+ * @param bool $show show section edit buttons?
+ * @return string
+ */
+function html_secedit($text,$show=true){
+ global $INFO;
+
+ if((isset($INFO) && !$INFO['writable']) || !$show || (isset($INFO) && $INFO['rev'])){
+ return preg_replace(SEC_EDIT_PATTERN,'',$text);
+ }
+
+ return preg_replace_callback(SEC_EDIT_PATTERN,
+ 'html_secedit_button', $text);
+}
+
+/**
+ * prepares section edit button data for event triggering
+ * used as a callback in html_secedit
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $matches matches with regexp
+ * @return string
+ * @triggers HTML_SECEDIT_BUTTON
+ */
+function html_secedit_button($matches){
+ $json = htmlspecialchars_decode($matches[1], ENT_QUOTES);
+ $data = json_decode($json, true);
+ if ($data == NULL) {
+ return;
+ }
+ $data ['target'] = strtolower($data['target']);
+ $data ['hid'] = strtolower($data['hid']);
+
+ return Event::createAndTrigger('HTML_SECEDIT_BUTTON', $data,
+ 'html_secedit_get_button');
+}
+
+/**
+ * prints a section editing button
+ * used as default action form HTML_SECEDIT_BUTTON
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ *
+ * @param array $data name, section id and target
+ * @return string html
+ */
+function html_secedit_get_button($data) {
+ global $ID;
+ global $INFO;
+
+ if (!isset($data['name']) || $data['name'] === '') return '';
+
+ $name = $data['name'];
+ unset($data['name']);
+
+ $secid = $data['secid'];
+ unset($data['secid']);
+
+ return "<div class='secedit editbutton_" . $data['target'] .
+ " editbutton_" . $secid . "'>" .
+ html_btn('secedit', $ID, '',
+ array_merge(array('do' => 'edit',
+ 'rev' => $INFO['lastmod'],
+ 'summary' => '['.$name.'] '), $data),
+ 'post', $name) . '</div>';
+}
+
+/**
+ * Just the back to top button (in its own form)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @return string html
+ */
+function html_topbtn(){
+ global $lang;
+
+ $ret = '<a class="nolink" href="#dokuwiki__top">' .
+ '<button class="button" onclick="window.scrollTo(0, 0)" title="' . $lang['btn_top'] . '">' .
+ $lang['btn_top'] .
+ '</button></a>';
+
+ return $ret;
+}
+
+/**
+ * Displays a button (using its own form)
+ * If tooltip exists, the access key tooltip is replaced.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $name
+ * @param string $id
+ * @param string $akey access key
+ * @param string[] $params key-value pairs added as hidden inputs
+ * @param string $method
+ * @param string $tooltip
+ * @param bool|string $label label text, false: lookup btn_$name in localization
+ * @param string $svg (optional) svg code, inserted into the button
+ * @return string
+ */
+function html_btn($name, $id, $akey, $params, $method='get', $tooltip='', $label=false, $svg=null){
+ global $conf;
+ global $lang;
+
+ if (!$label)
+ $label = $lang['btn_'.$name];
+
+ $ret = '';
+
+ //filter id (without urlencoding)
+ $id = idfilter($id,false);
+
+ //make nice URLs even for buttons
+ if($conf['userewrite'] == 2){
+ $script = DOKU_BASE.DOKU_SCRIPT.'/'.$id;
+ }elseif($conf['userewrite']){
+ $script = DOKU_BASE.$id;
+ }else{
+ $script = DOKU_BASE.DOKU_SCRIPT;
+ $params['id'] = $id;
+ }
+
+ $ret .= '<form class="button btn_'.$name.'" method="'.$method.'" action="'.$script.'"><div class="no">';
+
+ if(is_array($params)){
+ foreach($params as $key => $val) {
+ $ret .= '<input type="hidden" name="'.$key.'" ';
+ $ret .= 'value="'.hsc($val).'" />';
+ }
+ }
+
+ if ($tooltip!='') {
+ $tip = hsc($tooltip);
+ }else{
+ $tip = hsc($label);
+ }
+
+ $ret .= '<button type="submit" ';
+ if($akey){
+ $tip .= ' ['.strtoupper($akey).']';
+ $ret .= 'accesskey="'.$akey.'" ';
+ }
+ $ret .= 'title="'.$tip.'">';
+ if ($svg) {
+ $ret .= '<span>' . hsc($label) . '</span>';
+ $ret .= inlineSVG($svg);
+ } else {
+ $ret .= hsc($label);
+ }
+ $ret .= '</button>';
+ $ret .= '</div></form>';
+
+ return $ret;
+}
+/**
+ * show a revision warning
+ *
+ * @author Szymon Olewniczak <dokuwiki@imz.re>
+ */
+function html_showrev() {
+ print p_locale_xhtml('showrev');
+}
+
+/**
+ * Show a wiki page
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param null|string $txt wiki text or null for showing $ID
+ */
+function html_show($txt=null){
+ global $ID;
+ global $REV;
+ global $HIGH;
+ global $INFO;
+ global $DATE_AT;
+ //disable section editing for old revisions or in preview
+ if($txt || $REV){
+ $secedit = false;
+ }else{
+ $secedit = true;
+ }
+
+ if (!is_null($txt)){
+ //PreviewHeader
+ echo '<br id="scroll__here" />';
+ echo p_locale_xhtml('preview');
+ echo '<div class="preview"><div class="pad">';
+ $html = html_secedit(p_render('xhtml',p_get_instructions($txt),$info),$secedit);
+ if($INFO['prependTOC']) $html = tpl_toc(true).$html;
+ echo $html;
+ echo '<div class="clearer"></div>';
+ echo '</div></div>';
+
+ }else{
+ if ($REV||$DATE_AT){
+ $data = array('rev' => &$REV, 'date_at' => &$DATE_AT);
+ Event::createAndTrigger('HTML_SHOWREV_OUTPUT', $data, 'html_showrev');
+ }
+ $html = p_wiki_xhtml($ID,$REV,true,$DATE_AT);
+ $html = html_secedit($html,$secedit);
+ if($INFO['prependTOC']) $html = tpl_toc(true).$html;
+ $html = html_hilight($html,$HIGH);
+ echo $html;
+ }
+}
+
+/**
+ * ask the user about how to handle an exisiting draft
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function html_draft(){
+ global $INFO;
+ global $ID;
+ global $lang;
+ $draft = new \dokuwiki\Draft($ID, $INFO['client']);
+ $text = $draft->getDraftText();
+
+ print p_locale_xhtml('draft');
+ html_diff($text, false);
+ $form = new Doku_Form(array('id' => 'dw__editform'));
+ $form->addHidden('id', $ID);
+ $form->addHidden('date', $draft->getDraftDate());
+ $form->addHidden('wikitext', $text);
+ $form->addElement(form_makeOpenTag('div', array('id'=>'draft__status')));
+ $form->addElement($draft->getDraftMessage());
+ $form->addElement(form_makeCloseTag('div'));
+ $form->addElement(form_makeButton('submit', 'recover', $lang['btn_recover'], array('tabindex'=>'1')));
+ $form->addElement(form_makeButton('submit', 'draftdel', $lang['btn_draftdel'], array('tabindex'=>'2')));
+ $form->addElement(form_makeButton('submit', 'show', $lang['btn_cancel'], array('tabindex'=>'3')));
+ html_form('draft', $form);
+}
+
+/**
+ * Highlights searchqueries in HTML code
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ *
+ * @param string $html
+ * @param array|string $phrases
+ * @return string html
+ */
+function html_hilight($html,$phrases){
+ $phrases = (array) $phrases;
+ $phrases = array_map('preg_quote_cb', $phrases);
+ $phrases = array_map('ft_snippet_re_preprocess', $phrases);
+ $phrases = array_filter($phrases);
+ $regex = join('|',$phrases);
+
+ if ($regex === '') return $html;
+ if (!\dokuwiki\Utf8\Clean::isUtf8($regex)) return $html;
+ $html = @preg_replace_callback("/((<[^>]*)|$regex)/ui",'html_hilight_callback',$html);
+ return $html;
+}
+
+/**
+ * Callback used by html_hilight()
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ *
+ * @param array $m matches
+ * @return string html
+ */
+function html_hilight_callback($m) {
+ $hlight = unslash($m[0]);
+ if ( !isset($m[2])) {
+ $hlight = '<span class="search_hit">'.$hlight.'</span>';
+ }
+ return $hlight;
+}
+
+/**
+ * Display error on locked pages
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function html_locked(){
+ global $ID;
+ global $conf;
+ global $lang;
+ global $INFO;
+
+ $locktime = filemtime(wikiLockFN($ID));
+ $expire = dformat($locktime + $conf['locktime']);
+ $min = round(($conf['locktime'] - (time() - $locktime) )/60);
+
+ print p_locale_xhtml('locked');
+ print '<ul>';
+ print '<li><div class="li"><strong>'.$lang['lockedby'].'</strong> '.editorinfo($INFO['locked']).'</div></li>';
+ print '<li><div class="li"><strong>'.$lang['lockexpire'].'</strong> '.$expire.' ('.$min.' min)</div></li>';
+ print '</ul>';
+}
+
+/**
+ * list old revisions
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param int $first skip the first n changelog lines
+ * @param bool|string $media_id id of media, or false for current page
+ */
+function html_revisions($first=0, $media_id = false){
+ global $ID;
+ global $INFO;
+ global $conf;
+ global $lang;
+ $id = $ID;
+ if ($media_id) {
+ $id = $media_id;
+ $changelog = new MediaChangeLog($id);
+ } else {
+ $changelog = new PageChangeLog($id);
+ }
+
+ /* we need to get one additional log entry to be able to
+ * decide if this is the last page or is there another one.
+ * see html_recent()
+ */
+
+ $revisions = $changelog->getRevisions($first, $conf['recent']+1);
+
+ if(count($revisions)==0 && $first!=0){
+ $first=0;
+ $revisions = $changelog->getRevisions($first, $conf['recent']+1);
+ }
+ $hasNext = false;
+ if (count($revisions)>$conf['recent']) {
+ $hasNext = true;
+ array_pop($revisions); // remove extra log entry
+ }
+
+ if (!$media_id) print p_locale_xhtml('revisions');
+
+ $params = array('id' => 'page__revisions', 'class' => 'changes');
+ if($media_id) {
+ $params['action'] = media_managerURL(array('image' => $media_id), '&');
+ }
+
+ if(!$media_id) {
+ $exists = $INFO['exists'];
+ $display_name = useHeading('navigation') ? hsc(p_get_first_heading($id)) : $id;
+ if(!$display_name) {
+ $display_name = $id;
+ }
+ } else {
+ $exists = file_exists(mediaFN($id));
+ $display_name = $id;
+ }
+
+ $form = new Doku_Form($params);
+ $form->addElement(form_makeOpenTag('ul'));
+
+ if($exists && $first == 0) {
+ $minor = false;
+ if($media_id) {
+ $date = dformat(@filemtime(mediaFN($id)));
+ $href = media_managerURL(array('image' => $id, 'tab_details' => 'view'), '&');
+
+ $changelog->setChunkSize(1024);
+ $revinfo = $changelog->getRevisionInfo(@filemtime(fullpath(mediaFN($id))));
+
+ $summary = $revinfo['sum'];
+ if($revinfo['user']) {
+ $editor = $revinfo['user'];
+ } else {
+ $editor = $revinfo['ip'];
+ }
+ $sizechange = $revinfo['sizechange'];
+ } else {
+ $date = dformat($INFO['lastmod']);
+ if(isset($INFO['meta']) && isset($INFO['meta']['last_change'])) {
+ if($INFO['meta']['last_change']['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) {
+ $minor = true;
+ }
+ if(isset($INFO['meta']['last_change']['sizechange'])) {
+ $sizechange = $INFO['meta']['last_change']['sizechange'];
+ } else {
+ $sizechange = null;
+ }
+ }
+ $pagelog = new PageChangeLog($ID);
+ $latestrev = $pagelog->getRevisions(-1, 1);
+ $latestrev = array_pop($latestrev);
+ $href = wl($id,"rev=$latestrev",false,'&');
+ $summary = $INFO['sum'];
+ $editor = $INFO['editor'];
+ }
+
+ $form->addElement(form_makeOpenTag('li', array('class' => ($minor ? 'minor' : ''))));
+ $form->addElement(form_makeOpenTag('div', array('class' => 'li')));
+ $form->addElement(form_makeTag('input', array(
+ 'type' => 'checkbox',
+ 'name' => 'rev2[]',
+ 'value' => 'current')));
+
+ $form->addElement(form_makeOpenTag('span', array('class' => 'date')));
+ $form->addElement($date);
+ $form->addElement(form_makeCloseTag('span'));
+
+ $form->addElement('<img src="'.DOKU_BASE.'lib/images/blank.gif" width="15" height="11" alt="" />');
+
+ $form->addElement(form_makeOpenTag('a', array(
+ 'class' => 'wikilink1',
+ 'href' => $href)));
+ $form->addElement($display_name);
+ $form->addElement(form_makeCloseTag('a'));
+
+ if ($media_id) $form->addElement(form_makeOpenTag('div'));
+
+ if($summary) {
+ $form->addElement(form_makeOpenTag('span', array('class' => 'sum')));
+ if(!$media_id) $form->addElement(' – ');
+ $form->addElement('<bdi>' . hsc($summary) . '</bdi>');
+ $form->addElement(form_makeCloseTag('span'));
+ }
+
+ $form->addElement(form_makeOpenTag('span', array('class' => 'user')));
+ $form->addElement((empty($editor))?('('.$lang['external_edit'].')'):'<bdi>'.editorinfo($editor).'</bdi>');
+ $form->addElement(form_makeCloseTag('span'));
+
+ html_sizechange($sizechange, $form);
+
+ $form->addElement('('.$lang['current'].')');
+
+ if ($media_id) $form->addElement(form_makeCloseTag('div'));
+
+ $form->addElement(form_makeCloseTag('div'));
+ $form->addElement(form_makeCloseTag('li'));
+ }
+
+ foreach($revisions as $rev) {
+ $date = dformat($rev);
+ $info = $changelog->getRevisionInfo($rev);
+ if($media_id) {
+ $exists = file_exists(mediaFN($id, $rev));
+ } else {
+ $exists = page_exists($id, $rev);
+ }
+
+ $class = '';
+ if($info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) {
+ $class = 'minor';
+ }
+ $form->addElement(form_makeOpenTag('li', array('class' => $class)));
+ $form->addElement(form_makeOpenTag('div', array('class' => 'li')));
+ if($exists){
+ $form->addElement(form_makeTag('input', array(
+ 'type' => 'checkbox',
+ 'name' => 'rev2[]',
+ 'value' => $rev)));
+ }else{
+ $form->addElement('<img src="'.DOKU_BASE.'lib/images/blank.gif" width="15" height="11" alt="" />');
+ }
+
+ $form->addElement(form_makeOpenTag('span', array('class' => 'date')));
+ $form->addElement($date);
+ $form->addElement(form_makeCloseTag('span'));
+
+ if($exists){
+ if (!$media_id) {
+ $href = wl($id,"rev=$rev,do=diff", false, '&');
+ } else {
+ $href = media_managerURL(array('image' => $id, 'rev' => $rev, 'mediado' => 'diff'), '&');
+ }
+ $form->addElement(form_makeOpenTag('a', array(
+ 'class' => 'diff_link',
+ 'href' => $href)));
+ $form->addElement(form_makeTag('img', array(
+ 'src' => DOKU_BASE.'lib/images/diff.png',
+ 'width' => 15,
+ 'height' => 11,
+ 'title' => $lang['diff'],
+ 'alt' => $lang['diff'])));
+ $form->addElement(form_makeCloseTag('a'));
+
+ if (!$media_id) {
+ $href = wl($id,"rev=$rev",false,'&');
+ } else {
+ $href = media_managerURL(array('image' => $id, 'tab_details' => 'view', 'rev' => $rev), '&');
+ }
+ $form->addElement(form_makeOpenTag('a', array(
+ 'class' => 'wikilink1',
+ 'href' => $href)));
+ $form->addElement($display_name);
+ $form->addElement(form_makeCloseTag('a'));
+ }else{
+ $form->addElement('<img src="'.DOKU_BASE.'lib/images/blank.gif" width="15" height="11" alt="" />');
+ $form->addElement($display_name);
+ }
+
+ if ($media_id) $form->addElement(form_makeOpenTag('div'));
+
+ if ($info['sum']) {
+ $form->addElement(form_makeOpenTag('span', array('class' => 'sum')));
+ if(!$media_id) $form->addElement(' – ');
+ $form->addElement('<bdi>'.hsc($info['sum']).'</bdi>');
+ $form->addElement(form_makeCloseTag('span'));
+ }
+
+ $form->addElement(form_makeOpenTag('span', array('class' => 'user')));
+ if($info['user']){
+ $form->addElement('<bdi>'.editorinfo($info['user']).'</bdi>');
+ if(auth_ismanager()){
+ $form->addElement(' <bdo dir="ltr">('.$info['ip'].')</bdo>');
+ }
+ }else{
+ $form->addElement('<bdo dir="ltr">'.$info['ip'].'</bdo>');
+ }
+ $form->addElement(form_makeCloseTag('span'));
+
+ html_sizechange($info['sizechange'], $form);
+
+ if ($media_id) $form->addElement(form_makeCloseTag('div'));
+
+ $form->addElement(form_makeCloseTag('div'));
+ $form->addElement(form_makeCloseTag('li'));
+ }
+ $form->addElement(form_makeCloseTag('ul'));
+ if (!$media_id) {
+ $form->addElement(form_makeButton('submit', 'diff', $lang['diff2']));
+ } else {
+ $form->addHidden('mediado', 'diff');
+ $form->addElement(form_makeButton('submit', '', $lang['diff2']));
+ }
+ html_form('revisions', $form);
+
+ print '<div class="pagenav">';
+ $last = $first + $conf['recent'];
+ if ($first > 0) {
+ $first -= $conf['recent'];
+ if ($first < 0) $first = 0;
+ print '<div class="pagenav-prev">';
+ if ($media_id) {
+ print html_btn('newer',$media_id,"p",media_managerURL(array('first' => $first), '&amp;', false, true));
+ } else {
+ print html_btn('newer',$id,"p",array('do' => 'revisions', 'first' => $first));
+ }
+ print '</div>';
+ }
+ if ($hasNext) {
+ print '<div class="pagenav-next">';
+ if ($media_id) {
+ print html_btn('older',$media_id,"n",media_managerURL(array('first' => $last), '&amp;', false, true));
+ } else {
+ print html_btn('older',$id,"n",array('do' => 'revisions', 'first' => $last));
+ }
+ print '</div>';
+ }
+ print '</div>';
+
+}
+
+/**
+ * display recent changes
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param int $first
+ * @param string $show_changes
+ */
+function html_recent($first = 0, $show_changes = 'both') {
+ global $conf;
+ global $lang;
+ global $ID;
+ /* we need to get one additionally log entry to be able to
+ * decide if this is the last page or is there another one.
+ * This is the cheapest solution to get this information.
+ */
+ $flags = 0;
+ if($show_changes == 'mediafiles' && $conf['mediarevisions']) {
+ $flags = RECENTS_MEDIA_CHANGES;
+ } elseif($show_changes == 'pages') {
+ $flags = 0;
+ } elseif($conf['mediarevisions']) {
+ $show_changes = 'both';
+ $flags = RECENTS_MEDIA_PAGES_MIXED;
+ }
+
+ $recents = getRecents($first, $conf['recent'] + 1, getNS($ID), $flags);
+ if(count($recents) == 0 && $first != 0) {
+ $first = 0;
+ $recents = getRecents($first, $conf['recent'] + 1, getNS($ID), $flags);
+ }
+ $hasNext = false;
+ if(count($recents) > $conf['recent']) {
+ $hasNext = true;
+ array_pop($recents); // remove extra log entry
+ }
+
+ print p_locale_xhtml('recent');
+
+ if(getNS($ID) != '') {
+ print '<div class="level1"><p>' .
+ sprintf($lang['recent_global'], getNS($ID), wl('', 'do=recent')) .
+ '</p></div>';
+ }
+
+ $form = new Doku_Form(array('id' => 'dw__recent', 'method' => 'GET', 'class' => 'changes', 'action'=>wl($ID)));
+ $form->addHidden('sectok', null);
+ $form->addHidden('do', 'recent');
+ $form->addHidden('id', $ID);
+
+ if($conf['mediarevisions']) {
+ $form->addElement('<div class="changeType">');
+ $form->addElement(form_makeListboxField(
+ 'show_changes',
+ array(
+ 'pages' => $lang['pages_changes'],
+ 'mediafiles' => $lang['media_changes'],
+ 'both' => $lang['both_changes']
+ ),
+ $show_changes,
+ $lang['changes_type'],
+ '', '',
+ array('class' => 'quickselect')));
+
+ $form->addElement(form_makeButton('submit', 'recent', $lang['btn_apply']));
+ $form->addElement('</div>');
+ }
+
+ $form->addElement(form_makeOpenTag('ul'));
+
+ foreach($recents as $recent) {
+ $date = dformat($recent['date']);
+
+ $class = '';
+ if($recent['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) {
+ $class = 'minor';
+ }
+ $form->addElement(form_makeOpenTag('li', array('class' => $class)));
+ $form->addElement(form_makeOpenTag('div', array('class' => 'li')));
+
+ if(!empty($recent['media'])) {
+ $form->addElement(media_printicon($recent['id']));
+ } else {
+ $icon = DOKU_BASE . 'lib/images/fileicons/file.png';
+ $form->addElement('<img src="' . $icon . '" alt="' . $recent['id'] . '" class="icon" />');
+ }
+
+ $form->addElement(form_makeOpenTag('span', array('class' => 'date')));
+ $form->addElement($date);
+ $form->addElement(form_makeCloseTag('span'));
+
+ $diff = false;
+ $href = '';
+
+ if(!empty($recent['media'])) {
+ $changelog = new MediaChangeLog($recent['id']);
+ $revs = $changelog->getRevisions(0, 1);
+ $diff = (count($revs) && file_exists(mediaFN($recent['id'])));
+ if($diff) {
+ $href = media_managerURL(array(
+ 'tab_details' => 'history',
+ 'mediado' => 'diff',
+ 'image' => $recent['id'],
+ 'ns' => getNS($recent['id'])
+ ), '&');
+ }
+ } else {
+ $href = wl($recent['id'], "do=diff", false, '&');
+ }
+
+ if(!empty($recent['media']) && !$diff) {
+ $form->addElement('<img src="' . DOKU_BASE . 'lib/images/blank.gif" width="15" height="11" alt="" />');
+ } else {
+ $form->addElement(form_makeOpenTag('a', array('class' => 'diff_link', 'href' => $href)));
+ $form->addElement(form_makeTag('img', array(
+ 'src' => DOKU_BASE . 'lib/images/diff.png',
+ 'width' => 15,
+ 'height' => 11,
+ 'title' => $lang['diff'],
+ 'alt' => $lang['diff']
+ )));
+ $form->addElement(form_makeCloseTag('a'));
+ }
+
+ if(!empty($recent['media'])) {
+ $href = media_managerURL(
+ array(
+ 'tab_details' => 'history',
+ 'image' => $recent['id'],
+ 'ns' => getNS($recent['id'])
+ ),
+ '&'
+ );
+ } else {
+ $href = wl($recent['id'], "do=revisions", false, '&');
+ }
+ $form->addElement(form_makeOpenTag('a', array(
+ 'class' => 'revisions_link',
+ 'href' => $href)));
+ $form->addElement(form_makeTag('img', array(
+ 'src' => DOKU_BASE . 'lib/images/history.png',
+ 'width' => 12,
+ 'height' => 14,
+ 'title' => $lang['btn_revs'],
+ 'alt' => $lang['btn_revs']
+ )));
+ $form->addElement(form_makeCloseTag('a'));
+
+ if(!empty($recent['media'])) {
+ $href = media_managerURL(
+ array(
+ 'tab_details' => 'view',
+ 'image' => $recent['id'],
+ 'ns' => getNS($recent['id'])
+ ),
+ '&'
+ );
+ $class = file_exists(mediaFN($recent['id'])) ? 'wikilink1' : 'wikilink2';
+ $form->addElement(form_makeOpenTag('a', array(
+ 'class' => $class,
+ 'href' => $href)));
+ $form->addElement($recent['id']);
+ $form->addElement(form_makeCloseTag('a'));
+ } else {
+ $form->addElement(html_wikilink(':' . $recent['id'], useHeading('navigation') ? null : $recent['id']));
+ }
+ $form->addElement(form_makeOpenTag('span', array('class' => 'sum')));
+ $form->addElement(' – ' . hsc($recent['sum']));
+ $form->addElement(form_makeCloseTag('span'));
+
+ $form->addElement(form_makeOpenTag('span', array('class' => 'user')));
+ if($recent['user']) {
+ $form->addElement('<bdi>' . editorinfo($recent['user']) . '</bdi>');
+ if(auth_ismanager()) {
+ $form->addElement(' <bdo dir="ltr">(' . $recent['ip'] . ')</bdo>');
+ }
+ } else {
+ $form->addElement('<bdo dir="ltr">' . $recent['ip'] . '</bdo>');
+ }
+ $form->addElement(form_makeCloseTag('span'));
+
+ html_sizechange($recent['sizechange'], $form);
+
+ $form->addElement(form_makeCloseTag('div'));
+ $form->addElement(form_makeCloseTag('li'));
+ }
+ $form->addElement(form_makeCloseTag('ul'));
+
+ $form->addElement(form_makeOpenTag('div', array('class' => 'pagenav')));
+ $last = $first + $conf['recent'];
+ if($first > 0) {
+ $first -= $conf['recent'];
+ if($first < 0) $first = 0;
+ $form->addElement(form_makeOpenTag('div', array('class' => 'pagenav-prev')));
+ $form->addElement(form_makeOpenTag('button', array(
+ 'type' => 'submit',
+ 'name' => 'first[' . $first . ']',
+ 'accesskey' => 'n',
+ 'title' => $lang['btn_newer'] . ' [N]',
+ 'class' => 'button show'
+ )));
+ $form->addElement($lang['btn_newer']);
+ $form->addElement(form_makeCloseTag('button'));
+ $form->addElement(form_makeCloseTag('div'));
+ }
+ if($hasNext) {
+ $form->addElement(form_makeOpenTag('div', array('class' => 'pagenav-next')));
+ $form->addElement(form_makeOpenTag('button', array(
+ 'type' => 'submit',
+ 'name' => 'first[' . $last . ']',
+ 'accesskey' => 'p',
+ 'title' => $lang['btn_older'] . ' [P]',
+ 'class' => 'button show'
+ )));
+ $form->addElement($lang['btn_older']);
+ $form->addElement(form_makeCloseTag('button'));
+ $form->addElement(form_makeCloseTag('div'));
+ }
+ $form->addElement(form_makeCloseTag('div'));
+ html_form('recent', $form);
+}
+
+/**
+ * Display page index
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $ns
+ */
+function html_index($ns){
+ global $conf;
+ global $ID;
+ $ns = cleanID($ns);
+ if(empty($ns)){
+ $ns = getNS($ID);
+ if($ns === false) $ns ='';
+ }
+ $ns = utf8_encodeFN(str_replace(':','/',$ns));
+
+ echo p_locale_xhtml('index');
+ echo '<div id="index__tree" class="index__tree">';
+
+ $data = array();
+ search($data,$conf['datadir'],'search_index',array('ns' => $ns));
+ echo html_buildlist($data,'idx','html_list_index','html_li_index');
+
+ echo '</div>';
+}
+
+/**
+ * Index item formatter
+ *
+ * User function for html_buildlist()
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $item
+ * @return string
+ */
+function html_list_index($item){
+ global $ID, $conf;
+
+ // prevent searchbots needlessly following links
+ $nofollow = ($ID != $conf['start'] || $conf['sitemap']) ? 'rel="nofollow"' : '';
+
+ $ret = '';
+ $base = ':'.$item['id'];
+ $base = substr($base,strrpos($base,':')+1);
+ if($item['type']=='d'){
+ // FS#2766, no need for search bots to follow namespace links in the index
+ $link = wl($ID, 'idx=' . rawurlencode($item['id']));
+ $ret .= '<a href="' . $link . '" title="' . $item['id'] . '" class="idx_dir" ' . $nofollow . '><strong>';
+ $ret .= $base;
+ $ret .= '</strong></a>';
+ }else{
+ // default is noNSorNS($id), but we want noNS($id) when useheading is off FS#2605
+ $ret .= html_wikilink(':'.$item['id'], useHeading('navigation') ? null : noNS($item['id']));
+ }
+ return $ret;
+}
+
+/**
+ * Index List item
+ *
+ * This user function is used in html_buildlist to build the
+ * <li> tags for namespaces when displaying the page index
+ * it gives different classes to opened or closed "folders"
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $item
+ * @return string html
+ */
+function html_li_index($item){
+ global $INFO;
+ global $ACT;
+
+ $class = '';
+ $id = '';
+
+ if($item['type'] == "f"){
+ // scroll to the current item
+ if(isset($INFO) && $item['id'] == $INFO['id'] && $ACT == 'index') {
+ $id = ' id="scroll__here"';
+ $class = ' bounce';
+ }
+ return '<li class="level'.$item['level'].$class.'" '.$id.'>';
+ }elseif($item['open']){
+ return '<li class="open">';
+ }else{
+ return '<li class="closed">';
+ }
+}
+
+/**
+ * Default List item
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $item
+ * @return string html
+ */
+function html_li_default($item){
+ return '<li class="level'.$item['level'].'">';
+}
+
+/**
+ * Build an unordered list
+ *
+ * Build an unordered list from the given $data array
+ * Each item in the array has to have a 'level' property
+ * the item itself gets printed by the given $func user
+ * function. The second and optional function is used to
+ * print the <li> tag. Both user function need to accept
+ * a single item.
+ *
+ * Both user functions can be given as array to point to
+ * a member of an object.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $data array with item arrays
+ * @param string $class class of ul wrapper
+ * @param callable $func callback to print an list item
+ * @param callable $lifunc callback to the opening li tag
+ * @param bool $forcewrapper Trigger building a wrapper ul if the first level is
+ * 0 (we have a root object) or 1 (just the root content)
+ * @return string html of an unordered list
+ */
+function html_buildlist($data,$class,$func,$lifunc='html_li_default',$forcewrapper=false){
+ if (count($data) === 0) {
+ return '';
+ }
+
+ $firstElement = reset($data);
+ $start_level = $firstElement['level'];
+ $level = $start_level;
+ $ret = '';
+ $open = 0;
+
+ foreach ($data as $item){
+
+ if( $item['level'] > $level ){
+ //open new list
+ for($i=0; $i<($item['level'] - $level); $i++){
+ if ($i) $ret .= "<li class=\"clear\">";
+ $ret .= "\n<ul class=\"$class\">\n";
+ $open++;
+ }
+ $level = $item['level'];
+
+ }elseif( $item['level'] < $level ){
+ //close last item
+ $ret .= "</li>\n";
+ while( $level > $item['level'] && $open > 0 ){
+ //close higher lists
+ $ret .= "</ul>\n</li>\n";
+ $level--;
+ $open--;
+ }
+ } elseif ($ret !== '') {
+ //close previous item
+ $ret .= "</li>\n";
+ }
+
+ //print item
+ $ret .= call_user_func($lifunc,$item);
+ $ret .= '<div class="li">';
+
+ $ret .= call_user_func($func,$item);
+ $ret .= '</div>';
+ }
+
+ //close remaining items and lists
+ $ret .= "</li>\n";
+ while($open-- > 0) {
+ $ret .= "</ul></li>\n";
+ }
+
+ if ($forcewrapper || $start_level < 2) {
+ // Trigger building a wrapper ul if the first level is
+ // 0 (we have a root object) or 1 (just the root content)
+ $ret = "\n<ul class=\"$class\">\n".$ret."</ul>\n";
+ }
+
+ return $ret;
+}
+
+/**
+ * display backlinks
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Michael Klier <chi@chimeric.de>
+ */
+function html_backlinks(){
+ global $ID;
+ global $lang;
+
+ print p_locale_xhtml('backlinks');
+
+ $data = ft_backlinks($ID);
+
+ if(!empty($data)) {
+ print '<ul class="idx">';
+ foreach($data as $blink){
+ print '<li><div class="li">';
+ print html_wikilink(':'.$blink,useHeading('navigation')?null:$blink);
+ print '</div></li>';
+ }
+ print '</ul>';
+ } else {
+ print '<div class="level1"><p>' . $lang['nothingfound'] . '</p></div>';
+ }
+}
+
+/**
+ * Get header of diff HTML
+ *
+ * @param string $l_rev Left revisions
+ * @param string $r_rev Right revision
+ * @param string $id Page id, if null $ID is used
+ * @param bool $media If it is for media files
+ * @param bool $inline Return the header on a single line
+ * @return string[] HTML snippets for diff header
+ */
+function html_diff_head($l_rev, $r_rev, $id = null, $media = false, $inline = false) {
+ global $lang;
+ if ($id === null) {
+ global $ID;
+ $id = $ID;
+ }
+ $head_separator = $inline ? ' ' : '<br />';
+ $media_or_wikiFN = $media ? 'mediaFN' : 'wikiFN';
+ $ml_or_wl = $media ? 'ml' : 'wl';
+ $l_minor = $r_minor = '';
+
+ if($media) {
+ $changelog = new MediaChangeLog($id);
+ } else {
+ $changelog = new PageChangeLog($id);
+ }
+ if(!$l_rev){
+ $l_head = '&mdash;';
+ }else{
+ $l_info = $changelog->getRevisionInfo($l_rev);
+ if($l_info['user']){
+ $l_user = '<bdi>'.editorinfo($l_info['user']).'</bdi>';
+ if(auth_ismanager()) $l_user .= ' <bdo dir="ltr">('.$l_info['ip'].')</bdo>';
+ } else {
+ $l_user = '<bdo dir="ltr">'.$l_info['ip'].'</bdo>';
+ }
+ $l_user = '<span class="user">'.$l_user.'</span>';
+ $l_sum = ($l_info['sum']) ? '<span class="sum"><bdi>'.hsc($l_info['sum']).'</bdi></span>' : '';
+ if ($l_info['type']===DOKU_CHANGE_TYPE_MINOR_EDIT) $l_minor = 'class="minor"';
+
+ $l_head_title = ($media) ? dformat($l_rev) : $id.' ['.dformat($l_rev).']';
+ $l_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id,"rev=$l_rev").'">'.
+ $l_head_title.'</a></bdi>'.
+ $head_separator.$l_user.' '.$l_sum;
+ }
+
+ if($r_rev){
+ $r_info = $changelog->getRevisionInfo($r_rev);
+ if($r_info['user']){
+ $r_user = '<bdi>'.editorinfo($r_info['user']).'</bdi>';
+ if(auth_ismanager()) $r_user .= ' <bdo dir="ltr">('.$r_info['ip'].')</bdo>';
+ } else {
+ $r_user = '<bdo dir="ltr">'.$r_info['ip'].'</bdo>';
+ }
+ $r_user = '<span class="user">'.$r_user.'</span>';
+ $r_sum = ($r_info['sum']) ? '<span class="sum"><bdi>'.hsc($r_info['sum']).'</bdi></span>' : '';
+ if ($r_info['type']===DOKU_CHANGE_TYPE_MINOR_EDIT) $r_minor = 'class="minor"';
+
+ $r_head_title = ($media) ? dformat($r_rev) : $id.' ['.dformat($r_rev).']';
+ $r_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id,"rev=$r_rev").'">'.
+ $r_head_title.'</a></bdi>'.
+ $head_separator.$r_user.' '.$r_sum;
+ }elseif($_rev = @filemtime($media_or_wikiFN($id))){
+ $_info = $changelog->getRevisionInfo($_rev);
+ if($_info['user']){
+ $_user = '<bdi>'.editorinfo($_info['user']).'</bdi>';
+ if(auth_ismanager()) $_user .= ' <bdo dir="ltr">('.$_info['ip'].')</bdo>';
+ } else {
+ $_user = '<bdo dir="ltr">'.$_info['ip'].'</bdo>';
+ }
+ $_user = '<span class="user">'.$_user.'</span>';
+ $_sum = ($_info['sum']) ? '<span class="sum"><bdi>'.hsc($_info['sum']).'</span></bdi>' : '';
+ if ($_info['type']===DOKU_CHANGE_TYPE_MINOR_EDIT) $r_minor = 'class="minor"';
+
+ $r_head_title = ($media) ? dformat($_rev) : $id.' ['.dformat($_rev).']';
+ $r_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id).'">'.
+ $r_head_title.'</a></bdi> '.
+ '('.$lang['current'].')'.
+ $head_separator.$_user.' '.$_sum;
+ }else{
+ $r_head = '&mdash; ('.$lang['current'].')';
+ }
+
+ return array($l_head, $r_head, $l_minor, $r_minor);
+}
+
+/**
+ * Show diff
+ * between current page version and provided $text
+ * or between the revisions provided via GET or POST
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $text when non-empty: compare with this text with most current version
+ * @param bool $intro display the intro text
+ * @param string $type type of the diff (inline or sidebyside)
+ */
+function html_diff($text = '', $intro = true, $type = null) {
+ global $ID;
+ global $REV;
+ global $lang;
+ global $INPUT;
+ global $INFO;
+ $pagelog = new PageChangeLog($ID);
+
+ /*
+ * Determine diff type
+ */
+ if(!$type) {
+ $type = $INPUT->str('difftype');
+ if(empty($type)) {
+ $type = get_doku_pref('difftype', $type);
+ if(empty($type) && $INFO['ismobile']) {
+ $type = 'inline';
+ }
+ }
+ }
+ if($type != 'inline') $type = 'sidebyside';
+
+ /*
+ * Determine requested revision(s)
+ */
+ // we're trying to be clever here, revisions to compare can be either
+ // given as rev and rev2 parameters, with rev2 being optional. Or in an
+ // array in rev2.
+ $rev1 = $REV;
+
+ $rev2 = $INPUT->ref('rev2');
+ if(is_array($rev2)) {
+ $rev1 = (int) $rev2[0];
+ $rev2 = (int) $rev2[1];
+
+ if(!$rev1) {
+ $rev1 = $rev2;
+ unset($rev2);
+ }
+ } else {
+ $rev2 = $INPUT->int('rev2');
+ }
+
+ /*
+ * Determine left and right revision, its texts and the header
+ */
+ $r_minor = '';
+ $l_minor = '';
+
+ if($text) { // compare text to the most current revision
+ $l_rev = '';
+ $l_text = rawWiki($ID, '');
+ $l_head = '<a class="wikilink1" href="' . wl($ID) . '">' .
+ $ID . ' ' . dformat((int) @filemtime(wikiFN($ID))) . '</a> ' .
+ $lang['current'];
+
+ $r_rev = '';
+ $r_text = cleanText($text);
+ $r_head = $lang['yours'];
+ } else {
+ if($rev1 && isset($rev2) && $rev2) { // two specific revisions wanted
+ // make sure order is correct (older on the left)
+ if($rev1 < $rev2) {
+ $l_rev = $rev1;
+ $r_rev = $rev2;
+ } else {
+ $l_rev = $rev2;
+ $r_rev = $rev1;
+ }
+ } elseif($rev1) { // single revision given, compare to current
+ $r_rev = '';
+ $l_rev = $rev1;
+ } else { // no revision was given, compare previous to current
+ $r_rev = '';
+ $revs = $pagelog->getRevisions(0, 1);
+ $l_rev = $revs[0];
+ $REV = $l_rev; // store revision back in $REV
+ }
+
+ // when both revisions are empty then the page was created just now
+ if(!$l_rev && !$r_rev) {
+ $l_text = '';
+ } else {
+ $l_text = rawWiki($ID, $l_rev);
+ }
+ $r_text = rawWiki($ID, $r_rev);
+
+ list($l_head, $r_head, $l_minor, $r_minor) = html_diff_head($l_rev, $r_rev, null, false, $type == 'inline');
+ }
+
+ /*
+ * Build navigation
+ */
+ $l_nav = '';
+ $r_nav = '';
+ if(!$text) {
+ list($l_nav, $r_nav) = html_diff_navigation($pagelog, $type, $l_rev, $r_rev);
+ }
+ /*
+ * Create diff object and the formatter
+ */
+ $diff = new Diff(explode("\n", $l_text), explode("\n", $r_text));
+
+ if($type == 'inline') {
+ $diffformatter = new InlineDiffFormatter();
+ } else {
+ $diffformatter = new TableDiffFormatter();
+ }
+ /*
+ * Display intro
+ */
+ if($intro) print p_locale_xhtml('diff');
+
+ /*
+ * Display type and exact reference
+ */
+ if(!$text) {
+ ptln('<div class="diffoptions group">');
+
+
+ $form = new Doku_Form(array('action' => wl()));
+ $form->addHidden('id', $ID);
+ $form->addHidden('rev2[0]', $l_rev);
+ $form->addHidden('rev2[1]', $r_rev);
+ $form->addHidden('do', 'diff');
+ $form->addElement(
+ form_makeListboxField(
+ 'difftype',
+ array(
+ 'sidebyside' => $lang['diff_side'],
+ 'inline' => $lang['diff_inline']
+ ),
+ $type,
+ $lang['diff_type'],
+ '', '',
+ array('class' => 'quickselect')
+ )
+ );
+ $form->addElement(form_makeButton('submit', 'diff', 'Go'));
+ $form->printForm();
+
+ ptln('<p>');
+ // link to exactly this view FS#2835
+ echo html_diff_navigationlink($type, 'difflink', $l_rev, $r_rev ? $r_rev : $INFO['currentrev']);
+ ptln('</p>');
+
+ ptln('</div>'); // .diffoptions
+ }
+
+ /*
+ * Display diff view table
+ */
+ ?>
+ <div class="table">
+ <table class="diff diff_<?php echo $type ?>">
+
+ <?php
+ //navigation and header
+ if($type == 'inline') {
+ if(!$text) { ?>
+ <tr>
+ <td class="diff-lineheader">-</td>
+ <td class="diffnav"><?php echo $l_nav ?></td>
+ </tr>
+ <tr>
+ <th class="diff-lineheader">-</th>
+ <th <?php echo $l_minor ?>>
+ <?php echo $l_head ?>
+ </th>
+ </tr>
+ <?php } ?>
+ <tr>
+ <td class="diff-lineheader">+</td>
+ <td class="diffnav"><?php echo $r_nav ?></td>
+ </tr>
+ <tr>
+ <th class="diff-lineheader">+</th>
+ <th <?php echo $r_minor ?>>
+ <?php echo $r_head ?>
+ </th>
+ </tr>
+ <?php } else {
+ if(!$text) { ?>
+ <tr>
+ <td colspan="2" class="diffnav"><?php echo $l_nav ?></td>
+ <td colspan="2" class="diffnav"><?php echo $r_nav ?></td>
+ </tr>
+ <?php } ?>
+ <tr>
+ <th colspan="2" <?php echo $l_minor ?>>
+ <?php echo $l_head ?>
+ </th>
+ <th colspan="2" <?php echo $r_minor ?>>
+ <?php echo $r_head ?>
+ </th>
+ </tr>
+ <?php }
+
+ //diff view
+ echo html_insert_softbreaks($diffformatter->format($diff)); ?>
+
+ </table>
+ </div>
+<?php
+}
+
+/**
+ * Create html for revision navigation
+ *
+ * @param PageChangeLog $pagelog changelog object of current page
+ * @param string $type inline vs sidebyside
+ * @param int $l_rev left revision timestamp
+ * @param int $r_rev right revision timestamp
+ * @return string[] html of left and right navigation elements
+ */
+function html_diff_navigation($pagelog, $type, $l_rev, $r_rev) {
+ global $INFO, $ID;
+
+ // last timestamp is not in changelog, retrieve timestamp from metadata
+ // note: when page is removed, the metadata timestamp is zero
+ if(!$r_rev) {
+ if(isset($INFO['meta']['last_change']['date'])) {
+ $r_rev = $INFO['meta']['last_change']['date'];
+ } else {
+ $r_rev = 0;
+ }
+ }
+
+ //retrieve revisions with additional info
+ list($l_revs, $r_revs) = $pagelog->getRevisionsAround($l_rev, $r_rev);
+ $l_revisions = array();
+ if(!$l_rev) {
+ $l_revisions[0] = array(0, "", false); //no left revision given, add dummy
+ }
+ foreach($l_revs as $rev) {
+ $info = $pagelog->getRevisionInfo($rev);
+ $l_revisions[$rev] = array(
+ $rev,
+ dformat($info['date']) . ' ' . editorinfo($info['user'], true) . ' ' . $info['sum'],
+ $r_rev ? $rev >= $r_rev : false //disable?
+ );
+ }
+ $r_revisions = array();
+ if(!$r_rev) {
+ $r_revisions[0] = array(0, "", false); //no right revision given, add dummy
+ }
+ foreach($r_revs as $rev) {
+ $info = $pagelog->getRevisionInfo($rev);
+ $r_revisions[$rev] = array(
+ $rev,
+ dformat($info['date']) . ' ' . editorinfo($info['user'], true) . ' ' . $info['sum'],
+ $rev <= $l_rev //disable?
+ );
+ }
+
+ //determine previous/next revisions
+ $l_index = array_search($l_rev, $l_revs);
+ $l_prev = $l_revs[$l_index + 1];
+ $l_next = $l_revs[$l_index - 1];
+ if($r_rev) {
+ $r_index = array_search($r_rev, $r_revs);
+ $r_prev = $r_revs[$r_index + 1];
+ $r_next = $r_revs[$r_index - 1];
+ } else {
+ //removed page
+ if($l_next) {
+ $r_prev = $r_revs[0];
+ } else {
+ $r_prev = null;
+ }
+ $r_next = null;
+ }
+
+ /*
+ * Left side:
+ */
+ $l_nav = '';
+ //move back
+ if($l_prev) {
+ $l_nav .= html_diff_navigationlink($type, 'diffbothprevrev', $l_prev, $r_prev);
+ $l_nav .= html_diff_navigationlink($type, 'diffprevrev', $l_prev, $r_rev);
+ }
+ //dropdown
+ $form = new Doku_Form(array('action' => wl()));
+ $form->addHidden('id', $ID);
+ $form->addHidden('difftype', $type);
+ $form->addHidden('rev2[1]', $r_rev);
+ $form->addHidden('do', 'diff');
+ $form->addElement(
+ form_makeListboxField(
+ 'rev2[0]',
+ $l_revisions,
+ $l_rev,
+ '', '', '',
+ array('class' => 'quickselect')
+ )
+ );
+ $form->addElement(form_makeButton('submit', 'diff', 'Go'));
+ $l_nav .= $form->getForm();
+ //move forward
+ if($l_next && ($l_next < $r_rev || !$r_rev)) {
+ $l_nav .= html_diff_navigationlink($type, 'diffnextrev', $l_next, $r_rev);
+ }
+
+ /*
+ * Right side:
+ */
+ $r_nav = '';
+ //move back
+ if($l_rev < $r_prev) {
+ $r_nav .= html_diff_navigationlink($type, 'diffprevrev', $l_rev, $r_prev);
+ }
+ //dropdown
+ $form = new Doku_Form(array('action' => wl()));
+ $form->addHidden('id', $ID);
+ $form->addHidden('rev2[0]', $l_rev);
+ $form->addHidden('difftype', $type);
+ $form->addHidden('do', 'diff');
+ $form->addElement(
+ form_makeListboxField(
+ 'rev2[1]',
+ $r_revisions,
+ $r_rev,
+ '', '', '',
+ array('class' => 'quickselect')
+ )
+ );
+ $form->addElement(form_makeButton('submit', 'diff', 'Go'));
+ $r_nav .= $form->getForm();
+ //move forward
+ if($r_next) {
+ if($pagelog->isCurrentRevision($r_next)) {
+ $r_nav .= html_diff_navigationlink($type, 'difflastrev', $l_rev); //last revision is diff with current page
+ } else {
+ $r_nav .= html_diff_navigationlink($type, 'diffnextrev', $l_rev, $r_next);
+ }
+ $r_nav .= html_diff_navigationlink($type, 'diffbothnextrev', $l_next, $r_next);
+ }
+ return array($l_nav, $r_nav);
+}
+
+/**
+ * Create html link to a diff defined by two revisions
+ *
+ * @param string $difftype display type
+ * @param string $linktype
+ * @param int $lrev oldest revision
+ * @param int $rrev newest revision or null for diff with current revision
+ * @return string html of link to a diff
+ */
+function html_diff_navigationlink($difftype, $linktype, $lrev, $rrev = null) {
+ global $ID, $lang;
+ if(!$rrev) {
+ $urlparam = array(
+ 'do' => 'diff',
+ 'rev' => $lrev,
+ 'difftype' => $difftype,
+ );
+ } else {
+ $urlparam = array(
+ 'do' => 'diff',
+ 'rev2[0]' => $lrev,
+ 'rev2[1]' => $rrev,
+ 'difftype' => $difftype,
+ );
+ }
+ return '<a class="' . $linktype . '" href="' . wl($ID, $urlparam) . '" title="' . $lang[$linktype] . '">' .
+ '<span>' . $lang[$linktype] . '</span>' .
+ '</a>' . "\n";
+}
+
+/**
+ * Insert soft breaks in diff html
+ *
+ * @param string $diffhtml
+ * @return string
+ */
+function html_insert_softbreaks($diffhtml) {
+ // search the diff html string for both:
+ // - html tags, so these can be ignored
+ // - long strings of characters without breaking characters
+ return preg_replace_callback('/<[^>]*>|[^<> ]{12,}/','html_softbreak_callback',$diffhtml);
+}
+
+/**
+ * callback which adds softbreaks
+ *
+ * @param array $match array with first the complete match
+ * @return string the replacement
+ */
+function html_softbreak_callback($match){
+ // if match is an html tag, return it intact
+ if ($match[0][0] == '<') return $match[0];
+
+ // its a long string without a breaking character,
+ // make certain characters into breaking characters by inserting a
+ // word break opportunity (<wbr> tag) in front of them.
+ $regex = <<< REGEX
+(?(?= # start a conditional expression with a positive look ahead ...
+&\#?\\w{1,6};) # ... for html entities - we don't want to split them (ok to catch some invalid combinations)
+&\#?\\w{1,6}; # yes pattern - a quicker match for the html entity, since we know we have one
+|
+[?/,&\#;:] # no pattern - any other group of 'special' characters to insert a breaking character after
+)+ # end conditional expression
+REGEX;
+
+ return preg_replace('<'.$regex.'>xu','\0<wbr>',$match[0]);
+}
+
+/**
+ * show warning on conflict detection
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $text
+ * @param string $summary
+ */
+function html_conflict($text,$summary){
+ global $ID;
+ global $lang;
+
+ print p_locale_xhtml('conflict');
+ $form = new Doku_Form(array('id' => 'dw__editform'));
+ $form->addHidden('id', $ID);
+ $form->addHidden('wikitext', $text);
+ $form->addHidden('summary', $summary);
+ $form->addElement(form_makeButton('submit', 'save', $lang['btn_save'], array('accesskey'=>'s')));
+ $form->addElement(form_makeButton('submit', 'cancel', $lang['btn_cancel']));
+ html_form('conflict', $form);
+ print '<br /><br /><br /><br />'.NL;
+}
+
+/**
+ * Prints the global message array
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function html_msgarea(){
+ global $MSG, $MSG_shown;
+ /** @var array $MSG */
+ // store if the global $MSG has already been shown and thus HTML output has been started
+ $MSG_shown = true;
+
+ if(!isset($MSG)) return;
+
+ $shown = array();
+ foreach($MSG as $msg){
+ $hash = md5($msg['msg']);
+ if(isset($shown[$hash])) continue; // skip double messages
+ if(info_msg_allowed($msg)){
+ print '<div class="'.$msg['lvl'].'">';
+ print $msg['msg'];
+ print '</div>';
+ }
+ $shown[$hash] = 1;
+ }
+
+ unset($GLOBALS['MSG']);
+}
+
+/**
+ * Prints the registration form
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function html_register(){
+ global $lang;
+ global $conf;
+ global $INPUT;
+
+ $base_attrs = array('size'=>50,'required'=>'required');
+ $email_attrs = $base_attrs + array('type'=>'email','class'=>'edit');
+
+ print p_locale_xhtml('register');
+ print '<div class="centeralign">'.NL;
+ $form = new Doku_Form(array('id' => 'dw__register'));
+ $form->startFieldset($lang['btn_register']);
+ $form->addHidden('do', 'register');
+ $form->addHidden('save', '1');
+ $form->addElement(
+ form_makeTextField(
+ 'login',
+ $INPUT->post->str('login'),
+ $lang['user'],
+ '',
+ 'block',
+ $base_attrs
+ )
+ );
+ if (!$conf['autopasswd']) {
+ $form->addElement(form_makePasswordField('pass', $lang['pass'], '', 'block', $base_attrs));
+ $form->addElement(form_makePasswordField('passchk', $lang['passchk'], '', 'block', $base_attrs));
+ }
+ $form->addElement(
+ form_makeTextField(
+ 'fullname',
+ $INPUT->post->str('fullname'),
+ $lang['fullname'],
+ '',
+ 'block',
+ $base_attrs
+ )
+ );
+ $form->addElement(
+ form_makeField(
+ 'email',
+ 'email',
+ $INPUT->post->str('email'),
+ $lang['email'],
+ '',
+ 'block',
+ $email_attrs
+ )
+ );
+ $form->addElement(form_makeButton('submit', '', $lang['btn_register']));
+ $form->endFieldset();
+ html_form('register', $form);
+
+ print '</div>'.NL;
+}
+
+/**
+ * Print the update profile form
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function html_updateprofile(){
+ global $lang;
+ global $conf;
+ global $INPUT;
+ global $INFO;
+ /** @var AuthPlugin $auth */
+ global $auth;
+
+ print p_locale_xhtml('updateprofile');
+ print '<div class="centeralign">'.NL;
+
+ $fullname = $INPUT->post->str('fullname', $INFO['userinfo']['name'], true);
+ $email = $INPUT->post->str('email', $INFO['userinfo']['mail'], true);
+ $form = new Doku_Form(array('id' => 'dw__register'));
+ $form->startFieldset($lang['profile']);
+ $form->addHidden('do', 'profile');
+ $form->addHidden('save', '1');
+ $form->addElement(
+ form_makeTextField(
+ 'login',
+ $_SERVER['REMOTE_USER'],
+ $lang['user'],
+ '',
+ 'block',
+ array('size' => '50', 'disabled' => 'disabled')
+ )
+ );
+ $attr = array('size'=>'50');
+ if (!$auth->canDo('modName')) $attr['disabled'] = 'disabled';
+ $form->addElement(form_makeTextField('fullname', $fullname, $lang['fullname'], '', 'block', $attr));
+ $attr = array('size'=>'50', 'class'=>'edit');
+ if (!$auth->canDo('modMail')) $attr['disabled'] = 'disabled';
+ $form->addElement(form_makeField('email','email', $email, $lang['email'], '', 'block', $attr));
+ $form->addElement(form_makeTag('br'));
+ if ($auth->canDo('modPass')) {
+ $form->addElement(form_makePasswordField('newpass', $lang['newpass'], '', 'block', array('size'=>'50')));
+ $form->addElement(form_makePasswordField('passchk', $lang['passchk'], '', 'block', array('size'=>'50')));
+ }
+ if ($conf['profileconfirm']) {
+ $form->addElement(form_makeTag('br'));
+ $form->addElement(
+ form_makePasswordField(
+ 'oldpass',
+ $lang['oldpass'],
+ '',
+ 'block',
+ array('size' => '50', 'required' => 'required')
+ )
+ );
+ }
+ $form->addElement(form_makeButton('submit', '', $lang['btn_save']));
+ $form->addElement(form_makeButton('reset', '', $lang['btn_reset']));
+
+ $form->endFieldset();
+ html_form('updateprofile', $form);
+
+ if ($auth->canDo('delUser') && actionOK('profile_delete')) {
+ $form_profiledelete = new Doku_Form(array('id' => 'dw__profiledelete'));
+ $form_profiledelete->startFieldset($lang['profdeleteuser']);
+ $form_profiledelete->addHidden('do', 'profile_delete');
+ $form_profiledelete->addHidden('delete', '1');
+ $form_profiledelete->addElement(
+ form_makeCheckboxField(
+ 'confirm_delete',
+ '1',
+ $lang['profconfdelete'],
+ 'dw__confirmdelete',
+ '',
+ array('required' => 'required')
+ )
+ );
+ if ($conf['profileconfirm']) {
+ $form_profiledelete->addElement(form_makeTag('br'));
+ $form_profiledelete->addElement(
+ form_makePasswordField(
+ 'oldpass',
+ $lang['oldpass'],
+ '',
+ 'block',
+ array('size' => '50', 'required' => 'required')
+ )
+ );
+ }
+ $form_profiledelete->addElement(form_makeButton('submit', '', $lang['btn_deleteuser']));
+ $form_profiledelete->endFieldset();
+
+ html_form('profiledelete', $form_profiledelete);
+ }
+
+ print '</div>'.NL;
+}
+
+/**
+ * Preprocess edit form data
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @triggers HTML_EDITFORM_OUTPUT
+ */
+function html_edit(){
+ global $INPUT;
+ global $ID;
+ global $REV;
+ global $DATE;
+ global $PRE;
+ global $SUF;
+ global $INFO;
+ global $SUM;
+ global $lang;
+ global $conf;
+ global $TEXT;
+
+ if ($INPUT->has('changecheck')) {
+ $check = $INPUT->str('changecheck');
+ } elseif(!$INFO['exists']){
+ // $TEXT has been loaded from page template
+ $check = md5('');
+ } else {
+ $check = md5($TEXT);
+ }
+ $mod = md5($TEXT) !== $check;
+
+ $wr = $INFO['writable'] && !$INFO['locked'];
+ $include = 'edit';
+ if($wr){
+ if ($REV) $include = 'editrev';
+ }else{
+ // check pseudo action 'source'
+ if(!actionOK('source')){
+ msg('Command disabled: source',-1);
+ return;
+ }
+ $include = 'read';
+ }
+
+ global $license;
+
+ $form = new Doku_Form(array('id' => 'dw__editform'));
+ $form->addHidden('id', $ID);
+ $form->addHidden('rev', $REV);
+ $form->addHidden('date', $DATE);
+ $form->addHidden('prefix', $PRE . '.');
+ $form->addHidden('suffix', $SUF);
+ $form->addHidden('changecheck', $check);
+
+ $data = array('form' => $form,
+ 'wr' => $wr,
+ 'media_manager' => true,
+ 'target' => ($INPUT->has('target') && $wr) ? $INPUT->str('target') : 'section',
+ 'intro_locale' => $include);
+
+ if ($data['target'] !== 'section') {
+ // Only emit event if page is writable, section edit data is valid and
+ // edit target is not section.
+ Event::createAndTrigger('HTML_EDIT_FORMSELECTION', $data, 'html_edit_form', true);
+ } else {
+ html_edit_form($data);
+ }
+ if (isset($data['intro_locale'])) {
+ echo p_locale_xhtml($data['intro_locale']);
+ }
+
+ $form->addHidden('target', $data['target']);
+ if ($INPUT->has('hid')) {
+ $form->addHidden('hid', $INPUT->str('hid'));
+ }
+ if ($INPUT->has('codeblockOffset')) {
+ $form->addHidden('codeblockOffset', $INPUT->str('codeblockOffset'));
+ }
+ $form->addElement(form_makeOpenTag('div', array('id'=>'wiki__editbar', 'class'=>'editBar')));
+ $form->addElement(form_makeOpenTag('div', array('id'=>'size__ctl')));
+ $form->addElement(form_makeCloseTag('div'));
+ if ($wr) {
+ $form->addElement(form_makeOpenTag('div', array('class'=>'editButtons')));
+ $form->addElement(
+ form_makeButton(
+ 'submit',
+ 'save',
+ $lang['btn_save'],
+ array('id' => 'edbtn__save', 'accesskey' => 's', 'tabindex' => '4')
+ )
+ );
+ $form->addElement(
+ form_makeButton(
+ 'submit',
+ 'preview',
+ $lang['btn_preview'],
+ array('id' => 'edbtn__preview', 'accesskey' => 'p', 'tabindex' => '5')
+ )
+ );
+ $form->addElement(form_makeButton('submit', 'cancel', $lang['btn_cancel'], array('tabindex'=>'6')));
+ $form->addElement(form_makeCloseTag('div'));
+ $form->addElement(form_makeOpenTag('div', array('class'=>'summary')));
+ $form->addElement(
+ form_makeTextField(
+ 'summary',
+ $SUM,
+ $lang['summary'],
+ 'edit__summary',
+ 'nowrap',
+ array('size' => '50', 'tabindex' => '2')
+ )
+ );
+ $elem = html_minoredit();
+ if ($elem) $form->addElement($elem);
+ $form->addElement(form_makeCloseTag('div'));
+ }
+ $form->addElement(form_makeCloseTag('div'));
+ if($wr && $conf['license']){
+ $form->addElement(form_makeOpenTag('div', array('class'=>'license')));
+ $out = $lang['licenseok'];
+ $out .= ' <a href="'.$license[$conf['license']]['url'].'" rel="license" class="urlextern"';
+ if($conf['target']['extern']) $out .= ' target="'.$conf['target']['extern'].'"';
+ $out .= '>'.$license[$conf['license']]['name'].'</a>';
+ $form->addElement($out);
+ $form->addElement(form_makeCloseTag('div'));
+ }
+
+ if ($wr) {
+ // sets changed to true when previewed
+ echo '<script>/*<![CDATA[*/'. NL;
+ echo 'textChanged = ' . ($mod ? 'true' : 'false');
+ echo '/*!]]>*/</script>' . NL;
+ } ?>
+ <div class="editBox" role="application">
+
+ <div class="toolbar group">
+ <div id="tool__bar" class="tool__bar"><?php
+ if ($wr && $data['media_manager']){
+ ?><a href="<?php echo DOKU_BASE?>lib/exe/mediamanager.php?ns=<?php echo $INFO['namespace']?>"
+ target="_blank"><?php echo $lang['mediaselect'] ?></a><?php
+ }?>
+ </div>
+ </div>
+ <div id="draft__status" class="draft__status">
+ <?php
+ $draft = new \dokuwiki\Draft($ID, $INFO['client']);
+ if ($draft->isDraftAvailable()) {
+ echo $draft->getDraftMessage();
+ }
+ ?>
+ </div>
+ <?php
+
+ html_form('edit', $form);
+ print '</div>'.NL;
+}
+
+/**
+ * Display the default edit form
+ *
+ * Is the default action for HTML_EDIT_FORMSELECTION.
+ *
+ * @param mixed[] $param
+ */
+function html_edit_form($param) {
+ global $TEXT;
+
+ if ($param['target'] !== 'section') {
+ msg('No editor for edit target ' . hsc($param['target']) . ' found.', -1);
+ }
+
+ $attr = array('tabindex'=>'1');
+ if (!$param['wr']) $attr['readonly'] = 'readonly';
+
+ $param['form']->addElement(form_makeWikiText($TEXT, $attr));
+}
+
+/**
+ * Adds a checkbox for minor edits for logged in users
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @return array|bool
+ */
+function html_minoredit(){
+ global $conf;
+ global $lang;
+ global $INPUT;
+ // minor edits are for logged in users only
+ if(!$conf['useacl'] || !$_SERVER['REMOTE_USER']){
+ return false;
+ }
+
+ $p = array();
+ $p['tabindex'] = 3;
+ if($INPUT->bool('minor')) $p['checked']='checked';
+ return form_makeCheckboxField('minor', '1', $lang['minoredit'], 'minoredit', 'nowrap', $p);
+}
+
+/**
+ * prints some debug info
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function html_debug(){
+ global $conf;
+ global $lang;
+ /** @var AuthPlugin $auth */
+ global $auth;
+ global $INFO;
+
+ //remove sensitive data
+ $cnf = $conf;
+ debug_guard($cnf);
+ $nfo = $INFO;
+ debug_guard($nfo);
+ $ses = $_SESSION;
+ debug_guard($ses);
+
+ print '<html><body>';
+
+ print '<p>When reporting bugs please send all the following ';
+ print 'output as a mail to andi@splitbrain.org ';
+ print 'The best way to do this is to save this page in your browser</p>';
+
+ print '<b>$INFO:</b><pre>';
+ print_r($nfo);
+ print '</pre>';
+
+ print '<b>$_SERVER:</b><pre>';
+ print_r($_SERVER);
+ print '</pre>';
+
+ print '<b>$conf:</b><pre>';
+ print_r($cnf);
+ print '</pre>';
+
+ print '<b>DOKU_BASE:</b><pre>';
+ print DOKU_BASE;
+ print '</pre>';
+
+ print '<b>abs DOKU_BASE:</b><pre>';
+ print DOKU_URL;
+ print '</pre>';
+
+ print '<b>rel DOKU_BASE:</b><pre>';
+ print dirname($_SERVER['PHP_SELF']).'/';
+ print '</pre>';
+
+ print '<b>PHP Version:</b><pre>';
+ print phpversion();
+ print '</pre>';
+
+ print '<b>locale:</b><pre>';
+ print setlocale(LC_ALL,0);
+ print '</pre>';
+
+ print '<b>encoding:</b><pre>';
+ print $lang['encoding'];
+ print '</pre>';
+
+ if($auth){
+ print '<b>Auth backend capabilities:</b><pre>';
+ foreach ($auth->getCapabilities() as $cando){
+ print ' '.str_pad($cando,16) . ' => ' . (int)$auth->canDo($cando) . NL;
+ }
+ print '</pre>';
+ }
+
+ print '<b>$_SESSION:</b><pre>';
+ print_r($ses);
+ print '</pre>';
+
+ print '<b>Environment:</b><pre>';
+ print_r($_ENV);
+ print '</pre>';
+
+ print '<b>PHP settings:</b><pre>';
+ $inis = ini_get_all();
+ print_r($inis);
+ print '</pre>';
+
+ if (function_exists('apache_get_version')) {
+ $apache = array();
+ $apache['version'] = apache_get_version();
+
+ if (function_exists('apache_get_modules')) {
+ $apache['modules'] = apache_get_modules();
+ }
+ print '<b>Apache</b><pre>';
+ print_r($apache);
+ print '</pre>';
+ }
+
+ print '</body></html>';
+}
+
+/**
+ * Form to request a new password for an existing account
+ *
+ * @author Benoit Chesneau <benoit@bchesneau.info>
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+function html_resendpwd() {
+ global $lang;
+ global $conf;
+ global $INPUT;
+
+ $token = preg_replace('/[^a-f0-9]+/','',$INPUT->str('pwauth'));
+
+ if(!$conf['autopasswd'] && $token){
+ print p_locale_xhtml('resetpwd');
+ print '<div class="centeralign">'.NL;
+ $form = new Doku_Form(array('id' => 'dw__resendpwd'));
+ $form->startFieldset($lang['btn_resendpwd']);
+ $form->addHidden('token', $token);
+ $form->addHidden('do', 'resendpwd');
+
+ $form->addElement(form_makePasswordField('pass', $lang['pass'], '', 'block', array('size'=>'50')));
+ $form->addElement(form_makePasswordField('passchk', $lang['passchk'], '', 'block', array('size'=>'50')));
+
+ $form->addElement(form_makeButton('submit', '', $lang['btn_resendpwd']));
+ $form->endFieldset();
+ html_form('resendpwd', $form);
+ print '</div>'.NL;
+ }else{
+ print p_locale_xhtml('resendpwd');
+ print '<div class="centeralign">'.NL;
+ $form = new Doku_Form(array('id' => 'dw__resendpwd'));
+ $form->startFieldset($lang['resendpwd']);
+ $form->addHidden('do', 'resendpwd');
+ $form->addHidden('save', '1');
+ $form->addElement(form_makeTag('br'));
+ $form->addElement(form_makeTextField('login', $INPUT->post->str('login'), $lang['user'], '', 'block'));
+ $form->addElement(form_makeTag('br'));
+ $form->addElement(form_makeTag('br'));
+ $form->addElement(form_makeButton('submit', '', $lang['btn_resendpwd']));
+ $form->endFieldset();
+ html_form('resendpwd', $form);
+ print '</div>'.NL;
+ }
+}
+
+/**
+ * Return the TOC rendered to XHTML
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $toc
+ * @return string html
+ */
+function html_TOC($toc){
+ if(!count($toc)) return '';
+ global $lang;
+ $out = '<!-- TOC START -->'.DOKU_LF;
+ $out .= '<div id="dw__toc" class="dw__toc">'.DOKU_LF;
+ $out .= '<h3 class="toggle">';
+ $out .= $lang['toc'];
+ $out .= '</h3>'.DOKU_LF;
+ $out .= '<div>'.DOKU_LF;
+ $out .= html_buildlist($toc,'toc','html_list_toc','html_li_default',true);
+ $out .= '</div>'.DOKU_LF.'</div>'.DOKU_LF;
+ $out .= '<!-- TOC END -->'.DOKU_LF;
+ return $out;
+}
+
+/**
+ * Callback for html_buildlist
+ *
+ * @param array $item
+ * @return string html
+ */
+function html_list_toc($item){
+ if(isset($item['hid'])){
+ $link = '#'.$item['hid'];
+ }else{
+ $link = $item['link'];
+ }
+
+ return '<a href="'.$link.'">'.hsc($item['title']).'</a>';
+}
+
+/**
+ * Helper function to build TOC items
+ *
+ * Returns an array ready to be added to a TOC array
+ *
+ * @param string $link - where to link (if $hash set to '#' it's a local anchor)
+ * @param string $text - what to display in the TOC
+ * @param int $level - nesting level
+ * @param string $hash - is prepended to the given $link, set blank if you want full links
+ * @return array the toc item
+ */
+function html_mktocitem($link, $text, $level, $hash='#'){
+ return array( 'link' => $hash.$link,
+ 'title' => $text,
+ 'type' => 'ul',
+ 'level' => $level);
+}
+
+/**
+ * Output a Doku_Form object.
+ * Triggers an event with the form name: HTML_{$name}FORM_OUTPUT
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param string $name The name of the form
+ * @param Doku_Form $form The form
+ */
+function html_form($name, &$form) {
+ // Safety check in case the caller forgets.
+ $form->endFieldset();
+ Event::createAndTrigger('HTML_'.strtoupper($name).'FORM_OUTPUT', $form, 'html_form_output', false);
+}
+
+/**
+ * Form print function.
+ * Just calls printForm() on the data object.
+ *
+ * @param Doku_Form $data The form
+ */
+function html_form_output($data) {
+ $data->printForm();
+}
+
+/**
+ * Embed a flash object in HTML
+ *
+ * This will create the needed HTML to embed a flash movie in a cross browser
+ * compatble way using valid XHTML
+ *
+ * The parameters $params, $flashvars and $atts need to be associative arrays.
+ * No escaping needs to be done for them. The alternative content *has* to be
+ * escaped because it is used as is. If no alternative content is given
+ * $lang['noflash'] is used.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @link http://latrine.dgx.cz/how-to-correctly-insert-a-flash-into-xhtml
+ *
+ * @param string $swf - the SWF movie to embed
+ * @param int $width - width of the flash movie in pixels
+ * @param int $height - height of the flash movie in pixels
+ * @param array $params - additional parameters (<param>)
+ * @param array $flashvars - parameters to be passed in the flashvar parameter
+ * @param array $atts - additional attributes for the <object> tag
+ * @param string $alt - alternative content (is NOT automatically escaped!)
+ * @return string - the XHTML markup
+ */
+function html_flashobject($swf,$width,$height,$params=null,$flashvars=null,$atts=null,$alt=''){
+ global $lang;
+
+ $out = '';
+
+ // prepare the object attributes
+ if(is_null($atts)) $atts = array();
+ $atts['width'] = (int) $width;
+ $atts['height'] = (int) $height;
+ if(!$atts['width']) $atts['width'] = 425;
+ if(!$atts['height']) $atts['height'] = 350;
+
+ // add object attributes for standard compliant browsers
+ $std = $atts;
+ $std['type'] = 'application/x-shockwave-flash';
+ $std['data'] = $swf;
+
+ // add object attributes for IE
+ $ie = $atts;
+ $ie['classid'] = 'clsid:D27CDB6E-AE6D-11cf-96B8-444553540000';
+
+ // open object (with conditional comments)
+ $out .= '<!--[if !IE]> -->'.NL;
+ $out .= '<object '.buildAttributes($std).'>'.NL;
+ $out .= '<!-- <![endif]-->'.NL;
+ $out .= '<!--[if IE]>'.NL;
+ $out .= '<object '.buildAttributes($ie).'>'.NL;
+ $out .= ' <param name="movie" value="'.hsc($swf).'" />'.NL;
+ $out .= '<!--><!-- -->'.NL;
+
+ // print params
+ if(is_array($params)) foreach($params as $key => $val){
+ $out .= ' <param name="'.hsc($key).'" value="'.hsc($val).'" />'.NL;
+ }
+
+ // add flashvars
+ if(is_array($flashvars)){
+ $out .= ' <param name="FlashVars" value="'.buildURLparams($flashvars).'" />'.NL;
+ }
+
+ // alternative content
+ if($alt){
+ $out .= $alt.NL;
+ }else{
+ $out .= $lang['noflash'].NL;
+ }
+
+ // finish
+ $out .= '</object>'.NL;
+ $out .= '<!-- <![endif]-->'.NL;
+
+ return $out;
+}
+
+/**
+ * Prints HTML code for the given tab structure
+ *
+ * @param array $tabs tab structure
+ * @param string $current_tab the current tab id
+ */
+function html_tabs($tabs, $current_tab = null) {
+ echo '<ul class="tabs">'.NL;
+
+ foreach($tabs as $id => $tab) {
+ html_tab($tab['href'], $tab['caption'], $id === $current_tab);
+ }
+
+ echo '</ul>'.NL;
+}
+
+/**
+ * Prints a single tab
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ * @author Adrian Lang <mail@adrianlang.de>
+ *
+ * @param string $href - tab href
+ * @param string $caption - tab caption
+ * @param boolean $selected - is tab selected
+ */
+
+function html_tab($href, $caption, $selected=false) {
+ $tab = '<li>';
+ if ($selected) {
+ $tab .= '<strong>';
+ } else {
+ $tab .= '<a href="' . hsc($href) . '">';
+ }
+ $tab .= hsc($caption)
+ . '</' . ($selected ? 'strong' : 'a') . '>'
+ . '</li>'.NL;
+ echo $tab;
+}
+
+/**
+ * Display size change
+ *
+ * @param int $sizechange - size of change in Bytes
+ * @param Doku_Form $form - form to add elements to
+ */
+
+function html_sizechange($sizechange, Doku_Form $form) {
+ if(isset($sizechange)) {
+ $class = 'sizechange';
+ $value = filesize_h(abs($sizechange));
+ if($sizechange > 0) {
+ $class .= ' positive';
+ $value = '+' . $value;
+ } elseif($sizechange < 0) {
+ $class .= ' negative';
+ $value = '-' . $value;
+ } else {
+ $value = '±' . $value;
+ }
+ $form->addElement(form_makeOpenTag('span', array('class' => $class)));
+ $form->addElement($value);
+ $form->addElement(form_makeCloseTag('span'));
+ }
+}
diff --git a/platform/www/inc/httputils.php b/platform/www/inc/httputils.php
new file mode 100644
index 0000000..c365f4f
--- /dev/null
+++ b/platform/www/inc/httputils.php
@@ -0,0 +1,346 @@
+<?php
+/**
+ * Utilities for handling HTTP related tasks
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+define('HTTP_MULTIPART_BOUNDARY','D0KuW1K1B0uNDARY');
+define('HTTP_HEADER_LF',"\r\n");
+define('HTTP_CHUNK_SIZE',16*1024);
+
+/**
+ * Checks and sets HTTP headers for conditional HTTP requests
+ *
+ * @author Simon Willison <swillison@gmail.com>
+ * @link http://simonwillison.net/2003/Apr/23/conditionalGet/
+ *
+ * @param int $timestamp lastmodified time of the cache file
+ * @returns void or exits with previously header() commands executed
+ */
+function http_conditionalRequest($timestamp){
+ // A PHP implementation of conditional get, see
+ // http://fishbowl.pastiche.org/2002/10/21/http_conditional_get_for_rss_hackers/
+ $last_modified = substr(gmdate('r', $timestamp), 0, -5).'GMT';
+ $etag = '"'.md5($last_modified).'"';
+ // Send the headers
+ header("Last-Modified: $last_modified");
+ header("ETag: $etag");
+ // See if the client has provided the required headers
+ if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])){
+ $if_modified_since = stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']);
+ }else{
+ $if_modified_since = false;
+ }
+
+ if (isset($_SERVER['HTTP_IF_NONE_MATCH'])){
+ $if_none_match = stripslashes($_SERVER['HTTP_IF_NONE_MATCH']);
+ }else{
+ $if_none_match = false;
+ }
+
+ if (!$if_modified_since && !$if_none_match){
+ return;
+ }
+
+ // At least one of the headers is there - check them
+ if ($if_none_match && $if_none_match != $etag) {
+ return; // etag is there but doesn't match
+ }
+
+ if ($if_modified_since && $if_modified_since != $last_modified) {
+ return; // if-modified-since is there but doesn't match
+ }
+
+ // Nothing has changed since their last request - serve a 304 and exit
+ header('HTTP/1.0 304 Not Modified');
+
+ // don't produce output, even if compression is on
+ @ob_end_clean();
+ exit;
+}
+
+/**
+ * Let the webserver send the given file via x-sendfile method
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param string $file absolute path of file to send
+ * @returns void or exits with previous header() commands executed
+ */
+function http_sendfile($file) {
+ global $conf;
+
+ //use x-sendfile header to pass the delivery to compatible web servers
+ if($conf['xsendfile'] == 1){
+ header("X-LIGHTTPD-send-file: $file");
+ ob_end_clean();
+ exit;
+ }elseif($conf['xsendfile'] == 2){
+ header("X-Sendfile: $file");
+ ob_end_clean();
+ exit;
+ }elseif($conf['xsendfile'] == 3){
+ // FS#2388 nginx just needs the relative path.
+ $file = DOKU_REL.substr($file, strlen(fullpath(DOKU_INC)) + 1);
+ header("X-Accel-Redirect: $file");
+ ob_end_clean();
+ exit;
+ }
+}
+
+/**
+ * Send file contents supporting rangeRequests
+ *
+ * This function exits the running script
+ *
+ * @param resource $fh - file handle for an already open file
+ * @param int $size - size of the whole file
+ * @param int $mime - MIME type of the file
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function http_rangeRequest($fh,$size,$mime){
+ $ranges = array();
+ $isrange = false;
+
+ header('Accept-Ranges: bytes');
+
+ if(!isset($_SERVER['HTTP_RANGE'])){
+ // no range requested - send the whole file
+ $ranges[] = array(0,$size,$size);
+ }else{
+ $t = explode('=', $_SERVER['HTTP_RANGE']);
+ if (!$t[0]=='bytes') {
+ // we only understand byte ranges - send the whole file
+ $ranges[] = array(0,$size,$size);
+ }else{
+ $isrange = true;
+ // handle multiple ranges
+ $r = explode(',',$t[1]);
+ foreach($r as $x){
+ $p = explode('-', $x);
+ $start = (int)$p[0];
+ $end = (int)$p[1];
+ if (!$end) $end = $size - 1;
+ if ($start > $end || $start > $size || $end > $size){
+ header('HTTP/1.1 416 Requested Range Not Satisfiable');
+ print 'Bad Range Request!';
+ exit;
+ }
+ $len = $end - $start + 1;
+ $ranges[] = array($start,$end,$len);
+ }
+ }
+ }
+ $parts = count($ranges);
+
+ // now send the type and length headers
+ if(!$isrange){
+ header("Content-Type: $mime",true);
+ }else{
+ header('HTTP/1.1 206 Partial Content');
+ if($parts == 1){
+ header("Content-Type: $mime",true);
+ }else{
+ header('Content-Type: multipart/byteranges; boundary='.HTTP_MULTIPART_BOUNDARY,true);
+ }
+ }
+
+ // send all ranges
+ for($i=0; $i<$parts; $i++){
+ list($start,$end,$len) = $ranges[$i];
+
+ // multipart or normal headers
+ if($parts > 1){
+ echo HTTP_HEADER_LF.'--'.HTTP_MULTIPART_BOUNDARY.HTTP_HEADER_LF;
+ echo "Content-Type: $mime".HTTP_HEADER_LF;
+ echo "Content-Range: bytes $start-$end/$size".HTTP_HEADER_LF;
+ echo HTTP_HEADER_LF;
+ }else{
+ header("Content-Length: $len");
+ if($isrange){
+ header("Content-Range: bytes $start-$end/$size");
+ }
+ }
+
+ // send file content
+ fseek($fh,$start); //seek to start of range
+ $chunk = ($len > HTTP_CHUNK_SIZE) ? HTTP_CHUNK_SIZE : $len;
+ while (!feof($fh) && $chunk > 0) {
+ @set_time_limit(30); // large files can take a lot of time
+ print fread($fh, $chunk);
+ flush();
+ $len -= $chunk;
+ $chunk = ($len > HTTP_CHUNK_SIZE) ? HTTP_CHUNK_SIZE : $len;
+ }
+ }
+ if($parts > 1){
+ echo HTTP_HEADER_LF.'--'.HTTP_MULTIPART_BOUNDARY.'--'.HTTP_HEADER_LF;
+ }
+
+ // everything should be done here, exit (or return if testing)
+ if (defined('SIMPLE_TEST')) return;
+ exit;
+}
+
+/**
+ * Check for a gzipped version and create if necessary
+ *
+ * return true if there exists a gzip version of the uncompressed file
+ * (samepath/samefilename.sameext.gz) created after the uncompressed file
+ *
+ * @author Chris Smith <chris.eureka@jalakai.co.uk>
+ *
+ * @param string $uncompressed_file
+ * @return bool
+ */
+function http_gzip_valid($uncompressed_file) {
+ if(!DOKU_HAS_GZIP) return false;
+
+ $gzip = $uncompressed_file.'.gz';
+ if (filemtime($gzip) < filemtime($uncompressed_file)) { // filemtime returns false (0) if file doesn't exist
+ return copy($uncompressed_file, 'compress.zlib://'.$gzip);
+ }
+
+ return true;
+}
+
+/**
+ * Set HTTP headers and echo cachefile, if useable
+ *
+ * This function handles output of cacheable resource files. It ses the needed
+ * HTTP headers. If a useable cache is present, it is passed to the web server
+ * and the script is terminated.
+ *
+ * @param string $cache cache file name
+ * @param bool $cache_ok if cache can be used
+ */
+function http_cached($cache, $cache_ok) {
+ global $conf;
+
+ // check cache age & handle conditional request
+ // since the resource files are timestamped, we can use a long max age: 1 year
+ header('Cache-Control: public, max-age=31536000');
+ header('Pragma: public');
+ if($cache_ok){
+ http_conditionalRequest(filemtime($cache));
+ if($conf['allowdebug']) header("X-CacheUsed: $cache");
+
+ // finally send output
+ if ($conf['gzip_output'] && http_gzip_valid($cache)) {
+ header('Vary: Accept-Encoding');
+ header('Content-Encoding: gzip');
+ readfile($cache.".gz");
+ } else {
+ http_sendfile($cache);
+ readfile($cache);
+ }
+ exit;
+ }
+
+ http_conditionalRequest(time());
+}
+
+/**
+ * Cache content and print it
+ *
+ * @param string $file file name
+ * @param string $content
+ */
+function http_cached_finish($file, $content) {
+ global $conf;
+
+ // save cache file
+ io_saveFile($file, $content);
+ if(DOKU_HAS_GZIP) io_saveFile("$file.gz",$content);
+
+ // finally send output
+ if ($conf['gzip_output'] && DOKU_HAS_GZIP) {
+ header('Vary: Accept-Encoding');
+ header('Content-Encoding: gzip');
+ print gzencode($content,9,FORCE_GZIP);
+ } else {
+ print $content;
+ }
+}
+
+/**
+ * Fetches raw, unparsed POST data
+ *
+ * @return string
+ */
+function http_get_raw_post_data() {
+ static $postData = null;
+ if ($postData === null) {
+ $postData = file_get_contents('php://input');
+ }
+ return $postData;
+}
+
+/**
+ * Set the HTTP response status and takes care of the used PHP SAPI
+ *
+ * Inspired by CodeIgniter's set_status_header function
+ *
+ * @param int $code
+ * @param string $text
+ */
+function http_status($code = 200, $text = '') {
+ static $stati = array(
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 307 => 'Temporary Redirect',
+
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Long',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Requested Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version Not Supported'
+ );
+
+ if($text == '' && isset($stati[$code])) {
+ $text = $stati[$code];
+ }
+
+ $server_protocol = (isset($_SERVER['SERVER_PROTOCOL'])) ? $_SERVER['SERVER_PROTOCOL'] : false;
+
+ if(substr(php_sapi_name(), 0, 3) == 'cgi' || defined('SIMPLE_TEST')) {
+ header("Status: {$code} {$text}", true);
+ } elseif($server_protocol == 'HTTP/1.1' OR $server_protocol == 'HTTP/1.0') {
+ header($server_protocol." {$code} {$text}", true, $code);
+ } else {
+ header("HTTP/1.1 {$code} {$text}", true, $code);
+ }
+}
diff --git a/platform/www/inc/indexer.php b/platform/www/inc/indexer.php
new file mode 100644
index 0000000..ab02b8e
--- /dev/null
+++ b/platform/www/inc/indexer.php
@@ -0,0 +1,369 @@
+<?php
+/**
+ * Functions to create the fulltext search index
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+
+use dokuwiki\Extension\Event;
+use dokuwiki\Search\Indexer;
+
+// Version tag used to force rebuild on upgrade
+define('INDEXER_VERSION', 8);
+
+// set the minimum token length to use in the index (note, this doesn't apply to numeric tokens)
+if (!defined('IDX_MINWORDLENGTH')) define('IDX_MINWORDLENGTH',2);
+
+/**
+ * Version of the indexer taking into consideration the external tokenizer.
+ * The indexer is only compatible with data written by the same version.
+ *
+ * @triggers INDEXER_VERSION_GET
+ * Plugins that modify what gets indexed should hook this event and
+ * add their version info to the event data like so:
+ * $data[$plugin_name] = $plugin_version;
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ * @author Michael Hamann <michael@content-space.de>
+ *
+ * @return int|string
+ */
+function idx_get_version(){
+ static $indexer_version = null;
+ if ($indexer_version == null) {
+ $version = INDEXER_VERSION;
+
+ // DokuWiki version is included for the convenience of plugins
+ $data = array('dokuwiki'=>$version);
+ Event::createAndTrigger('INDEXER_VERSION_GET', $data, null, false);
+ unset($data['dokuwiki']); // this needs to be first
+ ksort($data);
+ foreach ($data as $plugin=>$vers)
+ $version .= '+'.$plugin.'='.$vers;
+ $indexer_version = $version;
+ }
+ return $indexer_version;
+}
+
+/**
+ * Measure the length of a string.
+ * Differs from strlen in handling of asian characters.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param string $w
+ * @return int
+ */
+function wordlen($w){
+ $l = strlen($w);
+ // If left alone, all chinese "words" will get put into w3.idx
+ // So the "length" of a "word" is faked
+ if(preg_match_all('/[\xE2-\xEF]/',$w,$leadbytes)) {
+ foreach($leadbytes[0] as $b)
+ $l += ord($b) - 0xE1;
+ }
+ return $l;
+}
+
+/**
+ * Create an instance of the indexer.
+ *
+ * @return Indexer an Indexer
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+function idx_get_indexer() {
+ static $Indexer;
+ if (!isset($Indexer)) {
+ $Indexer = new Indexer();
+ }
+ return $Indexer;
+}
+
+/**
+ * Returns words that will be ignored.
+ *
+ * @return array list of stop words
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+function & idx_get_stopwords() {
+ static $stopwords = null;
+ if (is_null($stopwords)) {
+ global $conf;
+ $swfile = DOKU_INC.'inc/lang/'.$conf['lang'].'/stopwords.txt';
+ if(file_exists($swfile)){
+ $stopwords = file($swfile, FILE_IGNORE_NEW_LINES);
+ }else{
+ $stopwords = array();
+ }
+ }
+ return $stopwords;
+}
+
+/**
+ * Adds/updates the search index for the given page
+ *
+ * Locking is handled internally.
+ *
+ * @param string $page name of the page to index
+ * @param boolean $verbose print status messages
+ * @param boolean $force force reindexing even when the index is up to date
+ * @return string|boolean the function completed successfully
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ */
+function idx_addPage($page, $verbose=false, $force=false) {
+ $idxtag = metaFN($page,'.indexed');
+ // check if page was deleted but is still in the index
+ if (!page_exists($page)) {
+ if (!file_exists($idxtag)) {
+ if ($verbose) print("Indexer: $page does not exist, ignoring".DOKU_LF);
+ return false;
+ }
+ $Indexer = idx_get_indexer();
+ $result = $Indexer->deletePage($page);
+ if ($result === "locked") {
+ if ($verbose) print("Indexer: locked".DOKU_LF);
+ return false;
+ }
+ @unlink($idxtag);
+ return $result;
+ }
+
+ // check if indexing needed
+ if(!$force && file_exists($idxtag)){
+ if(trim(io_readFile($idxtag)) == idx_get_version()){
+ $last = @filemtime($idxtag);
+ if($last > @filemtime(wikiFN($page))){
+ if ($verbose) print("Indexer: index for $page up to date".DOKU_LF);
+ return false;
+ }
+ }
+ }
+
+ $indexenabled = p_get_metadata($page, 'internal index', METADATA_RENDER_UNLIMITED);
+ if ($indexenabled === false) {
+ $result = false;
+ if (file_exists($idxtag)) {
+ $Indexer = idx_get_indexer();
+ $result = $Indexer->deletePage($page);
+ if ($result === "locked") {
+ if ($verbose) print("Indexer: locked".DOKU_LF);
+ return false;
+ }
+ @unlink($idxtag);
+ }
+ if ($verbose) print("Indexer: index disabled for $page".DOKU_LF);
+ return $result;
+ }
+
+ $Indexer = idx_get_indexer();
+ $pid = $Indexer->getPID($page);
+ if ($pid === false) {
+ if ($verbose) print("Indexer: getting the PID failed for $page".DOKU_LF);
+ return false;
+ }
+ $body = '';
+ $metadata = array();
+ $metadata['title'] = p_get_metadata($page, 'title', METADATA_RENDER_UNLIMITED);
+ if (($references = p_get_metadata($page, 'relation references', METADATA_RENDER_UNLIMITED)) !== null)
+ $metadata['relation_references'] = array_keys($references);
+ else
+ $metadata['relation_references'] = array();
+
+ if (($media = p_get_metadata($page, 'relation media', METADATA_RENDER_UNLIMITED)) !== null)
+ $metadata['relation_media'] = array_keys($media);
+ else
+ $metadata['relation_media'] = array();
+
+ $data = compact('page', 'body', 'metadata', 'pid');
+ $evt = new Event('INDEXER_PAGE_ADD', $data);
+ if ($evt->advise_before()) $data['body'] = $data['body'] . " " . rawWiki($page);
+ $evt->advise_after();
+ unset($evt);
+ extract($data);
+
+ $result = $Indexer->addPageWords($page, $body);
+ if ($result === "locked") {
+ if ($verbose) print("Indexer: locked".DOKU_LF);
+ return false;
+ }
+
+ if ($result) {
+ $result = $Indexer->addMetaKeys($page, $metadata);
+ if ($result === "locked") {
+ if ($verbose) print("Indexer: locked".DOKU_LF);
+ return false;
+ }
+ }
+
+ if ($result)
+ io_saveFile(metaFN($page,'.indexed'), idx_get_version());
+ if ($verbose) {
+ print("Indexer: finished".DOKU_LF);
+ return true;
+ }
+ return $result;
+}
+
+/**
+ * Find tokens in the fulltext index
+ *
+ * Takes an array of words and will return a list of matching
+ * pages for each one.
+ *
+ * Important: No ACL checking is done here! All results are
+ * returned, regardless of permissions
+ *
+ * @param array $words list of words to search for
+ * @return array list of pages found, associated with the search terms
+ */
+function idx_lookup(&$words) {
+ $Indexer = idx_get_indexer();
+ return $Indexer->lookup($words);
+}
+
+/**
+ * Split a string into tokens
+ *
+ * @param string $string
+ * @param bool $wc
+ *
+ * @return array
+ */
+function idx_tokenizer($string, $wc=false) {
+ $Indexer = idx_get_indexer();
+ return $Indexer->tokenizer($string, $wc);
+}
+
+/* For compatibility */
+
+/**
+ * Read the list of words in an index (if it exists).
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param string $idx
+ * @param string $suffix
+ * @return array
+ */
+function idx_getIndex($idx, $suffix) {
+ global $conf;
+ $fn = $conf['indexdir'].'/'.$idx.$suffix.'.idx';
+ if (!file_exists($fn)) return array();
+ return file($fn);
+}
+
+/**
+ * Get the list of lengths indexed in the wiki.
+ *
+ * Read the index directory or a cache file and returns
+ * a sorted array of lengths of the words used in the wiki.
+ *
+ * @author YoBoY <yoboy.leguesh@gmail.com>
+ *
+ * @return array
+ */
+function idx_listIndexLengths() {
+ global $conf;
+ // testing what we have to do, create a cache file or not.
+ if ($conf['readdircache'] == 0) {
+ $docache = false;
+ } else {
+ clearstatcache();
+ if (file_exists($conf['indexdir'].'/lengths.idx')
+ && (time() < @filemtime($conf['indexdir'].'/lengths.idx') + $conf['readdircache'])) {
+ if (
+ ($lengths = @file($conf['indexdir'].'/lengths.idx', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES))
+ !== false
+ ) {
+ $idx = array();
+ foreach ($lengths as $length) {
+ $idx[] = (int)$length;
+ }
+ return $idx;
+ }
+ }
+ $docache = true;
+ }
+
+ if ($conf['readdircache'] == 0 || $docache) {
+ $dir = @opendir($conf['indexdir']);
+ if ($dir === false)
+ return array();
+ $idx = array();
+ while (($f = readdir($dir)) !== false) {
+ if (substr($f, 0, 1) == 'i' && substr($f, -4) == '.idx') {
+ $i = substr($f, 1, -4);
+ if (is_numeric($i))
+ $idx[] = (int)$i;
+ }
+ }
+ closedir($dir);
+ sort($idx);
+ // save this in a file
+ if ($docache) {
+ $handle = @fopen($conf['indexdir'].'/lengths.idx', 'w');
+ @fwrite($handle, implode("\n", $idx));
+ @fclose($handle);
+ }
+ return $idx;
+ }
+
+ return array();
+}
+
+/**
+ * Get the word lengths that have been indexed.
+ *
+ * Reads the index directory and returns an array of lengths
+ * that there are indices for.
+ *
+ * @author YoBoY <yoboy.leguesh@gmail.com>
+ *
+ * @param array|int $filter
+ * @return array
+ */
+function idx_indexLengths($filter) {
+ global $conf;
+ $idx = array();
+ if (is_array($filter)) {
+ // testing if index files exist only
+ $path = $conf['indexdir']."/i";
+ foreach ($filter as $key => $value) {
+ if (file_exists($path.$key.'.idx'))
+ $idx[] = $key;
+ }
+ } else {
+ $lengths = idx_listIndexLengths();
+ foreach ($lengths as $key => $length) {
+ // keep all the values equal or superior
+ if ((int)$length >= (int)$filter)
+ $idx[] = $length;
+ }
+ }
+ return $idx;
+}
+
+/**
+ * Clean a name of a key for use as a file name.
+ *
+ * Romanizes non-latin characters, then strips away anything that's
+ * not a letter, number, or underscore.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param string $name
+ * @return string
+ */
+function idx_cleanName($name) {
+ $name = \dokuwiki\Utf8\Clean::romanize(trim((string)$name));
+ $name = preg_replace('#[ \./\\:-]+#', '_', $name);
+ $name = preg_replace('/[^A-Za-z0-9_]/', '', $name);
+ return strtolower($name);
+}
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/inc/infoutils.php b/platform/www/inc/infoutils.php
new file mode 100644
index 0000000..68d99ab
--- /dev/null
+++ b/platform/www/inc/infoutils.php
@@ -0,0 +1,527 @@
+<?php
+/**
+ * Information and debugging functions
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+use dokuwiki\HTTP\DokuHTTPClient;
+
+if(!defined('DOKU_MESSAGEURL')){
+ if(in_array('ssl', stream_get_transports())) {
+ define('DOKU_MESSAGEURL','https://update.dokuwiki.org/check/');
+ }else{
+ define('DOKU_MESSAGEURL','http://update.dokuwiki.org/check/');
+ }
+}
+
+/**
+ * Check for new messages from upstream
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function checkUpdateMessages(){
+ global $conf;
+ global $INFO;
+ global $updateVersion;
+ if(!$conf['updatecheck']) return;
+ if($conf['useacl'] && !$INFO['ismanager']) return;
+
+ $cf = getCacheName($updateVersion, '.updmsg');
+ $lm = @filemtime($cf);
+ $is_http = substr(DOKU_MESSAGEURL, 0, 5) != 'https';
+
+ // check if new messages needs to be fetched
+ if($lm < time()-(60*60*24) || $lm < @filemtime(DOKU_INC.DOKU_SCRIPT)){
+ @touch($cf);
+ dbglog("checkUpdateMessages(): downloading messages to ".$cf.($is_http?' (without SSL)':' (with SSL)'));
+ $http = new DokuHTTPClient();
+ $http->timeout = 12;
+ $resp = $http->get(DOKU_MESSAGEURL.$updateVersion);
+ if(is_string($resp) && ($resp == "" || substr(trim($resp), -1) == '%')) {
+ // basic sanity check that this is either an empty string response (ie "no messages")
+ // or it looks like one of our messages, not WiFi login or other interposed response
+ io_saveFile($cf,$resp);
+ } else {
+ dbglog("checkUpdateMessages(): unexpected HTTP response received");
+ }
+ }else{
+ dbglog("checkUpdateMessages(): messages up to date");
+ }
+
+ $data = io_readFile($cf);
+ // show messages through the usual message mechanism
+ $msgs = explode("\n%\n",$data);
+ foreach($msgs as $msg){
+ if($msg) msg($msg,2);
+ }
+}
+
+
+/**
+ * Return DokuWiki's version (split up in date and type)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function getVersionData(){
+ $version = array();
+ //import version string
+ if(file_exists(DOKU_INC.'VERSION')){
+ //official release
+ $version['date'] = trim(io_readFile(DOKU_INC.'VERSION'));
+ $version['type'] = 'Release';
+ }elseif(is_dir(DOKU_INC.'.git')){
+ $version['type'] = 'Git';
+ $version['date'] = 'unknown';
+
+ $inventory = DOKU_INC.'.git/logs/HEAD';
+ if(is_file($inventory)){
+ $sz = filesize($inventory);
+ $seek = max(0,$sz-2000); // read from back of the file
+ $fh = fopen($inventory,'rb');
+ fseek($fh,$seek);
+ $chunk = fread($fh,2000);
+ fclose($fh);
+ $chunk = trim($chunk);
+ $chunk = @array_pop(explode("\n",$chunk)); //last log line
+ $chunk = @array_shift(explode("\t",$chunk)); //strip commit msg
+ $chunk = explode(" ",$chunk);
+ array_pop($chunk); //strip timezone
+ $date = date('Y-m-d',array_pop($chunk));
+ if($date) $version['date'] = $date;
+ }
+ }else{
+ global $updateVersion;
+ $version['date'] = 'update version '.$updateVersion;
+ $version['type'] = 'snapshot?';
+ }
+ return $version;
+}
+
+/**
+ * Return DokuWiki's version (as a string)
+ *
+ * @author Anika Henke <anika@selfthinker.org>
+ */
+function getVersion(){
+ $version = getVersionData();
+ return $version['type'].' '.$version['date'];
+}
+
+/**
+ * Run a few sanity checks
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function check(){
+ global $conf;
+ global $INFO;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ if ($INFO['isadmin'] || $INFO['ismanager']){
+ msg('DokuWiki version: '.getVersion(),1);
+
+ if(version_compare(phpversion(),'5.6.0','<')){
+ msg('Your PHP version is too old ('.phpversion().' vs. 5.6.0+ needed)',-1);
+ }else{
+ msg('PHP version '.phpversion(),1);
+ }
+ } else {
+ if(version_compare(phpversion(),'5.6.0','<')){
+ msg('Your PHP version is too old',-1);
+ }
+ }
+
+ $mem = (int) php_to_byte(ini_get('memory_limit'));
+ if($mem){
+ if ($mem === -1) {
+ msg('PHP memory is unlimited', 1);
+ } else if ($mem < 16777216) {
+ msg('PHP is limited to less than 16MB RAM (' . filesize_h($mem) . ').
+ Increase memory_limit in php.ini', -1);
+ } else if ($mem < 20971520) {
+ msg('PHP is limited to less than 20MB RAM (' . filesize_h($mem) . '),
+ you might encounter problems with bigger pages. Increase memory_limit in php.ini', -1);
+ } else if ($mem < 33554432) {
+ msg('PHP is limited to less than 32MB RAM (' . filesize_h($mem) . '),
+ but that should be enough in most cases. If not, increase memory_limit in php.ini', 0);
+ } else {
+ msg('More than 32MB RAM (' . filesize_h($mem) . ') available.', 1);
+ }
+ }
+
+ if(is_writable($conf['changelog'])){
+ msg('Changelog is writable',1);
+ }else{
+ if (file_exists($conf['changelog'])) {
+ msg('Changelog is not writable',-1);
+ }
+ }
+
+ if (isset($conf['changelog_old']) && file_exists($conf['changelog_old'])) {
+ msg('Old changelog exists', 0);
+ }
+
+ if (file_exists($conf['changelog'].'_failed')) {
+ msg('Importing old changelog failed', -1);
+ } else if (file_exists($conf['changelog'].'_importing')) {
+ msg('Importing old changelog now.', 0);
+ } else if (file_exists($conf['changelog'].'_import_ok')) {
+ msg('Old changelog imported', 1);
+ if (!plugin_isdisabled('importoldchangelog')) {
+ msg('Importoldchangelog plugin not disabled after import', -1);
+ }
+ }
+
+ if(is_writable(DOKU_CONF)){
+ msg('conf directory is writable',1);
+ }else{
+ msg('conf directory is not writable',-1);
+ }
+
+ if($conf['authtype'] == 'plain'){
+ global $config_cascade;
+ if(is_writable($config_cascade['plainauth.users']['default'])){
+ msg('conf/users.auth.php is writable',1);
+ }else{
+ msg('conf/users.auth.php is not writable',0);
+ }
+ }
+
+ if(function_exists('mb_strpos')){
+ if(defined('UTF8_NOMBSTRING')){
+ msg('mb_string extension is available but will not be used',0);
+ }else{
+ msg('mb_string extension is available and will be used',1);
+ if(ini_get('mbstring.func_overload') != 0){
+ msg('mb_string function overloading is enabled, this will cause problems and should be disabled',-1);
+ }
+ }
+ }else{
+ msg('mb_string extension not available - PHP only replacements will be used',0);
+ }
+
+ if (!UTF8_PREGSUPPORT) {
+ msg('PHP is missing UTF-8 support in Perl-Compatible Regular Expressions (PCRE)', -1);
+ }
+ if (!UTF8_PROPERTYSUPPORT) {
+ msg('PHP is missing Unicode properties support in Perl-Compatible Regular Expressions (PCRE)', -1);
+ }
+
+ $loc = setlocale(LC_ALL, 0);
+ if(!$loc){
+ msg('No valid locale is set for your PHP setup. You should fix this',-1);
+ }elseif(stripos($loc,'utf') === false){
+ msg('Your locale <code>'.hsc($loc).'</code> seems not to be a UTF-8 locale,
+ you should fix this if you encounter problems.',0);
+ }else{
+ msg('Valid locale '.hsc($loc).' found.', 1);
+ }
+
+ if($conf['allowdebug']){
+ msg('Debugging support is enabled. If you don\'t need it you should set $conf[\'allowdebug\'] = 0',-1);
+ }else{
+ msg('Debugging support is disabled',1);
+ }
+
+ if($INFO['userinfo']['name']){
+ msg('You are currently logged in as '.$INPUT->server->str('REMOTE_USER').' ('.$INFO['userinfo']['name'].')',0);
+ msg('You are part of the groups '.join($INFO['userinfo']['grps'],', '),0);
+ }else{
+ msg('You are currently not logged in',0);
+ }
+
+ msg('Your current permission for this page is '.$INFO['perm'],0);
+
+ if (file_exists($INFO['filepath']) && is_writable($INFO['filepath'])) {
+ msg('The current page is writable by the webserver', 1);
+ } elseif (!file_exists($INFO['filepath']) && is_writable(dirname($INFO['filepath']))) {
+ msg('The current page can be created by the webserver', 1);
+ } else {
+ msg('The current page is not writable by the webserver', -1);
+ }
+
+ if ($INFO['writable']) {
+ msg('The current page is writable by you', 1);
+ } else {
+ msg('The current page is not writable by you', -1);
+ }
+
+ // Check for corrupted search index
+ $lengths = idx_listIndexLengths();
+ $index_corrupted = false;
+ foreach ($lengths as $length) {
+ if (count(idx_getIndex('w', $length)) != count(idx_getIndex('i', $length))) {
+ $index_corrupted = true;
+ break;
+ }
+ }
+
+ foreach (idx_getIndex('metadata', '') as $index) {
+ if (count(idx_getIndex($index.'_w', '')) != count(idx_getIndex($index.'_i', ''))) {
+ $index_corrupted = true;
+ break;
+ }
+ }
+
+ if($index_corrupted) {
+ msg(
+ 'The search index is corrupted. It might produce wrong results and most
+ probably needs to be rebuilt. See
+ <a href="http://www.dokuwiki.org/faq:searchindex">faq:searchindex</a>
+ for ways to rebuild the search index.', -1
+ );
+ } elseif(!empty($lengths)) {
+ msg('The search index seems to be working', 1);
+ } else {
+ msg(
+ 'The search index is empty. See
+ <a href="http://www.dokuwiki.org/faq:searchindex">faq:searchindex</a>
+ for help on how to fix the search index. If the default indexer
+ isn\'t used or the wiki is actually empty this is normal.'
+ );
+ }
+
+ // rough time check
+ $http = new DokuHTTPClient();
+ $http->max_redirect = 0;
+ $http->timeout = 3;
+ $http->sendRequest('http://www.dokuwiki.org', '', 'HEAD');
+ $now = time();
+ if(isset($http->resp_headers['date'])) {
+ $time = strtotime($http->resp_headers['date']);
+ $diff = $time - $now;
+
+ if(abs($diff) < 4) {
+ msg("Server time seems to be okay. Diff: {$diff}s", 1);
+ } else {
+ msg("Your server's clock seems to be out of sync!
+ Consider configuring a sync with a NTP server. Diff: {$diff}s");
+ }
+ }
+
+}
+
+/**
+ * Display a message to the user
+ *
+ * If HTTP headers were not sent yet the message is added
+ * to the global message array else it's printed directly
+ * using html_msgarea()
+ *
+ * Triggers INFOUTIL_MSG_SHOW
+ *
+ * @see html_msgarea()
+ * @param string $message
+ * @param int $lvl -1 = error, 0 = info, 1 = success, 2 = notify
+ * @param string $line line number
+ * @param string $file file number
+ * @param int $allow who's allowed to see the message, see MSG_* constants
+ */
+function msg($message,$lvl=0,$line='',$file='',$allow=MSG_PUBLIC){
+ global $MSG, $MSG_shown;
+ static $errors = [
+ -1 => 'error',
+ 0 => 'info',
+ 1 => 'success',
+ 2 => 'notify',
+ ];
+
+ $msgdata = [
+ 'msg' => $message,
+ 'lvl' => $errors[$lvl],
+ 'allow' => $allow,
+ 'line' => $line,
+ 'file' => $file,
+ ];
+
+ $evt = new \dokuwiki\Extension\Event('INFOUTIL_MSG_SHOW', $msgdata);
+ if ($evt->advise_before()) {
+ /* Show msg normally - event could suppress message show */
+ if($msgdata['line'] || $msgdata['file']) {
+ $basename = \dokuwiki\Utf8\PhpString::basename($msgdata['file']);
+ $msgdata['msg'] .=' ['.$basename.':'.$msgdata['line'].']';
+ }
+
+ if(!isset($MSG)) $MSG = array();
+ $MSG[] = $msgdata;
+ if(isset($MSG_shown) || headers_sent()){
+ if(function_exists('html_msgarea')){
+ html_msgarea();
+ }else{
+ print "ERROR(".$msgdata['lvl'].") ".$msgdata['msg']."\n";
+ }
+ unset($GLOBALS['MSG']);
+ }
+ }
+ $evt->advise_after();
+ unset($evt);
+}
+/**
+ * Determine whether the current user is allowed to view the message
+ * in the $msg data structure
+ *
+ * @param $msg array dokuwiki msg structure
+ * msg => string, the message
+ * lvl => int, level of the message (see msg() function)
+ * allow => int, flag used to determine who is allowed to see the message
+ * see MSG_* constants
+ * @return bool
+ */
+function info_msg_allowed($msg){
+ global $INFO, $auth;
+
+ // is the message public? - everyone and anyone can see it
+ if (empty($msg['allow']) || ($msg['allow'] == MSG_PUBLIC)) return true;
+
+ // restricted msg, but no authentication
+ if (empty($auth)) return false;
+
+ switch ($msg['allow']){
+ case MSG_USERS_ONLY:
+ return !empty($INFO['userinfo']);
+
+ case MSG_MANAGERS_ONLY:
+ return $INFO['ismanager'];
+
+ case MSG_ADMINS_ONLY:
+ return $INFO['isadmin'];
+
+ default:
+ trigger_error('invalid msg allow restriction. msg="'.$msg['msg'].'" allow='.$msg['allow'].'"',
+ E_USER_WARNING);
+ return $INFO['isadmin'];
+ }
+
+ return false;
+}
+
+/**
+ * print debug messages
+ *
+ * little function to print the content of a var
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $msg
+ * @param bool $hidden
+ */
+function dbg($msg,$hidden=false){
+ if($hidden){
+ echo "<!--\n";
+ print_r($msg);
+ echo "\n-->";
+ }else{
+ echo '<pre class="dbg">';
+ echo hsc(print_r($msg,true));
+ echo '</pre>';
+ }
+}
+
+/**
+ * Print info to a log file
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $msg
+ * @param string $header
+ */
+function dbglog($msg,$header=''){
+ global $conf;
+ /* @var Input $INPUT */
+ global $INPUT;
+
+ // The debug log isn't automatically cleaned thus only write it when
+ // debugging has been enabled by the user.
+ if($conf['allowdebug'] !== 1) return;
+ if(is_object($msg) || is_array($msg)){
+ $msg = print_r($msg,true);
+ }
+
+ if($header) $msg = "$header\n$msg";
+
+ $file = $conf['cachedir'].'/debug.log';
+ $fh = fopen($file,'a');
+ if($fh){
+ fwrite($fh,date('H:i:s ').$INPUT->server->str('REMOTE_ADDR').': '.$msg."\n");
+ fclose($fh);
+ }
+}
+
+/**
+ * Log accesses to deprecated fucntions to the debug log
+ *
+ * @param string $alternative The function or method that should be used instead
+ * @triggers INFO_DEPRECATION_LOG
+ */
+function dbg_deprecated($alternative = '') {
+ \dokuwiki\Debug\DebugHelper::dbgDeprecatedFunction($alternative, 2);
+}
+
+/**
+ * Print a reversed, prettyprinted backtrace
+ *
+ * @author Gary Owen <gary_owen@bigfoot.com>
+ */
+function dbg_backtrace(){
+ // Get backtrace
+ $backtrace = debug_backtrace();
+
+ // Unset call to debug_print_backtrace
+ array_shift($backtrace);
+
+ // Iterate backtrace
+ $calls = array();
+ $depth = count($backtrace) - 1;
+ foreach ($backtrace as $i => $call) {
+ $location = $call['file'] . ':' . $call['line'];
+ $function = (isset($call['class'])) ?
+ $call['class'] . $call['type'] . $call['function'] : $call['function'];
+
+ $params = array();
+ if (isset($call['args'])){
+ foreach($call['args'] as $arg){
+ if(is_object($arg)){
+ $params[] = '[Object '.get_class($arg).']';
+ }elseif(is_array($arg)){
+ $params[] = '[Array]';
+ }elseif(is_null($arg)){
+ $params[] = '[NULL]';
+ }else{
+ $params[] = (string) '"'.$arg.'"';
+ }
+ }
+ }
+ $params = implode(', ',$params);
+
+ $calls[$depth - $i] = sprintf('%s(%s) called at %s',
+ $function,
+ str_replace("\n", '\n', $params),
+ $location);
+ }
+ ksort($calls);
+
+ return implode("\n", $calls);
+}
+
+/**
+ * Remove all data from an array where the key seems to point to sensitive data
+ *
+ * This is used to remove passwords, mail addresses and similar data from the
+ * debug output
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $data
+ */
+function debug_guard(&$data){
+ foreach($data as $key => $value){
+ if(preg_match('/(notify|pass|auth|secret|ftp|userinfo|token|buid|mail|proxy)/i',$key)){
+ $data[$key] = '***';
+ continue;
+ }
+ if(is_array($value)) debug_guard($data[$key]);
+ }
+}
diff --git a/platform/www/inc/init.php b/platform/www/inc/init.php
new file mode 100644
index 0000000..f9bb534
--- /dev/null
+++ b/platform/www/inc/init.php
@@ -0,0 +1,623 @@
+<?php
+/**
+ * Initialize some defaults needed for DokuWiki
+ */
+
+use dokuwiki\Extension\Event;
+use dokuwiki\Extension\EventHandler;
+
+/**
+ * timing Dokuwiki execution
+ *
+ * @param integer $start
+ *
+ * @return mixed
+ */
+function delta_time($start=0) {
+ return microtime(true)-((float)$start);
+}
+define('DOKU_START_TIME', delta_time());
+
+global $config_cascade;
+$config_cascade = array();
+
+// if available load a preload config file
+$preload = fullpath(dirname(__FILE__)).'/preload.php';
+if (file_exists($preload)) include($preload);
+
+// define the include path
+if(!defined('DOKU_INC')) define('DOKU_INC',fullpath(dirname(__FILE__).'/../').'/');
+
+// define Plugin dir
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+
+// define config path (packagers may want to change this to /etc/dokuwiki/)
+if(!defined('DOKU_CONF')) define('DOKU_CONF',DOKU_INC.'conf/');
+
+// check for error reporting override or set error reporting to sane values
+if (!defined('DOKU_E_LEVEL') && file_exists(DOKU_CONF.'report_e_all')) {
+ define('DOKU_E_LEVEL', E_ALL);
+}
+if (!defined('DOKU_E_LEVEL')) {
+ error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT);
+} else {
+ error_reporting(DOKU_E_LEVEL);
+}
+
+// avoid caching issues #1594
+header('Vary: Cookie');
+
+// init memory caches
+global $cache_revinfo;
+ $cache_revinfo = array();
+global $cache_wikifn;
+ $cache_wikifn = array();
+global $cache_cleanid;
+ $cache_cleanid = array();
+global $cache_authname;
+ $cache_authname = array();
+global $cache_metadata;
+ $cache_metadata = array();
+
+// always include 'inc/config_cascade.php'
+// previously in preload.php set fields of $config_cascade will be merged with the defaults
+include(DOKU_INC.'inc/config_cascade.php');
+
+//prepare config array()
+global $conf;
+$conf = array();
+
+// load the global config file(s)
+foreach (array('default','local','protected') as $config_group) {
+ if (empty($config_cascade['main'][$config_group])) continue;
+ foreach ($config_cascade['main'][$config_group] as $config_file) {
+ if (file_exists($config_file)) {
+ include($config_file);
+ }
+ }
+}
+
+//prepare license array()
+global $license;
+$license = array();
+
+// load the license file(s)
+foreach (array('default','local') as $config_group) {
+ if (empty($config_cascade['license'][$config_group])) continue;
+ foreach ($config_cascade['license'][$config_group] as $config_file) {
+ if(file_exists($config_file)){
+ include($config_file);
+ }
+ }
+}
+
+// set timezone (as in pre 5.3.0 days)
+date_default_timezone_set(@date_default_timezone_get());
+
+// define baseURL
+if(!defined('DOKU_REL')) define('DOKU_REL',getBaseURL(false));
+if(!defined('DOKU_URL')) define('DOKU_URL',getBaseURL(true));
+if(!defined('DOKU_BASE')){
+ if($conf['canonical']){
+ define('DOKU_BASE',DOKU_URL);
+ }else{
+ define('DOKU_BASE',DOKU_REL);
+ }
+}
+
+// define whitespace
+if(!defined('NL')) define ('NL',"\n");
+if(!defined('DOKU_LF')) define ('DOKU_LF',"\n");
+if(!defined('DOKU_TAB')) define ('DOKU_TAB',"\t");
+
+// define cookie and session id, append server port when securecookie is configured FS#1664
+if (!defined('DOKU_COOKIE')) {
+ $serverPort = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : '';
+ define('DOKU_COOKIE', 'DW' . md5(DOKU_REL . (($conf['securecookie']) ? $serverPort : '')));
+ unset($serverPort);
+}
+
+// define main script
+if(!defined('DOKU_SCRIPT')) define('DOKU_SCRIPT','doku.php');
+
+if(!defined('DOKU_TPL')) {
+ /**
+ * @deprecated 2012-10-13 replaced by more dynamic method
+ * @see tpl_basedir()
+ */
+ define('DOKU_TPL', DOKU_BASE.'lib/tpl/'.$conf['template'].'/');
+}
+
+if(!defined('DOKU_TPLINC')) {
+ /**
+ * @deprecated 2012-10-13 replaced by more dynamic method
+ * @see tpl_incdir()
+ */
+ define('DOKU_TPLINC', DOKU_INC.'lib/tpl/'.$conf['template'].'/');
+}
+
+// make session rewrites XHTML compliant
+@ini_set('arg_separator.output', '&amp;');
+
+// make sure global zlib does not interfere FS#1132
+@ini_set('zlib.output_compression', 'off');
+
+// increase PCRE backtrack limit
+@ini_set('pcre.backtrack_limit', '20971520');
+
+// enable gzip compression if supported
+$httpAcceptEncoding = isset($_SERVER['HTTP_ACCEPT_ENCODING']) ? $_SERVER['HTTP_ACCEPT_ENCODING'] : '';
+$conf['gzip_output'] &= (strpos($httpAcceptEncoding, 'gzip') !== false);
+global $ACT;
+if ($conf['gzip_output'] &&
+ !defined('DOKU_DISABLE_GZIP_OUTPUT') &&
+ function_exists('ob_gzhandler') &&
+ // Disable compression when a (compressed) sitemap might be delivered
+ // See https://bugs.dokuwiki.org/index.php?do=details&task_id=2576
+ $ACT != 'sitemap') {
+ ob_start('ob_gzhandler');
+}
+
+// init session
+if(!headers_sent() && !defined('NOSESSION')) {
+ if(!defined('DOKU_SESSION_NAME')) define ('DOKU_SESSION_NAME', "DokuWiki");
+ if(!defined('DOKU_SESSION_LIFETIME')) define ('DOKU_SESSION_LIFETIME', 0);
+ if(!defined('DOKU_SESSION_PATH')) {
+ $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
+ define ('DOKU_SESSION_PATH', $cookieDir);
+ }
+ if(!defined('DOKU_SESSION_DOMAIN')) define ('DOKU_SESSION_DOMAIN', '');
+
+ // start the session
+ init_session();
+
+ // load left over messages
+ if(isset($_SESSION[DOKU_COOKIE]['msg'])) {
+ $MSG = $_SESSION[DOKU_COOKIE]['msg'];
+ unset($_SESSION[DOKU_COOKIE]['msg']);
+ }
+}
+
+// don't let cookies ever interfere with request vars
+$_REQUEST = array_merge($_GET,$_POST);
+
+// we don't want a purge URL to be digged
+if(isset($_REQUEST['purge']) && !empty($_SERVER['HTTP_REFERER'])) unset($_REQUEST['purge']);
+
+// precalculate file creation modes
+init_creationmodes();
+
+// make real paths and check them
+init_paths();
+init_files();
+
+// setup plugin controller class (can be overwritten in preload.php)
+global $plugin_controller_class, $plugin_controller;
+if (empty($plugin_controller_class)) $plugin_controller_class = dokuwiki\Extension\PluginController::class;
+
+// load libraries
+require_once(DOKU_INC.'vendor/autoload.php');
+require_once(DOKU_INC.'inc/load.php');
+
+// disable gzip if not available
+define('DOKU_HAS_BZIP', function_exists('bzopen'));
+define('DOKU_HAS_GZIP', function_exists('gzopen'));
+if($conf['compression'] == 'bz2' && !DOKU_HAS_BZIP) {
+ $conf['compression'] = 'gz';
+}
+if($conf['compression'] == 'gz' && !DOKU_HAS_GZIP) {
+ $conf['compression'] = 0;
+}
+
+// input handle class
+global $INPUT;
+$INPUT = new \dokuwiki\Input\Input();
+
+// initialize plugin controller
+$plugin_controller = new $plugin_controller_class();
+
+// initialize the event handler
+global $EVENT_HANDLER;
+$EVENT_HANDLER = new EventHandler();
+
+$local = $conf['lang'];
+Event::createAndTrigger('INIT_LANG_LOAD', $local, 'init_lang', true);
+
+
+// setup authentication system
+if (!defined('NOSESSION')) {
+ auth_setup();
+}
+
+// setup mail system
+mail_setup();
+
+/**
+ * Initializes the session
+ *
+ * Makes sure the passed session cookie is valid, invalid ones are ignored an a new session ID is issued
+ *
+ * @link http://stackoverflow.com/a/33024310/172068
+ * @link http://php.net/manual/en/session.configuration.php#ini.session.sid-length
+ */
+function init_session() {
+ global $conf;
+ session_name(DOKU_SESSION_NAME);
+ session_set_cookie_params(
+ DOKU_SESSION_LIFETIME,
+ DOKU_SESSION_PATH,
+ DOKU_SESSION_DOMAIN,
+ ($conf['securecookie'] && is_ssl()),
+ true
+ );
+
+ // make sure the session cookie contains a valid session ID
+ if(isset($_COOKIE[DOKU_SESSION_NAME]) && !preg_match('/^[-,a-zA-Z0-9]{22,256}$/', $_COOKIE[DOKU_SESSION_NAME])) {
+ unset($_COOKIE[DOKU_SESSION_NAME]);
+ }
+
+ session_start();
+}
+
+
+/**
+ * Checks paths from config file
+ */
+function init_paths(){
+ global $conf;
+
+ $paths = array('datadir' => 'pages',
+ 'olddir' => 'attic',
+ 'mediadir' => 'media',
+ 'mediaolddir' => 'media_attic',
+ 'metadir' => 'meta',
+ 'mediametadir' => 'media_meta',
+ 'cachedir' => 'cache',
+ 'indexdir' => 'index',
+ 'lockdir' => 'locks',
+ 'tmpdir' => 'tmp');
+
+ foreach($paths as $c => $p) {
+ $path = empty($conf[$c]) ? $conf['savedir'].'/'.$p : $conf[$c];
+ $conf[$c] = init_path($path);
+ if(empty($conf[$c]))
+ nice_die("The $c ('$p') at $path is not found, isn't accessible or writable.
+ You should check your config and permission settings.
+ Or maybe you want to <a href=\"install.php\">run the
+ installer</a>?");
+ }
+
+ // path to old changelog only needed for upgrading
+ $conf['changelog_old'] = init_path(
+ (isset($conf['changelog'])) ? ($conf['changelog']) : ($conf['savedir'] . '/changes.log')
+ );
+ if ($conf['changelog_old']=='') { unset($conf['changelog_old']); }
+ // hardcoded changelog because it is now a cache that lives in meta
+ $conf['changelog'] = $conf['metadir'].'/_dokuwiki.changes';
+ $conf['media_changelog'] = $conf['metadir'].'/_media.changes';
+}
+
+/**
+ * Load the language strings
+ *
+ * @param string $langCode language code, as passed by event handler
+ */
+function init_lang($langCode) {
+ //prepare language array
+ global $lang, $config_cascade;
+ $lang = array();
+
+ //load the language files
+ require(DOKU_INC.'inc/lang/en/lang.php');
+ foreach ($config_cascade['lang']['core'] as $config_file) {
+ if (file_exists($config_file . 'en/lang.php')) {
+ include($config_file . 'en/lang.php');
+ }
+ }
+
+ if ($langCode && $langCode != 'en') {
+ if (file_exists(DOKU_INC."inc/lang/$langCode/lang.php")) {
+ require(DOKU_INC."inc/lang/$langCode/lang.php");
+ }
+ foreach ($config_cascade['lang']['core'] as $config_file) {
+ if (file_exists($config_file . "$langCode/lang.php")) {
+ include($config_file . "$langCode/lang.php");
+ }
+ }
+ }
+}
+
+/**
+ * Checks the existence of certain files and creates them if missing.
+ */
+function init_files(){
+ global $conf;
+
+ $files = array($conf['indexdir'].'/page.idx');
+
+ foreach($files as $file){
+ if(!file_exists($file)){
+ $fh = @fopen($file,'a');
+ if($fh){
+ fclose($fh);
+ if($conf['fperm']) chmod($file, $conf['fperm']);
+ }else{
+ nice_die("$file is not writable. Check your permissions settings!");
+ }
+ }
+ }
+}
+
+/**
+ * Returns absolute path
+ *
+ * This tries the given path first, then checks in DOKU_INC.
+ * Check for accessibility on directories as well.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $path
+ *
+ * @return bool|string
+ */
+function init_path($path){
+ // check existence
+ $p = fullpath($path);
+ if(!file_exists($p)){
+ $p = fullpath(DOKU_INC.$path);
+ if(!file_exists($p)){
+ return '';
+ }
+ }
+
+ // check writability
+ if(!@is_writable($p)){
+ return '';
+ }
+
+ // check accessability (execute bit) for directories
+ if(@is_dir($p) && !file_exists("$p/.")){
+ return '';
+ }
+
+ return $p;
+}
+
+/**
+ * Sets the internal config values fperm and dperm which, when set,
+ * will be used to change the permission of a newly created dir or
+ * file with chmod. Considers the influence of the system's umask
+ * setting the values only if needed.
+ */
+function init_creationmodes(){
+ global $conf;
+
+ // Legacy support for old umask/dmask scheme
+ unset($conf['dmask']);
+ unset($conf['fmask']);
+ unset($conf['umask']);
+ unset($conf['fperm']);
+ unset($conf['dperm']);
+
+ // get system umask, fallback to 0 if none available
+ $umask = @umask();
+ if(!$umask) $umask = 0000;
+
+ // check what is set automatically by the system on file creation
+ // and set the fperm param if it's not what we want
+ $auto_fmode = $conf['fmode'] & ~$umask;
+ if($auto_fmode != $conf['fmode']) $conf['fperm'] = $conf['fmode'];
+
+ // check what is set automatically by the system on file creation
+ // and set the dperm param if it's not what we want
+ $auto_dmode = $conf['dmode'] & ~$umask;
+ if($auto_dmode != $conf['dmode']) $conf['dperm'] = $conf['dmode'];
+}
+
+/**
+ * Returns the full absolute URL to the directory where
+ * DokuWiki is installed in (includes a trailing slash)
+ *
+ * !! Can not access $_SERVER values through $INPUT
+ * !! here as this function is called before $INPUT is
+ * !! initialized.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param null|string $abs
+ *
+ * @return string
+ */
+function getBaseURL($abs=null){
+ global $conf;
+ //if canonical url enabled always return absolute
+ if(is_null($abs)) $abs = $conf['canonical'];
+
+ if(!empty($conf['basedir'])){
+ $dir = $conf['basedir'];
+ }elseif(substr($_SERVER['SCRIPT_NAME'],-4) == '.php'){
+ $dir = dirname($_SERVER['SCRIPT_NAME']);
+ }elseif(substr($_SERVER['PHP_SELF'],-4) == '.php'){
+ $dir = dirname($_SERVER['PHP_SELF']);
+ }elseif($_SERVER['DOCUMENT_ROOT'] && $_SERVER['SCRIPT_FILENAME']){
+ $dir = preg_replace ('/^'.preg_quote($_SERVER['DOCUMENT_ROOT'],'/').'/','',
+ $_SERVER['SCRIPT_FILENAME']);
+ $dir = dirname('/'.$dir);
+ }else{
+ $dir = '.'; //probably wrong
+ }
+
+ $dir = str_replace('\\','/',$dir); // bugfix for weird WIN behaviour
+ $dir = preg_replace('#//+#','/',"/$dir/"); // ensure leading and trailing slashes
+
+ //handle script in lib/exe dir
+ $dir = preg_replace('!lib/exe/$!','',$dir);
+
+ //handle script in lib/plugins dir
+ $dir = preg_replace('!lib/plugins/.*$!','',$dir);
+
+ //finish here for relative URLs
+ if(!$abs) return $dir;
+
+ //use config if available, trim any slash from end of baseurl to avoid multiple consecutive slashes in the path
+ if(!empty($conf['baseurl'])) return rtrim($conf['baseurl'],'/').$dir;
+
+ //split hostheader into host and port
+ if(isset($_SERVER['HTTP_HOST'])){
+ $parsed_host = parse_url('http://'.$_SERVER['HTTP_HOST']);
+ $host = isset($parsed_host['host']) ? $parsed_host['host'] : null;
+ $port = isset($parsed_host['port']) ? $parsed_host['port'] : null;
+ }elseif(isset($_SERVER['SERVER_NAME'])){
+ $parsed_host = parse_url('http://'.$_SERVER['SERVER_NAME']);
+ $host = isset($parsed_host['host']) ? $parsed_host['host'] : null;
+ $port = isset($parsed_host['port']) ? $parsed_host['port'] : null;
+ }else{
+ $host = php_uname('n');
+ $port = '';
+ }
+
+ if(is_null($port)){
+ $port = '';
+ }
+
+ if(!is_ssl()){
+ $proto = 'http://';
+ if ($port == '80') {
+ $port = '';
+ }
+ }else{
+ $proto = 'https://';
+ if ($port == '443') {
+ $port = '';
+ }
+ }
+
+ if($port !== '') $port = ':'.$port;
+
+ return $proto.$host.$port.$dir;
+}
+
+/**
+ * Check if accessed via HTTPS
+ *
+ * Apache leaves ,$_SERVER['HTTPS'] empty when not available, IIS sets it to 'off'.
+ * 'false' and 'disabled' are just guessing
+ *
+ * @returns bool true when SSL is active
+ */
+function is_ssl() {
+ // check if we are behind a reverse proxy
+ if(isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
+ if($_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ if(!isset($_SERVER['HTTPS']) ||
+ preg_match('/^(|off|false|disabled)$/i', $_SERVER['HTTPS'])) {
+ return false;
+ } else {
+ return true;
+ }
+}
+
+/**
+ * checks it is windows OS
+ * @return bool
+ */
+function isWindows() {
+ return (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? true : false;
+}
+
+/**
+ * print a nice message even if no styles are loaded yet.
+ *
+ * @param integer|string $msg
+ */
+function nice_die($msg){
+ echo<<<EOT
+<!DOCTYPE html>
+<html>
+<head><title>DokuWiki Setup Error</title></head>
+<body style="font-family: Arial, sans-serif">
+ <div style="width:60%; margin: auto; background-color: #fcc;
+ border: 1px solid #faa; padding: 0.5em 1em;">
+ <h1 style="font-size: 120%">DokuWiki Setup Error</h1>
+ <p>$msg</p>
+ </div>
+</body>
+</html>
+EOT;
+ if(defined('DOKU_UNITTEST')) {
+ throw new RuntimeException('nice_die: '.$msg);
+ }
+ exit(1);
+}
+
+/**
+ * A realpath() replacement
+ *
+ * This function behaves similar to PHP's realpath() but does not resolve
+ * symlinks or accesses upper directories
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author <richpageau at yahoo dot co dot uk>
+ * @link http://php.net/manual/en/function.realpath.php#75992
+ *
+ * @param string $path
+ * @param bool $exists
+ *
+ * @return bool|string
+ */
+function fullpath($path,$exists=false){
+ static $run = 0;
+ $root = '';
+ $iswin = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' || !empty($GLOBALS['DOKU_UNITTEST_ASSUME_WINDOWS']));
+
+ // find the (indestructable) root of the path - keeps windows stuff intact
+ if($path[0] == '/'){
+ $root = '/';
+ }elseif($iswin){
+ // match drive letter and UNC paths
+ if(preg_match('!^([a-zA-z]:)(.*)!',$path,$match)){
+ $root = $match[1].'/';
+ $path = $match[2];
+ }else if(preg_match('!^(\\\\\\\\[^\\\\/]+\\\\[^\\\\/]+[\\\\/])(.*)!',$path,$match)){
+ $root = $match[1];
+ $path = $match[2];
+ }
+ }
+ $path = str_replace('\\','/',$path);
+
+ // if the given path wasn't absolute already, prepend the script path and retry
+ if(!$root){
+ $base = dirname($_SERVER['SCRIPT_FILENAME']);
+ $path = $base.'/'.$path;
+ if($run == 0){ // avoid endless recursion when base isn't absolute for some reason
+ $run++;
+ return fullpath($path,$exists);
+ }
+ }
+ $run = 0;
+
+ // canonicalize
+ $path=explode('/', $path);
+ $newpath=array();
+ foreach($path as $p) {
+ if ($p === '' || $p === '.') continue;
+ if ($p==='..') {
+ array_pop($newpath);
+ continue;
+ }
+ array_push($newpath, $p);
+ }
+ $finalpath = $root.implode('/', $newpath);
+
+ // check for existence when needed (except when unit testing)
+ if($exists && !defined('DOKU_UNITTEST') && !file_exists($finalpath)) {
+ return false;
+ }
+ return $finalpath;
+}
+
diff --git a/platform/www/inc/io.php b/platform/www/inc/io.php
new file mode 100644
index 0000000..1dfabe8
--- /dev/null
+++ b/platform/www/inc/io.php
@@ -0,0 +1,781 @@
+<?php
+/**
+ * File IO functions
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+use dokuwiki\HTTP\DokuHTTPClient;
+use dokuwiki\Extension\Event;
+
+/**
+ * Removes empty directories
+ *
+ * Sends IO_NAMESPACE_DELETED events for 'pages' and 'media' namespaces.
+ * Event data:
+ * $data[0] ns: The colon separated namespace path minus the trailing page name.
+ * $data[1] ns_type: 'pages' or 'media' namespace tree.
+ *
+ * @param string $id - a pageid, the namespace of that id will be tried to deleted
+ * @param string $basedir - the config name of the type to delete (datadir or mediadir usally)
+ * @return bool - true if at least one namespace was deleted
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ */
+function io_sweepNS($id,$basedir='datadir'){
+ global $conf;
+ $types = array ('datadir'=>'pages', 'mediadir'=>'media');
+ $ns_type = (isset($types[$basedir])?$types[$basedir]:false);
+
+ $delone = false;
+
+ //scan all namespaces
+ while(($id = getNS($id)) !== false){
+ $dir = $conf[$basedir].'/'.utf8_encodeFN(str_replace(':','/',$id));
+
+ //try to delete dir else return
+ if(@rmdir($dir)) {
+ if ($ns_type!==false) {
+ $data = array($id, $ns_type);
+ $delone = true; // we deleted at least one dir
+ Event::createAndTrigger('IO_NAMESPACE_DELETED', $data);
+ }
+ } else { return $delone; }
+ }
+ return $delone;
+}
+
+/**
+ * Used to read in a DokuWiki page from file, and send IO_WIKIPAGE_READ events.
+ *
+ * Generates the action event which delegates to io_readFile().
+ * Action plugins are allowed to modify the page content in transit.
+ * The file path should not be changed.
+ *
+ * Event data:
+ * $data[0] The raw arguments for io_readFile as an array.
+ * $data[1] ns: The colon separated namespace path minus the trailing page name. (false if root ns)
+ * $data[2] page_name: The wiki page name.
+ * $data[3] rev: The page revision, false for current wiki pages.
+ *
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ *
+ * @param string $file filename
+ * @param string $id page id
+ * @param bool|int $rev revision timestamp
+ * @return string
+ */
+function io_readWikiPage($file, $id, $rev=false) {
+ if (empty($rev)) { $rev = false; }
+ $data = array(array($file, true), getNS($id), noNS($id), $rev);
+ return Event::createAndTrigger('IO_WIKIPAGE_READ', $data, '_io_readWikiPage_action', false);
+}
+
+/**
+ * Callback adapter for io_readFile().
+ *
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ *
+ * @param array $data event data
+ * @return string
+ */
+function _io_readWikiPage_action($data) {
+ if (is_array($data) && is_array($data[0]) && count($data[0])===2) {
+ return call_user_func_array('io_readFile', $data[0]);
+ } else {
+ return ''; //callback error
+ }
+}
+
+/**
+ * Returns content of $file as cleaned string.
+ *
+ * Uses gzip if extension is .gz
+ *
+ * If you want to use the returned value in unserialize
+ * be sure to set $clean to false!
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $file filename
+ * @param bool $clean
+ * @return string|bool the file contents or false on error
+ */
+function io_readFile($file,$clean=true){
+ $ret = '';
+ if(file_exists($file)){
+ if(substr($file,-3) == '.gz'){
+ if(!DOKU_HAS_GZIP) return false;
+ $ret = gzfile($file);
+ if(is_array($ret)) $ret = join('', $ret);
+ }else if(substr($file,-4) == '.bz2'){
+ if(!DOKU_HAS_BZIP) return false;
+ $ret = bzfile($file);
+ }else{
+ $ret = file_get_contents($file);
+ }
+ }
+ if($ret === null) return false;
+ if($ret !== false && $clean){
+ return cleanText($ret);
+ }else{
+ return $ret;
+ }
+}
+/**
+ * Returns the content of a .bz2 compressed file as string
+ *
+ * @author marcel senf <marcel@rucksackreinigung.de>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $file filename
+ * @param bool $array return array of lines
+ * @return string|array|bool content or false on error
+ */
+function bzfile($file, $array=false) {
+ $bz = bzopen($file,"r");
+ if($bz === false) return false;
+
+ if($array) $lines = array();
+ $str = '';
+ while (!feof($bz)) {
+ //8192 seems to be the maximum buffersize?
+ $buffer = bzread($bz,8192);
+ if(($buffer === false) || (bzerrno($bz) !== 0)) {
+ return false;
+ }
+ $str = $str . $buffer;
+ if($array) {
+ $pos = strpos($str, "\n");
+ while($pos !== false) {
+ $lines[] = substr($str, 0, $pos+1);
+ $str = substr($str, $pos+1);
+ $pos = strpos($str, "\n");
+ }
+ }
+ }
+ bzclose($bz);
+ if($array) {
+ if($str !== '') $lines[] = $str;
+ return $lines;
+ }
+ return $str;
+}
+
+/**
+ * Used to write out a DokuWiki page to file, and send IO_WIKIPAGE_WRITE events.
+ *
+ * This generates an action event and delegates to io_saveFile().
+ * Action plugins are allowed to modify the page content in transit.
+ * The file path should not be changed.
+ * (The append parameter is set to false.)
+ *
+ * Event data:
+ * $data[0] The raw arguments for io_saveFile as an array.
+ * $data[1] ns: The colon separated namespace path minus the trailing page name. (false if root ns)
+ * $data[2] page_name: The wiki page name.
+ * $data[3] rev: The page revision, false for current wiki pages.
+ *
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ *
+ * @param string $file filename
+ * @param string $content
+ * @param string $id page id
+ * @param int|bool $rev timestamp of revision
+ * @return bool
+ */
+function io_writeWikiPage($file, $content, $id, $rev=false) {
+ if (empty($rev)) { $rev = false; }
+ if ($rev===false) { io_createNamespace($id); } // create namespaces as needed
+ $data = array(array($file, $content, false), getNS($id), noNS($id), $rev);
+ return Event::createAndTrigger('IO_WIKIPAGE_WRITE', $data, '_io_writeWikiPage_action', false);
+}
+
+/**
+ * Callback adapter for io_saveFile().
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ *
+ * @param array $data event data
+ * @return bool
+ */
+function _io_writeWikiPage_action($data) {
+ if (is_array($data) && is_array($data[0]) && count($data[0])===3) {
+ $ok = call_user_func_array('io_saveFile', $data[0]);
+ // for attic files make sure the file has the mtime of the revision
+ if($ok && is_int($data[3]) && $data[3] > 0) {
+ @touch($data[0][0], $data[3]);
+ }
+ return $ok;
+ } else {
+ return false; //callback error
+ }
+}
+
+/**
+ * Internal function to save contents to a file.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $file filename path to file
+ * @param string $content
+ * @param bool $append
+ * @return bool true on success, otherwise false
+ */
+function _io_saveFile($file, $content, $append) {
+ global $conf;
+ $mode = ($append) ? 'ab' : 'wb';
+ $fileexists = file_exists($file);
+
+ if(substr($file,-3) == '.gz'){
+ if(!DOKU_HAS_GZIP) return false;
+ $fh = @gzopen($file,$mode.'9');
+ if(!$fh) return false;
+ gzwrite($fh, $content);
+ gzclose($fh);
+ }else if(substr($file,-4) == '.bz2'){
+ if(!DOKU_HAS_BZIP) return false;
+ if($append) {
+ $bzcontent = bzfile($file);
+ if($bzcontent === false) return false;
+ $content = $bzcontent.$content;
+ }
+ $fh = @bzopen($file,'w');
+ if(!$fh) return false;
+ bzwrite($fh, $content);
+ bzclose($fh);
+ }else{
+ $fh = @fopen($file,$mode);
+ if(!$fh) return false;
+ fwrite($fh, $content);
+ fclose($fh);
+ }
+
+ if(!$fileexists and $conf['fperm']) chmod($file, $conf['fperm']);
+ return true;
+}
+
+/**
+ * Saves $content to $file.
+ *
+ * If the third parameter is set to true the given content
+ * will be appended.
+ *
+ * Uses gzip if extension is .gz
+ * and bz2 if extension is .bz2
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $file filename path to file
+ * @param string $content
+ * @param bool $append
+ * @return bool true on success, otherwise false
+ */
+function io_saveFile($file, $content, $append=false) {
+ io_makeFileDir($file);
+ io_lock($file);
+ if(!_io_saveFile($file, $content, $append)) {
+ msg("Writing $file failed",-1);
+ io_unlock($file);
+ return false;
+ }
+ io_unlock($file);
+ return true;
+}
+
+/**
+ * Replace one or more occurrences of a line in a file.
+ *
+ * The default, when $maxlines is 0 is to delete all matching lines then append a single line.
+ * A regex that matches any part of the line will remove the entire line in this mode.
+ * Captures in $newline are not available.
+ *
+ * Otherwise each line is matched and replaced individually, up to the first $maxlines lines
+ * or all lines if $maxlines is -1. If $regex is true then captures can be used in $newline.
+ *
+ * Be sure to include the trailing newline in $oldline when replacing entire lines.
+ *
+ * Uses gzip if extension is .gz
+ * and bz2 if extension is .bz2
+ *
+ * @author Steven Danz <steven-danz@kc.rr.com>
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ * @author Patrick Brown <ptbrown@whoopdedo.org>
+ *
+ * @param string $file filename
+ * @param string $oldline exact linematch to remove
+ * @param string $newline new line to insert
+ * @param bool $regex use regexp?
+ * @param int $maxlines number of occurrences of the line to replace
+ * @return bool true on success
+ */
+function io_replaceInFile($file, $oldline, $newline, $regex=false, $maxlines=0) {
+ if ((string)$oldline === '') {
+ trigger_error('$oldline parameter cannot be empty in io_replaceInFile()', E_USER_WARNING);
+ return false;
+ }
+
+ if (!file_exists($file)) return true;
+
+ io_lock($file);
+
+ // load into array
+ if(substr($file,-3) == '.gz'){
+ if(!DOKU_HAS_GZIP) return false;
+ $lines = gzfile($file);
+ }else if(substr($file,-4) == '.bz2'){
+ if(!DOKU_HAS_BZIP) return false;
+ $lines = bzfile($file, true);
+ }else{
+ $lines = file($file);
+ }
+
+ // make non-regexes into regexes
+ $pattern = $regex ? $oldline : '/^'.preg_quote($oldline,'/').'$/';
+ $replace = $regex ? $newline : addcslashes($newline, '\$');
+
+ // remove matching lines
+ if ($maxlines > 0) {
+ $count = 0;
+ $matched = 0;
+ foreach($lines as $i => $line) {
+ if($count >= $maxlines) break;
+ // $matched will be set to 0|1 depending on whether pattern is matched and line replaced
+ $lines[$i] = preg_replace($pattern, $replace, $line, -1, $matched);
+ if ($matched) $count++;
+ }
+ } else if ($maxlines == 0) {
+ $lines = preg_grep($pattern, $lines, PREG_GREP_INVERT);
+
+ if ((string)$newline !== ''){
+ $lines[] = $newline;
+ }
+ } else {
+ $lines = preg_replace($pattern, $replace, $lines);
+ }
+
+ if(count($lines)){
+ if(!_io_saveFile($file, join('',$lines), false)) {
+ msg("Removing content from $file failed",-1);
+ io_unlock($file);
+ return false;
+ }
+ }else{
+ @unlink($file);
+ }
+
+ io_unlock($file);
+ return true;
+}
+
+/**
+ * Delete lines that match $badline from $file.
+ *
+ * Be sure to include the trailing newline in $badline
+ *
+ * @author Patrick Brown <ptbrown@whoopdedo.org>
+ *
+ * @param string $file filename
+ * @param string $badline exact linematch to remove
+ * @param bool $regex use regexp?
+ * @return bool true on success
+ */
+function io_deleteFromFile($file,$badline,$regex=false){
+ return io_replaceInFile($file,$badline,null,$regex,0);
+}
+
+/**
+ * Tries to lock a file
+ *
+ * Locking is only done for io_savefile and uses directories
+ * inside $conf['lockdir']
+ *
+ * It waits maximal 3 seconds for the lock, after this time
+ * the lock is assumed to be stale and the function goes on
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $file filename
+ */
+function io_lock($file){
+ global $conf;
+
+ $lockDir = $conf['lockdir'].'/'.md5($file);
+ @ignore_user_abort(1);
+
+ $timeStart = time();
+ do {
+ //waited longer than 3 seconds? -> stale lock
+ if ((time() - $timeStart) > 3) break;
+ $locked = @mkdir($lockDir, $conf['dmode']);
+ if($locked){
+ if(!empty($conf['dperm'])) chmod($lockDir, $conf['dperm']);
+ break;
+ }
+ usleep(50);
+ } while ($locked === false);
+}
+
+/**
+ * Unlocks a file
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $file filename
+ */
+function io_unlock($file){
+ global $conf;
+
+ $lockDir = $conf['lockdir'].'/'.md5($file);
+ @rmdir($lockDir);
+ @ignore_user_abort(0);
+}
+
+/**
+ * Create missing namespace directories and send the IO_NAMESPACE_CREATED events
+ * in the order of directory creation. (Parent directories first.)
+ *
+ * Event data:
+ * $data[0] ns: The colon separated namespace path minus the trailing page name.
+ * $data[1] ns_type: 'pages' or 'media' namespace tree.
+ *
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ *
+ * @param string $id page id
+ * @param string $ns_type 'pages' or 'media'
+ */
+function io_createNamespace($id, $ns_type='pages') {
+ // verify ns_type
+ $types = array('pages'=>'wikiFN', 'media'=>'mediaFN');
+ if (!isset($types[$ns_type])) {
+ trigger_error('Bad $ns_type parameter for io_createNamespace().');
+ return;
+ }
+ // make event list
+ $missing = array();
+ $ns_stack = explode(':', $id);
+ $ns = $id;
+ $tmp = dirname( $file = call_user_func($types[$ns_type], $ns) );
+ while (!@is_dir($tmp) && !(file_exists($tmp) && !is_dir($tmp))) {
+ array_pop($ns_stack);
+ $ns = implode(':', $ns_stack);
+ if (strlen($ns)==0) { break; }
+ $missing[] = $ns;
+ $tmp = dirname(call_user_func($types[$ns_type], $ns));
+ }
+ // make directories
+ io_makeFileDir($file);
+ // send the events
+ $missing = array_reverse($missing); // inside out
+ foreach ($missing as $ns) {
+ $data = array($ns, $ns_type);
+ Event::createAndTrigger('IO_NAMESPACE_CREATED', $data);
+ }
+}
+
+/**
+ * Create the directory needed for the given file
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $file file name
+ */
+function io_makeFileDir($file){
+ $dir = dirname($file);
+ if(!@is_dir($dir)){
+ io_mkdir_p($dir) || msg("Creating directory $dir failed",-1);
+ }
+}
+
+/**
+ * Creates a directory hierachy.
+ *
+ * @link http://php.net/manual/en/function.mkdir.php
+ * @author <saint@corenova.com>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $target filename
+ * @return bool|int|string
+ */
+function io_mkdir_p($target){
+ global $conf;
+ if (@is_dir($target)||empty($target)) return 1; // best case check first
+ if (file_exists($target) && !is_dir($target)) return 0;
+ //recursion
+ if (io_mkdir_p(substr($target,0,strrpos($target,'/')))){
+ $ret = @mkdir($target,$conf['dmode']); // crawl back up & create dir tree
+ if($ret && !empty($conf['dperm'])) chmod($target, $conf['dperm']);
+ return $ret;
+ }
+ return 0;
+}
+
+/**
+ * Recursively delete a directory
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $path
+ * @param bool $removefiles defaults to false which will delete empty directories only
+ * @return bool
+ */
+function io_rmdir($path, $removefiles = false) {
+ if(!is_string($path) || $path == "") return false;
+ if(!file_exists($path)) return true; // it's already gone or was never there, count as success
+
+ if(is_dir($path) && !is_link($path)) {
+ $dirs = array();
+ $files = array();
+
+ if(!$dh = @opendir($path)) return false;
+ while(false !== ($f = readdir($dh))) {
+ if($f == '..' || $f == '.') continue;
+
+ // collect dirs and files first
+ if(is_dir("$path/$f") && !is_link("$path/$f")) {
+ $dirs[] = "$path/$f";
+ } else if($removefiles) {
+ $files[] = "$path/$f";
+ } else {
+ return false; // abort when non empty
+ }
+
+ }
+ closedir($dh);
+
+ // now traverse into directories first
+ foreach($dirs as $dir) {
+ if(!io_rmdir($dir, $removefiles)) return false; // abort on any error
+ }
+
+ // now delete files
+ foreach($files as $file) {
+ if(!@unlink($file)) return false; //abort on any error
+ }
+
+ // remove self
+ return @rmdir($path);
+ } else if($removefiles) {
+ return @unlink($path);
+ }
+ return false;
+}
+
+/**
+ * Creates a unique temporary directory and returns
+ * its path.
+ *
+ * @author Michael Klier <chi@chimeric.de>
+ *
+ * @return false|string path to new directory or false
+ */
+function io_mktmpdir() {
+ global $conf;
+
+ $base = $conf['tmpdir'];
+ $dir = md5(uniqid(mt_rand(), true));
+ $tmpdir = $base.'/'.$dir;
+
+ if(io_mkdir_p($tmpdir)) {
+ return($tmpdir);
+ } else {
+ return false;
+ }
+}
+
+/**
+ * downloads a file from the net and saves it
+ *
+ * if $useAttachment is false,
+ * - $file is the full filename to save the file, incl. path
+ * - if successful will return true, false otherwise
+ *
+ * if $useAttachment is true,
+ * - $file is the directory where the file should be saved
+ * - if successful will return the name used for the saved file, false otherwise
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param string $url url to download
+ * @param string $file path to file or directory where to save
+ * @param bool $useAttachment true: try to use name of download, uses otherwise $defaultName
+ * false: uses $file as path to file
+ * @param string $defaultName fallback for if using $useAttachment
+ * @param int $maxSize maximum file size
+ * @return bool|string if failed false, otherwise true or the name of the file in the given dir
+ */
+function io_download($url,$file,$useAttachment=false,$defaultName='',$maxSize=2097152){
+ global $conf;
+ $http = new DokuHTTPClient();
+ $http->max_bodysize = $maxSize;
+ $http->timeout = 25; //max. 25 sec
+ $http->keep_alive = false; // we do single ops here, no need for keep-alive
+
+ $data = $http->get($url);
+ if(!$data) return false;
+
+ $name = '';
+ if ($useAttachment) {
+ if (isset($http->resp_headers['content-disposition'])) {
+ $content_disposition = $http->resp_headers['content-disposition'];
+ $match=array();
+ if (is_string($content_disposition) &&
+ preg_match('/attachment;\s*filename\s*=\s*"([^"]*)"/i', $content_disposition, $match)) {
+
+ $name = \dokuwiki\Utf8\PhpString::basename($match[1]);
+ }
+
+ }
+
+ if (!$name) {
+ if (!$defaultName) return false;
+ $name = $defaultName;
+ }
+
+ $file = $file.$name;
+ }
+
+ $fileexists = file_exists($file);
+ $fp = @fopen($file,"w");
+ if(!$fp) return false;
+ fwrite($fp,$data);
+ fclose($fp);
+ if(!$fileexists and $conf['fperm']) chmod($file, $conf['fperm']);
+ if ($useAttachment) return $name;
+ return true;
+}
+
+/**
+ * Windows compatible rename
+ *
+ * rename() can not overwrite existing files on Windows
+ * this function will use copy/unlink instead
+ *
+ * @param string $from
+ * @param string $to
+ * @return bool succes or fail
+ */
+function io_rename($from,$to){
+ global $conf;
+ if(!@rename($from,$to)){
+ if(@copy($from,$to)){
+ if($conf['fperm']) chmod($to, $conf['fperm']);
+ @unlink($from);
+ return true;
+ }
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Runs an external command with input and output pipes.
+ * Returns the exit code from the process.
+ *
+ * @author Tom N Harris <tnharris@whoopdedo.org>
+ *
+ * @param string $cmd
+ * @param string $input input pipe
+ * @param string $output output pipe
+ * @return int exit code from process
+ */
+function io_exec($cmd, $input, &$output){
+ $descspec = array(
+ 0=>array("pipe","r"),
+ 1=>array("pipe","w"),
+ 2=>array("pipe","w"));
+ $ph = proc_open($cmd, $descspec, $pipes);
+ if(!$ph) return -1;
+ fclose($pipes[2]); // ignore stderr
+ fwrite($pipes[0], $input);
+ fclose($pipes[0]);
+ $output = stream_get_contents($pipes[1]);
+ fclose($pipes[1]);
+ return proc_close($ph);
+}
+
+/**
+ * Search a file for matching lines
+ *
+ * This is probably not faster than file()+preg_grep() but less
+ * memory intensive because not the whole file needs to be loaded
+ * at once.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $file The file to search
+ * @param string $pattern PCRE pattern
+ * @param int $max How many lines to return (0 for all)
+ * @param bool $backref When true returns array with backreferences instead of lines
+ * @return array matching lines or backref, false on error
+ */
+function io_grep($file,$pattern,$max=0,$backref=false){
+ $fh = @fopen($file,'r');
+ if(!$fh) return false;
+ $matches = array();
+
+ $cnt = 0;
+ $line = '';
+ while (!feof($fh)) {
+ $line .= fgets($fh, 4096); // read full line
+ if(substr($line,-1) != "\n") continue;
+
+ // check if line matches
+ if(preg_match($pattern,$line,$match)){
+ if($backref){
+ $matches[] = $match;
+ }else{
+ $matches[] = $line;
+ }
+ $cnt++;
+ }
+ if($max && $max == $cnt) break;
+ $line = '';
+ }
+ fclose($fh);
+ return $matches;
+}
+
+
+/**
+ * Get size of contents of a file, for a compressed file the uncompressed size
+ * Warning: reading uncompressed size of content of bz-files requires uncompressing
+ *
+ * @author Gerrit Uitslag <klapinklapin@gmail.com>
+ *
+ * @param string $file filename path to file
+ * @return int size of file
+ */
+function io_getSizeFile($file) {
+ if (!file_exists($file)) return 0;
+
+ if(substr($file,-3) == '.gz'){
+ $fp = @fopen($file, "rb");
+ if($fp === false) return 0;
+
+ fseek($fp, -4, SEEK_END);
+ $buffer = fread($fp, 4);
+ fclose($fp);
+ $array = unpack("V", $buffer);
+ $uncompressedsize = end($array);
+ }else if(substr($file,-4) == '.bz2'){
+ if(!DOKU_HAS_BZIP) return 0;
+
+ $bz = bzopen($file,"r");
+ if($bz === false) return 0;
+
+ $uncompressedsize = 0;
+ while (!feof($bz)) {
+ //8192 seems to be the maximum buffersize?
+ $buffer = bzread($bz,8192);
+ if(($buffer === false) || (bzerrno($bz) !== 0)) {
+ return 0;
+ }
+ $uncompressedsize += strlen($buffer);
+ }
+ }else{
+ $uncompressedsize = filesize($file);
+ }
+
+ return $uncompressedsize;
+ }
diff --git a/platform/www/inc/lang/en/admin.txt b/platform/www/inc/lang/en/admin.txt
new file mode 100644
index 0000000..8998ca9
--- /dev/null
+++ b/platform/www/inc/lang/en/admin.txt
@@ -0,0 +1,3 @@
+====== Administration ======
+
+Below you can find a list of administrative tasks available in DokuWiki.
diff --git a/platform/www/inc/lang/en/adminplugins.txt b/platform/www/inc/lang/en/adminplugins.txt
new file mode 100644
index 0000000..3ec46cf
--- /dev/null
+++ b/platform/www/inc/lang/en/adminplugins.txt
@@ -0,0 +1,2 @@
+===== Additional Plugins =====
+
diff --git a/platform/www/inc/lang/en/backlinks.txt b/platform/www/inc/lang/en/backlinks.txt
new file mode 100644
index 0000000..55514bf
--- /dev/null
+++ b/platform/www/inc/lang/en/backlinks.txt
@@ -0,0 +1,3 @@
+====== Backlinks ======
+
+This is a list of pages that seem to link back to the current page.
diff --git a/platform/www/inc/lang/en/conflict.txt b/platform/www/inc/lang/en/conflict.txt
new file mode 100644
index 0000000..2586a2a
--- /dev/null
+++ b/platform/www/inc/lang/en/conflict.txt
@@ -0,0 +1,5 @@
+====== A newer version exists ======
+
+A newer version of the document you edited exists. This happens when another user changed the document while you were editing it.
+
+Examine the differences shown below thoroughly, then decide which version to keep. If you choose ''save'', your version will be saved. Hit ''cancel'' to keep the current version.
diff --git a/platform/www/inc/lang/en/denied.txt b/platform/www/inc/lang/en/denied.txt
new file mode 100644
index 0000000..e6fade4
--- /dev/null
+++ b/platform/www/inc/lang/en/denied.txt
@@ -0,0 +1,3 @@
+====== Permission Denied ======
+
+Sorry, you don't have enough rights to continue.
diff --git a/platform/www/inc/lang/en/diff.txt b/platform/www/inc/lang/en/diff.txt
new file mode 100644
index 0000000..46f0b34
--- /dev/null
+++ b/platform/www/inc/lang/en/diff.txt
@@ -0,0 +1,3 @@
+====== Differences ======
+
+This shows you the differences between two versions of the page.
diff --git a/platform/www/inc/lang/en/draft.txt b/platform/www/inc/lang/en/draft.txt
new file mode 100644
index 0000000..b6a930a
--- /dev/null
+++ b/platform/www/inc/lang/en/draft.txt
@@ -0,0 +1,5 @@
+====== Draft file found ======
+
+Your last edit session on this page was not completed correctly. DokuWiki automatically saved a draft during your work which you may now use to continue your editing. Below you can see the data that was saved from your last session.
+
+Please decide if you want to //recover// your lost edit session, //delete// the autosaved draft or //cancel// the editing process.
diff --git a/platform/www/inc/lang/en/edit.txt b/platform/www/inc/lang/en/edit.txt
new file mode 100644
index 0000000..0f395b5
--- /dev/null
+++ b/platform/www/inc/lang/en/edit.txt
@@ -0,0 +1 @@
+Edit the page and hit ''Save''. See [[wiki:syntax]] for Wiki syntax. Please edit the page only if you can **improve** it. If you want to test some things, learn to make your first steps on the [[playground:playground|playground]].
diff --git a/platform/www/inc/lang/en/editrev.txt b/platform/www/inc/lang/en/editrev.txt
new file mode 100644
index 0000000..638216b
--- /dev/null
+++ b/platform/www/inc/lang/en/editrev.txt
@@ -0,0 +1,2 @@
+**You've loaded an old revision of the document!** If you save it, you will create a new version with this data.
+----
diff --git a/platform/www/inc/lang/en/index.txt b/platform/www/inc/lang/en/index.txt
new file mode 100644
index 0000000..dced649
--- /dev/null
+++ b/platform/www/inc/lang/en/index.txt
@@ -0,0 +1,3 @@
+====== Sitemap ======
+
+This is a sitemap over all available pages ordered by [[doku>namespaces|namespaces]].
diff --git a/platform/www/inc/lang/en/install.html b/platform/www/inc/lang/en/install.html
new file mode 100644
index 0000000..fa6b99a
--- /dev/null
+++ b/platform/www/inc/lang/en/install.html
@@ -0,0 +1,7 @@
+<p>This page assists in the first time installation and configuration of <a href="http://dokuwiki.org">Dokuwiki</a>. More info on this installer is available on it's own <a href="http://dokuwiki.org/installer">documentation page</a>.</p>
+
+<p>DokuWiki uses ordinary files for the storage of wiki pages and other information associated with those pages (e.g. images, search indexes, old revisions, etc). In order to operate successfully DokuWiki <strong>must</strong> have write access to the directories that hold those files. This installer is not capable of setting up directory permissions. That normally needs to be done directly on a command shell or if you are using hosting, through FTP or your hosting control panel (e.g. cPanel).</p>
+
+<p>This installer will setup your DokuWiki configuration for <abbr title="access control list">ACL</abbr>, which in turn allows administrator login and access to DokuWiki's admin menu for installing plugins, managing users, managing access to wiki pages and alteration of configuration settings. It isn't required for DokuWiki to operate, however it will make Dokuwiki easier to administer.</p>
+
+<p>Experienced users or users with special setup requirements should use these links for details concerning <a href="http://dokuwiki.org/install">installation instructions</a> and <a href="http://dokuwiki.org/config">configuration settings</a>.</p>
diff --git a/platform/www/inc/lang/en/lang.php b/platform/www/inc/lang/en/lang.php
new file mode 100644
index 0000000..000368a
--- /dev/null
+++ b/platform/www/inc/lang/en/lang.php
@@ -0,0 +1,395 @@
+<?php
+/**
+ * english language file
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Anika Henke <anika@selfthinker.org>
+ * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
+ * @author Matthias Schulte <mailinglist@lupo49.de>
+ */
+$lang['encoding'] = 'utf-8';
+$lang['direction'] = 'ltr';
+$lang['doublequoteopening'] = '“'; //&ldquo;
+$lang['doublequoteclosing'] = '”'; //&rdquo;
+$lang['singlequoteopening'] = '‘'; //&lsquo;
+$lang['singlequoteclosing'] = '’'; //&rsquo;
+$lang['apostrophe'] = '’'; //&rsquo;
+
+$lang['btn_edit'] = 'Edit this page';
+$lang['btn_source'] = 'Show pagesource';
+$lang['btn_show'] = 'Show page';
+$lang['btn_create'] = 'Create this page';
+$lang['btn_search'] = 'Search';
+$lang['btn_save'] = 'Save';
+$lang['btn_preview'] = 'Preview';
+$lang['btn_top'] = 'Back to top';
+$lang['btn_newer'] = '<< more recent';
+$lang['btn_older'] = 'less recent >>';
+$lang['btn_revs'] = 'Old revisions';
+$lang['btn_recent'] = 'Recent Changes';
+$lang['btn_upload'] = 'Upload';
+$lang['btn_cancel'] = 'Cancel';
+$lang['btn_index'] = 'Sitemap';
+$lang['btn_secedit'] = 'Edit';
+$lang['btn_login'] = 'Log In';
+$lang['btn_logout'] = 'Log Out';
+$lang['btn_admin'] = 'Admin';
+$lang['btn_update'] = 'Update';
+$lang['btn_delete'] = 'Delete';
+$lang['btn_back'] = 'Back';
+$lang['btn_backlink'] = 'Backlinks';
+$lang['btn_subscribe'] = 'Manage Subscriptions';
+$lang['btn_profile'] = 'Update Profile';
+$lang['btn_reset'] = 'Reset';
+$lang['btn_resendpwd'] = 'Set new password';
+$lang['btn_draft'] = 'Edit draft';
+$lang['btn_recover'] = 'Recover draft';
+$lang['btn_draftdel'] = 'Delete draft';
+$lang['btn_revert'] = 'Restore';
+$lang['btn_register'] = 'Register';
+$lang['btn_apply'] = 'Apply';
+$lang['btn_media'] = 'Media Manager';
+$lang['btn_deleteuser'] = 'Remove My Account';
+$lang['btn_img_backto'] = 'Back to %s';
+$lang['btn_mediaManager'] = 'View in media manager';
+
+$lang['loggedinas'] = 'Logged in as:';
+$lang['user'] = 'Username';
+$lang['pass'] = 'Password';
+$lang['newpass'] = 'New password';
+$lang['oldpass'] = 'Confirm current password';
+$lang['passchk'] = 'once again';
+$lang['remember'] = 'Remember me';
+$lang['fullname'] = 'Real name';
+$lang['email'] = 'E-Mail';
+$lang['profile'] = 'User Profile';
+$lang['badlogin'] = 'Sorry, username or password was wrong.';
+$lang['badpassconfirm'] = 'Sorry, the password was wrong';
+$lang['minoredit'] = 'Minor Changes';
+$lang['draftdate'] = 'Draft autosaved on'; // full dformat date will be added
+$lang['nosecedit'] = 'The page was changed in the meantime, section info was out of date loaded full page instead.';
+$lang['searchcreatepage'] = 'If you didn\'t find what you were looking for, you can create or edit the page %s, named after your query.';
+
+$lang['search_fullresults'] = 'Fulltext results';
+$lang['js']['search_toggle_tools'] = 'Toggle Search Tools';
+$lang['search_exact_match'] = 'Exact match';
+$lang['search_starts_with'] = 'Starts with';
+$lang['search_ends_with'] = 'Ends with';
+$lang['search_contains'] = 'Contains';
+$lang['search_custom_match'] = 'Custom';
+$lang['search_any_ns'] = 'Any namespace';
+$lang['search_any_time'] = 'Any time';
+$lang['search_past_7_days'] = 'Past week';
+$lang['search_past_month'] = 'Past month';
+$lang['search_past_year'] = 'Past year';
+$lang['search_sort_by_hits'] = 'Sort by hits';
+$lang['search_sort_by_mtime'] = 'Sort by last modified';
+
+$lang['regmissing'] = 'Sorry, you must fill in all fields.';
+$lang['reguexists'] = 'Sorry, a user with this login already exists.';
+$lang['regsuccess'] = 'The user has been created and the password was sent by email.';
+$lang['regsuccess2'] = 'The user has been created.';
+$lang['regfail'] = 'The user could not be created.';
+$lang['regmailfail'] = 'Looks like there was an error on sending the password mail. Please contact the admin!';
+$lang['regbadmail'] = 'The given email address looks invalid - if you think this is an error, contact the admin';
+$lang['regbadpass'] = 'The two given passwords are not identical, please try again.';
+$lang['regpwmail'] = 'Your DokuWiki password';
+$lang['reghere'] = 'You don\'t have an account yet? Just get one';
+
+$lang['profna'] = 'This wiki does not support profile modification';
+$lang['profnochange'] = 'No changes, nothing to do.';
+$lang['profnoempty'] = 'An empty name or email address is not allowed.';
+$lang['profchanged'] = 'User profile successfully updated.';
+$lang['profnodelete'] = 'This wiki does not support deleting users';
+$lang['profdeleteuser'] = 'Delete Account';
+$lang['profdeleted'] = 'Your user account has been deleted from this wiki';
+$lang['profconfdelete'] = 'I wish to remove my account from this wiki. <br/> This action can not be undone.';
+$lang['profconfdeletemissing'] = 'Confirmation check box not ticked';
+$lang['proffail'] = 'User profile was not updated.';
+
+$lang['pwdforget'] = 'Forgotten your password? Get a new one';
+$lang['resendna'] = 'This wiki does not support password resending.';
+$lang['resendpwd'] = 'Set new password for';
+$lang['resendpwdmissing'] = 'Sorry, you must fill in all fields.';
+$lang['resendpwdnouser'] = 'Sorry, we can\'t find this user in our database.';
+$lang['resendpwdbadauth'] = 'Sorry, this auth code is not valid. Make sure you used the complete confirmation link.';
+$lang['resendpwdconfirm'] = 'A confirmation link has been sent by email.';
+$lang['resendpwdsuccess'] = 'Your new password has been sent by email.';
+
+$lang['license'] = 'Except where otherwise noted, content on this wiki is licensed under the following license:';
+$lang['licenseok'] = 'Note: By editing this page you agree to license your content under the following license:';
+
+$lang['searchmedia'] = 'Search file name:';
+$lang['searchmedia_in'] = 'Search in %s';
+$lang['txt_upload'] = 'Select file to upload:';
+$lang['txt_filename'] = 'Upload as (optional):';
+$lang['txt_overwrt'] = 'Overwrite existing file';
+$lang['maxuploadsize'] = 'Upload max. %s per file.';
+$lang['allowedmime'] = 'List of allowed file extensions';
+$lang['lockedby'] = 'Currently locked by:';
+$lang['lockexpire'] = 'Lock expires at:';
+
+$lang['js']['willexpire'] = 'Your lock for editing this page is about to expire in a minute.\nTo avoid conflicts use the preview button to reset the locktimer.';
+$lang['js']['notsavedyet'] = 'Unsaved changes will be lost.';
+$lang['js']['searchmedia'] = 'Search for files';
+$lang['js']['keepopen'] = 'Keep window open on selection';
+$lang['js']['hidedetails'] = 'Hide Details';
+$lang['js']['mediatitle'] = 'Link settings';
+$lang['js']['mediadisplay'] = 'Link type';
+$lang['js']['mediaalign'] = 'Alignment';
+$lang['js']['mediasize'] = 'Image size';
+$lang['js']['mediatarget'] = 'Link target';
+$lang['js']['mediaclose'] = 'Close';
+$lang['js']['mediainsert'] = 'Insert';
+$lang['js']['mediadisplayimg'] = 'Show the image.';
+$lang['js']['mediadisplaylnk'] = 'Show only the link.';
+$lang['js']['mediasmall'] = 'Small version';
+$lang['js']['mediamedium'] = 'Medium version';
+$lang['js']['medialarge'] = 'Large version';
+$lang['js']['mediaoriginal'] = 'Original version';
+$lang['js']['medialnk'] = 'Link to detail page';
+$lang['js']['mediadirect'] = 'Direct link to original';
+$lang['js']['medianolnk'] = 'No link';
+$lang['js']['medianolink'] = 'Do not link the image';
+$lang['js']['medialeft'] = 'Align the image on the left.';
+$lang['js']['mediaright'] = 'Align the image on the right.';
+$lang['js']['mediacenter'] = 'Align the image in the middle.';
+$lang['js']['medianoalign'] = 'Use no align.';
+$lang['js']['nosmblinks'] = 'Linking to Windows shares only works in Microsoft Internet Explorer.\nYou still can copy and paste the link.';
+$lang['js']['linkwiz'] = 'Link Wizard';
+$lang['js']['linkto'] = 'Link to:';
+$lang['js']['del_confirm'] = 'Really delete selected item(s)?';
+$lang['js']['restore_confirm'] = 'Really restore this version?';
+$lang['js']['media_diff'] = 'View differences:';
+$lang['js']['media_diff_both'] = 'Side by Side';
+$lang['js']['media_diff_opacity'] = 'Shine-through';
+$lang['js']['media_diff_portions'] = 'Swipe';
+$lang['js']['media_select'] = 'Select files…';
+$lang['js']['media_upload_btn'] = 'Upload';
+$lang['js']['media_done_btn'] = 'Done';
+$lang['js']['media_drop'] = 'Drop files here to upload';
+$lang['js']['media_cancel'] = 'remove';
+$lang['js']['media_overwrt'] = 'Overwrite existing files';
+
+$lang['rssfailed'] = 'An error occurred while fetching this feed: ';
+$lang['nothingfound'] = 'Nothing was found.';
+
+$lang['mediaselect'] = 'Media Files';
+$lang['uploadsucc'] = 'Upload successful';
+$lang['uploadfail'] = 'Upload failed. Maybe wrong permissions?';
+$lang['uploadwrong'] = 'Upload denied. This file extension is forbidden!';
+$lang['uploadexist'] = 'File already exists. Nothing done.';
+$lang['uploadbadcontent'] = 'The uploaded content did not match the %s file extension.';
+$lang['uploadspam'] = 'The upload was blocked by the spam blacklist.';
+$lang['uploadxss'] = 'The upload was blocked for possibly malicious content.';
+$lang['uploadsize'] = 'The uploaded file was too big. (max. %s)';
+$lang['deletesucc'] = 'The file "%s" has been deleted.';
+$lang['deletefail'] = '"%s" couldn\'t be deleted - check permissions.';
+$lang['mediainuse'] = 'The file "%s" hasn\'t been deleted - it is still in use.';
+$lang['namespaces'] = 'Namespaces';
+$lang['mediafiles'] = 'Available files in';
+$lang['accessdenied'] = 'You are not allowed to view this page.';
+$lang['mediausage'] = 'Use the following syntax to reference this file:';
+$lang['mediaview'] = 'View original file';
+$lang['mediaroot'] = 'root';
+$lang['mediaupload'] = 'Upload a file to the current namespace here. To create subnamespaces, prepend them to your filename separated by colons after you selected the files. Files can also be selected by drag and drop.';
+$lang['mediaextchange'] = 'Filextension changed from .%s to .%s!';
+$lang['reference'] = 'References for';
+$lang['ref_inuse'] = 'The file can\'t be deleted, because it\'s still used by the following pages:';
+$lang['ref_hidden'] = 'Some references are on pages you don\'t have permission to read';
+
+$lang['hits'] = 'Hits';
+$lang['quickhits'] = 'Matching pagenames';
+$lang['toc'] = 'Table of Contents';
+$lang['current'] = 'current';
+$lang['yours'] = 'Your Version';
+$lang['diff'] = 'Show differences to current revisions';
+$lang['diff2'] = 'Show differences between selected revisions';
+$lang['difflink'] = 'Link to this comparison view';
+$lang['diff_type'] = 'View differences:';
+$lang['diff_inline'] = 'Inline';
+$lang['diff_side'] = 'Side by Side';
+$lang['diffprevrev'] = 'Previous revision';
+$lang['diffnextrev'] = 'Next revision';
+$lang['difflastrev'] = 'Last revision';
+$lang['diffbothprevrev'] = 'Both sides previous revision';
+$lang['diffbothnextrev'] = 'Both sides next revision';
+$lang['line'] = 'Line';
+$lang['breadcrumb'] = 'Trace:';
+$lang['youarehere'] = 'You are here:';
+$lang['lastmod'] = 'Last modified:';
+$lang['by'] = 'by';
+$lang['deleted'] = 'removed';
+$lang['created'] = 'created';
+$lang['restored'] = 'old revision restored (%s)';
+$lang['external_edit'] = 'external edit';
+$lang['summary'] = 'Edit summary';
+$lang['noflash'] = 'The <a href="http://get.adobe.com/flashplayer">Adobe Flash Plugin</a> is needed to display this content.';
+$lang['download'] = 'Download Snippet';
+$lang['tools'] = 'Tools';
+$lang['user_tools'] = 'User Tools';
+$lang['site_tools'] = 'Site Tools';
+$lang['page_tools'] = 'Page Tools';
+$lang['skip_to_content'] = 'skip to content';
+$lang['sidebar'] = 'Sidebar';
+
+$lang['mail_newpage'] = 'page added:';
+$lang['mail_changed'] = 'page changed:';
+$lang['mail_subscribe_list'] = 'pages changed in namespace:';
+$lang['mail_new_user'] = 'new user:';
+$lang['mail_upload'] = 'file uploaded:';
+
+$lang['changes_type'] = 'View changes of';
+$lang['pages_changes'] = 'Pages';
+$lang['media_changes'] = 'Media files';
+$lang['both_changes'] = 'Both pages and media files';
+
+$lang['qb_bold'] = 'Bold Text';
+$lang['qb_italic'] = 'Italic Text';
+$lang['qb_underl'] = 'Underlined Text';
+$lang['qb_code'] = 'Monospaced Text';
+$lang['qb_strike'] = 'Strike-through Text';
+$lang['qb_h1'] = 'Level 1 Headline';
+$lang['qb_h2'] = 'Level 2 Headline';
+$lang['qb_h3'] = 'Level 3 Headline';
+$lang['qb_h4'] = 'Level 4 Headline';
+$lang['qb_h5'] = 'Level 5 Headline';
+$lang['qb_h'] = 'Headline';
+$lang['qb_hs'] = 'Select Headline';
+$lang['qb_hplus'] = 'Higher Headline';
+$lang['qb_hminus'] = 'Lower Headline';
+$lang['qb_hequal'] = 'Same Level Headline';
+$lang['qb_link'] = 'Internal Link';
+$lang['qb_extlink'] = 'External Link';
+$lang['qb_hr'] = 'Horizontal Rule';
+$lang['qb_ol'] = 'Ordered List Item';
+$lang['qb_ul'] = 'Unordered List Item';
+$lang['qb_media'] = 'Add Images and other files (opens in a new window)';
+$lang['qb_sig'] = 'Insert Signature';
+$lang['qb_smileys'] = 'Smileys';
+$lang['qb_chars'] = 'Special Chars';
+
+$lang['upperns'] = 'jump to parent namespace';
+
+$lang['metaedit'] = 'Edit Metadata';
+$lang['metasaveerr'] = 'Writing metadata failed';
+$lang['metasaveok'] = 'Metadata saved';
+$lang['img_title'] = 'Title:';
+$lang['img_caption'] = 'Caption:';
+$lang['img_date'] = 'Date:';
+$lang['img_fname'] = 'Filename:';
+$lang['img_fsize'] = 'Size:';
+$lang['img_artist'] = 'Photographer:';
+$lang['img_copyr'] = 'Copyright:';
+$lang['img_format'] = 'Format:';
+$lang['img_camera'] = 'Camera:';
+$lang['img_keywords'] = 'Keywords:';
+$lang['img_width'] = 'Width:';
+$lang['img_height'] = 'Height:';
+
+$lang['subscr_subscribe_success'] = 'Added %s to subscription list for %s';
+$lang['subscr_subscribe_error'] = 'Error adding %s to subscription list for %s';
+$lang['subscr_subscribe_noaddress'] = 'There is no address associated with your login, you cannot be added to the subscription list';
+$lang['subscr_unsubscribe_success'] = 'Removed %s from subscription list for %s';
+$lang['subscr_unsubscribe_error'] = 'Error removing %s from subscription list for %s';
+$lang['subscr_already_subscribed'] = '%s is already subscribed to %s';
+$lang['subscr_not_subscribed'] = '%s is not subscribed to %s';
+// Manage page for subscriptions
+$lang['subscr_m_not_subscribed'] = 'You are currently not subscribed to the current page or namespace.';
+$lang['subscr_m_new_header'] = 'Add subscription';
+$lang['subscr_m_current_header'] = 'Current subscriptions';
+$lang['subscr_m_unsubscribe'] = 'Unsubscribe';
+$lang['subscr_m_subscribe'] = 'Subscribe';
+$lang['subscr_m_receive'] = 'Receive';
+$lang['subscr_style_every'] = 'email on every change';
+$lang['subscr_style_digest'] = 'digest email of changes for each page (every %.2f days)';
+$lang['subscr_style_list'] = 'list of changed pages since last email (every %.2f days)';
+
+/* auth.class language support */
+$lang['authtempfail'] = 'User authentication is temporarily unavailable. If this situation persists, please inform your Wiki Admin.';
+
+/* installer strings */
+$lang['i_chooselang'] = 'Choose your language';
+$lang['i_installer'] = 'DokuWiki Installer';
+$lang['i_wikiname'] = 'Wiki Name';
+$lang['i_enableacl'] = 'Enable ACL (recommended)';
+$lang['i_superuser'] = 'Superuser';
+$lang['i_problems'] = 'The installer found some problems, indicated below. You can not continue until you have fixed them.';
+$lang['i_modified'] = 'For security reasons this script will only work with a new and unmodified Dokuwiki installation.
+ You should either re-extract the files from the downloaded package or consult the complete
+ <a href="https://www.dokuwiki.org/install">Dokuwiki installation instructions</a>';
+$lang['i_funcna'] = 'PHP function <code>%s</code> is not available. Maybe your hosting provider disabled it for some reason?';
+$lang['i_disabled'] = 'It has been disabled by your provider.';
+$lang['i_funcnmail'] = '<b>Note:</b> The PHP mail function is not available. %s' .
+ ' If it remains unavailable, you may install the <a href="https://www.dokuwiki.org/plugin:smtp">smtp plugin</a>.';
+$lang['i_phpver'] = 'Your PHP version <code>%s</code> is lower than the needed <code>%s</code>. You need to upgrade your PHP install.';
+$lang['i_mbfuncoverload'] = 'mbstring.func_overload must be disabled in php.ini to run DokuWiki.';
+$lang['i_urandom'] = 'DokuWiki cannot create cryptographically secure numbers for cookies. You may want to check your open_basedir settings in php.ini for proper <code>/dev/urandom</code> access.';
+$lang['i_permfail'] = '<code>%s</code> is not writable by DokuWiki. You need to fix the permission settings of this directory!';
+$lang['i_confexists'] = '<code>%s</code> already exists';
+$lang['i_writeerr'] = 'Unable to create <code>%s</code>. You will need to check directory/file permissions and create the file manually.';
+$lang['i_badhash'] = 'unrecognised or modified dokuwiki.php (hash=<code>%s</code>)';
+$lang['i_badval'] = '<code>%s</code> - illegal or empty value';
+$lang['i_success'] = 'The configuration was finished successfully. You may delete the install.php file now. Continue to
+ <a href="doku.php?id=wiki:welcome">your new DokuWiki</a>.';
+$lang['i_failure'] = 'Some errors occurred while writing the configuration files. You may need to fix them manually before
+ you can use <a href="doku.php?id=wiki:welcome">your new DokuWiki</a>.';
+$lang['i_policy'] = 'Initial ACL policy';
+$lang['i_pol0'] = 'Open Wiki (read, write, upload for everyone)';
+$lang['i_pol1'] = 'Public Wiki (read for everyone, write and upload for registered users)';
+$lang['i_pol2'] = 'Closed Wiki (read, write, upload for registered users only)';
+$lang['i_allowreg'] = 'Allow users to register themselves';
+$lang['i_retry'] = 'Retry';
+$lang['i_license'] = 'Please choose the license you want to put your content under:';
+$lang['i_license_none'] = 'Do not show any license information';
+$lang['i_pop_field'] = 'Please, help us to improve the DokuWiki experience:';
+$lang['i_pop_label'] = 'Once a month, send anonymous usage data to the DokuWiki developers';
+
+$lang['recent_global'] = 'You\'re currently watching the changes inside the <b>%s</b> namespace. You can also <a href="%s">view the recent changes of the whole wiki</a>.';
+$lang['years'] = '%d years ago';
+$lang['months'] = '%d months ago';
+$lang['weeks'] = '%d weeks ago';
+$lang['days'] = '%d days ago';
+$lang['hours'] = '%d hours ago';
+$lang['minutes'] = '%d minutes ago';
+$lang['seconds'] = '%d seconds ago';
+
+$lang['wordblock'] = 'Your change was not saved because it contains blocked text (spam).';
+
+$lang['media_uploadtab'] = 'Upload';
+$lang['media_searchtab'] = 'Search';
+$lang['media_file'] = 'File';
+$lang['media_viewtab'] = 'View';
+$lang['media_edittab'] = 'Edit';
+$lang['media_historytab'] = 'History';
+$lang['media_list_thumbs'] = 'Thumbnails';
+$lang['media_list_rows'] = 'Rows';
+$lang['media_sort_name'] = 'Name';
+$lang['media_sort_date'] = 'Date';
+$lang['media_namespaces'] = 'Choose namespace';
+$lang['media_files'] = 'Files in %s';
+$lang['media_upload'] = 'Upload to %s';
+$lang['media_search'] = 'Search in %s';
+$lang['media_view'] = '%s';
+$lang['media_viewold'] = '%s at %s';
+$lang['media_edit'] = 'Edit %s';
+$lang['media_history'] = 'History of %s';
+$lang['media_meta_edited'] = 'metadata edited';
+$lang['media_perm_read'] = 'Sorry, you don\'t have enough rights to read files.';
+$lang['media_perm_upload'] = 'Sorry, you don\'t have enough rights to upload files.';
+$lang['media_update'] = 'Upload new version';
+$lang['media_restore'] = 'Restore this version';
+$lang['media_acl_warning'] = 'This list might not be complete due to ACL restrictions and hidden pages.';
+
+$lang['email_fail'] = 'PHP mail() missing or disabled. The following email was not sent: ';
+$lang['currentns'] = 'Current namespace';
+$lang['searchresult'] = 'Search Result';
+$lang['plainhtml'] = 'Plain HTML';
+$lang['wikimarkup'] = 'Wiki Markup';
+$lang['page_nonexist_rev'] = 'Page did not exist at %s. It was subsequently created at <a href="%s">%s</a>.';
+$lang['unable_to_parse_date'] = 'Unable to parse at parameter "%s".';
+$lang['email_signature_text'] = 'This mail was generated by DokuWiki at
+@DOKUWIKIURL@';
+#$lang['email_signature_html'] = ''; # the empty default will copy the text signature, you can override it in a local lang file
+
diff --git a/platform/www/inc/lang/en/locked.txt b/platform/www/inc/lang/en/locked.txt
new file mode 100644
index 0000000..af6347a
--- /dev/null
+++ b/platform/www/inc/lang/en/locked.txt
@@ -0,0 +1,3 @@
+====== Page locked ======
+
+This page is currently locked for editing by another user. You have to wait until this user finishes editing or the lock expires.
diff --git a/platform/www/inc/lang/en/login.txt b/platform/www/inc/lang/en/login.txt
new file mode 100644
index 0000000..151bf7f
--- /dev/null
+++ b/platform/www/inc/lang/en/login.txt
@@ -0,0 +1,3 @@
+====== Login ======
+
+You are currently not logged in! Enter your authentication credentials below to log in. You need to have cookies enabled to log in.
diff --git a/platform/www/inc/lang/en/mailtext.txt b/platform/www/inc/lang/en/mailtext.txt
new file mode 100644
index 0000000..eac4035
--- /dev/null
+++ b/platform/www/inc/lang/en/mailtext.txt
@@ -0,0 +1,15 @@
+A page in your DokuWiki was added or changed. Here are the details:
+
+Browser : @BROWSER@
+IP Address : @IPADDRESS@
+Hostname : @HOSTNAME@
+Old Revision : @OLDPAGE@
+New Revision : @NEWPAGE@
+Date of New Revision: @DATE@
+Edit Summary : @SUMMARY@
+User : @USER@
+
+There may be newer changes after this revision. If this
+happens, a message will be shown on the top of the rev page.
+
+@DIFF@
diff --git a/platform/www/inc/lang/en/mailwrap.html b/platform/www/inc/lang/en/mailwrap.html
new file mode 100644
index 0000000..7df0cdc
--- /dev/null
+++ b/platform/www/inc/lang/en/mailwrap.html
@@ -0,0 +1,13 @@
+<html>
+<head>
+ <title>@TITLE@</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+
+@HTMLBODY@
+
+<br /><hr />
+<small>@EMAILSIGNATURE@</small>
+</body>
+</html>
diff --git a/platform/www/inc/lang/en/newpage.txt b/platform/www/inc/lang/en/newpage.txt
new file mode 100644
index 0000000..c9ae6e6
--- /dev/null
+++ b/platform/www/inc/lang/en/newpage.txt
@@ -0,0 +1,3 @@
+====== This topic does not exist yet ======
+
+You've followed a link to a topic that doesn't exist yet. If permissions allow, you may create it by clicking on **Create this page**.
diff --git a/platform/www/inc/lang/en/norev.txt b/platform/www/inc/lang/en/norev.txt
new file mode 100644
index 0000000..b24c792
--- /dev/null
+++ b/platform/www/inc/lang/en/norev.txt
@@ -0,0 +1,3 @@
+====== No such revision ======
+
+The specified revision doesn't exist. Click on "Old revisions" for a list of old revisions of this document.
diff --git a/platform/www/inc/lang/en/onceexisted.txt b/platform/www/inc/lang/en/onceexisted.txt
new file mode 100644
index 0000000..87cc057
--- /dev/null
+++ b/platform/www/inc/lang/en/onceexisted.txt
@@ -0,0 +1,3 @@
+======= This page does not exist anymore ======
+
+You've followed a link to a page that no longer exists. You can check the list of [[?do=revisions|old revisions]] to see when and why it was deleted, access old revisions or restore it. \ No newline at end of file
diff --git a/platform/www/inc/lang/en/password.txt b/platform/www/inc/lang/en/password.txt
new file mode 100644
index 0000000..0a0dfb5
--- /dev/null
+++ b/platform/www/inc/lang/en/password.txt
@@ -0,0 +1,6 @@
+Hi @FULLNAME@!
+
+Here is your userdata for @TITLE@ at @DOKUWIKIURL@
+
+Login : @LOGIN@
+Password : @PASSWORD@
diff --git a/platform/www/inc/lang/en/preview.txt b/platform/www/inc/lang/en/preview.txt
new file mode 100644
index 0000000..6727056
--- /dev/null
+++ b/platform/www/inc/lang/en/preview.txt
@@ -0,0 +1,3 @@
+====== Preview ======
+
+This is a preview of what your text will look like. **Remember: It is not saved yet**!
diff --git a/platform/www/inc/lang/en/pwconfirm.txt b/platform/www/inc/lang/en/pwconfirm.txt
new file mode 100644
index 0000000..44bdeb4
--- /dev/null
+++ b/platform/www/inc/lang/en/pwconfirm.txt
@@ -0,0 +1,9 @@
+Hi @FULLNAME@!
+
+Someone requested a new password for your @TITLE@ login at @DOKUWIKIURL@
+
+If you did not request a new password then just ignore this email.
+
+To confirm that the request was really sent by you please use the following link.
+
+@CONFIRM@
diff --git a/platform/www/inc/lang/en/read.txt b/platform/www/inc/lang/en/read.txt
new file mode 100644
index 0000000..6e2af13
--- /dev/null
+++ b/platform/www/inc/lang/en/read.txt
@@ -0,0 +1 @@
+This page is read only. You can view the source, but not change it. Ask your administrator if you think this is wrong.
diff --git a/platform/www/inc/lang/en/recent.txt b/platform/www/inc/lang/en/recent.txt
new file mode 100644
index 0000000..0f9a7f6
--- /dev/null
+++ b/platform/www/inc/lang/en/recent.txt
@@ -0,0 +1,3 @@
+====== Recent Changes ======
+
+The following pages were changed recently:
diff --git a/platform/www/inc/lang/en/register.txt b/platform/www/inc/lang/en/register.txt
new file mode 100644
index 0000000..7778402
--- /dev/null
+++ b/platform/www/inc/lang/en/register.txt
@@ -0,0 +1,3 @@
+====== Register as new user ======
+
+Fill in all the information below to create a new account in this wiki. Make sure you supply a **valid e-mail address** - if you are not asked to enter a password here, a new one will be sent to that address. The login name should be a valid [[doku>pagename|pagename]].
diff --git a/platform/www/inc/lang/en/registermail.txt b/platform/www/inc/lang/en/registermail.txt
new file mode 100644
index 0000000..5517ca1
--- /dev/null
+++ b/platform/www/inc/lang/en/registermail.txt
@@ -0,0 +1,10 @@
+A new user has registered. Here are the details:
+
+User name : @NEWUSER@
+Full name : @NEWNAME@
+E-mail : @NEWEMAIL@
+
+Date : @DATE@
+Browser : @BROWSER@
+IP-Address : @IPADDRESS@
+Hostname : @HOSTNAME@
diff --git a/platform/www/inc/lang/en/resendpwd.txt b/platform/www/inc/lang/en/resendpwd.txt
new file mode 100644
index 0000000..2696fe4
--- /dev/null
+++ b/platform/www/inc/lang/en/resendpwd.txt
@@ -0,0 +1,3 @@
+====== Send new password ======
+
+Please enter your user name in the form below to request a new password for your account in this wiki. A confirmation link will be sent to your registered email address.
diff --git a/platform/www/inc/lang/en/resetpwd.txt b/platform/www/inc/lang/en/resetpwd.txt
new file mode 100644
index 0000000..5f59f0f
--- /dev/null
+++ b/platform/www/inc/lang/en/resetpwd.txt
@@ -0,0 +1,3 @@
+====== Set new password ======
+
+Please enter a new password for your account in this wiki.
diff --git a/platform/www/inc/lang/en/revisions.txt b/platform/www/inc/lang/en/revisions.txt
new file mode 100644
index 0000000..90b036a
--- /dev/null
+++ b/platform/www/inc/lang/en/revisions.txt
@@ -0,0 +1,3 @@
+====== Old Revisions ======
+
+These are the older revisons of the current document. To revert to an old revision, select it from below, click ''Edit this page'' and save it.
diff --git a/platform/www/inc/lang/en/searchpage.txt b/platform/www/inc/lang/en/searchpage.txt
new file mode 100644
index 0000000..0cd0160
--- /dev/null
+++ b/platform/www/inc/lang/en/searchpage.txt
@@ -0,0 +1,3 @@
+====== Search ======
+
+You can find the results of your search below. @CREATEPAGEINFO@
diff --git a/platform/www/inc/lang/en/showrev.txt b/platform/www/inc/lang/en/showrev.txt
new file mode 100644
index 0000000..3608de3
--- /dev/null
+++ b/platform/www/inc/lang/en/showrev.txt
@@ -0,0 +1,2 @@
+**This is an old revision of the document!**
+----
diff --git a/platform/www/inc/lang/en/stopwords.txt b/platform/www/inc/lang/en/stopwords.txt
new file mode 100644
index 0000000..afc3016
--- /dev/null
+++ b/platform/www/inc/lang/en/stopwords.txt
@@ -0,0 +1,39 @@
+# This is a list of words the indexer ignores, one word per line
+# When you edit this file be sure to use UNIX line endings (single newline)
+# No need to include words shorter than 3 chars - these are ignored anyway
+# This list is based upon the ones found at http://www.ranks.nl/stopwords/
+about
+are
+as
+an
+and
+you
+your
+them
+their
+com
+for
+from
+into
+if
+in
+is
+it
+how
+of
+on
+or
+that
+the
+this
+to
+was
+what
+when
+where
+who
+will
+with
+und
+the
+www
diff --git a/platform/www/inc/lang/en/subscr_digest.txt b/platform/www/inc/lang/en/subscr_digest.txt
new file mode 100644
index 0000000..cc42e08
--- /dev/null
+++ b/platform/www/inc/lang/en/subscr_digest.txt
@@ -0,0 +1,16 @@
+Hello!
+
+The page @PAGE@ in the @TITLE@ wiki changed.
+Here are the changes:
+
+--------------------------------------------------------
+@DIFF@
+--------------------------------------------------------
+
+Old Revision: @OLDPAGE@
+New Revision: @NEWPAGE@
+
+To cancel the page notifications, log into the wiki at
+@DOKUWIKIURL@ then visit
+@SUBSCRIBE@
+and unsubscribe page and/or namespace changes.
diff --git a/platform/www/inc/lang/en/subscr_form.txt b/platform/www/inc/lang/en/subscr_form.txt
new file mode 100644
index 0000000..d606508
--- /dev/null
+++ b/platform/www/inc/lang/en/subscr_form.txt
@@ -0,0 +1,3 @@
+====== Subscription Management ======
+
+This page allows you to manage your subscriptions for the current page and namespace.
diff --git a/platform/www/inc/lang/en/subscr_list.txt b/platform/www/inc/lang/en/subscr_list.txt
new file mode 100644
index 0000000..dcf8000
--- /dev/null
+++ b/platform/www/inc/lang/en/subscr_list.txt
@@ -0,0 +1,13 @@
+Hello!
+
+Pages in the namespace @PAGE@ of the @TITLE@ wiki changed.
+Here are the changed pages:
+
+--------------------------------------------------------
+@DIFF@
+--------------------------------------------------------
+
+To cancel the page notifications, log into the wiki at
+@DOKUWIKIURL@ then visit
+@SUBSCRIBE@
+and unsubscribe page and/or namespace changes.
diff --git a/platform/www/inc/lang/en/subscr_single.txt b/platform/www/inc/lang/en/subscr_single.txt
new file mode 100644
index 0000000..046b994
--- /dev/null
+++ b/platform/www/inc/lang/en/subscr_single.txt
@@ -0,0 +1,19 @@
+Hello!
+
+The page @PAGE@ in the @TITLE@ wiki changed.
+Here are the changes:
+
+--------------------------------------------------------
+@DIFF@
+--------------------------------------------------------
+
+User : @USER@
+Edit Summary : @SUMMARY@
+Old Revision : @OLDPAGE@
+New Revision : @NEWPAGE@
+Date of New Revision: @DATE@
+
+To cancel the page notifications, log into the wiki at
+@DOKUWIKIURL@ then visit
+@SUBSCRIBE@
+and unsubscribe page and/or namespace changes.
diff --git a/platform/www/inc/lang/en/updateprofile.txt b/platform/www/inc/lang/en/updateprofile.txt
new file mode 100644
index 0000000..73e53aa
--- /dev/null
+++ b/platform/www/inc/lang/en/updateprofile.txt
@@ -0,0 +1,3 @@
+====== Update your account profile ======
+
+You only need to complete those fields you wish to change. You may not change your user name.
diff --git a/platform/www/inc/lang/en/uploadmail.txt b/platform/www/inc/lang/en/uploadmail.txt
new file mode 100644
index 0000000..dca8e33
--- /dev/null
+++ b/platform/www/inc/lang/en/uploadmail.txt
@@ -0,0 +1,11 @@
+A file was uploaded to your DokuWiki. Here are the details:
+
+File : @MEDIA@
+Old revision: @OLD@
+Date : @DATE@
+Browser : @BROWSER@
+IP-Address : @IPADDRESS@
+Hostname : @HOSTNAME@
+Size : @SIZE@
+MIME Type : @MIME@
+User : @USER@
diff --git a/platform/www/inc/legacy.php b/platform/www/inc/legacy.php
new file mode 100644
index 0000000..fa72649
--- /dev/null
+++ b/platform/www/inc/legacy.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * We map legacy class names to the new namespaced versions here
+ *
+ * These are names that we will probably never change because they have been part of DokuWiki's
+ * public interface for years and renaming would break just too many plugins
+ */
+
+class_alias('\dokuwiki\Extension\EventHandler', 'Doku_Event_Handler');
+class_alias('\dokuwiki\Extension\Event', 'Doku_Event');
+
+class_alias('\dokuwiki\Extension\ActionPlugin', 'DokuWiki_Action_Plugin');
+class_alias('\dokuwiki\Extension\AdminPlugin', 'DokuWiki_Admin_Plugin');
+class_alias('\dokuwiki\Extension\AuthPlugin', 'DokuWiki_Auth_Plugin');
+class_alias('\dokuwiki\Extension\CLIPlugin', 'DokuWiki_CLI_Plugin');
+class_alias('\dokuwiki\Extension\Plugin', 'DokuWiki_Plugin');
+class_alias('\dokuwiki\Extension\RemotePlugin', 'DokuWiki_Remote_Plugin');
+class_alias('\dokuwiki\Extension\SyntaxPlugin', 'DokuWiki_Syntax_Plugin');
diff --git a/platform/www/inc/load.php b/platform/www/inc/load.php
new file mode 100644
index 0000000..46cd91f
--- /dev/null
+++ b/platform/www/inc/load.php
@@ -0,0 +1,153 @@
+<?php
+/**
+ * Load all internal libraries and setup class autoloader
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+use dokuwiki\Extension\PluginController;
+
+// setup class autoloader
+spl_autoload_register('load_autoload');
+
+// require all the common libraries
+// for a few of these order does matter
+require_once(DOKU_INC.'inc/defines.php');
+require_once(DOKU_INC.'inc/actions.php');
+require_once(DOKU_INC.'inc/changelog.php');
+require_once(DOKU_INC.'inc/common.php');
+require_once(DOKU_INC.'inc/confutils.php');
+require_once(DOKU_INC.'inc/pluginutils.php');
+require_once(DOKU_INC.'inc/form.php');
+require_once(DOKU_INC.'inc/fulltext.php');
+require_once(DOKU_INC.'inc/html.php');
+require_once(DOKU_INC.'inc/httputils.php');
+require_once(DOKU_INC.'inc/indexer.php');
+require_once(DOKU_INC.'inc/infoutils.php');
+require_once(DOKU_INC.'inc/io.php');
+require_once(DOKU_INC.'inc/mail.php');
+require_once(DOKU_INC.'inc/media.php');
+require_once(DOKU_INC.'inc/pageutils.php');
+require_once(DOKU_INC.'inc/parserutils.php');
+require_once(DOKU_INC.'inc/search.php');
+require_once(DOKU_INC.'inc/template.php');
+require_once(DOKU_INC.'inc/toolbar.php');
+require_once(DOKU_INC.'inc/utf8.php');
+require_once(DOKU_INC.'inc/auth.php');
+require_once(DOKU_INC.'inc/compatibility.php');
+require_once(DOKU_INC.'inc/deprecated.php');
+require_once(DOKU_INC.'inc/legacy.php');
+
+/**
+ * spl_autoload_register callback
+ *
+ * Contains a static list of DokuWiki's core classes and automatically
+ * require()s their associated php files when an object is instantiated.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @todo add generic loading of renderers and auth backends
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+function load_autoload($name){
+ static $classes = null;
+ if($classes === null) $classes = array(
+ 'Diff' => DOKU_INC.'inc/DifferenceEngine.php',
+ 'UnifiedDiffFormatter' => DOKU_INC.'inc/DifferenceEngine.php',
+ 'TableDiffFormatter' => DOKU_INC.'inc/DifferenceEngine.php',
+ 'cache' => DOKU_INC.'inc/cache.php',
+ 'cache_parser' => DOKU_INC.'inc/cache.php',
+ 'cache_instructions' => DOKU_INC.'inc/cache.php',
+ 'cache_renderer' => DOKU_INC.'inc/cache.php',
+ 'Input' => DOKU_INC.'inc/Input.class.php',
+ 'JpegMeta' => DOKU_INC.'inc/JpegMeta.php',
+ 'SimplePie' => DOKU_INC.'inc/SimplePie.php',
+ 'FeedParser' => DOKU_INC.'inc/FeedParser.php',
+ 'IXR_Server' => DOKU_INC.'inc/IXR_Library.php',
+ 'IXR_Client' => DOKU_INC.'inc/IXR_Library.php',
+ 'IXR_Error' => DOKU_INC.'inc/IXR_Library.php',
+ 'IXR_IntrospectionServer' => DOKU_INC.'inc/IXR_Library.php',
+ 'SafeFN' => DOKU_INC.'inc/SafeFN.class.php',
+ 'Sitemapper' => DOKU_INC.'inc/Sitemapper.php',
+ 'Mailer' => DOKU_INC.'inc/Mailer.class.php',
+
+ 'Doku_Handler' => DOKU_INC.'inc/parser/handler.php',
+ 'Doku_Renderer' => DOKU_INC.'inc/parser/renderer.php',
+ 'Doku_Renderer_xhtml' => DOKU_INC.'inc/parser/xhtml.php',
+ 'Doku_Renderer_code' => DOKU_INC.'inc/parser/code.php',
+ 'Doku_Renderer_xhtmlsummary' => DOKU_INC.'inc/parser/xhtmlsummary.php',
+ 'Doku_Renderer_metadata' => DOKU_INC.'inc/parser/metadata.php',
+
+ 'DokuCLI' => DOKU_INC.'inc/cli.php',
+ 'DokuCLI_Options' => DOKU_INC.'inc/cli.php',
+ 'DokuCLI_Colors' => DOKU_INC.'inc/cli.php',
+
+ );
+
+ if(isset($classes[$name])){
+ require ($classes[$name]);
+ return true;
+ }
+
+ // namespace to directory conversion
+ $name = str_replace('\\', '/', $name);
+
+ // test namespace
+ if(substr($name, 0, 14) === 'dokuwiki/test/') {
+ $file = DOKU_INC . '_test/' . substr($name, 14) . '.php';
+ if(file_exists($file)) {
+ require $file;
+ return true;
+ }
+ }
+
+ // plugin namespace
+ if(substr($name, 0, 16) === 'dokuwiki/plugin/') {
+ $name = str_replace('/test/', '/_test/', $name); // no underscore in test namespace
+ $file = DOKU_PLUGIN . substr($name, 16) . '.php';
+ if(file_exists($file)) {
+ require $file;
+ return true;
+ }
+ }
+
+ // template namespace
+ if(substr($name, 0, 18) === 'dokuwiki/template/') {
+ $name = str_replace('/test/', '/_test/', $name); // no underscore in test namespace
+ $file = DOKU_INC.'lib/tpl/' . substr($name, 18) . '.php';
+ if(file_exists($file)) {
+ require $file;
+ return true;
+ }
+ }
+
+ // our own namespace
+ if(substr($name, 0, 9) === 'dokuwiki/') {
+ $file = DOKU_INC . 'inc/' . substr($name, 9) . '.php';
+ if(file_exists($file)) {
+ require $file;
+ return true;
+ }
+ }
+
+ // Plugin loading
+ if(preg_match(
+ '/^(' . implode('|', PluginController::PLUGIN_TYPES) . ')_plugin_(' .
+ DOKU_PLUGIN_NAME_REGEX .
+ ')(?:_([^_]+))?$/',
+ $name,
+ $m
+ )) {
+ // try to load the wanted plugin file
+ $c = ((count($m) === 4) ? "/{$m[3]}" : '');
+ $plg = DOKU_PLUGIN . "{$m[2]}/{$m[1]}$c.php";
+ if(file_exists($plg)){
+ require $plg;
+ }
+ return true;
+ }
+ return false;
+}
+
diff --git a/platform/www/inc/mail.php b/platform/www/inc/mail.php
new file mode 100644
index 0000000..ef4f440
--- /dev/null
+++ b/platform/www/inc/mail.php
@@ -0,0 +1,166 @@
+<?php
+/**
+ * Mail functions
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+// end of line for mail lines - RFC822 says CRLF but postfix (and other MTAs?)
+// think different
+if(!defined('MAILHEADER_EOL')) define('MAILHEADER_EOL',"\n");
+#define('MAILHEADER_ASCIIONLY',1);
+
+/**
+ * Patterns for use in email detection and validation
+ *
+ * NOTE: there is an unquoted '/' in RFC2822_ATEXT, it must remain unquoted to be used in the parser
+ * the pattern uses non-capturing groups as captured groups aren't allowed in the parser
+ * select pattern delimiters with care!
+ *
+ * May not be completly RFC conform!
+ * @link http://www.faqs.org/rfcs/rfc2822.html (paras 3.4.1 & 3.2.4)
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ * Check if a given mail address is valid
+ */
+if (!defined('RFC2822_ATEXT')) define('RFC2822_ATEXT',"0-9a-zA-Z!#$%&'*+/=?^_`{|}~-");
+if (!defined('PREG_PATTERN_VALID_EMAIL')) define(
+ 'PREG_PATTERN_VALID_EMAIL',
+ '['.RFC2822_ATEXT.']+(?:\.['.RFC2822_ATEXT.']+)*@(?i:[0-9a-z][0-9a-z-]*\.)+(?i:[a-z]{2,63})'
+);
+
+/**
+ * Prepare mailfrom replacement patterns
+ *
+ * Also prepares a mailfromnobody config that contains an autoconstructed address
+ * if the mailfrom one is userdependent and this might not be wanted (subscriptions)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function mail_setup(){
+ global $conf;
+ global $USERINFO;
+ /** @var Input $INPUT */
+ global $INPUT;
+
+ // auto constructed address
+ $host = @parse_url(DOKU_URL,PHP_URL_HOST);
+ if(!$host) $host = 'example.com';
+ $noreply = 'noreply@'.$host;
+
+ $replace = array();
+ if(!empty($USERINFO['mail'])){
+ $replace['@MAIL@'] = $USERINFO['mail'];
+ }else{
+ $replace['@MAIL@'] = $noreply;
+ }
+
+ // use 'noreply' if no user
+ $replace['@USER@'] = $INPUT->server->str('REMOTE_USER', 'noreply', true);
+
+ if(!empty($USERINFO['name'])){
+ $replace['@NAME@'] = $USERINFO['name'];
+ }else{
+ $replace['@NAME@'] = '';
+ }
+
+ // apply replacements
+ $from = str_replace(array_keys($replace),
+ array_values($replace),
+ $conf['mailfrom']);
+
+ // any replacements done? set different mailfromnone
+ if($from != $conf['mailfrom']){
+ $conf['mailfromnobody'] = $noreply;
+ }else{
+ $conf['mailfromnobody'] = $from;
+ }
+ $conf['mailfrom'] = $from;
+}
+
+/**
+ * Check if a given mail address is valid
+ *
+ * @param string $email the address to check
+ * @return bool true if address is valid
+ */
+function mail_isvalid($email) {
+ return EmailAddressValidator::checkEmailAddress($email, true);
+}
+
+/**
+ * Quoted printable encoding
+ *
+ * @author umu <umuAThrz.tu-chemnitz.de>
+ * @link http://php.net/manual/en/function.imap-8bit.php#61216
+ *
+ * @param string $sText
+ * @param int $maxlen
+ * @param bool $bEmulate_imap_8bit
+ *
+ * @return string
+ */
+function mail_quotedprintable_encode($sText,$maxlen=74,$bEmulate_imap_8bit=true) {
+ // split text into lines
+ $aLines= preg_split("/(?:\r\n|\r|\n)/", $sText);
+ $cnt = count($aLines);
+
+ for ($i=0;$i<$cnt;$i++) {
+ $sLine =& $aLines[$i];
+ if (strlen($sLine)===0) continue; // do nothing, if empty
+
+ $sRegExp = '/[^\x09\x20\x21-\x3C\x3E-\x7E]/e';
+
+ // imap_8bit encodes x09 everywhere, not only at lineends,
+ // for EBCDIC safeness encode !"#$@[\]^`{|}~,
+ // for complete safeness encode every character :)
+ if ($bEmulate_imap_8bit)
+ $sRegExp = '/[^\x20\x21-\x3C\x3E-\x7E]/';
+
+ $sLine = preg_replace_callback( $sRegExp, 'mail_quotedprintable_encode_callback', $sLine );
+
+ // encode x09,x20 at lineends
+ {
+ $iLength = strlen($sLine);
+ $iLastChar = ord($sLine[$iLength-1]);
+
+ // !!!!!!!!
+ // imap_8_bit does not encode x20 at the very end of a text,
+ // here is, where I don't agree with imap_8_bit,
+ // please correct me, if I'm wrong,
+ // or comment next line for RFC2045 conformance, if you like
+ if (!($bEmulate_imap_8bit && ($i==count($aLines)-1))){
+ if (($iLastChar==0x09)||($iLastChar==0x20)) {
+ $sLine[$iLength-1]='=';
+ $sLine .= ($iLastChar==0x09)?'09':'20';
+ }
+ }
+ } // imap_8bit encodes x20 before chr(13), too
+ // although IMHO not requested by RFC2045, why not do it safer :)
+ // and why not encode any x20 around chr(10) or chr(13)
+ if ($bEmulate_imap_8bit) {
+ $sLine=str_replace(' =0D','=20=0D',$sLine);
+ //$sLine=str_replace(' =0A','=20=0A',$sLine);
+ //$sLine=str_replace('=0D ','=0D=20',$sLine);
+ //$sLine=str_replace('=0A ','=0A=20',$sLine);
+ }
+
+ // finally split into softlines no longer than $maxlen chars,
+ // for even more safeness one could encode x09,x20
+ // at the very first character of the line
+ // and after soft linebreaks, as well,
+ // but this wouldn't be caught by such an easy RegExp
+ if($maxlen){
+ preg_match_all( '/.{1,'.($maxlen - 2).'}([^=]{0,2})?/', $sLine, $aMatch );
+ $sLine = implode( '=' . MAILHEADER_EOL, $aMatch[0] ); // add soft crlf's
+ }
+ }
+
+ // join lines into text
+ return implode(MAILHEADER_EOL,$aLines);
+}
+
+function mail_quotedprintable_encode_callback($matches){
+ return sprintf( "=%02X", ord ( $matches[0] ) ) ;
+}
diff --git a/platform/www/inc/media.php b/platform/www/inc/media.php
new file mode 100644
index 0000000..3cdefcc
--- /dev/null
+++ b/platform/www/inc/media.php
@@ -0,0 +1,2541 @@
+<?php
+/**
+ * All output and handler function needed for the media management popup
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+use dokuwiki\ChangeLog\MediaChangeLog;
+use dokuwiki\HTTP\DokuHTTPClient;
+use dokuwiki\Subscriptions\MediaSubscriptionSender;
+use dokuwiki\Extension\Event;
+
+/**
+ * Lists pages which currently use a media file selected for deletion
+ *
+ * References uses the same visual as search results and share
+ * their CSS tags except pagenames won't be links.
+ *
+ * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
+ *
+ * @param array $data
+ * @param string $id
+ */
+function media_filesinuse($data,$id){
+ global $lang;
+ echo '<h1>'.$lang['reference'].' <code>'.hsc(noNS($id)).'</code></h1>';
+ echo '<p>'.hsc($lang['ref_inuse']).'</p>';
+
+ $hidden=0; //count of hits without read permission
+ foreach($data as $row){
+ if(auth_quickaclcheck($row) >= AUTH_READ && isVisiblePage($row)){
+ echo '<div class="search_result">';
+ echo '<span class="mediaref_ref">'.hsc($row).'</span>';
+ echo '</div>';
+ }else
+ $hidden++;
+ }
+ if ($hidden){
+ print '<div class="mediaref_hidden">'.$lang['ref_hidden'].'</div>';
+ }
+}
+
+/**
+ * Handles the saving of image meta data
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $id media id
+ * @param int $auth permission level
+ * @param array $data
+ * @return false|string
+ */
+function media_metasave($id,$auth,$data){
+ if($auth < AUTH_UPLOAD) return false;
+ if(!checkSecurityToken()) return false;
+ global $lang;
+ global $conf;
+ $src = mediaFN($id);
+
+ $meta = new JpegMeta($src);
+ $meta->_parseAll();
+
+ foreach($data as $key => $val){
+ $val=trim($val);
+ if(empty($val)){
+ $meta->deleteField($key);
+ }else{
+ $meta->setField($key,$val);
+ }
+ }
+
+ $old = @filemtime($src);
+ if(!file_exists(mediaFN($id, $old)) && file_exists($src)) {
+ // add old revision to the attic
+ media_saveOldRevision($id);
+ }
+ $filesize_old = filesize($src);
+ if($meta->save()){
+ if($conf['fperm']) chmod($src, $conf['fperm']);
+ @clearstatcache(true, $src);
+ $new = @filemtime($src);
+ $filesize_new = filesize($src);
+ $sizechange = $filesize_new - $filesize_old;
+
+ // add a log entry to the media changelog
+ addMediaLogEntry($new, $id, DOKU_CHANGE_TYPE_EDIT, $lang['media_meta_edited'], '', null, $sizechange);
+
+ msg($lang['metasaveok'],1);
+ return $id;
+ }else{
+ msg($lang['metasaveerr'],-1);
+ return false;
+ }
+}
+
+/**
+ * check if a media is external source
+ *
+ * @author Gerrit Uitslag <klapinklapin@gmail.com>
+ *
+ * @param string $id the media ID or URL
+ * @return bool
+ */
+function media_isexternal($id){
+ if (preg_match('#^(?:https?|ftp)://#i', $id)) return true;
+ return false;
+}
+
+/**
+ * Check if a media item is public (eg, external URL or readable by @ALL)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id the media ID or URL
+ * @return bool
+ */
+function media_ispublic($id){
+ if(media_isexternal($id)) return true;
+ $id = cleanID($id);
+ if(auth_aclcheck(getNS($id).':*', '', array()) >= AUTH_READ) return true;
+ return false;
+}
+
+/**
+ * Display the form to edit image meta data
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $id media id
+ * @param int $auth permission level
+ * @return bool
+ */
+function media_metaform($id,$auth){
+ global $lang;
+
+ if($auth < AUTH_UPLOAD) {
+ echo '<div class="nothing">'.$lang['media_perm_upload'].'</div>'.NL;
+ return false;
+ }
+
+ // load the field descriptions
+ static $fields = null;
+ if(is_null($fields)){
+ $config_files = getConfigFiles('mediameta');
+ foreach ($config_files as $config_file) {
+ if(file_exists($config_file)) include($config_file);
+ }
+ }
+
+ $src = mediaFN($id);
+
+ // output
+ $form = new Doku_Form(array('action' => media_managerURL(array('tab_details' => 'view'), '&'),
+ 'class' => 'meta'));
+ $form->addHidden('img', $id);
+ $form->addHidden('mediado', 'save');
+ foreach($fields as $key => $field){
+ // get current value
+ if (empty($field[0])) continue;
+ $tags = array($field[0]);
+ if(is_array($field[3])) $tags = array_merge($tags,$field[3]);
+ $value = tpl_img_getTag($tags,'',$src);
+ $value = cleanText($value);
+
+ // prepare attributes
+ $p = array();
+ $p['class'] = 'edit';
+ $p['id'] = 'meta__'.$key;
+ $p['name'] = 'meta['.$field[0].']';
+ $p_attrs = array('class' => 'edit');
+
+ $form->addElement('<div class="row">');
+ if($field[2] == 'text'){
+ $form->addElement(
+ form_makeField(
+ 'text',
+ $p['name'],
+ $value,
+ ($lang[$field[1]]) ? $lang[$field[1]] : $field[1] . ':',
+ $p['id'],
+ $p['class'],
+ $p_attrs
+ )
+ );
+ }else{
+ $att = buildAttributes($p);
+ $form->addElement('<label for="meta__'.$key.'">'.$lang[$field[1]].'</label>');
+ $form->addElement("<textarea $att rows=\"6\" cols=\"50\">".formText($value).'</textarea>');
+ }
+ $form->addElement('</div>'.NL);
+ }
+ $form->addElement('<div class="buttons">');
+ $form->addElement(
+ form_makeButton(
+ 'submit',
+ '',
+ $lang['btn_save'],
+ array('accesskey' => 's', 'name' => 'mediado[save]')
+ )
+ );
+ $form->addElement('</div>'.NL);
+ $form->printForm();
+
+ return true;
+}
+
+/**
+ * Convenience function to check if a media file is still in use
+ *
+ * @author Michael Klier <chi@chimeric.de>
+ *
+ * @param string $id media id
+ * @return array|bool
+ */
+function media_inuse($id) {
+ global $conf;
+
+ if($conf['refcheck']){
+ $mediareferences = ft_mediause($id,true);
+ if(!count($mediareferences)) {
+ return false;
+ } else {
+ return $mediareferences;
+ }
+ } else {
+ return false;
+ }
+}
+
+/**
+ * Handles media file deletions
+ *
+ * If configured, checks for media references before deletion
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id media id
+ * @param int $auth no longer used
+ * @return int One of: 0,
+ * DOKU_MEDIA_DELETED,
+ * DOKU_MEDIA_DELETED | DOKU_MEDIA_EMPTY_NS,
+ * DOKU_MEDIA_NOT_AUTH,
+ * DOKU_MEDIA_INUSE
+ */
+function media_delete($id,$auth){
+ global $lang;
+ $auth = auth_quickaclcheck(ltrim(getNS($id).':*', ':'));
+ if($auth < AUTH_DELETE) return DOKU_MEDIA_NOT_AUTH;
+ if(media_inuse($id)) return DOKU_MEDIA_INUSE;
+
+ $file = mediaFN($id);
+
+ // trigger an event - MEDIA_DELETE_FILE
+ $data = array();
+ $data['id'] = $id;
+ $data['name'] = \dokuwiki\Utf8\PhpString::basename($file);
+ $data['path'] = $file;
+ $data['size'] = (file_exists($file)) ? filesize($file) : 0;
+
+ $data['unl'] = false;
+ $data['del'] = false;
+ $evt = new Event('MEDIA_DELETE_FILE',$data);
+ if ($evt->advise_before()) {
+ $old = @filemtime($file);
+ if(!file_exists(mediaFN($id, $old)) && file_exists($file)) {
+ // add old revision to the attic
+ media_saveOldRevision($id);
+ }
+
+ $data['unl'] = @unlink($file);
+ if($data['unl']) {
+ $sizechange = 0 - $data['size'];
+ addMediaLogEntry(time(), $id, DOKU_CHANGE_TYPE_DELETE, $lang['deleted'], '', null, $sizechange);
+
+ $data['del'] = io_sweepNS($id, 'mediadir');
+ }
+ }
+ $evt->advise_after();
+ unset($evt);
+
+ if($data['unl'] && $data['del']){
+ return DOKU_MEDIA_DELETED | DOKU_MEDIA_EMPTY_NS;
+ }
+
+ return $data['unl'] ? DOKU_MEDIA_DELETED : 0;
+}
+
+/**
+ * Handle file uploads via XMLHttpRequest
+ *
+ * @param string $ns target namespace
+ * @param int $auth current auth check result
+ * @return false|string false on error, id of the new file on success
+ */
+function media_upload_xhr($ns,$auth){
+ if(!checkSecurityToken()) return false;
+ global $INPUT;
+
+ $id = $INPUT->get->str('qqfile');
+ list($ext,$mime) = mimetype($id);
+ $input = fopen("php://input", "r");
+ if (!($tmp = io_mktmpdir())) return false;
+ $path = $tmp.'/'.md5($id);
+ $target = fopen($path, "w");
+ $realSize = stream_copy_to_stream($input, $target);
+ fclose($target);
+ fclose($input);
+ if (isset($_SERVER["CONTENT_LENGTH"]) && ($realSize != (int)$_SERVER["CONTENT_LENGTH"])){
+ unlink($path);
+ return false;
+ }
+
+ $res = media_save(
+ array('name' => $path,
+ 'mime' => $mime,
+ 'ext' => $ext),
+ $ns.':'.$id,
+ (($INPUT->get->str('ow') == 'true') ? true : false),
+ $auth,
+ 'copy'
+ );
+ unlink($path);
+ if ($tmp) io_rmdir($tmp, true);
+ if (is_array($res)) {
+ msg($res[0], $res[1]);
+ return false;
+ }
+ return $res;
+}
+
+/**
+ * Handles media file uploads
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Michael Klier <chi@chimeric.de>
+ *
+ * @param string $ns target namespace
+ * @param int $auth current auth check result
+ * @param bool|array $file $_FILES member, $_FILES['upload'] if false
+ * @return false|string false on error, id of the new file on success
+ */
+function media_upload($ns,$auth,$file=false){
+ if(!checkSecurityToken()) return false;
+ global $lang;
+ global $INPUT;
+
+ // get file and id
+ $id = $INPUT->post->str('mediaid');
+ if (!$file) $file = $_FILES['upload'];
+ if(empty($id)) $id = $file['name'];
+
+ // check for errors (messages are done in lib/exe/mediamanager.php)
+ if($file['error']) return false;
+
+ // check extensions
+ list($fext,$fmime) = mimetype($file['name']);
+ list($iext,$imime) = mimetype($id);
+ if($fext && !$iext){
+ // no extension specified in id - read original one
+ $id .= '.'.$fext;
+ $imime = $fmime;
+ }elseif($fext && $fext != $iext){
+ // extension was changed, print warning
+ msg(sprintf($lang['mediaextchange'],$fext,$iext));
+ }
+
+ $res = media_save(array('name' => $file['tmp_name'],
+ 'mime' => $imime,
+ 'ext' => $iext), $ns.':'.$id,
+ $INPUT->post->bool('ow'), $auth, 'copy_uploaded_file');
+ if (is_array($res)) {
+ msg($res[0], $res[1]);
+ return false;
+ }
+ return $res;
+}
+
+/**
+ * An alternative to move_uploaded_file that copies
+ *
+ * Using copy, makes sure any setgid bits on the media directory are honored
+ *
+ * @see move_uploaded_file()
+ *
+ * @param string $from
+ * @param string $to
+ * @return bool
+ */
+function copy_uploaded_file($from, $to){
+ if(!is_uploaded_file($from)) return false;
+ $ok = copy($from, $to);
+ @unlink($from);
+ return $ok;
+}
+
+/**
+ * This generates an action event and delegates to _media_upload_action().
+ * Action plugins are allowed to pre/postprocess the uploaded file.
+ * (The triggered event is preventable.)
+ *
+ * Event data:
+ * $data[0] fn_tmp: the temporary file name (read from $_FILES)
+ * $data[1] fn: the file name of the uploaded file
+ * $data[2] id: the future directory id of the uploaded file
+ * $data[3] imime: the mimetype of the uploaded file
+ * $data[4] overwrite: if an existing file is going to be overwritten
+ * $data[5] move: name of function that performs move/copy/..
+ *
+ * @triggers MEDIA_UPLOAD_FINISH
+ *
+ * @param array $file
+ * @param string $id media id
+ * @param bool $ow overwrite?
+ * @param int $auth permission level
+ * @param string $move name of functions that performs move/copy/..
+ * @return false|array|string
+ */
+function media_save($file, $id, $ow, $auth, $move) {
+ if($auth < AUTH_UPLOAD) {
+ return array("You don't have permissions to upload files.", -1);
+ }
+
+ if (!isset($file['mime']) || !isset($file['ext'])) {
+ list($ext, $mime) = mimetype($id);
+ if (!isset($file['mime'])) {
+ $file['mime'] = $mime;
+ }
+ if (!isset($file['ext'])) {
+ $file['ext'] = $ext;
+ }
+ }
+
+ global $lang, $conf;
+
+ // get filename
+ $id = cleanID($id);
+ $fn = mediaFN($id);
+
+ // get filetype regexp
+ $types = array_keys(getMimeTypes());
+ $types = array_map(
+ function ($q) {
+ return preg_quote($q, "/");
+ },
+ $types
+ );
+ $regex = join('|',$types);
+
+ // because a temp file was created already
+ if(!preg_match('/\.('.$regex.')$/i',$fn)) {
+ return array($lang['uploadwrong'],-1);
+ }
+
+ //check for overwrite
+ $overwrite = file_exists($fn);
+ $auth_ow = (($conf['mediarevisions']) ? AUTH_UPLOAD : AUTH_DELETE);
+ if($overwrite && (!$ow || $auth < $auth_ow)) {
+ return array($lang['uploadexist'], 0);
+ }
+ // check for valid content
+ $ok = media_contentcheck($file['name'], $file['mime']);
+ if($ok == -1){
+ return array(sprintf($lang['uploadbadcontent'],'.' . $file['ext']),-1);
+ }elseif($ok == -2){
+ return array($lang['uploadspam'],-1);
+ }elseif($ok == -3){
+ return array($lang['uploadxss'],-1);
+ }
+
+ // prepare event data
+ $data = array();
+ $data[0] = $file['name'];
+ $data[1] = $fn;
+ $data[2] = $id;
+ $data[3] = $file['mime'];
+ $data[4] = $overwrite;
+ $data[5] = $move;
+
+ // trigger event
+ return Event::createAndTrigger('MEDIA_UPLOAD_FINISH', $data, '_media_upload_action', true);
+}
+
+/**
+ * Callback adapter for media_upload_finish() triggered by MEDIA_UPLOAD_FINISH
+ *
+ * @author Michael Klier <chi@chimeric.de>
+ *
+ * @param array $data event data
+ * @return false|array|string
+ */
+function _media_upload_action($data) {
+ // fixme do further sanity tests of given data?
+ if(is_array($data) && count($data)===6) {
+ return media_upload_finish($data[0], $data[1], $data[2], $data[3], $data[4], $data[5]);
+ } else {
+ return false; //callback error
+ }
+}
+
+/**
+ * Saves an uploaded media file
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Michael Klier <chi@chimeric.de>
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $fn_tmp
+ * @param string $fn
+ * @param string $id media id
+ * @param string $imime mime type
+ * @param bool $overwrite overwrite existing?
+ * @param string $move function name
+ * @return array|string
+ */
+function media_upload_finish($fn_tmp, $fn, $id, $imime, $overwrite, $move = 'move_uploaded_file') {
+ global $conf;
+ global $lang;
+ global $REV;
+
+ $old = @filemtime($fn);
+ if(!file_exists(mediaFN($id, $old)) && file_exists($fn)) {
+ // add old revision to the attic if missing
+ media_saveOldRevision($id);
+ }
+
+ // prepare directory
+ io_createNamespace($id, 'media');
+
+ $filesize_old = file_exists($fn) ? filesize($fn) : 0;
+
+ if($move($fn_tmp, $fn)) {
+ @clearstatcache(true,$fn);
+ $new = @filemtime($fn);
+ // Set the correct permission here.
+ // Always chmod media because they may be saved with different permissions than expected from the php umask.
+ // (Should normally chmod to $conf['fperm'] only if $conf['fperm'] is set.)
+ chmod($fn, $conf['fmode']);
+ msg($lang['uploadsucc'],1);
+ media_notify($id,$fn,$imime,$old,$new);
+ // add a log entry to the media changelog
+ $filesize_new = filesize($fn);
+ $sizechange = $filesize_new - $filesize_old;
+ if($REV) {
+ addMediaLogEntry(
+ $new,
+ $id,
+ DOKU_CHANGE_TYPE_REVERT,
+ sprintf($lang['restored'], dformat($REV)),
+ $REV,
+ null,
+ $sizechange
+ );
+ } elseif($overwrite) {
+ addMediaLogEntry($new, $id, DOKU_CHANGE_TYPE_EDIT, '', '', null, $sizechange);
+ } else {
+ addMediaLogEntry($new, $id, DOKU_CHANGE_TYPE_CREATE, $lang['created'], '', null, $sizechange);
+ }
+ return $id;
+ }else{
+ return array($lang['uploadfail'],-1);
+ }
+}
+
+/**
+ * Moves the current version of media file to the media_attic
+ * directory
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $id
+ * @return int - revision date
+ */
+function media_saveOldRevision($id){
+ global $conf, $lang;
+
+ $oldf = mediaFN($id);
+ if(!file_exists($oldf)) return '';
+ $date = filemtime($oldf);
+ if (!$conf['mediarevisions']) return $date;
+
+ $medialog = new MediaChangeLog($id);
+ if (!$medialog->getRevisionInfo($date)) {
+ // there was an external edit,
+ // there is no log entry for current version of file
+ $sizechange = filesize($oldf);
+ if(!file_exists(mediaMetaFN($id, '.changes'))) {
+ addMediaLogEntry($date, $id, DOKU_CHANGE_TYPE_CREATE, $lang['created'], '', null, $sizechange);
+ } else {
+ $oldRev = $medialog->getRevisions(-1, 1); // from changelog
+ $oldRev = (int) (empty($oldRev) ? 0 : $oldRev[0]);
+ $filesize_old = filesize(mediaFN($id, $oldRev));
+ $sizechange = $sizechange - $filesize_old;
+
+ addMediaLogEntry($date, $id, DOKU_CHANGE_TYPE_EDIT, '', '', null, $sizechange);
+ }
+ }
+
+ $newf = mediaFN($id,$date);
+ io_makeFileDir($newf);
+ if(copy($oldf, $newf)) {
+ // Set the correct permission here.
+ // Always chmod media because they may be saved with different permissions than expected from the php umask.
+ // (Should normally chmod to $conf['fperm'] only if $conf['fperm'] is set.)
+ chmod($newf, $conf['fmode']);
+ }
+ return $date;
+}
+
+/**
+ * This function checks if the uploaded content is really what the
+ * mimetype says it is. We also do spam checking for text types here.
+ *
+ * We need to do this stuff because we can not rely on the browser
+ * to do this check correctly. Yes, IE is broken as usual.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @link http://www.splitbrain.org/blog/2007-02/12-internet_explorer_facilitates_cross_site_scripting
+ * @fixme check all 26 magic IE filetypes here?
+ *
+ * @param string $file path to file
+ * @param string $mime mimetype
+ * @return int
+ */
+function media_contentcheck($file,$mime){
+ global $conf;
+ if($conf['iexssprotect']){
+ $fh = @fopen($file, 'rb');
+ if($fh){
+ $bytes = fread($fh, 256);
+ fclose($fh);
+ if(preg_match('/<(script|a|img|html|body|iframe)[\s>]/i',$bytes)){
+ return -3; //XSS: possibly malicious content
+ }
+ }
+ }
+ if(substr($mime,0,6) == 'image/'){
+ $info = @getimagesize($file);
+ if($mime == 'image/gif' && $info[2] != 1){
+ return -1; // uploaded content did not match the file extension
+ }elseif($mime == 'image/jpeg' && $info[2] != 2){
+ return -1;
+ }elseif($mime == 'image/png' && $info[2] != 3){
+ return -1;
+ }
+ # fixme maybe check other images types as well
+ }elseif(substr($mime,0,5) == 'text/'){
+ global $TEXT;
+ $TEXT = io_readFile($file);
+ if(checkwordblock()){
+ return -2; //blocked by the spam blacklist
+ }
+ }
+ return 0;
+}
+
+/**
+ * Send a notify mail on uploads
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id media id
+ * @param string $file path to file
+ * @param string $mime mime type
+ * @param bool|int $old_rev revision timestamp or false
+ * @return bool
+ */
+function media_notify($id,$file,$mime,$old_rev=false,$current_rev=false){
+ global $conf;
+ if(empty($conf['notify'])) return false; //notify enabled?
+
+ $subscription = new MediaSubscriptionSender();
+ return $subscription->sendMediaDiff($conf['notify'], 'uploadmail', $id, $old_rev, $current_rev);
+}
+
+/**
+ * List all files in a given Media namespace
+ *
+ * @param string $ns namespace
+ * @param null|int $auth permission level
+ * @param string $jump id
+ * @param bool $fullscreenview
+ * @param bool|string $sort sorting order, false skips sorting
+ */
+function media_filelist($ns,$auth=null,$jump='',$fullscreenview=false,$sort=false){
+ global $conf;
+ global $lang;
+ $ns = cleanID($ns);
+
+ // check auth our self if not given (needed for ajax calls)
+ if(is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
+
+ if (!$fullscreenview) echo '<h1 id="media__ns">:'.hsc($ns).'</h1>'.NL;
+
+ if($auth < AUTH_READ){
+ // FIXME: print permission warning here instead?
+ echo '<div class="nothing">'.$lang['nothingfound'].'</div>'.NL;
+ }else{
+ if (!$fullscreenview) {
+ media_uploadform($ns, $auth);
+ media_searchform($ns);
+ }
+
+ $dir = utf8_encodeFN(str_replace(':','/',$ns));
+ $data = array();
+ search($data,$conf['mediadir'],'search_media',
+ array('showmsg'=>true,'depth'=>1),$dir,1,$sort);
+
+ if(!count($data)){
+ echo '<div class="nothing">'.$lang['nothingfound'].'</div>'.NL;
+ }else {
+ if ($fullscreenview) {
+ echo '<ul class="' . _media_get_list_type() . '">';
+ }
+ foreach($data as $item){
+ if (!$fullscreenview) {
+ media_printfile($item,$auth,$jump);
+ } else {
+ media_printfile_thumbs($item,$auth,$jump);
+ }
+ }
+ if ($fullscreenview) echo '</ul>'.NL;
+ }
+ }
+}
+
+/**
+ * Prints tabs for files list actions
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ * @author Adrian Lang <mail@adrianlang.de>
+ *
+ * @param string $selected_tab - opened tab
+ */
+
+function media_tabs_files($selected_tab = ''){
+ global $lang;
+ $tabs = array();
+ foreach(array('files' => 'mediaselect',
+ 'upload' => 'media_uploadtab',
+ 'search' => 'media_searchtab') as $tab => $caption) {
+ $tabs[$tab] = array('href' => media_managerURL(array('tab_files' => $tab), '&'),
+ 'caption' => $lang[$caption]);
+ }
+
+ html_tabs($tabs, $selected_tab);
+}
+
+/**
+ * Prints tabs for files details actions
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ * @param string $image filename of the current image
+ * @param string $selected_tab opened tab
+ */
+function media_tabs_details($image, $selected_tab = ''){
+ global $lang, $conf;
+
+ $tabs = array();
+ $tabs['view'] = array('href' => media_managerURL(array('tab_details' => 'view'), '&'),
+ 'caption' => $lang['media_viewtab']);
+
+ list(, $mime) = mimetype($image);
+ if ($mime == 'image/jpeg' && file_exists(mediaFN($image))) {
+ $tabs['edit'] = array('href' => media_managerURL(array('tab_details' => 'edit'), '&'),
+ 'caption' => $lang['media_edittab']);
+ }
+ if ($conf['mediarevisions']) {
+ $tabs['history'] = array('href' => media_managerURL(array('tab_details' => 'history'), '&'),
+ 'caption' => $lang['media_historytab']);
+ }
+
+ html_tabs($tabs, $selected_tab);
+}
+
+/**
+ * Prints options for the tab that displays a list of all files
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ */
+function media_tab_files_options(){
+ global $lang;
+ global $INPUT;
+ global $ID;
+ $form = new Doku_Form(array('class' => 'options', 'method' => 'get',
+ 'action' => wl($ID)));
+ $media_manager_params = media_managerURL(array(), '', false, true);
+ foreach($media_manager_params as $pKey => $pVal){
+ $form->addHidden($pKey, $pVal);
+ }
+ $form->addHidden('sectok', null);
+ if ($INPUT->has('q')) {
+ $form->addHidden('q', $INPUT->str('q'));
+ }
+ $form->addElement('<ul>'.NL);
+ foreach(array('list' => array('listType', array('thumbs', 'rows')),
+ 'sort' => array('sortBy', array('name', 'date')))
+ as $group => $content) {
+ $checked = "_media_get_${group}_type";
+ $checked = $checked();
+
+ $form->addElement('<li class="' . $content[0] . '">');
+ foreach($content[1] as $option) {
+ $attrs = array();
+ if ($checked == $option) {
+ $attrs['checked'] = 'checked';
+ }
+ $form->addElement(form_makeRadioField($group . '_dwmedia', $option,
+ $lang['media_' . $group . '_' . $option],
+ $content[0] . '__' . $option,
+ $option, $attrs));
+ }
+ $form->addElement('</li>'.NL);
+ }
+ $form->addElement('<li>');
+ $form->addElement(form_makeButton('submit', '', $lang['btn_apply']));
+ $form->addElement('</li>'.NL);
+ $form->addElement('</ul>'.NL);
+ $form->printForm();
+}
+
+/**
+ * Returns type of sorting for the list of files in media manager
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @return string - sort type
+ */
+function _media_get_sort_type() {
+ return _media_get_display_param('sort', array('default' => 'name', 'date'));
+}
+
+/**
+ * Returns type of listing for the list of files in media manager
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @return string - list type
+ */
+function _media_get_list_type() {
+ return _media_get_display_param('list', array('default' => 'thumbs', 'rows'));
+}
+
+/**
+ * Get display parameters
+ *
+ * @param string $param name of parameter
+ * @param array $values allowed values, where default value has index key 'default'
+ * @return string the parameter value
+ */
+function _media_get_display_param($param, $values) {
+ global $INPUT;
+ if (in_array($INPUT->str($param), $values)) {
+ // FIXME: Set cookie
+ return $INPUT->str($param);
+ } else {
+ $val = get_doku_pref($param, $values['default']);
+ if (!in_array($val, $values)) {
+ $val = $values['default'];
+ }
+ return $val;
+ }
+}
+
+/**
+ * Prints tab that displays a list of all files
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $ns
+ * @param null|int $auth permission level
+ * @param string $jump item id
+ */
+function media_tab_files($ns,$auth=null,$jump='') {
+ global $lang;
+ if(is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
+
+ if($auth < AUTH_READ){
+ echo '<div class="nothing">'.$lang['media_perm_read'].'</div>'.NL;
+ }else{
+ media_filelist($ns,$auth,$jump,true,_media_get_sort_type());
+ }
+}
+
+/**
+ * Prints tab that displays uploading form
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $ns
+ * @param null|int $auth permission level
+ * @param string $jump item id
+ */
+function media_tab_upload($ns,$auth=null,$jump='') {
+ global $lang;
+ if(is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
+
+ echo '<div class="upload">'.NL;
+ if ($auth >= AUTH_UPLOAD) {
+ echo '<p>' . $lang['mediaupload'] . '</p>';
+ }
+ media_uploadform($ns, $auth, true);
+ echo '</div>'.NL;
+}
+
+/**
+ * Prints tab that displays search form
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $ns
+ * @param null|int $auth permission level
+ */
+function media_tab_search($ns,$auth=null) {
+ global $INPUT;
+
+ $do = $INPUT->str('mediado');
+ $query = $INPUT->str('q');
+ echo '<div class="search">'.NL;
+
+ media_searchform($ns, $query, true);
+ if ($do == 'searchlist' || $query) {
+ media_searchlist($query,$ns,$auth,true,_media_get_sort_type());
+ }
+ echo '</div>'.NL;
+}
+
+/**
+ * Prints tab that displays mediafile details
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $image media id
+ * @param string $ns
+ * @param null|int $auth permission level
+ * @param string|int $rev revision timestamp or empty string
+ */
+function media_tab_view($image, $ns, $auth=null, $rev='') {
+ global $lang;
+ if(is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
+
+ if ($image && $auth >= AUTH_READ) {
+ $meta = new JpegMeta(mediaFN($image, $rev));
+ media_preview($image, $auth, $rev, $meta);
+ media_preview_buttons($image, $auth, $rev);
+ media_details($image, $auth, $rev, $meta);
+
+ } else {
+ echo '<div class="nothing">'.$lang['media_perm_read'].'</div>'.NL;
+ }
+}
+
+/**
+ * Prints tab that displays form for editing mediafile metadata
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $image media id
+ * @param string $ns
+ * @param null|int $auth permission level
+ */
+function media_tab_edit($image, $ns, $auth=null) {
+ if(is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
+
+ if ($image) {
+ list(, $mime) = mimetype($image);
+ if ($mime == 'image/jpeg') media_metaform($image,$auth);
+ }
+}
+
+/**
+ * Prints tab that displays mediafile revisions
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $image media id
+ * @param string $ns
+ * @param null|int $auth permission level
+ */
+function media_tab_history($image, $ns, $auth=null) {
+ global $lang;
+ global $INPUT;
+
+ if(is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
+ $do = $INPUT->str('mediado');
+
+ if ($auth >= AUTH_READ && $image) {
+ if ($do == 'diff'){
+ media_diff($image, $ns, $auth);
+ } else {
+ $first = $INPUT->int('first');
+ html_revisions($first, $image);
+ }
+ } else {
+ echo '<div class="nothing">'.$lang['media_perm_read'].'</div>'.NL;
+ }
+}
+
+/**
+ * Prints mediafile details
+ *
+ * @param string $image media id
+ * @param int $auth permission level
+ * @param int|string $rev revision timestamp or empty string
+ * @param JpegMeta|bool $meta
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ */
+function media_preview($image, $auth, $rev='', $meta=false) {
+
+ $size = media_image_preview_size($image, $rev, $meta);
+
+ if ($size) {
+ global $lang;
+ echo '<div class="image">';
+
+ $more = array();
+ if ($rev) {
+ $more['rev'] = $rev;
+ } else {
+ $t = @filemtime(mediaFN($image));
+ $more['t'] = $t;
+ }
+
+ $more['w'] = $size[0];
+ $more['h'] = $size[1];
+ $src = ml($image, $more);
+
+ echo '<a href="'.$src.'" target="_blank" title="'.$lang['mediaview'].'">';
+ echo '<img src="'.$src.'" alt="" style="max-width: '.$size[0].'px;" />';
+ echo '</a>';
+
+ echo '</div>'.NL;
+ }
+}
+
+/**
+ * Prints mediafile action buttons
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $image media id
+ * @param int $auth permission level
+ * @param string|int $rev revision timestamp, or empty string
+ */
+function media_preview_buttons($image, $auth, $rev='') {
+ global $lang, $conf;
+
+ echo '<ul class="actions">'.NL;
+
+ if($auth >= AUTH_DELETE && !$rev && file_exists(mediaFN($image))){
+
+ // delete button
+ $form = new Doku_Form(array('id' => 'mediamanager__btn_delete',
+ 'action'=>media_managerURL(array('delete' => $image), '&')));
+ $form->addElement(form_makeButton('submit','',$lang['btn_delete']));
+ echo '<li>';
+ $form->printForm();
+ echo '</li>'.NL;
+ }
+
+ $auth_ow = (($conf['mediarevisions']) ? AUTH_UPLOAD : AUTH_DELETE);
+ if($auth >= $auth_ow && !$rev){
+
+ // upload new version button
+ $form = new Doku_Form(array('id' => 'mediamanager__btn_update',
+ 'action'=>media_managerURL(array('image' => $image, 'mediado' => 'update'), '&')));
+ $form->addElement(form_makeButton('submit','',$lang['media_update']));
+ echo '<li>';
+ $form->printForm();
+ echo '</li>'.NL;
+ }
+
+ if($auth >= AUTH_UPLOAD && $rev && $conf['mediarevisions'] && file_exists(mediaFN($image, $rev))){
+
+ // restore button
+ $form = new Doku_Form(array('id' => 'mediamanager__btn_restore',
+ 'action'=>media_managerURL(array('image' => $image), '&')));
+ $form->addHidden('mediado','restore');
+ $form->addHidden('rev',$rev);
+ $form->addElement(form_makeButton('submit','',$lang['media_restore']));
+ echo '<li>';
+ $form->printForm();
+ echo '</li>'.NL;
+ }
+
+ echo '</ul>'.NL;
+}
+
+/**
+ * Returns image width and height for mediamanager preview panel
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ * @param string $image
+ * @param int|string $rev
+ * @param JpegMeta|bool $meta
+ * @param int $size
+ * @return array|false
+ */
+function media_image_preview_size($image, $rev, $meta, $size = 500) {
+ if (!preg_match("/\.(jpe?g|gif|png)$/", $image) || !file_exists(mediaFN($image, $rev))) return false;
+
+ $info = getimagesize(mediaFN($image, $rev));
+ $w = (int) $info[0];
+ $h = (int) $info[1];
+
+ if($meta && ($w > $size || $h > $size)){
+ $ratio = $meta->getResizeRatio($size, $size);
+ $w = floor($w * $ratio);
+ $h = floor($h * $ratio);
+ }
+ return array($w, $h);
+}
+
+/**
+ * Returns the requested EXIF/IPTC tag from the image meta
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param array $tags array with tags, first existing is returned
+ * @param JpegMeta $meta
+ * @param string $alt alternative value
+ * @return string
+ */
+function media_getTag($tags,$meta,$alt=''){
+ if($meta === false) return $alt;
+ $info = $meta->getField($tags);
+ if($info == false) return $alt;
+ return $info;
+}
+
+/**
+ * Returns mediafile tags
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param JpegMeta $meta
+ * @return array list of tags of the mediafile
+ */
+function media_file_tags($meta) {
+ // load the field descriptions
+ static $fields = null;
+ if(is_null($fields)){
+ $config_files = getConfigFiles('mediameta');
+ foreach ($config_files as $config_file) {
+ if(file_exists($config_file)) include($config_file);
+ }
+ }
+
+ $tags = array();
+
+ foreach($fields as $key => $tag){
+ $t = array();
+ if (!empty($tag[0])) $t = array($tag[0]);
+ if(isset($tag[3]) && is_array($tag[3])) $t = array_merge($t,$tag[3]);
+ $value = media_getTag($t, $meta);
+ $tags[] = array('tag' => $tag, 'value' => $value);
+ }
+
+ return $tags;
+}
+
+/**
+ * Prints mediafile tags
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $image image id
+ * @param int $auth permission level
+ * @param string|int $rev revision timestamp, or empty string
+ * @param bool|JpegMeta $meta image object, or create one if false
+ */
+function media_details($image, $auth, $rev='', $meta=false) {
+ global $lang;
+
+ if (!$meta) $meta = new JpegMeta(mediaFN($image, $rev));
+ $tags = media_file_tags($meta);
+
+ echo '<dl>'.NL;
+ foreach($tags as $tag){
+ if ($tag['value']) {
+ $value = cleanText($tag['value']);
+ echo '<dt>'.$lang[$tag['tag'][1]].'</dt><dd>';
+ if ($tag['tag'][2] == 'date') echo dformat($value);
+ else echo hsc($value);
+ echo '</dd>'.NL;
+ }
+ }
+ echo '</dl>'.NL;
+ echo '<dl>'.NL;
+ echo '<dt>'.$lang['reference'].':</dt>';
+ $media_usage = ft_mediause($image,true);
+ if(count($media_usage) > 0){
+ foreach($media_usage as $path){
+ echo '<dd>'.html_wikilink($path).'</dd>';
+ }
+ }else{
+ echo '<dd>'.$lang['nothingfound'].'</dd>';
+ }
+ echo '</dl>'.NL;
+
+}
+
+/**
+ * Shows difference between two revisions of file
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $image image id
+ * @param string $ns
+ * @param int $auth permission level
+ * @param bool $fromajax
+ * @return false|null|string
+ */
+function media_diff($image, $ns, $auth, $fromajax = false) {
+ global $conf;
+ global $INPUT;
+
+ if ($auth < AUTH_READ || !$image || !$conf['mediarevisions']) return '';
+
+ $rev1 = $INPUT->int('rev');
+
+ $rev2 = $INPUT->ref('rev2');
+ if(is_array($rev2)){
+ $rev1 = (int) $rev2[0];
+ $rev2 = (int) $rev2[1];
+
+ if(!$rev1){
+ $rev1 = $rev2;
+ unset($rev2);
+ }
+ }else{
+ $rev2 = $INPUT->int('rev2');
+ }
+
+ if ($rev1 && !file_exists(mediaFN($image, $rev1))) $rev1 = false;
+ if ($rev2 && !file_exists(mediaFN($image, $rev2))) $rev2 = false;
+
+ if($rev1 && $rev2){ // two specific revisions wanted
+ // make sure order is correct (older on the left)
+ if($rev1 < $rev2){
+ $l_rev = $rev1;
+ $r_rev = $rev2;
+ }else{
+ $l_rev = $rev2;
+ $r_rev = $rev1;
+ }
+ }elseif($rev1){ // single revision given, compare to current
+ $r_rev = '';
+ $l_rev = $rev1;
+ }else{ // no revision was given, compare previous to current
+ $r_rev = '';
+ $medialog = new MediaChangeLog($image);
+ $revs = $medialog->getRevisions(0, 1);
+ if (file_exists(mediaFN($image, $revs[0]))) {
+ $l_rev = $revs[0];
+ } else {
+ $l_rev = '';
+ }
+ }
+
+ // prepare event data
+ $data = array();
+ $data[0] = $image;
+ $data[1] = $l_rev;
+ $data[2] = $r_rev;
+ $data[3] = $ns;
+ $data[4] = $auth;
+ $data[5] = $fromajax;
+
+ // trigger event
+ return Event::createAndTrigger('MEDIA_DIFF', $data, '_media_file_diff', true);
+}
+
+/**
+ * Callback for media file diff
+ *
+ * @param array $data event data
+ * @return false|null
+ */
+function _media_file_diff($data) {
+ if(is_array($data) && count($data)===6) {
+ media_file_diff($data[0], $data[1], $data[2], $data[3], $data[4], $data[5]);
+ } else {
+ return false;
+ }
+}
+
+/**
+ * Shows difference between two revisions of image
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $image
+ * @param string|int $l_rev revision timestamp, or empty string
+ * @param string|int $r_rev revision timestamp, or empty string
+ * @param string $ns
+ * @param int $auth permission level
+ * @param bool $fromajax
+ */
+function media_file_diff($image, $l_rev, $r_rev, $ns, $auth, $fromajax){
+ global $lang;
+ global $INPUT;
+
+ $l_meta = new JpegMeta(mediaFN($image, $l_rev));
+ $r_meta = new JpegMeta(mediaFN($image, $r_rev));
+
+ $is_img = preg_match('/\.(jpe?g|gif|png)$/', $image);
+ if ($is_img) {
+ $l_size = media_image_preview_size($image, $l_rev, $l_meta);
+ $r_size = media_image_preview_size($image, $r_rev, $r_meta);
+ $is_img = ($l_size && $r_size && ($l_size[0] >= 30 || $r_size[0] >= 30));
+
+ $difftype = $INPUT->str('difftype');
+
+ if (!$fromajax) {
+ $form = new Doku_Form(array(
+ 'action' => media_managerURL(array(), '&'),
+ 'method' => 'get',
+ 'id' => 'mediamanager__form_diffview',
+ 'class' => 'diffView'
+ ));
+ $form->addHidden('sectok', null);
+ $form->addElement('<input type="hidden" name="rev2[]" value="'.$l_rev.'" ></input>');
+ $form->addElement('<input type="hidden" name="rev2[]" value="'.$r_rev.'" ></input>');
+ $form->addHidden('mediado', 'diff');
+ $form->printForm();
+
+ echo NL.'<div id="mediamanager__diff" >'.NL;
+ }
+
+ if ($difftype == 'opacity' || $difftype == 'portions') {
+ media_image_diff($image, $l_rev, $r_rev, $l_size, $r_size, $difftype);
+ if (!$fromajax) echo '</div>';
+ return;
+ }
+ }
+
+ list($l_head, $r_head) = html_diff_head($l_rev, $r_rev, $image, true);
+
+ ?>
+ <div class="table">
+ <table>
+ <tr>
+ <th><?php echo $l_head; ?></th>
+ <th><?php echo $r_head; ?></th>
+ </tr>
+ <?php
+
+ echo '<tr class="image">';
+ echo '<td>';
+ media_preview($image, $auth, $l_rev, $l_meta);
+ echo '</td>';
+
+ echo '<td>';
+ media_preview($image, $auth, $r_rev, $r_meta);
+ echo '</td>';
+ echo '</tr>'.NL;
+
+ echo '<tr class="actions">';
+ echo '<td>';
+ media_preview_buttons($image, $auth, $l_rev);
+ echo '</td>';
+
+ echo '<td>';
+ media_preview_buttons($image, $auth, $r_rev);
+ echo '</td>';
+ echo '</tr>'.NL;
+
+ $l_tags = media_file_tags($l_meta);
+ $r_tags = media_file_tags($r_meta);
+ // FIXME r_tags-only stuff
+ foreach ($l_tags as $key => $l_tag) {
+ if ($l_tag['value'] != $r_tags[$key]['value']) {
+ $r_tags[$key]['highlighted'] = true;
+ $l_tags[$key]['highlighted'] = true;
+ } else if (!$l_tag['value'] || !$r_tags[$key]['value']) {
+ unset($r_tags[$key]);
+ unset($l_tags[$key]);
+ }
+ }
+
+ echo '<tr>';
+ foreach(array($l_tags,$r_tags) as $tags){
+ echo '<td>'.NL;
+
+ echo '<dl class="img_tags">';
+ foreach($tags as $tag){
+ $value = cleanText($tag['value']);
+ if (!$value) $value = '-';
+ echo '<dt>'.$lang[$tag['tag'][1]].'</dt>';
+ echo '<dd>';
+ if ($tag['highlighted']) {
+ echo '<strong>';
+ }
+ if ($tag['tag'][2] == 'date') echo dformat($value);
+ else echo hsc($value);
+ if ($tag['highlighted']) {
+ echo '</strong>';
+ }
+ echo '</dd>';
+ }
+ echo '</dl>'.NL;
+
+ echo '</td>';
+ }
+ echo '</tr>'.NL;
+
+ echo '</table>'.NL;
+ echo '</div>'.NL;
+
+ if ($is_img && !$fromajax) echo '</div>';
+}
+
+/**
+ * Prints two images side by side
+ * and slider
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $image image id
+ * @param int $l_rev revision timestamp, or empty string
+ * @param int $r_rev revision timestamp, or empty string
+ * @param array $l_size array with width and height
+ * @param array $r_size array with width and height
+ * @param string $type
+ */
+function media_image_diff($image, $l_rev, $r_rev, $l_size, $r_size, $type) {
+ if ($l_size != $r_size) {
+ if ($r_size[0] > $l_size[0]) {
+ $l_size = $r_size;
+ }
+ }
+
+ $l_more = array('rev' => $l_rev, 'h' => $l_size[1], 'w' => $l_size[0]);
+ $r_more = array('rev' => $r_rev, 'h' => $l_size[1], 'w' => $l_size[0]);
+
+ $l_src = ml($image, $l_more);
+ $r_src = ml($image, $r_more);
+
+ // slider
+ echo '<div class="slider" style="max-width: '.($l_size[0]-20).'px;" ></div>'.NL;
+
+ // two images in divs
+ echo '<div class="imageDiff ' . $type . '">'.NL;
+ echo '<div class="image1" style="max-width: '.$l_size[0].'px;">';
+ echo '<img src="'.$l_src.'" alt="" />';
+ echo '</div>'.NL;
+ echo '<div class="image2" style="max-width: '.$l_size[0].'px;">';
+ echo '<img src="'.$r_src.'" alt="" />';
+ echo '</div>'.NL;
+ echo '</div>'.NL;
+}
+
+/**
+ * Restores an old revision of a media file
+ *
+ * @param string $image media id
+ * @param int $rev revision timestamp or empty string
+ * @param int $auth
+ * @return string - file's id
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ */
+function media_restore($image, $rev, $auth){
+ global $conf;
+ if ($auth < AUTH_UPLOAD || !$conf['mediarevisions']) return false;
+ $removed = (!file_exists(mediaFN($image)) && file_exists(mediaMetaFN($image, '.changes')));
+ if (!$image || (!file_exists(mediaFN($image)) && !$removed)) return false;
+ if (!$rev || !file_exists(mediaFN($image, $rev))) return false;
+ list(,$imime,) = mimetype($image);
+ $res = media_upload_finish(mediaFN($image, $rev),
+ mediaFN($image),
+ $image,
+ $imime,
+ true,
+ 'copy');
+ if (is_array($res)) {
+ msg($res[0], $res[1]);
+ return false;
+ }
+ return $res;
+}
+
+/**
+ * List all files found by the search request
+ *
+ * @author Tobias Sarnowski <sarnowski@cosmocode.de>
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ * @triggers MEDIA_SEARCH
+ *
+ * @param string $query
+ * @param string $ns
+ * @param null|int $auth
+ * @param bool $fullscreen
+ * @param string $sort
+ */
+function media_searchlist($query,$ns,$auth=null,$fullscreen=false,$sort='natural'){
+ global $conf;
+ global $lang;
+
+ $ns = cleanID($ns);
+ $evdata = array(
+ 'ns' => $ns,
+ 'data' => array(),
+ 'query' => $query
+ );
+ if (!blank($query)) {
+ $evt = new Event('MEDIA_SEARCH', $evdata);
+ if ($evt->advise_before()) {
+ $dir = utf8_encodeFN(str_replace(':','/',$evdata['ns']));
+ $quoted = preg_quote($evdata['query'],'/');
+ //apply globbing
+ $quoted = str_replace(array('\*', '\?'), array('.*', '.'), $quoted, $count);
+
+ //if we use globbing file name must match entirely but may be preceded by arbitrary namespace
+ if ($count > 0) $quoted = '^([^:]*:)*'.$quoted.'$';
+
+ $pattern = '/'.$quoted.'/i';
+ search($evdata['data'],
+ $conf['mediadir'],
+ 'search_media',
+ array('showmsg'=>false,'pattern'=>$pattern),
+ $dir,
+ 1,
+ $sort);
+ }
+ $evt->advise_after();
+ unset($evt);
+ }
+
+ if (!$fullscreen) {
+ echo '<h1 id="media__ns">'.sprintf($lang['searchmedia_in'],hsc($ns).':*').'</h1>'.NL;
+ media_searchform($ns,$query);
+ }
+
+ if(!count($evdata['data'])){
+ echo '<div class="nothing">'.$lang['nothingfound'].'</div>'.NL;
+ }else {
+ if ($fullscreen) {
+ echo '<ul class="' . _media_get_list_type() . '">';
+ }
+ foreach($evdata['data'] as $item){
+ if (!$fullscreen) media_printfile($item,$item['perm'],'',true);
+ else media_printfile_thumbs($item,$item['perm'],false,true);
+ }
+ if ($fullscreen) echo '</ul>'.NL;
+ }
+}
+
+/**
+ * Formats and prints one file in the list
+ *
+ * @param array $item
+ * @param int $auth permission level
+ * @param string $jump item id
+ * @param bool $display_namespace
+ */
+function media_printfile($item,$auth,$jump,$display_namespace=false){
+ global $lang;
+
+ // Prepare zebra coloring
+ // I always wanted to use this variable name :-D
+ static $twibble = 1;
+ $twibble *= -1;
+ $zebra = ($twibble == -1) ? 'odd' : 'even';
+
+ // Automatically jump to recent action
+ if($jump == $item['id']) {
+ $jump = ' id="scroll__here" ';
+ }else{
+ $jump = '';
+ }
+
+ // Prepare fileicons
+ list($ext) = mimetype($item['file'],false);
+ $class = preg_replace('/[^_\-a-z0-9]+/i','_',$ext);
+ $class = 'select mediafile mf_'.$class;
+
+ // Prepare filename
+ $file = utf8_decodeFN($item['file']);
+
+ // Prepare info
+ $info = '';
+ if($item['isimg']){
+ $info .= (int) $item['meta']->getField('File.Width');
+ $info .= '&#215;';
+ $info .= (int) $item['meta']->getField('File.Height');
+ $info .= ' ';
+ }
+ $info .= '<i>'.dformat($item['mtime']).'</i>';
+ $info .= ' ';
+ $info .= filesize_h($item['size']);
+
+ // output
+ echo '<div class="'.$zebra.'"'.$jump.' title="'.hsc($item['id']).'">'.NL;
+ if (!$display_namespace) {
+ echo '<a id="h_:'.$item['id'].'" class="'.$class.'">'.hsc($file).'</a> ';
+ } else {
+ echo '<a id="h_:'.$item['id'].'" class="'.$class.'">'.hsc($item['id']).'</a><br/>';
+ }
+ echo '<span class="info">('.$info.')</span>'.NL;
+
+ // view button
+ $link = ml($item['id'],'',true);
+ echo ' <a href="'.$link.'" target="_blank"><img src="'.DOKU_BASE.'lib/images/magnifier.png" '.
+ 'alt="'.$lang['mediaview'].'" title="'.$lang['mediaview'].'" class="btn" /></a>';
+
+ // mediamanager button
+ $link = wl('',array('do'=>'media','image'=>$item['id'],'ns'=>getNS($item['id'])));
+ echo ' <a href="'.$link.'" target="_blank"><img src="'.DOKU_BASE.'lib/images/mediamanager.png" '.
+ 'alt="'.$lang['btn_media'].'" title="'.$lang['btn_media'].'" class="btn" /></a>';
+
+ // delete button
+ if($item['writable'] && $auth >= AUTH_DELETE){
+ $link = DOKU_BASE.'lib/exe/mediamanager.php?delete='.rawurlencode($item['id']).
+ '&amp;sectok='.getSecurityToken();
+ echo ' <a href="'.$link.'" class="btn_media_delete" title="'.$item['id'].'">'.
+ '<img src="'.DOKU_BASE.'lib/images/trash.png" alt="'.$lang['btn_delete'].'" '.
+ 'title="'.$lang['btn_delete'].'" class="btn" /></a>';
+ }
+
+ echo '<div class="example" id="ex_'.str_replace(':','_',$item['id']).'">';
+ echo $lang['mediausage'].' <code>{{:'.$item['id'].'}}</code>';
+ echo '</div>';
+ if($item['isimg']) media_printimgdetail($item);
+ echo '<div class="clearer"></div>'.NL;
+ echo '</div>'.NL;
+}
+
+/**
+ * Display a media icon
+ *
+ * @param string $filename media id
+ * @param string $size the size subfolder, if not specified 16x16 is used
+ * @return string html
+ */
+function media_printicon($filename, $size=''){
+ list($ext) = mimetype(mediaFN($filename),false);
+
+ if (file_exists(DOKU_INC.'lib/images/fileicons/'.$size.'/'.$ext.'.png')) {
+ $icon = DOKU_BASE.'lib/images/fileicons/'.$size.'/'.$ext.'.png';
+ } else {
+ $icon = DOKU_BASE.'lib/images/fileicons/'.$size.'/file.png';
+ }
+
+ return '<img src="'.$icon.'" alt="'.$filename.'" class="icon" />';
+}
+
+/**
+ * Formats and prints one file in the list in the thumbnails view
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param array $item
+ * @param int $auth permission level
+ * @param bool|string $jump item id
+ * @param bool $display_namespace
+ */
+function media_printfile_thumbs($item,$auth,$jump=false,$display_namespace=false){
+
+ // Prepare filename
+ $file = utf8_decodeFN($item['file']);
+
+ // output
+ echo '<li><dl title="'.hsc($item['id']).'">'.NL;
+
+ echo '<dt>';
+ if($item['isimg']) {
+ media_printimgdetail($item, true);
+
+ } else {
+ echo '<a id="d_:'.$item['id'].'" class="image" title="'.$item['id'].'" href="'.
+ media_managerURL(array('image' => hsc($item['id']), 'ns' => getNS($item['id']),
+ 'tab_details' => 'view')).'">';
+ echo media_printicon($item['id'], '32x32');
+ echo '</a>';
+ }
+ echo '</dt>'.NL;
+ if (!$display_namespace) {
+ $name = hsc($file);
+ } else {
+ $name = hsc($item['id']);
+ }
+ echo '<dd class="name"><a href="'.media_managerURL(array('image' => hsc($item['id']), 'ns' => getNS($item['id']),
+ 'tab_details' => 'view')).'" id="h_:'.$item['id'].'">'.$name.'</a></dd>'.NL;
+
+ if($item['isimg']){
+ $size = '';
+ $size .= (int) $item['meta']->getField('File.Width');
+ $size .= '&#215;';
+ $size .= (int) $item['meta']->getField('File.Height');
+ echo '<dd class="size">'.$size.'</dd>'.NL;
+ } else {
+ echo '<dd class="size">&#160;</dd>'.NL;
+ }
+ $date = dformat($item['mtime']);
+ echo '<dd class="date">'.$date.'</dd>'.NL;
+ $filesize = filesize_h($item['size']);
+ echo '<dd class="filesize">'.$filesize.'</dd>'.NL;
+ echo '</dl></li>'.NL;
+}
+
+/**
+ * Prints a thumbnail and metainfo
+ *
+ * @param array $item
+ * @param bool $fullscreen
+ */
+function media_printimgdetail($item, $fullscreen=false){
+ // prepare thumbnail
+ $size = $fullscreen ? 90 : 120;
+
+ $w = (int) $item['meta']->getField('File.Width');
+ $h = (int) $item['meta']->getField('File.Height');
+ if($w>$size || $h>$size){
+ if (!$fullscreen) {
+ $ratio = $item['meta']->getResizeRatio($size);
+ } else {
+ $ratio = $item['meta']->getResizeRatio($size,$size);
+ }
+ $w = floor($w * $ratio);
+ $h = floor($h * $ratio);
+ }
+ $src = ml($item['id'],array('w'=>$w,'h'=>$h,'t'=>$item['mtime']));
+ $p = array();
+ if (!$fullscreen) {
+ // In fullscreen mediamanager view, image resizing is done via CSS.
+ $p['width'] = $w;
+ $p['height'] = $h;
+ }
+ $p['alt'] = $item['id'];
+ $att = buildAttributes($p);
+
+ // output
+ if ($fullscreen) {
+ echo '<a id="l_:'.$item['id'].'" class="image thumb" href="'.
+ media_managerURL(['image' => hsc($item['id']), 'ns' => getNS($item['id']), 'tab_details' => 'view']).'">';
+ echo '<img src="'.$src.'" '.$att.' />';
+ echo '</a>';
+ }
+
+ if ($fullscreen) return;
+
+ echo '<div class="detail">';
+ echo '<div class="thumb">';
+ echo '<a id="d_:'.$item['id'].'" class="select">';
+ echo '<img src="'.$src.'" '.$att.' />';
+ echo '</a>';
+ echo '</div>';
+
+ // read EXIF/IPTC data
+ $t = $item['meta']->getField(array('IPTC.Headline','xmp.dc:title'));
+ $d = $item['meta']->getField(array('IPTC.Caption','EXIF.UserComment',
+ 'EXIF.TIFFImageDescription',
+ 'EXIF.TIFFUserComment'));
+ if(\dokuwiki\Utf8\PhpString::strlen($d) > 250) $d = \dokuwiki\Utf8\PhpString::substr($d,0,250).'...';
+ $k = $item['meta']->getField(array('IPTC.Keywords','IPTC.Category','xmp.dc:subject'));
+
+ // print EXIF/IPTC data
+ if($t || $d || $k ){
+ echo '<p>';
+ if($t) echo '<strong>'.hsc($t).'</strong><br />';
+ if($d) echo hsc($d).'<br />';
+ if($t) echo '<em>'.hsc($k).'</em>';
+ echo '</p>';
+ }
+ echo '</div>';
+}
+
+/**
+ * Build link based on the current, adding/rewriting parameters
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param array|bool $params
+ * @param string $amp separator
+ * @param bool $abs absolute url?
+ * @param bool $params_array return the parmeters array?
+ * @return string|array - link or link parameters
+ */
+function media_managerURL($params=false, $amp='&amp;', $abs=false, $params_array=false) {
+ global $ID;
+ global $INPUT;
+
+ $gets = array('do' => 'media');
+ $media_manager_params = array('tab_files', 'tab_details', 'image', 'ns', 'list', 'sort');
+ foreach ($media_manager_params as $x) {
+ if ($INPUT->has($x)) $gets[$x] = $INPUT->str($x);
+ }
+
+ if ($params) {
+ $gets = $params + $gets;
+ }
+ unset($gets['id']);
+ if (isset($gets['delete'])) {
+ unset($gets['image']);
+ unset($gets['tab_details']);
+ }
+
+ if ($params_array) return $gets;
+
+ return wl($ID,$gets,$abs,$amp);
+}
+
+/**
+ * Print the media upload form if permissions are correct
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $ns
+ * @param int $auth permission level
+ * @param bool $fullscreen
+ */
+function media_uploadform($ns, $auth, $fullscreen = false){
+ global $lang;
+ global $conf;
+ global $INPUT;
+
+ if($auth < AUTH_UPLOAD) {
+ echo '<div class="nothing">'.$lang['media_perm_upload'].'</div>'.NL;
+ return;
+ }
+ $auth_ow = (($conf['mediarevisions']) ? AUTH_UPLOAD : AUTH_DELETE);
+
+ $update = false;
+ $id = '';
+ if ($auth >= $auth_ow && $fullscreen && $INPUT->str('mediado') == 'update') {
+ $update = true;
+ $id = cleanID($INPUT->str('image'));
+ }
+
+ // The default HTML upload form
+ $params = array('id' => 'dw__upload',
+ 'enctype' => 'multipart/form-data');
+ if (!$fullscreen) {
+ $params['action'] = DOKU_BASE.'lib/exe/mediamanager.php';
+ } else {
+ $params['action'] = media_managerURL(array('tab_files' => 'files',
+ 'tab_details' => 'view'), '&');
+ }
+
+ $form = new Doku_Form($params);
+ if (!$fullscreen) echo '<div class="upload">' . $lang['mediaupload'] . '</div>';
+ $form->addElement(formSecurityToken());
+ $form->addHidden('ns', hsc($ns));
+ $form->addElement(form_makeOpenTag('p'));
+ $form->addElement(form_makeFileField('upload', $lang['txt_upload'], 'upload__file'));
+ $form->addElement(form_makeCloseTag('p'));
+ $form->addElement(form_makeOpenTag('p'));
+ $form->addElement(form_makeTextField('mediaid', noNS($id), $lang['txt_filename'], 'upload__name'));
+ $form->addElement(form_makeButton('submit', '', $lang['btn_upload']));
+ $form->addElement(form_makeCloseTag('p'));
+
+ if($auth >= $auth_ow){
+ $form->addElement(form_makeOpenTag('p'));
+ $attrs = array();
+ if ($update) $attrs['checked'] = 'checked';
+ $form->addElement(form_makeCheckboxField('ow', 1, $lang['txt_overwrt'], 'dw__ow', 'check', $attrs));
+ $form->addElement(form_makeCloseTag('p'));
+ }
+
+ echo NL.'<div id="mediamanager__uploader">'.NL;
+ html_form('upload', $form);
+
+ echo '</div>'.NL;
+
+ echo '<p class="maxsize">';
+ printf($lang['maxuploadsize'],filesize_h(media_getuploadsize()));
+ echo ' <a class="allowedmime" href="#">' . $lang['allowedmime'] . '</a>';
+ echo ' <span>' . implode(', ', array_keys(getMimeTypes())) .'</span>';
+ echo '</p>'.NL;
+}
+
+/**
+ * Returns the size uploaded files may have
+ *
+ * This uses a conservative approach using the lowest number found
+ * in any of the limiting ini settings
+ *
+ * @returns int size in bytes
+ */
+function media_getuploadsize(){
+ $okay = 0;
+
+ $post = (int) php_to_byte(@ini_get('post_max_size'));
+ $suho = (int) php_to_byte(@ini_get('suhosin.post.max_value_length'));
+ $upld = (int) php_to_byte(@ini_get('upload_max_filesize'));
+
+ if($post && ($post < $okay || $okay == 0)) $okay = $post;
+ if($suho && ($suho < $okay || $okay == 0)) $okay = $suho;
+ if($upld && ($upld < $okay || $okay == 0)) $okay = $upld;
+
+ return $okay;
+}
+
+/**
+ * Print the search field form
+ *
+ * @author Tobias Sarnowski <sarnowski@cosmocode.de>
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $ns
+ * @param string $query
+ * @param bool $fullscreen
+ */
+function media_searchform($ns,$query='',$fullscreen=false){
+ global $lang;
+
+ // The default HTML search form
+ $params = array('id' => 'dw__mediasearch');
+ if (!$fullscreen) {
+ $params['action'] = DOKU_BASE.'lib/exe/mediamanager.php';
+ } else {
+ $params['action'] = media_managerURL(array(), '&');
+ }
+ $form = new Doku_Form($params);
+ $form->addHidden('ns', $ns);
+ $form->addHidden($fullscreen ? 'mediado' : 'do', 'searchlist');
+
+ $form->addElement(form_makeOpenTag('p'));
+ $form->addElement(
+ form_makeTextField(
+ 'q',
+ $query,
+ $lang['searchmedia'],
+ '',
+ '',
+ array('title' => sprintf($lang['searchmedia_in'], hsc($ns) . ':*'))
+ )
+ );
+ $form->addElement(form_makeButton('submit', '', $lang['btn_search']));
+ $form->addElement(form_makeCloseTag('p'));
+ html_form('searchmedia', $form);
+}
+
+/**
+ * Build a tree outline of available media namespaces
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $ns
+ */
+function media_nstree($ns){
+ global $conf;
+ global $lang;
+
+ // currently selected namespace
+ $ns = cleanID($ns);
+ if(empty($ns)){
+ global $ID;
+ $ns = (string)getNS($ID);
+ }
+
+ $ns_dir = utf8_encodeFN(str_replace(':','/',$ns));
+
+ $data = array();
+ search($data,$conf['mediadir'],'search_index',array('ns' => $ns_dir, 'nofiles' => true));
+
+ // wrap a list with the root level around the other namespaces
+ array_unshift($data, array('level' => 0, 'id' => '', 'open' =>'true',
+ 'label' => '['.$lang['mediaroot'].']'));
+
+ // insert the current ns into the hierarchy if it isn't already part of it
+ $ns_parts = explode(':', $ns);
+ $tmp_ns = '';
+ $pos = 0;
+ foreach ($ns_parts as $level => $part) {
+ if ($tmp_ns) $tmp_ns .= ':'.$part;
+ else $tmp_ns = $part;
+
+ // find the namespace parts or insert them
+ while ($data[$pos]['id'] != $tmp_ns) {
+ if (
+ $pos >= count($data) ||
+ (
+ $data[$pos]['level'] <= $level+1 &&
+ strnatcmp(utf8_encodeFN($data[$pos]['id']), utf8_encodeFN($tmp_ns)) > 0
+ )
+ ) {
+ array_splice($data, $pos, 0, array(array('level' => $level+1, 'id' => $tmp_ns, 'open' => 'true')));
+ break;
+ }
+ ++$pos;
+ }
+ }
+
+ echo html_buildlist($data,'idx','media_nstree_item','media_nstree_li');
+}
+
+/**
+ * Userfunction for html_buildlist
+ *
+ * Prints a media namespace tree item
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $item
+ * @return string html
+ */
+function media_nstree_item($item){
+ global $INPUT;
+ $pos = strrpos($item['id'], ':');
+ $label = substr($item['id'], $pos > 0 ? $pos + 1 : 0);
+ if(empty($item['label'])) $item['label'] = $label;
+
+ $ret = '';
+ if (!($INPUT->str('do') == 'media'))
+ $ret .= '<a href="'.DOKU_BASE.'lib/exe/mediamanager.php?ns='.idfilter($item['id']).'" class="idx_dir">';
+ else $ret .= '<a href="'.media_managerURL(array('ns' => idfilter($item['id'], false), 'tab_files' => 'files'))
+ .'" class="idx_dir">';
+ $ret .= $item['label'];
+ $ret .= '</a>';
+ return $ret;
+}
+
+/**
+ * Userfunction for html_buildlist
+ *
+ * Prints a media namespace tree item opener
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $item
+ * @return string html
+ */
+function media_nstree_li($item){
+ $class='media level'.$item['level'];
+ if($item['open']){
+ $class .= ' open';
+ $img = DOKU_BASE.'lib/images/minus.gif';
+ $alt = '−';
+ }else{
+ $class .= ' closed';
+ $img = DOKU_BASE.'lib/images/plus.gif';
+ $alt = '+';
+ }
+ // TODO: only deliver an image if it actually has a subtree...
+ return '<li class="'.$class.'">'.
+ '<img src="'.$img.'" alt="'.$alt.'" />';
+}
+
+/**
+ * Resizes the given image to the given size
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $file filename, path to file
+ * @param string $ext extension
+ * @param int $w desired width
+ * @param int $h desired height
+ * @return string path to resized or original size if failed
+ */
+function media_resize_image($file, $ext, $w, $h=0){
+ global $conf;
+
+ $info = @getimagesize($file); //get original size
+ if($info == false) return $file; // that's no image - it's a spaceship!
+
+ if(!$h) $h = round(($w * $info[1]) / $info[0]);
+ if(!$w) $w = round(($h * $info[0]) / $info[1]);
+
+ // we wont scale up to infinity
+ if($w > 2000 || $h > 2000) return $file;
+
+ // resize necessary? - (w,h) = native dimensions
+ if(($w == $info[0]) && ($h == $info[1])) return $file;
+
+ //cache
+ $local = getCacheName($file,'.media.'.$w.'x'.$h.'.'.$ext);
+ $mtime = @filemtime($local); // 0 if not exists
+
+ if($mtime > filemtime($file) ||
+ media_resize_imageIM($ext, $file, $info[0], $info[1], $local, $w, $h) ||
+ media_resize_imageGD($ext, $file, $info[0], $info[1], $local, $w, $h)
+ ) {
+ if($conf['fperm']) @chmod($local, $conf['fperm']);
+ return $local;
+ }
+ //still here? resizing failed
+ return $file;
+}
+
+/**
+ * Crops the given image to the wanted ratio, then calls media_resize_image to scale it
+ * to the wanted size
+ *
+ * Crops are centered horizontally but prefer the upper third of an vertical
+ * image because most pics are more interesting in that area (rule of thirds)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $file filename, path to file
+ * @param string $ext extension
+ * @param int $w desired width
+ * @param int $h desired height
+ * @return string path to resized or original size if failed
+ */
+function media_crop_image($file, $ext, $w, $h=0){
+ global $conf;
+
+ if(!$h) $h = $w;
+ $info = @getimagesize($file); //get original size
+ if($info == false) return $file; // that's no image - it's a spaceship!
+
+ // calculate crop size
+ $fr = $info[0]/$info[1];
+ $tr = $w/$h;
+
+ // check if the crop can be handled completely by resize,
+ // i.e. the specified width & height match the aspect ratio of the source image
+ if ($w == round($h*$fr)) {
+ return media_resize_image($file, $ext, $w);
+ }
+
+ if($tr >= 1){
+ if($tr > $fr){
+ $cw = $info[0];
+ $ch = (int) ($info[0]/$tr);
+ }else{
+ $cw = (int) ($info[1]*$tr);
+ $ch = $info[1];
+ }
+ }else{
+ if($tr < $fr){
+ $cw = (int) ($info[1]*$tr);
+ $ch = $info[1];
+ }else{
+ $cw = $info[0];
+ $ch = (int) ($info[0]/$tr);
+ }
+ }
+ // calculate crop offset
+ $cx = (int) (($info[0]-$cw)/2);
+ $cy = (int) (($info[1]-$ch)/3);
+
+ //cache
+ $local = getCacheName($file,'.media.'.$cw.'x'.$ch.'.crop.'.$ext);
+ $mtime = @filemtime($local); // 0 if not exists
+
+ if( $mtime > @filemtime($file) ||
+ media_crop_imageIM($ext,$file,$info[0],$info[1],$local,$cw,$ch,$cx,$cy) ||
+ media_resize_imageGD($ext,$file,$cw,$ch,$local,$cw,$ch,$cx,$cy) ){
+ if($conf['fperm']) @chmod($local, $conf['fperm']);
+ return media_resize_image($local,$ext, $w, $h);
+ }
+
+ //still here? cropping failed
+ return media_resize_image($file,$ext, $w, $h);
+}
+
+/**
+ * Calculate a token to be used to verify fetch requests for resized or
+ * cropped images have been internally generated - and prevent external
+ * DDOS attacks via fetch
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ *
+ * @param string $id id of the image
+ * @param int $w resize/crop width
+ * @param int $h resize/crop height
+ * @return string token or empty string if no token required
+ */
+function media_get_token($id,$w,$h){
+ // token is only required for modified images
+ if ($w || $h || media_isexternal($id)) {
+ $token = $id;
+ if ($w) $token .= '.'.$w;
+ if ($h) $token .= '.'.$h;
+
+ return substr(\dokuwiki\PassHash::hmac('md5', $token, auth_cookiesalt()),0,6);
+ }
+
+ return '';
+}
+
+/**
+ * Download a remote file and return local filename
+ *
+ * returns false if download fails. Uses cached file if available and
+ * wanted
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Pavel Vitis <Pavel.Vitis@seznam.cz>
+ *
+ * @param string $url
+ * @param string $ext extension
+ * @param int $cache cachetime in seconds
+ * @return false|string path to cached file
+ */
+function media_get_from_URL($url,$ext,$cache){
+ global $conf;
+
+ // if no cache or fetchsize just redirect
+ if ($cache==0) return false;
+ if (!$conf['fetchsize']) return false;
+
+ $local = getCacheName(strtolower($url),".media.$ext");
+ $mtime = @filemtime($local); // 0 if not exists
+
+ //decide if download needed:
+ if(($mtime == 0) || // cache does not exist
+ ($cache != -1 && $mtime < time() - $cache) // 'recache' and cache has expired
+ ) {
+ if(media_image_download($url, $local)) {
+ return $local;
+ } else {
+ return false;
+ }
+ }
+
+ //if cache exists use it else
+ if($mtime) return $local;
+
+ //else return false
+ return false;
+}
+
+/**
+ * Download image files
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $url
+ * @param string $file path to file in which to put the downloaded content
+ * @return bool
+ */
+function media_image_download($url,$file){
+ global $conf;
+ $http = new DokuHTTPClient();
+ $http->keep_alive = false; // we do single ops here, no need for keep-alive
+
+ $http->max_bodysize = $conf['fetchsize'];
+ $http->timeout = 25; //max. 25 sec
+ $http->header_regexp = '!\r\nContent-Type: image/(jpe?g|gif|png)!i';
+
+ $data = $http->get($url);
+ if(!$data) return false;
+
+ $fileexists = file_exists($file);
+ $fp = @fopen($file,"w");
+ if(!$fp) return false;
+ fwrite($fp,$data);
+ fclose($fp);
+ if(!$fileexists and $conf['fperm']) chmod($file, $conf['fperm']);
+
+ // check if it is really an image
+ $info = @getimagesize($file);
+ if(!$info){
+ @unlink($file);
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * resize images using external ImageMagick convert program
+ *
+ * @author Pavel Vitis <Pavel.Vitis@seznam.cz>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $ext extension
+ * @param string $from filename path to file
+ * @param int $from_w original width
+ * @param int $from_h original height
+ * @param string $to path to resized file
+ * @param int $to_w desired width
+ * @param int $to_h desired height
+ * @return bool
+ */
+function media_resize_imageIM($ext,$from,$from_w,$from_h,$to,$to_w,$to_h){
+ global $conf;
+
+ // check if convert is configured
+ if(!$conf['im_convert']) return false;
+
+ // prepare command
+ $cmd = $conf['im_convert'];
+ $cmd .= ' -resize '.$to_w.'x'.$to_h.'!';
+ if ($ext == 'jpg' || $ext == 'jpeg') {
+ $cmd .= ' -quality '.$conf['jpg_quality'];
+ }
+ $cmd .= " $from $to";
+
+ @exec($cmd,$out,$retval);
+ if ($retval == 0) return true;
+ return false;
+}
+
+/**
+ * crop images using external ImageMagick convert program
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $ext extension
+ * @param string $from filename path to file
+ * @param int $from_w original width
+ * @param int $from_h original height
+ * @param string $to path to resized file
+ * @param int $to_w desired width
+ * @param int $to_h desired height
+ * @param int $ofs_x offset of crop centre
+ * @param int $ofs_y offset of crop centre
+ * @return bool
+ */
+function media_crop_imageIM($ext,$from,$from_w,$from_h,$to,$to_w,$to_h,$ofs_x,$ofs_y){
+ global $conf;
+
+ // check if convert is configured
+ if(!$conf['im_convert']) return false;
+
+ // prepare command
+ $cmd = $conf['im_convert'];
+ $cmd .= ' -crop '.$to_w.'x'.$to_h.'+'.$ofs_x.'+'.$ofs_y;
+ if ($ext == 'jpg' || $ext == 'jpeg') {
+ $cmd .= ' -quality '.$conf['jpg_quality'];
+ }
+ $cmd .= " $from $to";
+
+ @exec($cmd,$out,$retval);
+ if ($retval == 0) return true;
+ return false;
+}
+
+/**
+ * resize or crop images using PHP's libGD support
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Sebastian Wienecke <s_wienecke@web.de>
+ *
+ * @param string $ext extension
+ * @param string $from filename path to file
+ * @param int $from_w original width
+ * @param int $from_h original height
+ * @param string $to path to resized file
+ * @param int $to_w desired width
+ * @param int $to_h desired height
+ * @param int $ofs_x offset of crop centre
+ * @param int $ofs_y offset of crop centre
+ * @return bool
+ */
+function media_resize_imageGD($ext,$from,$from_w,$from_h,$to,$to_w,$to_h,$ofs_x=0,$ofs_y=0){
+ global $conf;
+
+ if($conf['gdlib'] < 1) return false; //no GDlib available or wanted
+
+ // check available memory
+ if(!is_mem_available(($from_w * $from_h * 4) + ($to_w * $to_h * 4))){
+ return false;
+ }
+
+ // create an image of the given filetype
+ $image = false;
+ if ($ext == 'jpg' || $ext == 'jpeg'){
+ if(!function_exists("imagecreatefromjpeg")) return false;
+ $image = @imagecreatefromjpeg($from);
+ }elseif($ext == 'png') {
+ if(!function_exists("imagecreatefrompng")) return false;
+ $image = @imagecreatefrompng($from);
+
+ }elseif($ext == 'gif') {
+ if(!function_exists("imagecreatefromgif")) return false;
+ $image = @imagecreatefromgif($from);
+ }
+ if(!$image) return false;
+
+ $newimg = false;
+ if(($conf['gdlib']>1) && function_exists("imagecreatetruecolor") && $ext != 'gif'){
+ $newimg = @imagecreatetruecolor ($to_w, $to_h);
+ }
+ if(!$newimg) $newimg = @imagecreate($to_w, $to_h);
+ if(!$newimg){
+ imagedestroy($image);
+ return false;
+ }
+
+ //keep png alpha channel if possible
+ if($ext == 'png' && $conf['gdlib']>1 && function_exists('imagesavealpha')){
+ imagealphablending($newimg, false);
+ imagesavealpha($newimg,true);
+ }
+
+ //keep gif transparent color if possible
+ if($ext == 'gif' && function_exists('imagefill') && function_exists('imagecolorallocate')) {
+ if(function_exists('imagecolorsforindex') && function_exists('imagecolortransparent')) {
+ $transcolorindex = @imagecolortransparent($image);
+ if($transcolorindex >= 0 ) { //transparent color exists
+ $transcolor = @imagecolorsforindex($image, $transcolorindex);
+ $transcolorindex = @imagecolorallocate(
+ $newimg,
+ $transcolor['red'],
+ $transcolor['green'],
+ $transcolor['blue']
+ );
+ @imagefill($newimg, 0, 0, $transcolorindex);
+ @imagecolortransparent($newimg, $transcolorindex);
+ }else{ //filling with white
+ $whitecolorindex = @imagecolorallocate($newimg, 255, 255, 255);
+ @imagefill($newimg, 0, 0, $whitecolorindex);
+ }
+ }else{ //filling with white
+ $whitecolorindex = @imagecolorallocate($newimg, 255, 255, 255);
+ @imagefill($newimg, 0, 0, $whitecolorindex);
+ }
+ }
+
+ //try resampling first
+ if(function_exists("imagecopyresampled")){
+ if(!@imagecopyresampled($newimg, $image, 0, 0, $ofs_x, $ofs_y, $to_w, $to_h, $from_w, $from_h)) {
+ imagecopyresized($newimg, $image, 0, 0, $ofs_x, $ofs_y, $to_w, $to_h, $from_w, $from_h);
+ }
+ }else{
+ imagecopyresized($newimg, $image, 0, 0, $ofs_x, $ofs_y, $to_w, $to_h, $from_w, $from_h);
+ }
+
+ $okay = false;
+ if ($ext == 'jpg' || $ext == 'jpeg'){
+ if(!function_exists('imagejpeg')){
+ $okay = false;
+ }else{
+ $okay = imagejpeg($newimg, $to, $conf['jpg_quality']);
+ }
+ }elseif($ext == 'png') {
+ if(!function_exists('imagepng')){
+ $okay = false;
+ }else{
+ $okay = imagepng($newimg, $to);
+ }
+ }elseif($ext == 'gif') {
+ if(!function_exists('imagegif')){
+ $okay = false;
+ }else{
+ $okay = imagegif($newimg, $to);
+ }
+ }
+
+ // destroy GD image ressources
+ if($image) imagedestroy($image);
+ if($newimg) imagedestroy($newimg);
+
+ return $okay;
+}
+
+/**
+ * Return other media files with the same base name
+ * but different extensions.
+ *
+ * @param string $src - ID of media file
+ * @param string[] $exts - alternative extensions to find other files for
+ * @return array - array(mime type => file ID)
+ *
+ * @author Anika Henke <anika@selfthinker.org>
+ */
+function media_alternativefiles($src, $exts){
+
+ $files = array();
+ list($srcExt, /* $srcMime */) = mimetype($src);
+ $filebase = substr($src, 0, -1 * (strlen($srcExt)+1));
+
+ foreach($exts as $ext) {
+ $fileid = $filebase.'.'.$ext;
+ $file = mediaFN($fileid);
+ if(file_exists($file)) {
+ list(/* $fileExt */, $fileMime) = mimetype($file);
+ $files[$fileMime] = $fileid;
+ }
+ }
+ return $files;
+}
+
+/**
+ * Check if video/audio is supported to be embedded.
+ *
+ * @param string $mime - mimetype of media file
+ * @param string $type - type of media files to check ('video', 'audio', or null for all)
+ * @return boolean
+ *
+ * @author Anika Henke <anika@selfthinker.org>
+ */
+function media_supportedav($mime, $type=NULL){
+ $supportedAudio = array(
+ 'ogg' => 'audio/ogg',
+ 'mp3' => 'audio/mpeg',
+ 'wav' => 'audio/wav',
+ );
+ $supportedVideo = array(
+ 'webm' => 'video/webm',
+ 'ogv' => 'video/ogg',
+ 'mp4' => 'video/mp4',
+ );
+ if ($type == 'audio') {
+ $supportedAv = $supportedAudio;
+ } elseif ($type == 'video') {
+ $supportedAv = $supportedVideo;
+ } else {
+ $supportedAv = array_merge($supportedAudio, $supportedVideo);
+ }
+ return in_array($mime, $supportedAv);
+}
+
+/**
+ * Return track media files with the same base name
+ * but extensions that indicate kind and lang.
+ * ie for foo.webm search foo.sub.lang.vtt, foo.cap.lang.vtt...
+ *
+ * @param string $src - ID of media file
+ * @return array - array(mediaID => array( kind, srclang ))
+ *
+ * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
+ */
+function media_trackfiles($src){
+ $kinds=array(
+ 'sub' => 'subtitles',
+ 'cap' => 'captions',
+ 'des' => 'descriptions',
+ 'cha' => 'chapters',
+ 'met' => 'metadata'
+ );
+
+ $files = array();
+ $re='/\\.(sub|cap|des|cha|met)\\.([^.]+)\\.vtt$/';
+ $baseid=pathinfo($src, PATHINFO_FILENAME);
+ $pattern=mediaFN($baseid).'.*.*.vtt';
+ $list=glob($pattern);
+ foreach($list as $track) {
+ if(preg_match($re, $track, $matches)){
+ $files[$baseid.'.'.$matches[1].'.'.$matches[2].'.vtt']=array(
+ $kinds[$matches[1]],
+ $matches[2],
+ );
+ }
+ }
+ return $files;
+}
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
diff --git a/platform/www/inc/pageutils.php b/platform/www/inc/pageutils.php
new file mode 100644
index 0000000..d4a8bb7
--- /dev/null
+++ b/platform/www/inc/pageutils.php
@@ -0,0 +1,778 @@
+<?php
+/**
+ * Utilities for handling pagenames
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @todo Combine similar functions like {wiki,media,meta}FN()
+ */
+
+use dokuwiki\ChangeLog\MediaChangeLog;
+use dokuwiki\ChangeLog\PageChangeLog;
+
+/**
+ * Fetch the an ID from request
+ *
+ * Uses either standard $_REQUEST variable or extracts it from
+ * the full request URI when userewrite is set to 2
+ *
+ * For $param='id' $conf['start'] is returned if no id was found.
+ * If the second parameter is true (default) the ID is cleaned.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $param the $_REQUEST variable name, default 'id'
+ * @param bool $clean if true, ID is cleaned
+ * @return string
+ */
+function getID($param='id',$clean=true){
+ /** @var Input $INPUT */
+ global $INPUT;
+ global $conf;
+ global $ACT;
+
+ $id = $INPUT->str($param);
+
+ //construct page id from request URI
+ if(empty($id) && $conf['userewrite'] == 2){
+ $request = $INPUT->server->str('REQUEST_URI');
+ $script = '';
+
+ //get the script URL
+ if($conf['basedir']){
+ $relpath = '';
+ if($param != 'id') {
+ $relpath = 'lib/exe/';
+ }
+ $script = $conf['basedir'] . $relpath .
+ \dokuwiki\Utf8\PhpString::basename($INPUT->server->str('SCRIPT_FILENAME'));
+
+ }elseif($INPUT->server->str('PATH_INFO')){
+ $request = $INPUT->server->str('PATH_INFO');
+ }elseif($INPUT->server->str('SCRIPT_NAME')){
+ $script = $INPUT->server->str('SCRIPT_NAME');
+ }elseif($INPUT->server->str('DOCUMENT_ROOT') && $INPUT->server->str('SCRIPT_FILENAME')){
+ $script = preg_replace ('/^'.preg_quote($INPUT->server->str('DOCUMENT_ROOT'),'/').'/','',
+ $INPUT->server->str('SCRIPT_FILENAME'));
+ $script = '/'.$script;
+ }
+
+ //clean script and request (fixes a windows problem)
+ $script = preg_replace('/\/\/+/','/',$script);
+ $request = preg_replace('/\/\/+/','/',$request);
+
+ //remove script URL and Querystring to gain the id
+ if(preg_match('/^'.preg_quote($script,'/').'(.*)/',$request, $match)){
+ $id = preg_replace ('/\?.*/','',$match[1]);
+ }
+ $id = urldecode($id);
+ //strip leading slashes
+ $id = preg_replace('!^/+!','',$id);
+ }
+
+ // Namespace autolinking from URL
+ if(substr($id,-1) == ':' || ($conf['useslash'] && substr($id,-1) == '/')){
+ if(page_exists($id.$conf['start'])){
+ // start page inside namespace
+ $id = $id.$conf['start'];
+ }elseif(page_exists($id.noNS(cleanID($id)))){
+ // page named like the NS inside the NS
+ $id = $id.noNS(cleanID($id));
+ }elseif(page_exists($id)){
+ // page like namespace exists
+ $id = substr($id,0,-1);
+ }else{
+ // fall back to default
+ $id = $id.$conf['start'];
+ }
+ if (isset($ACT) && $ACT === 'show') {
+ $urlParameters = $_GET;
+ if (isset($urlParameters['id'])) {
+ unset($urlParameters['id']);
+ }
+ send_redirect(wl($id, $urlParameters, true, '&'));
+ }
+ }
+ if($clean) $id = cleanID($id);
+ if($id === '' && $param=='id') $id = $conf['start'];
+
+ return $id;
+}
+
+/**
+ * Remove unwanted chars from ID
+ *
+ * Cleans a given ID to only use allowed characters. Accented characters are
+ * converted to unaccented ones
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $raw_id The pageid to clean
+ * @param boolean $ascii Force ASCII
+ * @return string cleaned id
+ */
+function cleanID($raw_id,$ascii=false){
+ global $conf;
+ static $sepcharpat = null;
+
+ global $cache_cleanid;
+ $cache = & $cache_cleanid;
+
+ // check if it's already in the memory cache
+ if (!$ascii && isset($cache[(string)$raw_id])) {
+ return $cache[(string)$raw_id];
+ }
+
+ $sepchar = $conf['sepchar'];
+ if($sepcharpat == null) // build string only once to save clock cycles
+ $sepcharpat = '#\\'.$sepchar.'+#';
+
+ $id = trim((string)$raw_id);
+ $id = \dokuwiki\Utf8\PhpString::strtolower($id);
+
+ //alternative namespace seperator
+ if($conf['useslash']){
+ $id = strtr($id,';/','::');
+ }else{
+ $id = strtr($id,';/',':'.$sepchar);
+ }
+
+ if($conf['deaccent'] == 2 || $ascii) $id = \dokuwiki\Utf8\Clean::romanize($id);
+ if($conf['deaccent'] || $ascii) $id = \dokuwiki\Utf8\Clean::deaccent($id,-1);
+
+ //remove specials
+ $id = \dokuwiki\Utf8\Clean::stripspecials($id,$sepchar,'\*');
+
+ if($ascii) $id = \dokuwiki\Utf8\Clean::strip($id);
+
+ //clean up
+ $id = preg_replace($sepcharpat,$sepchar,$id);
+ $id = preg_replace('#:+#',':',$id);
+ $id = trim($id,':._-');
+ $id = preg_replace('#:[:\._\-]+#',':',$id);
+ $id = preg_replace('#[:\._\-]+:#',':',$id);
+
+ if (!$ascii) $cache[(string)$raw_id] = $id;
+ return($id);
+}
+
+/**
+ * Return namespacepart of a wiki ID
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id
+ * @return string|false the namespace part or false if the given ID has no namespace (root)
+ */
+function getNS($id){
+ $pos = strrpos((string)$id,':');
+ if($pos!==false){
+ return substr((string)$id,0,$pos);
+ }
+ return false;
+}
+
+/**
+ * Returns the ID without the namespace
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id
+ * @return string
+ */
+function noNS($id) {
+ $pos = strrpos($id, ':');
+ if ($pos!==false) {
+ return substr($id, $pos+1);
+ } else {
+ return $id;
+ }
+}
+
+/**
+ * Returns the current namespace
+ *
+ * @author Nathan Fritz <fritzn@crown.edu>
+ *
+ * @param string $id
+ * @return string
+ */
+function curNS($id) {
+ return noNS(getNS($id));
+}
+
+/**
+ * Returns the ID without the namespace or current namespace for 'start' pages
+ *
+ * @author Nathan Fritz <fritzn@crown.edu>
+ *
+ * @param string $id
+ * @return string
+ */
+function noNSorNS($id) {
+ global $conf;
+
+ $p = noNS($id);
+ if ($p === $conf['start'] || $p === false || $p === '') {
+ $p = curNS($id);
+ if ($p === false || $p === '') {
+ return $conf['start'];
+ }
+ }
+ return $p;
+}
+
+/**
+ * Creates a XHTML valid linkid from a given headline title
+ *
+ * @param string $title The headline title
+ * @param array|bool $check Existing IDs (title => number)
+ * @return string the title
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function sectionID($title,&$check) {
+ $title = str_replace(array(':','.'),'',cleanID($title));
+ $new = ltrim($title,'0123456789_-');
+ if(empty($new)){
+ $title = 'section'.preg_replace('/[^0-9]+/','',$title); //keep numbers from headline
+ }else{
+ $title = $new;
+ }
+
+ if(is_array($check)){
+ // make sure tiles are unique
+ if (!array_key_exists ($title,$check)) {
+ $check[$title] = 0;
+ } else {
+ $title .= ++ $check[$title];
+ }
+ }
+
+ return $title;
+}
+
+/**
+ * Wiki page existence check
+ *
+ * parameters as for wikiFN
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param string $id page id
+ * @param string|int $rev empty or revision timestamp
+ * @param bool $clean flag indicating that $id should be cleaned (see wikiFN as well)
+ * @param bool $date_at
+ * @return bool exists?
+ */
+function page_exists($id,$rev='',$clean=true, $date_at=false) {
+ if($rev !== '' && $date_at) {
+ $pagelog = new PageChangeLog($id);
+ $pagelog_rev = $pagelog->getLastRevisionAt($rev);
+ if($pagelog_rev !== false)
+ $rev = $pagelog_rev;
+ }
+ return file_exists(wikiFN($id,$rev,$clean));
+}
+
+/**
+ * returns the full path to the datafile specified by ID and optional revision
+ *
+ * The filename is URL encoded to protect Unicode chars
+ *
+ * @param $raw_id string id of wikipage
+ * @param $rev int|string page revision, empty string for current
+ * @param $clean bool flag indicating that $raw_id should be cleaned. Only set to false
+ * when $id is guaranteed to have been cleaned already.
+ * @return string full path
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function wikiFN($raw_id,$rev='',$clean=true){
+ global $conf;
+
+ global $cache_wikifn;
+ $cache = & $cache_wikifn;
+
+ $id = $raw_id;
+
+ if ($clean) $id = cleanID($id);
+ $id = str_replace(':','/',$id);
+
+ if (isset($cache[$id]) && isset($cache[$id][$rev])) {
+ return $cache[$id][$rev];
+ }
+
+ if(empty($rev)){
+ $fn = $conf['datadir'].'/'.utf8_encodeFN($id).'.txt';
+ }else{
+ $fn = $conf['olddir'].'/'.utf8_encodeFN($id).'.'.$rev.'.txt';
+ if($conf['compression']){
+ //test for extensions here, we want to read both compressions
+ if (file_exists($fn . '.gz')){
+ $fn .= '.gz';
+ }else if(file_exists($fn . '.bz2')){
+ $fn .= '.bz2';
+ }else{
+ //file doesnt exist yet, so we take the configured extension
+ $fn .= '.' . $conf['compression'];
+ }
+ }
+ }
+
+ if (!isset($cache[$id])) { $cache[$id] = array(); }
+ $cache[$id][$rev] = $fn;
+ return $fn;
+}
+
+/**
+ * Returns the full path to the file for locking the page while editing.
+ *
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ *
+ * @param string $id page id
+ * @return string full path
+ */
+function wikiLockFN($id) {
+ global $conf;
+ return $conf['lockdir'].'/'.md5(cleanID($id)).'.lock';
+}
+
+
+/**
+ * returns the full path to the meta file specified by ID and extension
+ *
+ * @author Steven Danz <steven-danz@kc.rr.com>
+ *
+ * @param string $id page id
+ * @param string $ext file extension
+ * @return string full path
+ */
+function metaFN($id,$ext){
+ global $conf;
+ $id = cleanID($id);
+ $id = str_replace(':','/',$id);
+ $fn = $conf['metadir'].'/'.utf8_encodeFN($id).$ext;
+ return $fn;
+}
+
+/**
+ * returns the full path to the media's meta file specified by ID and extension
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $id media id
+ * @param string $ext extension of media
+ * @return string
+ */
+function mediaMetaFN($id,$ext){
+ global $conf;
+ $id = cleanID($id);
+ $id = str_replace(':','/',$id);
+ $fn = $conf['mediametadir'].'/'.utf8_encodeFN($id).$ext;
+ return $fn;
+}
+
+/**
+ * returns an array of full paths to all metafiles of a given ID
+ *
+ * @author Esther Brunner <esther@kaffeehaus.ch>
+ * @author Michael Hamann <michael@content-space.de>
+ *
+ * @param string $id page id
+ * @return array
+ */
+function metaFiles($id){
+ $basename = metaFN($id, '');
+ $files = glob($basename.'.*', GLOB_MARK);
+ // filter files like foo.bar.meta when $id == 'foo'
+ return $files ? preg_grep('/^'.preg_quote($basename, '/').'\.[^.\/]*$/u', $files) : array();
+}
+
+/**
+ * returns the full path to the mediafile specified by ID
+ *
+ * The filename is URL encoded to protect Unicode chars
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $id media id
+ * @param string|int $rev empty string or revision timestamp
+ * @param bool $clean
+ *
+ * @return string full path
+ */
+function mediaFN($id, $rev='', $clean=true){
+ global $conf;
+ if ($clean) $id = cleanID($id);
+ $id = str_replace(':','/',$id);
+ if(empty($rev)){
+ $fn = $conf['mediadir'].'/'.utf8_encodeFN($id);
+ }else{
+ $ext = mimetype($id);
+ $name = substr($id,0, -1*strlen($ext[0])-1);
+ $fn = $conf['mediaolddir'].'/'.utf8_encodeFN($name .'.'.( (int) $rev ).'.'.$ext[0]);
+ }
+ return $fn;
+}
+
+/**
+ * Returns the full filepath to a localized file if local
+ * version isn't found the english one is returned
+ *
+ * @param string $id The id of the local file
+ * @param string $ext The file extension (usually txt)
+ * @return string full filepath to localized file
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function localeFN($id,$ext='txt'){
+ global $conf;
+ $file = DOKU_CONF.'lang/'.$conf['lang'].'/'.$id.'.'.$ext;
+ if(!file_exists($file)){
+ $file = DOKU_INC.'inc/lang/'.$conf['lang'].'/'.$id.'.'.$ext;
+ if(!file_exists($file)){
+ //fall back to english
+ $file = DOKU_INC.'inc/lang/en/'.$id.'.'.$ext;
+ }
+ }
+ return $file;
+}
+
+/**
+ * Resolve relative paths in IDs
+ *
+ * Do not call directly use resolve_mediaid or resolve_pageid
+ * instead
+ *
+ * Partyly based on a cleanPath function found at
+ * http://php.net/manual/en/function.realpath.php#57016
+ *
+ * @author <bart at mediawave dot nl>
+ *
+ * @param string $ns namespace which is context of id
+ * @param string $id relative id
+ * @param bool $clean flag indicating that id should be cleaned
+ * @return string
+ */
+function resolve_id($ns,$id,$clean=true){
+ global $conf;
+
+ // some pre cleaning for useslash:
+ if($conf['useslash']) $id = str_replace('/',':',$id);
+
+ // if the id starts with a dot we need to handle the
+ // relative stuff
+ if($id && $id[0] == '.'){
+ // normalize initial dots without a colon
+ $id = preg_replace('/^((\.+:)*)(\.+)(?=[^:\.])/','\1\3:',$id);
+ // prepend the current namespace
+ $id = $ns.':'.$id;
+
+ // cleanup relatives
+ $result = array();
+ $pathA = explode(':', $id);
+ if (!$pathA[0]) $result[] = '';
+ foreach ($pathA AS $key => $dir) {
+ if ($dir == '..') {
+ if (end($result) == '..') {
+ $result[] = '..';
+ } elseif (!array_pop($result)) {
+ $result[] = '..';
+ }
+ } elseif ($dir && $dir != '.') {
+ $result[] = $dir;
+ }
+ }
+ if (!end($pathA)) $result[] = '';
+ $id = implode(':', $result);
+ }elseif($ns !== false && strpos($id,':') === false){
+ //if link contains no namespace. add current namespace (if any)
+ $id = $ns.':'.$id;
+ }
+
+ if($clean) $id = cleanID($id);
+ return $id;
+}
+
+/**
+ * Returns a full media id
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $ns namespace which is context of id
+ * @param string &$page (reference) relative media id, updated to resolved id
+ * @param bool &$exists (reference) updated with existance of media
+ * @param int|string $rev
+ * @param bool $date_at
+ */
+function resolve_mediaid($ns,&$page,&$exists,$rev='',$date_at=false){
+ $page = resolve_id($ns,$page);
+ if($rev !== '' && $date_at){
+ $medialog = new MediaChangeLog($page);
+ $medialog_rev = $medialog->getLastRevisionAt($rev);
+ if($medialog_rev !== false) {
+ $rev = $medialog_rev;
+ }
+ }
+
+ $file = mediaFN($page,$rev);
+ $exists = file_exists($file);
+}
+
+/**
+ * Returns a full page id
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $ns namespace which is context of id
+ * @param string &$page (reference) relative page id, updated to resolved id
+ * @param bool &$exists (reference) updated with existance of media
+ * @param string $rev
+ * @param bool $date_at
+ */
+function resolve_pageid($ns,&$page,&$exists,$rev='',$date_at=false ){
+ global $conf;
+ global $ID;
+ $exists = false;
+
+ //empty address should point to current page
+ if ($page === "") {
+ $page = $ID;
+ }
+
+ //keep hashlink if exists then clean both parts
+ if (strpos($page,'#')) {
+ list($page,$hash) = explode('#',$page,2);
+ } else {
+ $hash = '';
+ }
+ $hash = cleanID($hash);
+ $page = resolve_id($ns,$page,false); // resolve but don't clean, yet
+
+ // get filename (calls clean itself)
+ if($rev !== '' && $date_at) {
+ $pagelog = new PageChangeLog($page);
+ $pagelog_rev = $pagelog->getLastRevisionAt($rev);
+ if($pagelog_rev !== false)//something found
+ $rev = $pagelog_rev;
+ }
+ $file = wikiFN($page,$rev);
+
+ // if ends with colon or slash we have a namespace link
+ if(in_array(substr($page,-1), array(':', ';')) ||
+ ($conf['useslash'] && substr($page,-1) == '/')){
+ if(page_exists($page.$conf['start'],$rev,true,$date_at)){
+ // start page inside namespace
+ $page = $page.$conf['start'];
+ $exists = true;
+ }elseif(page_exists($page.noNS(cleanID($page)),$rev,true,$date_at)){
+ // page named like the NS inside the NS
+ $page = $page.noNS(cleanID($page));
+ $exists = true;
+ }elseif(page_exists($page,$rev,true,$date_at)){
+ // page like namespace exists
+ $page = $page;
+ $exists = true;
+ }else{
+ // fall back to default
+ $page = $page.$conf['start'];
+ }
+ }else{
+ //check alternative plural/nonplural form
+ if(!file_exists($file)){
+ if( $conf['autoplural'] ){
+ if(substr($page,-1) == 's'){
+ $try = substr($page,0,-1);
+ }else{
+ $try = $page.'s';
+ }
+ if(page_exists($try,$rev,true,$date_at)){
+ $page = $try;
+ $exists = true;
+ }
+ }
+ }else{
+ $exists = true;
+ }
+ }
+
+ // now make sure we have a clean page
+ $page = cleanID($page);
+
+ //add hash if any
+ if(!empty($hash)) $page .= '#'.$hash;
+}
+
+/**
+ * Returns the name of a cachefile from given data
+ *
+ * The needed directory is created by this function!
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $data This data is used to create a unique md5 name
+ * @param string $ext This is appended to the filename if given
+ * @return string The filename of the cachefile
+ */
+function getCacheName($data,$ext=''){
+ global $conf;
+ $md5 = md5($data);
+ $file = $conf['cachedir'].'/'.$md5[0].'/'.$md5.$ext;
+ io_makeFileDir($file);
+ return $file;
+}
+
+/**
+ * Checks a pageid against $conf['hidepages']
+ *
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ *
+ * @param string $id page id
+ * @return bool
+ */
+function isHiddenPage($id){
+ $data = array(
+ 'id' => $id,
+ 'hidden' => false
+ );
+ \dokuwiki\Extension\Event::createAndTrigger('PAGEUTILS_ID_HIDEPAGE', $data, '_isHiddenPage');
+ return $data['hidden'];
+}
+
+/**
+ * callback checks if page is hidden
+ *
+ * @param array $data event data - see isHiddenPage()
+ */
+function _isHiddenPage(&$data) {
+ global $conf;
+ global $ACT;
+
+ if ($data['hidden']) return;
+ if(empty($conf['hidepages'])) return;
+ if($ACT == 'admin') return;
+
+ if(preg_match('/'.$conf['hidepages'].'/ui',':'.$data['id'])){
+ $data['hidden'] = true;
+ }
+}
+
+/**
+ * Reverse of isHiddenPage
+ *
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ *
+ * @param string $id page id
+ * @return bool
+ */
+function isVisiblePage($id){
+ return !isHiddenPage($id);
+}
+
+/**
+ * Format an id for output to a user
+ *
+ * Namespaces are denoted by a trailing “:*”. The root namespace is
+ * “*”. Output is escaped.
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ *
+ * @param string $id page id
+ * @return string
+ */
+function prettyprint_id($id) {
+ if (!$id || $id === ':') {
+ return '*';
+ }
+ if ((substr($id, -1, 1) === ':')) {
+ $id .= '*';
+ }
+ return hsc($id);
+}
+
+/**
+ * Encode a UTF-8 filename to use on any filesystem
+ *
+ * Uses the 'fnencode' option to determine encoding
+ *
+ * When the second parameter is true the string will
+ * be encoded only if non ASCII characters are detected -
+ * This makes it safe to run it multiple times on the
+ * same string (default is true)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @see urlencode
+ *
+ * @param string $file file name
+ * @param bool $safe if true, only encoded when non ASCII characters detected
+ * @return string
+ */
+function utf8_encodeFN($file,$safe=true){
+ global $conf;
+ if($conf['fnencode'] == 'utf-8') return $file;
+
+ if($safe && preg_match('#^[a-zA-Z0-9/_\-\.%]+$#',$file)){
+ return $file;
+ }
+
+ if($conf['fnencode'] == 'safe'){
+ return SafeFN::encode($file);
+ }
+
+ $file = urlencode($file);
+ $file = str_replace('%2F','/',$file);
+ return $file;
+}
+
+/**
+ * Decode a filename back to UTF-8
+ *
+ * Uses the 'fnencode' option to determine encoding
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @see urldecode
+ *
+ * @param string $file file name
+ * @return string
+ */
+function utf8_decodeFN($file){
+ global $conf;
+ if($conf['fnencode'] == 'utf-8') return $file;
+
+ if($conf['fnencode'] == 'safe'){
+ return SafeFN::decode($file);
+ }
+
+ return urldecode($file);
+}
+
+/**
+ * Find a page in the current namespace (determined from $ID) or any
+ * higher namespace that can be accessed by the current user,
+ * this condition can be overriden by an optional parameter.
+ *
+ * Used for sidebars, but can be used other stuff as well
+ *
+ * @todo add event hook
+ *
+ * @param string $page the pagename you're looking for
+ * @param bool $useacl only return pages readable by the current user, false to ignore ACLs
+ * @return false|string the full page id of the found page, false if any
+ */
+function page_findnearest($page, $useacl = true){
+ if ((string) $page === '') return false;
+ global $ID;
+
+ $ns = $ID;
+ do {
+ $ns = getNS($ns);
+ $pageid = cleanID("$ns:$page");
+ if(page_exists($pageid) && (!$useacl || auth_quickaclcheck($pageid) >= AUTH_READ)){
+ return $pageid;
+ }
+ } while($ns !== false);
+
+ return false;
+}
diff --git a/platform/www/inc/parser/code.php b/platform/www/inc/parser/code.php
new file mode 100644
index 0000000..cded87d
--- /dev/null
+++ b/platform/www/inc/parser/code.php
@@ -0,0 +1,71 @@
+<?php
+/**
+ * A simple renderer that allows downloading of code and file snippets
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+class Doku_Renderer_code extends Doku_Renderer {
+ protected $_codeblock = 0;
+
+ /**
+ * Send the wanted code block to the browser
+ *
+ * When the correct block was found it exits the script.
+ *
+ * @param string $text
+ * @param string $language
+ * @param string $filename
+ */
+ public function code($text, $language = null, $filename = '') {
+ global $INPUT;
+ if(!$language) $language = 'txt';
+ $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language);
+ if(!$filename) $filename = 'snippet.'.$language;
+ $filename = \dokuwiki\Utf8\PhpString::basename($filename);
+ $filename = \dokuwiki\Utf8\Clean::stripspecials($filename, '_');
+
+ // send CRLF to Windows clients
+ if(strpos($INPUT->server->str('HTTP_USER_AGENT'), 'Windows') !== false) {
+ $text = str_replace("\n", "\r\n", $text);
+ }
+
+ if($this->_codeblock == $INPUT->str('codeblock')) {
+ header("Content-Type: text/plain; charset=utf-8");
+ header("Content-Disposition: attachment; filename=$filename");
+ header("X-Robots-Tag: noindex");
+ echo trim($text, "\r\n");
+ exit;
+ }
+
+ $this->_codeblock++;
+ }
+
+ /**
+ * Wraps around code()
+ *
+ * @param string $text
+ * @param string $language
+ * @param string $filename
+ */
+ public function file($text, $language = null, $filename = '') {
+ $this->code($text, $language, $filename);
+ }
+
+ /**
+ * This should never be reached, if it is send a 404
+ */
+ public function document_end() {
+ http_status(404);
+ echo '404 - Not found';
+ exit;
+ }
+
+ /**
+ * Return the format of the renderer
+ *
+ * @returns string 'code'
+ */
+ public function getFormat() {
+ return 'code';
+ }
+}
diff --git a/platform/www/inc/parser/handler.php b/platform/www/inc/parser/handler.php
new file mode 100644
index 0000000..a360960
--- /dev/null
+++ b/platform/www/inc/parser/handler.php
@@ -0,0 +1,1157 @@
+<?php
+
+use dokuwiki\Extension\Event;
+use dokuwiki\Extension\SyntaxPlugin;
+use dokuwiki\Parsing\Handler\Block;
+use dokuwiki\Parsing\Handler\CallWriter;
+use dokuwiki\Parsing\Handler\CallWriterInterface;
+use dokuwiki\Parsing\Handler\Lists;
+use dokuwiki\Parsing\Handler\Nest;
+use dokuwiki\Parsing\Handler\Preformatted;
+use dokuwiki\Parsing\Handler\Quote;
+use dokuwiki\Parsing\Handler\Table;
+
+/**
+ * Class Doku_Handler
+ */
+class Doku_Handler {
+ /** @var CallWriterInterface */
+ protected $callWriter = null;
+
+ /** @var array The current CallWriter will write directly to this list of calls, Parser reads it */
+ public $calls = array();
+
+ /** @var array internal status holders for some modes */
+ protected $status = array(
+ 'section' => false,
+ 'doublequote' => 0,
+ );
+
+ /** @var bool should blocks be rewritten? FIXME seems to always be true */
+ protected $rewriteBlocks = true;
+
+ /**
+ * Doku_Handler constructor.
+ */
+ public function __construct() {
+ $this->callWriter = new CallWriter($this);
+ }
+
+ /**
+ * Add a new call by passing it to the current CallWriter
+ *
+ * @param string $handler handler method name (see mode handlers below)
+ * @param mixed $args arguments for this call
+ * @param int $pos byte position in the original source file
+ */
+ public function addCall($handler, $args, $pos) {
+ $call = array($handler,$args, $pos);
+ $this->callWriter->writeCall($call);
+ }
+
+ /**
+ * Accessor for the current CallWriter
+ *
+ * @return CallWriterInterface
+ */
+ public function getCallWriter() {
+ return $this->callWriter;
+ }
+
+ /**
+ * Set a new CallWriter
+ *
+ * @param CallWriterInterface $callWriter
+ */
+ public function setCallWriter($callWriter) {
+ $this->callWriter = $callWriter;
+ }
+
+ /**
+ * Return the current internal status of the given name
+ *
+ * @param string $status
+ * @return mixed|null
+ */
+ public function getStatus($status) {
+ if (!isset($this->status[$status])) return null;
+ return $this->status[$status];
+ }
+
+ /**
+ * Set a new internal status
+ *
+ * @param string $status
+ * @param mixed $value
+ */
+ public function setStatus($status, $value) {
+ $this->status[$status] = $value;
+ }
+
+ /** @deprecated 2019-10-31 use addCall() instead */
+ public function _addCall($handler, $args, $pos) {
+ dbg_deprecated('addCall');
+ $this->addCall($handler, $args, $pos);
+ }
+
+ /**
+ * Similar to addCall, but adds a plugin call
+ *
+ * @param string $plugin name of the plugin
+ * @param mixed $args arguments for this call
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @param string $match matched syntax
+ */
+ public function addPluginCall($plugin, $args, $state, $pos, $match) {
+ $call = array('plugin',array($plugin, $args, $state, $match), $pos);
+ $this->callWriter->writeCall($call);
+ }
+
+ /**
+ * Finishes handling
+ *
+ * Called from the parser. Calls finalise() on the call writer, closes open
+ * sections, rewrites blocks and adds document_start and document_end calls.
+ *
+ * @triggers PARSER_HANDLER_DONE
+ */
+ public function finalize(){
+ $this->callWriter->finalise();
+
+ if ( $this->status['section'] ) {
+ $last_call = end($this->calls);
+ array_push($this->calls,array('section_close',array(), $last_call[2]));
+ }
+
+ if ( $this->rewriteBlocks ) {
+ $B = new Block();
+ $this->calls = $B->process($this->calls);
+ }
+
+ Event::createAndTrigger('PARSER_HANDLER_DONE',$this);
+
+ array_unshift($this->calls,array('document_start',array(),0));
+ $last_call = end($this->calls);
+ array_push($this->calls,array('document_end',array(),$last_call[2]));
+ }
+
+ /**
+ * fetch the current call and advance the pointer to the next one
+ *
+ * @fixme seems to be unused?
+ * @return bool|mixed
+ */
+ public function fetch() {
+ $call = current($this->calls);
+ if($call !== false) {
+ next($this->calls); //advance the pointer
+ return $call;
+ }
+ return false;
+ }
+
+
+ /**
+ * Internal function for parsing highlight options.
+ * $options is parsed for key value pairs separated by commas.
+ * A value might also be missing in which case the value will simple
+ * be set to true. Commas in strings are ignored, e.g. option="4,56"
+ * will work as expected and will only create one entry.
+ *
+ * @param string $options space separated list of key-value pairs,
+ * e.g. option1=123, option2="456"
+ * @return array|null Array of key-value pairs $array['key'] = 'value';
+ * or null if no entries found
+ */
+ protected function parse_highlight_options($options) {
+ $result = array();
+ preg_match_all('/(\w+(?:="[^"]*"))|(\w+(?:=[^\s]*))|(\w+[^=\s\]])(?:\s*)/', $options, $matches, PREG_SET_ORDER);
+ foreach ($matches as $match) {
+ $equal_sign = strpos($match [0], '=');
+ if ($equal_sign === false) {
+ $key = trim($match[0]);
+ $result [$key] = 1;
+ } else {
+ $key = substr($match[0], 0, $equal_sign);
+ $value = substr($match[0], $equal_sign+1);
+ $value = trim($value, '"');
+ if (strlen($value) > 0) {
+ $result [$key] = $value;
+ } else {
+ $result [$key] = 1;
+ }
+ }
+ }
+
+ // Check for supported options
+ $result = array_intersect_key(
+ $result,
+ array_flip(array(
+ 'enable_line_numbers',
+ 'start_line_numbers_at',
+ 'highlight_lines_extra',
+ 'enable_keyword_links')
+ )
+ );
+
+ // Sanitize values
+ if(isset($result['enable_line_numbers'])) {
+ if($result['enable_line_numbers'] === 'false') {
+ $result['enable_line_numbers'] = false;
+ }
+ $result['enable_line_numbers'] = (bool) $result['enable_line_numbers'];
+ }
+ if(isset($result['highlight_lines_extra'])) {
+ $result['highlight_lines_extra'] = array_map('intval', explode(',', $result['highlight_lines_extra']));
+ $result['highlight_lines_extra'] = array_filter($result['highlight_lines_extra']);
+ $result['highlight_lines_extra'] = array_unique($result['highlight_lines_extra']);
+ }
+ if(isset($result['start_line_numbers_at'])) {
+ $result['start_line_numbers_at'] = (int) $result['start_line_numbers_at'];
+ }
+ if(isset($result['enable_keyword_links'])) {
+ if($result['enable_keyword_links'] === 'false') {
+ $result['enable_keyword_links'] = false;
+ }
+ $result['enable_keyword_links'] = (bool) $result['enable_keyword_links'];
+ }
+ if (count($result) == 0) {
+ return null;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Simplifies handling for the formatting tags which all behave the same
+ *
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @param string $name actual mode name
+ */
+ protected function nestingTag($match, $state, $pos, $name) {
+ switch ( $state ) {
+ case DOKU_LEXER_ENTER:
+ $this->addCall($name.'_open', array(), $pos);
+ break;
+ case DOKU_LEXER_EXIT:
+ $this->addCall($name.'_close', array(), $pos);
+ break;
+ case DOKU_LEXER_UNMATCHED:
+ $this->addCall('cdata', array($match), $pos);
+ break;
+ }
+ }
+
+
+ /**
+ * The following methods define the handlers for the different Syntax modes
+ *
+ * The handlers are called from dokuwiki\Parsing\Lexer\Lexer\invokeParser()
+ *
+ * @todo it might make sense to move these into their own class or merge them with the
+ * ParserMode classes some time.
+ */
+ // region mode handlers
+
+ /**
+ * Special plugin handler
+ *
+ * This handler is called for all modes starting with 'plugin_'.
+ * An additional parameter with the plugin name is passed. The plugin's handle()
+ * method is called here
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @param string $pluginname name of the plugin
+ * @return bool mode handled?
+ */
+ public function plugin($match, $state, $pos, $pluginname){
+ $data = array($match);
+ /** @var SyntaxPlugin $plugin */
+ $plugin = plugin_load('syntax',$pluginname);
+ if($plugin != null){
+ $data = $plugin->handle($match, $state, $pos, $this);
+ }
+ if ($data !== false) {
+ $this->addPluginCall($pluginname,$data,$state,$pos,$match);
+ }
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function base($match, $state, $pos) {
+ switch ( $state ) {
+ case DOKU_LEXER_UNMATCHED:
+ $this->addCall('cdata', array($match), $pos);
+ return true;
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function header($match, $state, $pos) {
+ // get level and title
+ $title = trim($match);
+ $level = 7 - strspn($title,'=');
+ if($level < 1) $level = 1;
+ $title = trim($title,'=');
+ $title = trim($title);
+
+ if ($this->status['section']) $this->addCall('section_close', array(), $pos);
+
+ $this->addCall('header', array($title, $level, $pos), $pos);
+
+ $this->addCall('section_open', array($level), $pos);
+ $this->status['section'] = true;
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function notoc($match, $state, $pos) {
+ $this->addCall('notoc', array(), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function nocache($match, $state, $pos) {
+ $this->addCall('nocache', array(), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function linebreak($match, $state, $pos) {
+ $this->addCall('linebreak', array(), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function eol($match, $state, $pos) {
+ $this->addCall('eol', array(), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function hr($match, $state, $pos) {
+ $this->addCall('hr', array(), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function strong($match, $state, $pos) {
+ $this->nestingTag($match, $state, $pos, 'strong');
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function emphasis($match, $state, $pos) {
+ $this->nestingTag($match, $state, $pos, 'emphasis');
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function underline($match, $state, $pos) {
+ $this->nestingTag($match, $state, $pos, 'underline');
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function monospace($match, $state, $pos) {
+ $this->nestingTag($match, $state, $pos, 'monospace');
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function subscript($match, $state, $pos) {
+ $this->nestingTag($match, $state, $pos, 'subscript');
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function superscript($match, $state, $pos) {
+ $this->nestingTag($match, $state, $pos, 'superscript');
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function deleted($match, $state, $pos) {
+ $this->nestingTag($match, $state, $pos, 'deleted');
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function footnote($match, $state, $pos) {
+ if (!isset($this->_footnote)) $this->_footnote = false;
+
+ switch ( $state ) {
+ case DOKU_LEXER_ENTER:
+ // footnotes can not be nested - however due to limitations in lexer it can't be prevented
+ // we will still enter a new footnote mode, we just do nothing
+ if ($this->_footnote) {
+ $this->addCall('cdata', array($match), $pos);
+ break;
+ }
+ $this->_footnote = true;
+
+ $this->callWriter = new Nest($this->callWriter, 'footnote_close');
+ $this->addCall('footnote_open', array(), $pos);
+ break;
+ case DOKU_LEXER_EXIT:
+ // check whether we have already exitted the footnote mode, can happen if the modes were nested
+ if (!$this->_footnote) {
+ $this->addCall('cdata', array($match), $pos);
+ break;
+ }
+
+ $this->_footnote = false;
+ $this->addCall('footnote_close', array(), $pos);
+
+ /** @var Nest $reWriter */
+ $reWriter = $this->callWriter;
+ $this->callWriter = $reWriter->process();
+ break;
+ case DOKU_LEXER_UNMATCHED:
+ $this->addCall('cdata', array($match), $pos);
+ break;
+ }
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function listblock($match, $state, $pos) {
+ switch ( $state ) {
+ case DOKU_LEXER_ENTER:
+ $this->callWriter = new Lists($this->callWriter);
+ $this->addCall('list_open', array($match), $pos);
+ break;
+ case DOKU_LEXER_EXIT:
+ $this->addCall('list_close', array(), $pos);
+ /** @var Lists $reWriter */
+ $reWriter = $this->callWriter;
+ $this->callWriter = $reWriter->process();
+ break;
+ case DOKU_LEXER_MATCHED:
+ $this->addCall('list_item', array($match), $pos);
+ break;
+ case DOKU_LEXER_UNMATCHED:
+ $this->addCall('cdata', array($match), $pos);
+ break;
+ }
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function unformatted($match, $state, $pos) {
+ if ( $state == DOKU_LEXER_UNMATCHED ) {
+ $this->addCall('unformatted', array($match), $pos);
+ }
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function php($match, $state, $pos) {
+ if ( $state == DOKU_LEXER_UNMATCHED ) {
+ $this->addCall('php', array($match), $pos);
+ }
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function phpblock($match, $state, $pos) {
+ if ( $state == DOKU_LEXER_UNMATCHED ) {
+ $this->addCall('phpblock', array($match), $pos);
+ }
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function html($match, $state, $pos) {
+ if ( $state == DOKU_LEXER_UNMATCHED ) {
+ $this->addCall('html', array($match), $pos);
+ }
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function htmlblock($match, $state, $pos) {
+ if ( $state == DOKU_LEXER_UNMATCHED ) {
+ $this->addCall('htmlblock', array($match), $pos);
+ }
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function preformatted($match, $state, $pos) {
+ switch ( $state ) {
+ case DOKU_LEXER_ENTER:
+ $this->callWriter = new Preformatted($this->callWriter);
+ $this->addCall('preformatted_start', array(), $pos);
+ break;
+ case DOKU_LEXER_EXIT:
+ $this->addCall('preformatted_end', array(), $pos);
+ /** @var Preformatted $reWriter */
+ $reWriter = $this->callWriter;
+ $this->callWriter = $reWriter->process();
+ break;
+ case DOKU_LEXER_MATCHED:
+ $this->addCall('preformatted_newline', array(), $pos);
+ break;
+ case DOKU_LEXER_UNMATCHED:
+ $this->addCall('preformatted_content', array($match), $pos);
+ break;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function quote($match, $state, $pos) {
+
+ switch ( $state ) {
+
+ case DOKU_LEXER_ENTER:
+ $this->callWriter = new Quote($this->callWriter);
+ $this->addCall('quote_start', array($match), $pos);
+ break;
+
+ case DOKU_LEXER_EXIT:
+ $this->addCall('quote_end', array(), $pos);
+ /** @var Lists $reWriter */
+ $reWriter = $this->callWriter;
+ $this->callWriter = $reWriter->process();
+ break;
+
+ case DOKU_LEXER_MATCHED:
+ $this->addCall('quote_newline', array($match), $pos);
+ break;
+
+ case DOKU_LEXER_UNMATCHED:
+ $this->addCall('cdata', array($match), $pos);
+ break;
+
+ }
+
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function file($match, $state, $pos) {
+ return $this->code($match, $state, $pos, 'file');
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @param string $type either 'code' or 'file'
+ * @return bool mode handled?
+ */
+ public function code($match, $state, $pos, $type='code') {
+ if ( $state == DOKU_LEXER_UNMATCHED ) {
+ $matches = explode('>',$match,2);
+ // Cut out variable options enclosed in []
+ preg_match('/\[.*\]/', $matches[0], $options);
+ if (!empty($options[0])) {
+ $matches[0] = str_replace($options[0], '', $matches[0]);
+ }
+ $param = preg_split('/\s+/', $matches[0], 2, PREG_SPLIT_NO_EMPTY);
+ while(count($param) < 2) array_push($param, null);
+ // We shortcut html here.
+ if ($param[0] == 'html') $param[0] = 'html4strict';
+ if ($param[0] == '-') $param[0] = null;
+ array_unshift($param, $matches[1]);
+ if (!empty($options[0])) {
+ $param [] = $this->parse_highlight_options ($options[0]);
+ }
+ $this->addCall($type, $param, $pos);
+ }
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function acronym($match, $state, $pos) {
+ $this->addCall('acronym', array($match), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function smiley($match, $state, $pos) {
+ $this->addCall('smiley', array($match), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function wordblock($match, $state, $pos) {
+ $this->addCall('wordblock', array($match), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function entity($match, $state, $pos) {
+ $this->addCall('entity', array($match), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function multiplyentity($match, $state, $pos) {
+ preg_match_all('/\d+/',$match,$matches);
+ $this->addCall('multiplyentity', array($matches[0][0], $matches[0][1]), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function singlequoteopening($match, $state, $pos) {
+ $this->addCall('singlequoteopening', array(), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function singlequoteclosing($match, $state, $pos) {
+ $this->addCall('singlequoteclosing', array(), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function apostrophe($match, $state, $pos) {
+ $this->addCall('apostrophe', array(), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function doublequoteopening($match, $state, $pos) {
+ $this->addCall('doublequoteopening', array(), $pos);
+ $this->status['doublequote']++;
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function doublequoteclosing($match, $state, $pos) {
+ if ($this->status['doublequote'] <= 0) {
+ $this->doublequoteopening($match, $state, $pos);
+ } else {
+ $this->addCall('doublequoteclosing', array(), $pos);
+ $this->status['doublequote'] = max(0, --$this->status['doublequote']);
+ }
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function camelcaselink($match, $state, $pos) {
+ $this->addCall('camelcaselink', array($match), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function internallink($match, $state, $pos) {
+ // Strip the opening and closing markup
+ $link = preg_replace(array('/^\[\[/','/\]\]$/u'),'',$match);
+
+ // Split title from URL
+ $link = explode('|',$link,2);
+ if ( !isset($link[1]) ) {
+ $link[1] = null;
+ } else if ( preg_match('/^\{\{[^\}]+\}\}$/',$link[1]) ) {
+ // If the title is an image, convert it to an array containing the image details
+ $link[1] = Doku_Handler_Parse_Media($link[1]);
+ }
+ $link[0] = trim($link[0]);
+
+ //decide which kind of link it is
+
+ if ( link_isinterwiki($link[0]) ) {
+ // Interwiki
+ $interwiki = explode('>',$link[0],2);
+ $this->addCall(
+ 'interwikilink',
+ array($link[0],$link[1],strtolower($interwiki[0]),$interwiki[1]),
+ $pos
+ );
+ }elseif ( preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u',$link[0]) ) {
+ // Windows Share
+ $this->addCall(
+ 'windowssharelink',
+ array($link[0],$link[1]),
+ $pos
+ );
+ }elseif ( preg_match('#^([a-z0-9\-\.+]+?)://#i',$link[0]) ) {
+ // external link (accepts all protocols)
+ $this->addCall(
+ 'externallink',
+ array($link[0],$link[1]),
+ $pos
+ );
+ }elseif ( preg_match('<'.PREG_PATTERN_VALID_EMAIL.'>',$link[0]) ) {
+ // E-Mail (pattern above is defined in inc/mail.php)
+ $this->addCall(
+ 'emaillink',
+ array($link[0],$link[1]),
+ $pos
+ );
+ }elseif ( preg_match('!^#.+!',$link[0]) ){
+ // local link
+ $this->addCall(
+ 'locallink',
+ array(substr($link[0],1),$link[1]),
+ $pos
+ );
+ }else{
+ // internal link
+ $this->addCall(
+ 'internallink',
+ array($link[0],$link[1]),
+ $pos
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function filelink($match, $state, $pos) {
+ $this->addCall('filelink', array($match, null), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function windowssharelink($match, $state, $pos) {
+ $this->addCall('windowssharelink', array($match, null), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function media($match, $state, $pos) {
+ $p = Doku_Handler_Parse_Media($match);
+
+ $this->addCall(
+ $p['type'],
+ array($p['src'], $p['title'], $p['align'], $p['width'],
+ $p['height'], $p['cache'], $p['linking']),
+ $pos
+ );
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function rss($match, $state, $pos) {
+ $link = preg_replace(array('/^\{\{rss>/','/\}\}$/'),'',$match);
+
+ // get params
+ list($link,$params) = explode(' ',$link,2);
+
+ $p = array();
+ if(preg_match('/\b(\d+)\b/',$params,$match)){
+ $p['max'] = $match[1];
+ }else{
+ $p['max'] = 8;
+ }
+ $p['reverse'] = (preg_match('/rev/',$params));
+ $p['author'] = (preg_match('/\b(by|author)/',$params));
+ $p['date'] = (preg_match('/\b(date)/',$params));
+ $p['details'] = (preg_match('/\b(desc|detail)/',$params));
+ $p['nosort'] = (preg_match('/\b(nosort)\b/',$params));
+
+ if (preg_match('/\b(\d+)([dhm])\b/',$params,$match)) {
+ $period = array('d' => 86400, 'h' => 3600, 'm' => 60);
+ $p['refresh'] = max(600,$match[1]*$period[$match[2]]); // n * period in seconds, minimum 10 minutes
+ } else {
+ $p['refresh'] = 14400; // default to 4 hours
+ }
+
+ $this->addCall('rss', array($link, $p), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function externallink($match, $state, $pos) {
+ $url = $match;
+ $title = null;
+
+ // add protocol on simple short URLs
+ if(substr($url,0,3) == 'ftp' && (substr($url,0,6) != 'ftp://')){
+ $title = $url;
+ $url = 'ftp://'.$url;
+ }
+ if(substr($url,0,3) == 'www' && (substr($url,0,7) != 'http://')){
+ $title = $url;
+ $url = 'http://'.$url;
+ }
+
+ $this->addCall('externallink', array($url, $title), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function emaillink($match, $state, $pos) {
+ $email = preg_replace(array('/^</','/>$/'),'',$match);
+ $this->addCall('emaillink', array($email, null), $pos);
+ return true;
+ }
+
+ /**
+ * @param string $match matched syntax
+ * @param int $state a LEXER_STATE_* constant
+ * @param int $pos byte position in the original source file
+ * @return bool mode handled?
+ */
+ public function table($match, $state, $pos) {
+ switch ( $state ) {
+
+ case DOKU_LEXER_ENTER:
+
+ $this->callWriter = new Table($this->callWriter);
+
+ $this->addCall('table_start', array($pos + 1), $pos);
+ if ( trim($match) == '^' ) {
+ $this->addCall('tableheader', array(), $pos);
+ } else {
+ $this->addCall('tablecell', array(), $pos);
+ }
+ break;
+
+ case DOKU_LEXER_EXIT:
+ $this->addCall('table_end', array($pos), $pos);
+ /** @var Table $reWriter */
+ $reWriter = $this->callWriter;
+ $this->callWriter = $reWriter->process();
+ break;
+
+ case DOKU_LEXER_UNMATCHED:
+ if ( trim($match) != '' ) {
+ $this->addCall('cdata', array($match), $pos);
+ }
+ break;
+
+ case DOKU_LEXER_MATCHED:
+ if ( $match == ' ' ){
+ $this->addCall('cdata', array($match), $pos);
+ } else if ( preg_match('/:::/',$match) ) {
+ $this->addCall('rowspan', array($match), $pos);
+ } else if ( preg_match('/\t+/',$match) ) {
+ $this->addCall('table_align', array($match), $pos);
+ } else if ( preg_match('/ {2,}/',$match) ) {
+ $this->addCall('table_align', array($match), $pos);
+ } else if ( $match == "\n|" ) {
+ $this->addCall('table_row', array(), $pos);
+ $this->addCall('tablecell', array(), $pos);
+ } else if ( $match == "\n^" ) {
+ $this->addCall('table_row', array(), $pos);
+ $this->addCall('tableheader', array(), $pos);
+ } else if ( $match == '|' ) {
+ $this->addCall('tablecell', array(), $pos);
+ } else if ( $match == '^' ) {
+ $this->addCall('tableheader', array(), $pos);
+ }
+ break;
+ }
+ return true;
+ }
+
+ // endregion modes
+}
+
+//------------------------------------------------------------------------
+function Doku_Handler_Parse_Media($match) {
+
+ // Strip the opening and closing markup
+ $link = preg_replace(array('/^\{\{/','/\}\}$/u'),'',$match);
+
+ // Split title from URL
+ $link = explode('|',$link,2);
+
+ // Check alignment
+ $ralign = (bool)preg_match('/^ /',$link[0]);
+ $lalign = (bool)preg_match('/ $/',$link[0]);
+
+ // Logic = what's that ;)...
+ if ( $lalign & $ralign ) {
+ $align = 'center';
+ } else if ( $ralign ) {
+ $align = 'right';
+ } else if ( $lalign ) {
+ $align = 'left';
+ } else {
+ $align = null;
+ }
+
+ // The title...
+ if ( !isset($link[1]) ) {
+ $link[1] = null;
+ }
+
+ //remove aligning spaces
+ $link[0] = trim($link[0]);
+
+ //split into src and parameters (using the very last questionmark)
+ $pos = strrpos($link[0], '?');
+ if($pos !== false){
+ $src = substr($link[0],0,$pos);
+ $param = substr($link[0],$pos+1);
+ }else{
+ $src = $link[0];
+ $param = '';
+ }
+
+ //parse width and height
+ if(preg_match('#(\d+)(x(\d+))?#i',$param,$size)){
+ !empty($size[1]) ? $w = $size[1] : $w = null;
+ !empty($size[3]) ? $h = $size[3] : $h = null;
+ } else {
+ $w = null;
+ $h = null;
+ }
+
+ //get linking command
+ if(preg_match('/nolink/i',$param)){
+ $linking = 'nolink';
+ }else if(preg_match('/direct/i',$param)){
+ $linking = 'direct';
+ }else if(preg_match('/linkonly/i',$param)){
+ $linking = 'linkonly';
+ }else{
+ $linking = 'details';
+ }
+
+ //get caching command
+ if (preg_match('/(nocache|recache)/i',$param,$cachemode)){
+ $cache = $cachemode[1];
+ }else{
+ $cache = 'cache';
+ }
+
+ // Check whether this is a local or remote image or interwiki
+ if (media_isexternal($src) || link_isinterwiki($src)){
+ $call = 'externalmedia';
+ } else {
+ $call = 'internalmedia';
+ }
+
+ $params = array(
+ 'type'=>$call,
+ 'src'=>$src,
+ 'title'=>$link[1],
+ 'align'=>$align,
+ 'width'=>$w,
+ 'height'=>$h,
+ 'cache'=>$cache,
+ 'linking'=>$linking,
+ );
+
+ return $params;
+}
+
diff --git a/platform/www/inc/parser/metadata.php b/platform/www/inc/parser/metadata.php
new file mode 100644
index 0000000..849fffe
--- /dev/null
+++ b/platform/www/inc/parser/metadata.php
@@ -0,0 +1,751 @@
+<?php
+/**
+ * The MetaData Renderer
+ *
+ * Metadata is additional information about a DokuWiki page that gets extracted mainly from the page's content
+ * but also it's own filesystem data (like the creation time). All metadata is stored in the fields $meta and
+ * $persistent.
+ *
+ * Some simplified rendering to $doc is done to gather the page's (text-only) abstract.
+ *
+ * @author Esther Brunner <wikidesign@gmail.com>
+ */
+class Doku_Renderer_metadata extends Doku_Renderer
+{
+ /** the approximate byte lenght to capture for the abstract */
+ const ABSTRACT_LEN = 250;
+
+ /** the maximum UTF8 character length for the abstract */
+ const ABSTRACT_MAX = 500;
+
+ /** @var array transient meta data, will be reset on each rendering */
+ public $meta = array();
+
+ /** @var array persistent meta data, will be kept until explicitly deleted */
+ public $persistent = array();
+
+ /** @var array the list of headers used to create unique link ids */
+ protected $headers = array();
+
+ /** @var string temporary $doc store */
+ protected $store = '';
+
+ /** @var string keeps the first image reference */
+ protected $firstimage = '';
+
+ /** @var bool whether or not data is being captured for the abstract, public to be accessible by plugins */
+ public $capturing = true;
+
+ /** @var bool determines if enough data for the abstract was collected, yet */
+ public $capture = true;
+
+ /** @var int number of bytes captured for abstract */
+ protected $captured = 0;
+
+ /**
+ * Returns the format produced by this renderer.
+ *
+ * @return string always 'metadata'
+ */
+ public function getFormat()
+ {
+ return 'metadata';
+ }
+
+ /**
+ * Initialize the document
+ *
+ * Sets up some of the persistent info about the page if it doesn't exist, yet.
+ */
+ public function document_start()
+ {
+ global $ID;
+
+ $this->headers = array();
+
+ // external pages are missing create date
+ if (!isset($this->persistent['date']['created']) || !$this->persistent['date']['created']) {
+ $this->persistent['date']['created'] = filectime(wikiFN($ID));
+ }
+ if (!isset($this->persistent['user'])) {
+ $this->persistent['user'] = '';
+ }
+ if (!isset($this->persistent['creator'])) {
+ $this->persistent['creator'] = '';
+ }
+ // reset metadata to persistent values
+ $this->meta = $this->persistent;
+ }
+
+ /**
+ * Finalize the document
+ *
+ * Stores collected data in the metadata
+ */
+ public function document_end()
+ {
+ global $ID;
+
+ // store internal info in metadata (notoc,nocache)
+ $this->meta['internal'] = $this->info;
+
+ if (!isset($this->meta['description']['abstract'])) {
+ // cut off too long abstracts
+ $this->doc = trim($this->doc);
+ if (strlen($this->doc) > self::ABSTRACT_MAX) {
+ $this->doc = \dokuwiki\Utf8\PhpString::substr($this->doc, 0, self::ABSTRACT_MAX).'…';
+ }
+ $this->meta['description']['abstract'] = $this->doc;
+ }
+
+ $this->meta['relation']['firstimage'] = $this->firstimage;
+
+ if (!isset($this->meta['date']['modified'])) {
+ $this->meta['date']['modified'] = filemtime(wikiFN($ID));
+ }
+ }
+
+ /**
+ * Render plain text data
+ *
+ * This function takes care of the amount captured data and will stop capturing when
+ * enough abstract data is available
+ *
+ * @param $text
+ */
+ public function cdata($text)
+ {
+ if (!$this->capture || !$this->capturing) {
+ return;
+ }
+
+ $this->doc .= $text;
+
+ $this->captured += strlen($text);
+ if ($this->captured > self::ABSTRACT_LEN) {
+ $this->capture = false;
+ }
+ }
+
+ /**
+ * Add an item to the TOC
+ *
+ * @param string $id the hash link
+ * @param string $text the text to display
+ * @param int $level the nesting level
+ */
+ public function toc_additem($id, $text, $level)
+ {
+ global $conf;
+
+ //only add items within configured levels
+ if ($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']) {
+ // the TOC is one of our standard ul list arrays ;-)
+ $this->meta['description']['tableofcontents'][] = array(
+ 'hid' => $id,
+ 'title' => $text,
+ 'type' => 'ul',
+ 'level' => $level - $conf['toptoclevel'] + 1
+ );
+ }
+ }
+
+ /**
+ * Render a heading
+ *
+ * @param string $text the text to display
+ * @param int $level header level
+ * @param int $pos byte position in the original source
+ */
+ public function header($text, $level, $pos)
+ {
+ if (!isset($this->meta['title'])) {
+ $this->meta['title'] = $text;
+ }
+
+ // add the header to the TOC
+ $hid = $this->_headerToLink($text, true);
+ $this->toc_additem($hid, $text, $level);
+
+ // add to summary
+ $this->cdata(DOKU_LF.$text.DOKU_LF);
+ }
+
+ /**
+ * Open a paragraph
+ */
+ public function p_open()
+ {
+ $this->cdata(DOKU_LF);
+ }
+
+ /**
+ * Close a paragraph
+ */
+ public function p_close()
+ {
+ $this->cdata(DOKU_LF);
+ }
+
+ /**
+ * Create a line break
+ */
+ public function linebreak()
+ {
+ $this->cdata(DOKU_LF);
+ }
+
+ /**
+ * Create a horizontal line
+ */
+ public function hr()
+ {
+ $this->cdata(DOKU_LF.'----------'.DOKU_LF);
+ }
+
+ /**
+ * Callback for footnote start syntax
+ *
+ * All following content will go to the footnote instead of
+ * the document. To achieve this the previous rendered content
+ * is moved to $store and $doc is cleared
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function footnote_open()
+ {
+ if ($this->capture) {
+ // move current content to store
+ // this is required to ensure safe behaviour of plugins accessed within footnotes
+ $this->store = $this->doc;
+ $this->doc = '';
+
+ // disable capturing
+ $this->capturing = false;
+ }
+ }
+
+ /**
+ * Callback for footnote end syntax
+ *
+ * All content rendered whilst within footnote syntax mode is discarded,
+ * the previously rendered content is restored and capturing is re-enabled.
+ *
+ * @author Andreas Gohr
+ */
+ public function footnote_close()
+ {
+ if ($this->capture) {
+ // re-enable capturing
+ $this->capturing = true;
+ // restore previously rendered content
+ $this->doc = $this->store;
+ $this->store = '';
+ }
+ }
+
+ /**
+ * Open an unordered list
+ */
+ public function listu_open()
+ {
+ $this->cdata(DOKU_LF);
+ }
+
+ /**
+ * Open an ordered list
+ */
+ public function listo_open()
+ {
+ $this->cdata(DOKU_LF);
+ }
+
+ /**
+ * Open a list item
+ *
+ * @param int $level the nesting level
+ * @param bool $node true when a node; false when a leaf
+ */
+ public function listitem_open($level, $node=false)
+ {
+ $this->cdata(str_repeat(DOKU_TAB, $level).'* ');
+ }
+
+ /**
+ * Close a list item
+ */
+ public function listitem_close()
+ {
+ $this->cdata(DOKU_LF);
+ }
+
+ /**
+ * Output preformatted text
+ *
+ * @param string $text
+ */
+ public function preformatted($text)
+ {
+ $this->cdata($text);
+ }
+
+ /**
+ * Start a block quote
+ */
+ public function quote_open()
+ {
+ $this->cdata(DOKU_LF.DOKU_TAB.'"');
+ }
+
+ /**
+ * Stop a block quote
+ */
+ public function quote_close()
+ {
+ $this->cdata('"'.DOKU_LF);
+ }
+
+ /**
+ * Display text as file content, optionally syntax highlighted
+ *
+ * @param string $text text to show
+ * @param string $lang programming language to use for syntax highlighting
+ * @param string $file file path label
+ */
+ public function file($text, $lang = null, $file = null)
+ {
+ $this->cdata(DOKU_LF.$text.DOKU_LF);
+ }
+
+ /**
+ * Display text as code content, optionally syntax highlighted
+ *
+ * @param string $text text to show
+ * @param string $language programming language to use for syntax highlighting
+ * @param string $file file path label
+ */
+ public function code($text, $language = null, $file = null)
+ {
+ $this->cdata(DOKU_LF.$text.DOKU_LF);
+ }
+
+ /**
+ * Format an acronym
+ *
+ * Uses $this->acronyms
+ *
+ * @param string $acronym
+ */
+ public function acronym($acronym)
+ {
+ $this->cdata($acronym);
+ }
+
+ /**
+ * Format a smiley
+ *
+ * Uses $this->smiley
+ *
+ * @param string $smiley
+ */
+ public function smiley($smiley)
+ {
+ $this->cdata($smiley);
+ }
+
+ /**
+ * Format an entity
+ *
+ * Entities are basically small text replacements
+ *
+ * Uses $this->entities
+ *
+ * @param string $entity
+ */
+ public function entity($entity)
+ {
+ $this->cdata($entity);
+ }
+
+ /**
+ * Typographically format a multiply sign
+ *
+ * Example: ($x=640, $y=480) should result in "640×480"
+ *
+ * @param string|int $x first value
+ * @param string|int $y second value
+ */
+ public function multiplyentity($x, $y)
+ {
+ $this->cdata($x.'×'.$y);
+ }
+
+ /**
+ * Render an opening single quote char (language specific)
+ */
+ public function singlequoteopening()
+ {
+ global $lang;
+ $this->cdata($lang['singlequoteopening']);
+ }
+
+ /**
+ * Render a closing single quote char (language specific)
+ */
+ public function singlequoteclosing()
+ {
+ global $lang;
+ $this->cdata($lang['singlequoteclosing']);
+ }
+
+ /**
+ * Render an apostrophe char (language specific)
+ */
+ public function apostrophe()
+ {
+ global $lang;
+ $this->cdata($lang['apostrophe']);
+ }
+
+ /**
+ * Render an opening double quote char (language specific)
+ */
+ public function doublequoteopening()
+ {
+ global $lang;
+ $this->cdata($lang['doublequoteopening']);
+ }
+
+ /**
+ * Render an closinging double quote char (language specific)
+ */
+ public function doublequoteclosing()
+ {
+ global $lang;
+ $this->cdata($lang['doublequoteclosing']);
+ }
+
+ /**
+ * Render a CamelCase link
+ *
+ * @param string $link The link name
+ * @see http://en.wikipedia.org/wiki/CamelCase
+ */
+ public function camelcaselink($link)
+ {
+ $this->internallink($link, $link);
+ }
+
+ /**
+ * Render a page local link
+ *
+ * @param string $hash hash link identifier
+ * @param string $name name for the link
+ */
+ public function locallink($hash, $name = null)
+ {
+ if (is_array($name)) {
+ $this->_firstimage($name['src']);
+ if ($name['type'] == 'internalmedia') {
+ $this->_recordMediaUsage($name['src']);
+ }
+ }
+ }
+
+ /**
+ * keep track of internal links in $this->meta['relation']['references']
+ *
+ * @param string $id page ID to link to. eg. 'wiki:syntax'
+ * @param string|array|null $name name for the link, array for media file
+ */
+ public function internallink($id, $name = null)
+ {
+ global $ID;
+
+ if (is_array($name)) {
+ $this->_firstimage($name['src']);
+ if ($name['type'] == 'internalmedia') {
+ $this->_recordMediaUsage($name['src']);
+ }
+ }
+
+ $parts = explode('?', $id, 2);
+ if (count($parts) === 2) {
+ $id = $parts[0];
+ }
+
+ $default = $this->_simpleTitle($id);
+
+ // first resolve and clean up the $id
+ resolve_pageid(getNS($ID), $id, $exists);
+ @list($page) = explode('#', $id, 2);
+
+ // set metadata
+ $this->meta['relation']['references'][$page] = $exists;
+ // $data = array('relation' => array('isreferencedby' => array($ID => true)));
+ // p_set_metadata($id, $data);
+
+ // add link title to summary
+ if ($this->capture) {
+ $name = $this->_getLinkTitle($name, $default, $id);
+ $this->doc .= $name;
+ }
+ }
+
+ /**
+ * Render an external link
+ *
+ * @param string $url full URL with scheme
+ * @param string|array|null $name name for the link, array for media file
+ */
+ public function externallink($url, $name = null)
+ {
+ if (is_array($name)) {
+ $this->_firstimage($name['src']);
+ if ($name['type'] == 'internalmedia') {
+ $this->_recordMediaUsage($name['src']);
+ }
+ }
+
+ if ($this->capture) {
+ $this->doc .= $this->_getLinkTitle($name, '<'.$url.'>');
+ }
+ }
+
+ /**
+ * Render an interwiki link
+ *
+ * You may want to use $this->_resolveInterWiki() here
+ *
+ * @param string $match original link - probably not much use
+ * @param string|array $name name for the link, array for media file
+ * @param string $wikiName indentifier (shortcut) for the remote wiki
+ * @param string $wikiUri the fragment parsed from the original link
+ */
+ public function interwikilink($match, $name, $wikiName, $wikiUri)
+ {
+ if (is_array($name)) {
+ $this->_firstimage($name['src']);
+ if ($name['type'] == 'internalmedia') {
+ $this->_recordMediaUsage($name['src']);
+ }
+ }
+
+ if ($this->capture) {
+ list($wikiUri) = explode('#', $wikiUri, 2);
+ $name = $this->_getLinkTitle($name, $wikiUri);
+ $this->doc .= $name;
+ }
+ }
+
+ /**
+ * Link to windows share
+ *
+ * @param string $url the link
+ * @param string|array $name name for the link, array for media file
+ */
+ public function windowssharelink($url, $name = null)
+ {
+ if (is_array($name)) {
+ $this->_firstimage($name['src']);
+ if ($name['type'] == 'internalmedia') {
+ $this->_recordMediaUsage($name['src']);
+ }
+ }
+
+ if ($this->capture) {
+ if ($name) {
+ $this->doc .= $name;
+ } else {
+ $this->doc .= '<'.$url.'>';
+ }
+ }
+ }
+
+ /**
+ * Render a linked E-Mail Address
+ *
+ * Should honor $conf['mailguard'] setting
+ *
+ * @param string $address Email-Address
+ * @param string|array $name name for the link, array for media file
+ */
+ public function emaillink($address, $name = null)
+ {
+ if (is_array($name)) {
+ $this->_firstimage($name['src']);
+ if ($name['type'] == 'internalmedia') {
+ $this->_recordMediaUsage($name['src']);
+ }
+ }
+
+ if ($this->capture) {
+ if ($name) {
+ $this->doc .= $name;
+ } else {
+ $this->doc .= '<'.$address.'>';
+ }
+ }
+ }
+
+ /**
+ * Render an internal media file
+ *
+ * @param string $src media ID
+ * @param string $title descriptive text
+ * @param string $align left|center|right
+ * @param int $width width of media in pixel
+ * @param int $height height of media in pixel
+ * @param string $cache cache|recache|nocache
+ * @param string $linking linkonly|detail|nolink
+ */
+ public function internalmedia($src, $title = null, $align = null, $width = null,
+ $height = null, $cache = null, $linking = null)
+ {
+ if ($this->capture && $title) {
+ $this->doc .= '['.$title.']';
+ }
+ $this->_firstimage($src);
+ $this->_recordMediaUsage($src);
+ }
+
+ /**
+ * Render an external media file
+ *
+ * @param string $src full media URL
+ * @param string $title descriptive text
+ * @param string $align left|center|right
+ * @param int $width width of media in pixel
+ * @param int $height height of media in pixel
+ * @param string $cache cache|recache|nocache
+ * @param string $linking linkonly|detail|nolink
+ */
+ public function externalmedia($src, $title = null, $align = null, $width = null,
+ $height = null, $cache = null, $linking = null)
+ {
+ if ($this->capture && $title) {
+ $this->doc .= '['.$title.']';
+ }
+ $this->_firstimage($src);
+ }
+
+ /**
+ * Render the output of an RSS feed
+ *
+ * @param string $url URL of the feed
+ * @param array $params Finetuning of the output
+ */
+ public function rss($url, $params)
+ {
+ $this->meta['relation']['haspart'][$url] = true;
+
+ $this->meta['date']['valid']['age'] =
+ isset($this->meta['date']['valid']['age']) ?
+ min($this->meta['date']['valid']['age'], $params['refresh']) :
+ $params['refresh'];
+ }
+
+ #region Utils
+
+ /**
+ * Removes any Namespace from the given name but keeps
+ * casing and special chars
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $name
+ *
+ * @return mixed|string
+ */
+ public function _simpleTitle($name)
+ {
+ global $conf;
+
+ if (is_array($name)) {
+ return '';
+ }
+
+ if ($conf['useslash']) {
+ $nssep = '[:;/]';
+ } else {
+ $nssep = '[:;]';
+ }
+ $name = preg_replace('!.*'.$nssep.'!', '', $name);
+ //if there is a hash we use the anchor name only
+ $name = preg_replace('!.*#!', '', $name);
+ return $name;
+ }
+
+ /**
+ * Construct a title and handle images in titles
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @param string|array|null $title either string title or media array
+ * @param string $default default title if nothing else is found
+ * @param null|string $id linked page id (used to extract title from first heading)
+ * @return string title text
+ */
+ public function _getLinkTitle($title, $default, $id = null)
+ {
+ if (is_array($title)) {
+ if ($title['title']) {
+ return '['.$title['title'].']';
+ } else {
+ return $default;
+ }
+ } elseif (is_null($title) || trim($title) == '') {
+ if (useHeading('content') && $id) {
+ $heading = p_get_first_heading($id, METADATA_DONT_RENDER);
+ if ($heading) {
+ return $heading;
+ }
+ }
+ return $default;
+ } else {
+ return $title;
+ }
+ }
+
+ /**
+ * Remember first image
+ *
+ * @param string $src image URL or ID
+ */
+ protected function _firstimage($src)
+ {
+ global $ID;
+
+ if ($this->firstimage) {
+ return;
+ }
+
+ list($src) = explode('#', $src, 2);
+ if (!media_isexternal($src)) {
+ resolve_mediaid(getNS($ID), $src, $exists);
+ }
+ if (preg_match('/.(jpe?g|gif|png)$/i', $src)) {
+ $this->firstimage = $src;
+ }
+ }
+
+ /**
+ * Store list of used media files in metadata
+ *
+ * @param string $src media ID
+ */
+ protected function _recordMediaUsage($src)
+ {
+ global $ID;
+
+ list ($src) = explode('#', $src, 2);
+ if (media_isexternal($src)) {
+ return;
+ }
+ resolve_mediaid(getNS($ID), $src, $exists);
+ $this->meta['relation']['media'][$src] = $exists;
+ }
+
+ #endregion
+}
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/inc/parser/parser.php b/platform/www/inc/parser/parser.php
new file mode 100644
index 0000000..aee82f0
--- /dev/null
+++ b/platform/www/inc/parser/parser.php
@@ -0,0 +1,99 @@
+<?php
+
+use dokuwiki\Debug\PropertyDeprecationHelper;
+
+/**
+ * Define various types of modes used by the parser - they are used to
+ * populate the list of modes another mode accepts
+ */
+global $PARSER_MODES;
+$PARSER_MODES = array(
+ // containers are complex modes that can contain many other modes
+ // hr breaks the principle but they shouldn't be used in tables / lists
+ // so they are put here
+ 'container' => array('listblock', 'table', 'quote', 'hr'),
+
+ // some mode are allowed inside the base mode only
+ 'baseonly' => array('header'),
+
+ // modes for styling text -- footnote behaves similar to styling
+ 'formatting' => array(
+ 'strong', 'emphasis', 'underline', 'monospace',
+ 'subscript', 'superscript', 'deleted', 'footnote'
+ ),
+
+ // modes where the token is simply replaced - they can not contain any
+ // other modes
+ 'substition' => array(
+ 'acronym', 'smiley', 'wordblock', 'entity',
+ 'camelcaselink', 'internallink', 'media',
+ 'externallink', 'linebreak', 'emaillink',
+ 'windowssharelink', 'filelink', 'notoc',
+ 'nocache', 'multiplyentity', 'quotes', 'rss'
+ ),
+
+ // modes which have a start and end token but inside which
+ // no other modes should be applied
+ 'protected' => array('preformatted', 'code', 'file', 'php', 'html', 'htmlblock', 'phpblock'),
+
+ // inside this mode no wiki markup should be applied but lineendings
+ // and whitespace isn't preserved
+ 'disabled' => array('unformatted'),
+
+ // used to mark paragraph boundaries
+ 'paragraphs' => array('eol')
+);
+
+/**
+ * Class Doku_Parser
+ *
+ * @deprecated 2018-05-04
+ */
+class Doku_Parser extends \dokuwiki\Parsing\Parser {
+ use PropertyDeprecationHelper {
+ __set as protected deprecationHelperMagicSet;
+ __get as protected deprecationHelperMagicGet;
+ }
+
+ /** @inheritdoc */
+ public function __construct(Doku_Handler $handler = null) {
+ dbg_deprecated(\dokuwiki\Parsing\Parser::class);
+ $this->deprecatePublicProperty('modes', __CLASS__);
+ $this->deprecatePublicProperty('connected', __CLASS__);
+
+ if ($handler === null) {
+ $handler = new Doku_Handler();
+ }
+
+ parent::__construct($handler);
+ }
+
+ public function __set($name, $value)
+ {
+
+ if ($name === 'Handler') {
+ $this->handler = $value;
+ return;
+ }
+
+ if ($name === 'Lexer') {
+ $this->lexer = $value;
+ return;
+ }
+
+ $this->deprecationHelperMagicSet($name, $value);
+ }
+
+ public function __get($name)
+ {
+ if ($name === 'Handler') {
+ return $this->handler;
+ }
+
+ if ($name === 'Lexer') {
+ return $this->lexer;
+ }
+
+ return $this->deprecationHelperMagicGet($name);
+ }
+}
diff --git a/platform/www/inc/parser/renderer.php b/platform/www/inc/parser/renderer.php
new file mode 100644
index 0000000..e4eff2a
--- /dev/null
+++ b/platform/www/inc/parser/renderer.php
@@ -0,0 +1,910 @@
+<?php
+/**
+ * Renderer output base class
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+use dokuwiki\Extension\Plugin;
+use dokuwiki\Extension\SyntaxPlugin;
+
+/**
+ * Allowed chars in $language for code highlighting
+ * @see GeSHi::set_language()
+ */
+define('PREG_PATTERN_VALID_LANGUAGE', '#[^a-zA-Z0-9\-_]#');
+
+/**
+ * An empty renderer, produces no output
+ *
+ * Inherits from dokuwiki\Plugin\DokuWiki_Plugin for giving additional functions to render plugins
+ *
+ * The renderer transforms the syntax instructions created by the parser and handler into the
+ * desired output format. For each instruction a corresponding method defined in this class will
+ * be called. That method needs to produce the desired output for the instruction and add it to the
+ * $doc field. When all instructions are processed, the $doc field contents will be cached by
+ * DokuWiki and sent to the user.
+ */
+abstract class Doku_Renderer extends Plugin {
+ /** @var array Settings, control the behavior of the renderer */
+ public $info = array(
+ 'cache' => true, // may the rendered result cached?
+ 'toc' => true, // render the TOC?
+ );
+
+ /** @var array contains the smiley configuration, set in p_render() */
+ public $smileys = array();
+ /** @var array contains the entity configuration, set in p_render() */
+ public $entities = array();
+ /** @var array contains the acronym configuration, set in p_render() */
+ public $acronyms = array();
+ /** @var array contains the interwiki configuration, set in p_render() */
+ public $interwiki = array();
+
+ /** @var array the list of headers used to create unique link ids */
+ protected $headers = array();
+
+ /**
+ * @var string the rendered document, this will be cached after the renderer ran through
+ */
+ public $doc = '';
+
+ /**
+ * clean out any per-use values
+ *
+ * This is called before each use of the renderer object and should be used to
+ * completely reset the state of the renderer to be reused for a new document
+ */
+ public function reset(){
+ $this->headers = array();
+ $this->doc = '';
+ $this->info['cache'] = true;
+ $this->info['toc'] = true;
+ }
+
+ /**
+ * Allow the plugin to prevent DokuWiki from reusing an instance
+ *
+ * Since most renderer plugins fail to implement Doku_Renderer::reset() we default
+ * to reinstantiating the renderer here
+ *
+ * @return bool false if the plugin has to be instantiated
+ */
+ public function isSingleton() {
+ return false;
+ }
+
+ /**
+ * Returns the format produced by this renderer.
+ *
+ * Has to be overidden by sub classes
+ *
+ * @return string
+ */
+ abstract public function getFormat();
+
+ /**
+ * Disable caching of this renderer's output
+ */
+ public function nocache() {
+ $this->info['cache'] = false;
+ }
+
+ /**
+ * Disable TOC generation for this renderer's output
+ *
+ * This might not be used for certain sub renderer
+ */
+ public function notoc() {
+ $this->info['toc'] = false;
+ }
+
+ /**
+ * Handle plugin rendering
+ *
+ * Most likely this needs NOT to be overwritten by sub classes
+ *
+ * @param string $name Plugin name
+ * @param mixed $data custom data set by handler
+ * @param string $state matched state if any
+ * @param string $match raw matched syntax
+ */
+ public function plugin($name, $data, $state = '', $match = '') {
+ /** @var SyntaxPlugin $plugin */
+ $plugin = plugin_load('syntax', $name);
+ if($plugin != null) {
+ $plugin->render($this->getFormat(), $this, $data);
+ }
+ }
+
+ /**
+ * handle nested render instructions
+ * this method (and nest_close method) should not be overloaded in actual renderer output classes
+ *
+ * @param array $instructions
+ */
+ public function nest($instructions) {
+ foreach($instructions as $instruction) {
+ // execute the callback against ourself
+ if(method_exists($this, $instruction[0])) {
+ call_user_func_array(array($this, $instruction[0]), $instruction[1] ? $instruction[1] : array());
+ }
+ }
+ }
+
+ /**
+ * dummy closing instruction issued by Doku_Handler_Nest
+ *
+ * normally the syntax mode should override this instruction when instantiating Doku_Handler_Nest -
+ * however plugins will not be able to - as their instructions require data.
+ */
+ public function nest_close() {
+ }
+
+ #region Syntax modes - sub classes will need to implement them to fill $doc
+
+ /**
+ * Initialize the document
+ */
+ public function document_start() {
+ }
+
+ /**
+ * Finalize the document
+ */
+ public function document_end() {
+ }
+
+ /**
+ * Render the Table of Contents
+ *
+ * @return string
+ */
+ public function render_TOC() {
+ return '';
+ }
+
+ /**
+ * Add an item to the TOC
+ *
+ * @param string $id the hash link
+ * @param string $text the text to display
+ * @param int $level the nesting level
+ */
+ public function toc_additem($id, $text, $level) {
+ }
+
+ /**
+ * Render a heading
+ *
+ * @param string $text the text to display
+ * @param int $level header level
+ * @param int $pos byte position in the original source
+ */
+ public function header($text, $level, $pos) {
+ }
+
+ /**
+ * Open a new section
+ *
+ * @param int $level section level (as determined by the previous header)
+ */
+ public function section_open($level) {
+ }
+
+ /**
+ * Close the current section
+ */
+ public function section_close() {
+ }
+
+ /**
+ * Render plain text data
+ *
+ * @param string $text
+ */
+ public function cdata($text) {
+ }
+
+ /**
+ * Open a paragraph
+ */
+ public function p_open() {
+ }
+
+ /**
+ * Close a paragraph
+ */
+ public function p_close() {
+ }
+
+ /**
+ * Create a line break
+ */
+ public function linebreak() {
+ }
+
+ /**
+ * Create a horizontal line
+ */
+ public function hr() {
+ }
+
+ /**
+ * Start strong (bold) formatting
+ */
+ public function strong_open() {
+ }
+
+ /**
+ * Stop strong (bold) formatting
+ */
+ public function strong_close() {
+ }
+
+ /**
+ * Start emphasis (italics) formatting
+ */
+ public function emphasis_open() {
+ }
+
+ /**
+ * Stop emphasis (italics) formatting
+ */
+ public function emphasis_close() {
+ }
+
+ /**
+ * Start underline formatting
+ */
+ public function underline_open() {
+ }
+
+ /**
+ * Stop underline formatting
+ */
+ public function underline_close() {
+ }
+
+ /**
+ * Start monospace formatting
+ */
+ public function monospace_open() {
+ }
+
+ /**
+ * Stop monospace formatting
+ */
+ public function monospace_close() {
+ }
+
+ /**
+ * Start a subscript
+ */
+ public function subscript_open() {
+ }
+
+ /**
+ * Stop a subscript
+ */
+ public function subscript_close() {
+ }
+
+ /**
+ * Start a superscript
+ */
+ public function superscript_open() {
+ }
+
+ /**
+ * Stop a superscript
+ */
+ public function superscript_close() {
+ }
+
+ /**
+ * Start deleted (strike-through) formatting
+ */
+ public function deleted_open() {
+ }
+
+ /**
+ * Stop deleted (strike-through) formatting
+ */
+ public function deleted_close() {
+ }
+
+ /**
+ * Start a footnote
+ */
+ public function footnote_open() {
+ }
+
+ /**
+ * Stop a footnote
+ */
+ public function footnote_close() {
+ }
+
+ /**
+ * Open an unordered list
+ */
+ public function listu_open() {
+ }
+
+ /**
+ * Close an unordered list
+ */
+ public function listu_close() {
+ }
+
+ /**
+ * Open an ordered list
+ */
+ public function listo_open() {
+ }
+
+ /**
+ * Close an ordered list
+ */
+ public function listo_close() {
+ }
+
+ /**
+ * Open a list item
+ *
+ * @param int $level the nesting level
+ * @param bool $node true when a node; false when a leaf
+ */
+ public function listitem_open($level,$node=false) {
+ }
+
+ /**
+ * Close a list item
+ */
+ public function listitem_close() {
+ }
+
+ /**
+ * Start the content of a list item
+ */
+ public function listcontent_open() {
+ }
+
+ /**
+ * Stop the content of a list item
+ */
+ public function listcontent_close() {
+ }
+
+ /**
+ * Output unformatted $text
+ *
+ * Defaults to $this->cdata()
+ *
+ * @param string $text
+ */
+ public function unformatted($text) {
+ $this->cdata($text);
+ }
+
+ /**
+ * Output inline PHP code
+ *
+ * If $conf['phpok'] is true this should evaluate the given code and append the result
+ * to $doc
+ *
+ * @param string $text The PHP code
+ */
+ public function php($text) {
+ }
+
+ /**
+ * Output block level PHP code
+ *
+ * If $conf['phpok'] is true this should evaluate the given code and append the result
+ * to $doc
+ *
+ * @param string $text The PHP code
+ */
+ public function phpblock($text) {
+ }
+
+ /**
+ * Output raw inline HTML
+ *
+ * If $conf['htmlok'] is true this should add the code as is to $doc
+ *
+ * @param string $text The HTML
+ */
+ public function html($text) {
+ }
+
+ /**
+ * Output raw block-level HTML
+ *
+ * If $conf['htmlok'] is true this should add the code as is to $doc
+ *
+ * @param string $text The HTML
+ */
+ public function htmlblock($text) {
+ }
+
+ /**
+ * Output preformatted text
+ *
+ * @param string $text
+ */
+ public function preformatted($text) {
+ }
+
+ /**
+ * Start a block quote
+ */
+ public function quote_open() {
+ }
+
+ /**
+ * Stop a block quote
+ */
+ public function quote_close() {
+ }
+
+ /**
+ * Display text as file content, optionally syntax highlighted
+ *
+ * @param string $text text to show
+ * @param string $lang programming language to use for syntax highlighting
+ * @param string $file file path label
+ */
+ public function file($text, $lang = null, $file = null) {
+ }
+
+ /**
+ * Display text as code content, optionally syntax highlighted
+ *
+ * @param string $text text to show
+ * @param string $lang programming language to use for syntax highlighting
+ * @param string $file file path label
+ */
+ public function code($text, $lang = null, $file = null) {
+ }
+
+ /**
+ * Format an acronym
+ *
+ * Uses $this->acronyms
+ *
+ * @param string $acronym
+ */
+ public function acronym($acronym) {
+ }
+
+ /**
+ * Format a smiley
+ *
+ * Uses $this->smiley
+ *
+ * @param string $smiley
+ */
+ public function smiley($smiley) {
+ }
+
+ /**
+ * Format an entity
+ *
+ * Entities are basically small text replacements
+ *
+ * Uses $this->entities
+ *
+ * @param string $entity
+ */
+ public function entity($entity) {
+ }
+
+ /**
+ * Typographically format a multiply sign
+ *
+ * Example: ($x=640, $y=480) should result in "640×480"
+ *
+ * @param string|int $x first value
+ * @param string|int $y second value
+ */
+ public function multiplyentity($x, $y) {
+ }
+
+ /**
+ * Render an opening single quote char (language specific)
+ */
+ public function singlequoteopening() {
+ }
+
+ /**
+ * Render a closing single quote char (language specific)
+ */
+ public function singlequoteclosing() {
+ }
+
+ /**
+ * Render an apostrophe char (language specific)
+ */
+ public function apostrophe() {
+ }
+
+ /**
+ * Render an opening double quote char (language specific)
+ */
+ public function doublequoteopening() {
+ }
+
+ /**
+ * Render an closinging double quote char (language specific)
+ */
+ public function doublequoteclosing() {
+ }
+
+ /**
+ * Render a CamelCase link
+ *
+ * @param string $link The link name
+ * @see http://en.wikipedia.org/wiki/CamelCase
+ */
+ public function camelcaselink($link) {
+ }
+
+ /**
+ * Render a page local link
+ *
+ * @param string $hash hash link identifier
+ * @param string $name name for the link
+ */
+ public function locallink($hash, $name = null) {
+ }
+
+ /**
+ * Render a wiki internal link
+ *
+ * @param string $link page ID to link to. eg. 'wiki:syntax'
+ * @param string|array $title name for the link, array for media file
+ */
+ public function internallink($link, $title = null) {
+ }
+
+ /**
+ * Render an external link
+ *
+ * @param string $link full URL with scheme
+ * @param string|array $title name for the link, array for media file
+ */
+ public function externallink($link, $title = null) {
+ }
+
+ /**
+ * Render the output of an RSS feed
+ *
+ * @param string $url URL of the feed
+ * @param array $params Finetuning of the output
+ */
+ public function rss($url, $params) {
+ }
+
+ /**
+ * Render an interwiki link
+ *
+ * You may want to use $this->_resolveInterWiki() here
+ *
+ * @param string $link original link - probably not much use
+ * @param string|array $title name for the link, array for media file
+ * @param string $wikiName indentifier (shortcut) for the remote wiki
+ * @param string $wikiUri the fragment parsed from the original link
+ */
+ public function interwikilink($link, $title, $wikiName, $wikiUri) {
+ }
+
+ /**
+ * Link to file on users OS
+ *
+ * @param string $link the link
+ * @param string|array $title name for the link, array for media file
+ */
+ public function filelink($link, $title = null) {
+ }
+
+ /**
+ * Link to windows share
+ *
+ * @param string $link the link
+ * @param string|array $title name for the link, array for media file
+ */
+ public function windowssharelink($link, $title = null) {
+ }
+
+ /**
+ * Render a linked E-Mail Address
+ *
+ * Should honor $conf['mailguard'] setting
+ *
+ * @param string $address Email-Address
+ * @param string|array $name name for the link, array for media file
+ */
+ public function emaillink($address, $name = null) {
+ }
+
+ /**
+ * Render an internal media file
+ *
+ * @param string $src media ID
+ * @param string $title descriptive text
+ * @param string $align left|center|right
+ * @param int $width width of media in pixel
+ * @param int $height height of media in pixel
+ * @param string $cache cache|recache|nocache
+ * @param string $linking linkonly|detail|nolink
+ */
+ public function internalmedia($src, $title = null, $align = null, $width = null,
+ $height = null, $cache = null, $linking = null) {
+ }
+
+ /**
+ * Render an external media file
+ *
+ * @param string $src full media URL
+ * @param string $title descriptive text
+ * @param string $align left|center|right
+ * @param int $width width of media in pixel
+ * @param int $height height of media in pixel
+ * @param string $cache cache|recache|nocache
+ * @param string $linking linkonly|detail|nolink
+ */
+ public function externalmedia($src, $title = null, $align = null, $width = null,
+ $height = null, $cache = null, $linking = null) {
+ }
+
+ /**
+ * Render a link to an internal media file
+ *
+ * @param string $src media ID
+ * @param string $title descriptive text
+ * @param string $align left|center|right
+ * @param int $width width of media in pixel
+ * @param int $height height of media in pixel
+ * @param string $cache cache|recache|nocache
+ */
+ public function internalmedialink($src, $title = null, $align = null,
+ $width = null, $height = null, $cache = null) {
+ }
+
+ /**
+ * Render a link to an external media file
+ *
+ * @param string $src media ID
+ * @param string $title descriptive text
+ * @param string $align left|center|right
+ * @param int $width width of media in pixel
+ * @param int $height height of media in pixel
+ * @param string $cache cache|recache|nocache
+ */
+ public function externalmedialink($src, $title = null, $align = null,
+ $width = null, $height = null, $cache = null) {
+ }
+
+ /**
+ * Start a table
+ *
+ * @param int $maxcols maximum number of columns
+ * @param int $numrows NOT IMPLEMENTED
+ * @param int $pos byte position in the original source
+ */
+ public function table_open($maxcols = null, $numrows = null, $pos = null) {
+ }
+
+ /**
+ * Close a table
+ *
+ * @param int $pos byte position in the original source
+ */
+ public function table_close($pos = null) {
+ }
+
+ /**
+ * Open a table header
+ */
+ public function tablethead_open() {
+ }
+
+ /**
+ * Close a table header
+ */
+ public function tablethead_close() {
+ }
+
+ /**
+ * Open a table body
+ */
+ public function tabletbody_open() {
+ }
+
+ /**
+ * Close a table body
+ */
+ public function tabletbody_close() {
+ }
+
+ /**
+ * Open a table footer
+ */
+ public function tabletfoot_open() {
+ }
+
+ /**
+ * Close a table footer
+ */
+ public function tabletfoot_close() {
+ }
+
+ /**
+ * Open a table row
+ */
+ public function tablerow_open() {
+ }
+
+ /**
+ * Close a table row
+ */
+ public function tablerow_close() {
+ }
+
+ /**
+ * Open a table header cell
+ *
+ * @param int $colspan
+ * @param string $align left|center|right
+ * @param int $rowspan
+ */
+ public function tableheader_open($colspan = 1, $align = null, $rowspan = 1) {
+ }
+
+ /**
+ * Close a table header cell
+ */
+ public function tableheader_close() {
+ }
+
+ /**
+ * Open a table cell
+ *
+ * @param int $colspan
+ * @param string $align left|center|right
+ * @param int $rowspan
+ */
+ public function tablecell_open($colspan = 1, $align = null, $rowspan = 1) {
+ }
+
+ /**
+ * Close a table cell
+ */
+ public function tablecell_close() {
+ }
+
+ #endregion
+
+ #region util functions, you probably won't need to reimplement them
+
+ /**
+ * Creates a linkid from a headline
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $title The headline title
+ * @param boolean $create Create a new unique ID?
+ * @return string
+ */
+ public function _headerToLink($title, $create = false) {
+ if($create) {
+ return sectionID($title, $this->headers);
+ } else {
+ $check = false;
+ return sectionID($title, $check);
+ }
+ }
+
+ /**
+ * Removes any Namespace from the given name but keeps
+ * casing and special chars
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $name
+ * @return string
+ */
+ public function _simpleTitle($name) {
+ global $conf;
+
+ //if there is a hash we use the ancor name only
+ @list($name, $hash) = explode('#', $name, 2);
+ if($hash) return $hash;
+
+ if($conf['useslash']) {
+ $name = strtr($name, ';/', ';:');
+ } else {
+ $name = strtr($name, ';', ':');
+ }
+
+ return noNSorNS($name);
+ }
+
+ /**
+ * Resolve an interwikilink
+ *
+ * @param string $shortcut identifier for the interwiki link
+ * @param string $reference fragment that refers the content
+ * @param null|bool $exists reference which returns if an internal page exists
+ * @return string interwikilink
+ */
+ public function _resolveInterWiki(&$shortcut, $reference, &$exists = null) {
+ //get interwiki URL
+ if(isset($this->interwiki[$shortcut])) {
+ $url = $this->interwiki[$shortcut];
+ }elseif(isset($this->interwiki['default'])) {
+ $shortcut = 'default';
+ $url = $this->interwiki[$shortcut];
+ }else{
+ // not parsable interwiki outputs '' to make sure string manipluation works
+ $shortcut = '';
+ $url = '';
+ }
+
+ //split into hash and url part
+ $hash = strrchr($reference, '#');
+ if($hash) {
+ $reference = substr($reference, 0, -strlen($hash));
+ $hash = substr($hash, 1);
+ }
+
+ //replace placeholder
+ if(preg_match('#\{(URL|NAME|SCHEME|HOST|PORT|PATH|QUERY)\}#', $url)) {
+ //use placeholders
+ $url = str_replace('{URL}', rawurlencode($reference), $url);
+ //wiki names will be cleaned next, otherwise urlencode unsafe chars
+ $url = str_replace('{NAME}', ($url[0] === ':') ? $reference :
+ preg_replace_callback('/[[\\\\\]^`{|}#%]/', function($match) {
+ return rawurlencode($match[0]);
+ }, $reference), $url);
+ $parsed = parse_url($reference);
+ if (empty($parsed['scheme'])) $parsed['scheme'] = '';
+ if (empty($parsed['host'])) $parsed['host'] = '';
+ if (empty($parsed['port'])) $parsed['port'] = 80;
+ if (empty($parsed['path'])) $parsed['path'] = '';
+ if (empty($parsed['query'])) $parsed['query'] = '';
+ $url = strtr($url,[
+ '{SCHEME}' => $parsed['scheme'],
+ '{HOST}' => $parsed['host'],
+ '{PORT}' => $parsed['port'],
+ '{PATH}' => $parsed['path'],
+ '{QUERY}' => $parsed['query'] ,
+ ]);
+ } else if($url != '') {
+ // make sure when no url is defined, we keep it null
+ // default
+ $url = $url.rawurlencode($reference);
+ }
+ //handle as wiki links
+ if($url[0] === ':') {
+ $urlparam = null;
+ $id = $url;
+ if (strpos($url, '?') !== false) {
+ list($id, $urlparam) = explode('?', $url, 2);
+ }
+ $url = wl(cleanID($id), $urlparam);
+ $exists = page_exists($id);
+ }
+ if($hash) $url .= '#'.rawurlencode($hash);
+
+ return $url;
+ }
+
+ #endregion
+}
+
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/inc/parser/xhtml.php b/platform/www/inc/parser/xhtml.php
new file mode 100644
index 0000000..a135130
--- /dev/null
+++ b/platform/www/inc/parser/xhtml.php
@@ -0,0 +1,1999 @@
+<?php
+
+use dokuwiki\ChangeLog\MediaChangeLog;
+
+/**
+ * Renderer for XHTML output
+ *
+ * This is DokuWiki's main renderer used to display page content in the wiki
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ */
+class Doku_Renderer_xhtml extends Doku_Renderer {
+ /** @var array store the table of contents */
+ public $toc = array();
+
+ /** @var array A stack of section edit data */
+ protected $sectionedits = array();
+
+ /** @var string|int link pages and media against this revision */
+ public $date_at = '';
+
+ /** @var int last section edit id, used by startSectionEdit */
+ protected $lastsecid = 0;
+
+ /** @var array a list of footnotes, list starts at 1! */
+ protected $footnotes = array();
+
+ /** @var int current section level */
+ protected $lastlevel = 0;
+ /** @var array section node tracker */
+ protected $node = array(0, 0, 0, 0, 0);
+
+ /** @var string temporary $doc store */
+ protected $store = '';
+
+ /** @var array global counter, for table classes etc. */
+ protected $_counter = array(); //
+
+ /** @var int counts the code and file blocks, used to provide download links */
+ protected $_codeblock = 0;
+
+ /** @var array list of allowed URL schemes */
+ protected $schemes = null;
+
+ /**
+ * Register a new edit section range
+ *
+ * @param int $start The byte position for the edit start
+ * @param array $data Associative array with section data:
+ * Key 'name': the section name/title
+ * Key 'target': the target for the section edit,
+ * e.g. 'section' or 'table'
+ * Key 'hid': header id
+ * Key 'codeblockOffset': actual code block index
+ * Key 'start': set in startSectionEdit(),
+ * do not set yourself
+ * Key 'range': calculated from 'start' and
+ * $key in finishSectionEdit(),
+ * do not set yourself
+ * @return string A marker class for the starting HTML element
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+ public function startSectionEdit($start, $data) {
+ if (!is_array($data)) {
+ msg(
+ sprintf(
+ 'startSectionEdit: $data "%s" is NOT an array! One of your plugins needs an update.',
+ hsc((string) $data)
+ ), -1
+ );
+
+ // @deprecated 2018-04-14, backward compatibility
+ $args = func_get_args();
+ $data = array();
+ if(isset($args[1])) $data['target'] = $args[1];
+ if(isset($args[2])) $data['name'] = $args[2];
+ if(isset($args[3])) $data['hid'] = $args[3];
+ }
+ $data['secid'] = ++$this->lastsecid;
+ $data['start'] = $start;
+ $this->sectionedits[] = $data;
+ return 'sectionedit'.$data['secid'];
+ }
+
+ /**
+ * Finish an edit section range
+ *
+ * @param int $end The byte position for the edit end; null for the rest of the page
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+ public function finishSectionEdit($end = null, $hid = null) {
+ $data = array_pop($this->sectionedits);
+ if(!is_null($end) && $end <= $data['start']) {
+ return;
+ }
+ if(!is_null($hid)) {
+ $data['hid'] .= $hid;
+ }
+ $data['range'] = $data['start'].'-'.(is_null($end) ? '' : $end);
+ unset($data['start']);
+ $this->doc .= '<!-- EDIT'.hsc(json_encode ($data)).' -->';
+ }
+
+ /**
+ * Returns the format produced by this renderer.
+ *
+ * @return string always 'xhtml'
+ */
+ public function getFormat() {
+ return 'xhtml';
+ }
+
+ /**
+ * Initialize the document
+ */
+ public function document_start() {
+ //reset some internals
+ $this->toc = array();
+ }
+
+ /**
+ * Finalize the document
+ */
+ public function document_end() {
+ // Finish open section edits.
+ while(count($this->sectionedits) > 0) {
+ if($this->sectionedits[count($this->sectionedits) - 1]['start'] <= 1) {
+ // If there is only one section, do not write a section edit
+ // marker.
+ array_pop($this->sectionedits);
+ } else {
+ $this->finishSectionEdit();
+ }
+ }
+
+ if(count($this->footnotes) > 0) {
+ $this->doc .= '<div class="footnotes">'.DOKU_LF;
+
+ foreach($this->footnotes as $id => $footnote) {
+ // check its not a placeholder that indicates actual footnote text is elsewhere
+ if(substr($footnote, 0, 5) != "@@FNT") {
+
+ // open the footnote and set the anchor and backlink
+ $this->doc .= '<div class="fn">';
+ $this->doc .= '<sup><a href="#fnt__'.$id.'" id="fn__'.$id.'" class="fn_bot">';
+ $this->doc .= $id.')</a></sup> '.DOKU_LF;
+
+ // get any other footnotes that use the same markup
+ $alt = array_keys($this->footnotes, "@@FNT$id");
+
+ if(count($alt)) {
+ foreach($alt as $ref) {
+ // set anchor and backlink for the other footnotes
+ $this->doc .= ', <sup><a href="#fnt__'.($ref).'" id="fn__'.($ref).'" class="fn_bot">';
+ $this->doc .= ($ref).')</a></sup> '.DOKU_LF;
+ }
+ }
+
+ // add footnote markup and close this footnote
+ $this->doc .= '<div class="content">'.$footnote.'</div>';
+ $this->doc .= '</div>'.DOKU_LF;
+ }
+ }
+ $this->doc .= '</div>'.DOKU_LF;
+ }
+
+ // Prepare the TOC
+ global $conf;
+ if(
+ $this->info['toc'] &&
+ is_array($this->toc) &&
+ $conf['tocminheads'] && count($this->toc) >= $conf['tocminheads']
+ ) {
+ global $TOC;
+ $TOC = $this->toc;
+ }
+
+ // make sure there are no empty paragraphs
+ $this->doc = preg_replace('#<p>\s*</p>#', '', $this->doc);
+ }
+
+ /**
+ * Add an item to the TOC
+ *
+ * @param string $id the hash link
+ * @param string $text the text to display
+ * @param int $level the nesting level
+ */
+ public function toc_additem($id, $text, $level) {
+ global $conf;
+
+ //handle TOC
+ if($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']) {
+ $this->toc[] = html_mktocitem($id, $text, $level - $conf['toptoclevel'] + 1);
+ }
+ }
+
+ /**
+ * Render a heading
+ *
+ * @param string $text the text to display
+ * @param int $level header level
+ * @param int $pos byte position in the original source
+ */
+ public function header($text, $level, $pos) {
+ global $conf;
+
+ if(blank($text)) return; //skip empty headlines
+
+ $hid = $this->_headerToLink($text, true);
+
+ //only add items within configured levels
+ $this->toc_additem($hid, $text, $level);
+
+ // adjust $node to reflect hierarchy of levels
+ $this->node[$level - 1]++;
+ if($level < $this->lastlevel) {
+ for($i = 0; $i < $this->lastlevel - $level; $i++) {
+ $this->node[$this->lastlevel - $i - 1] = 0;
+ }
+ }
+ $this->lastlevel = $level;
+
+ if($level <= $conf['maxseclevel'] &&
+ count($this->sectionedits) > 0 &&
+ $this->sectionedits[count($this->sectionedits) - 1]['target'] === 'section'
+ ) {
+ $this->finishSectionEdit($pos - 1);
+ }
+
+ // write the header
+ $this->doc .= DOKU_LF.'<h'.$level;
+ if($level <= $conf['maxseclevel']) {
+ $data = array();
+ $data['target'] = 'section';
+ $data['name'] = $text;
+ $data['hid'] = $hid;
+ $data['codeblockOffset'] = $this->_codeblock;
+ $this->doc .= ' class="'.$this->startSectionEdit($pos, $data).'"';
+ }
+ $this->doc .= ' id="'.$hid.'">';
+ $this->doc .= $this->_xmlEntities($text);
+ $this->doc .= "</h$level>".DOKU_LF;
+ }
+
+ /**
+ * Open a new section
+ *
+ * @param int $level section level (as determined by the previous header)
+ */
+ public function section_open($level) {
+ $this->doc .= '<div class="level'.$level.'">'.DOKU_LF;
+ }
+
+ /**
+ * Close the current section
+ */
+ public function section_close() {
+ $this->doc .= DOKU_LF.'</div>'.DOKU_LF;
+ }
+
+ /**
+ * Render plain text data
+ *
+ * @param $text
+ */
+ public function cdata($text) {
+ $this->doc .= $this->_xmlEntities($text);
+ }
+
+ /**
+ * Open a paragraph
+ */
+ public function p_open() {
+ $this->doc .= DOKU_LF.'<p>'.DOKU_LF;
+ }
+
+ /**
+ * Close a paragraph
+ */
+ public function p_close() {
+ $this->doc .= DOKU_LF.'</p>'.DOKU_LF;
+ }
+
+ /**
+ * Create a line break
+ */
+ public function linebreak() {
+ $this->doc .= '<br/>'.DOKU_LF;
+ }
+
+ /**
+ * Create a horizontal line
+ */
+ public function hr() {
+ $this->doc .= '<hr />'.DOKU_LF;
+ }
+
+ /**
+ * Start strong (bold) formatting
+ */
+ public function strong_open() {
+ $this->doc .= '<strong>';
+ }
+
+ /**
+ * Stop strong (bold) formatting
+ */
+ public function strong_close() {
+ $this->doc .= '</strong>';
+ }
+
+ /**
+ * Start emphasis (italics) formatting
+ */
+ public function emphasis_open() {
+ $this->doc .= '<em>';
+ }
+
+ /**
+ * Stop emphasis (italics) formatting
+ */
+ public function emphasis_close() {
+ $this->doc .= '</em>';
+ }
+
+ /**
+ * Start underline formatting
+ */
+ public function underline_open() {
+ $this->doc .= '<em class="u">';
+ }
+
+ /**
+ * Stop underline formatting
+ */
+ public function underline_close() {
+ $this->doc .= '</em>';
+ }
+
+ /**
+ * Start monospace formatting
+ */
+ public function monospace_open() {
+ $this->doc .= '<code>';
+ }
+
+ /**
+ * Stop monospace formatting
+ */
+ public function monospace_close() {
+ $this->doc .= '</code>';
+ }
+
+ /**
+ * Start a subscript
+ */
+ public function subscript_open() {
+ $this->doc .= '<sub>';
+ }
+
+ /**
+ * Stop a subscript
+ */
+ public function subscript_close() {
+ $this->doc .= '</sub>';
+ }
+
+ /**
+ * Start a superscript
+ */
+ public function superscript_open() {
+ $this->doc .= '<sup>';
+ }
+
+ /**
+ * Stop a superscript
+ */
+ public function superscript_close() {
+ $this->doc .= '</sup>';
+ }
+
+ /**
+ * Start deleted (strike-through) formatting
+ */
+ public function deleted_open() {
+ $this->doc .= '<del>';
+ }
+
+ /**
+ * Stop deleted (strike-through) formatting
+ */
+ public function deleted_close() {
+ $this->doc .= '</del>';
+ }
+
+ /**
+ * Callback for footnote start syntax
+ *
+ * All following content will go to the footnote instead of
+ * the document. To achieve this the previous rendered content
+ * is moved to $store and $doc is cleared
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function footnote_open() {
+
+ // move current content to store and record footnote
+ $this->store = $this->doc;
+ $this->doc = '';
+ }
+
+ /**
+ * Callback for footnote end syntax
+ *
+ * All rendered content is moved to the $footnotes array and the old
+ * content is restored from $store again
+ *
+ * @author Andreas Gohr
+ */
+ public function footnote_close() {
+ /** @var $fnid int takes track of seen footnotes, assures they are unique even across multiple docs FS#2841 */
+ static $fnid = 0;
+ // assign new footnote id (we start at 1)
+ $fnid++;
+
+ // recover footnote into the stack and restore old content
+ $footnote = $this->doc;
+ $this->doc = $this->store;
+ $this->store = '';
+
+ // check to see if this footnote has been seen before
+ $i = array_search($footnote, $this->footnotes);
+
+ if($i === false) {
+ // its a new footnote, add it to the $footnotes array
+ $this->footnotes[$fnid] = $footnote;
+ } else {
+ // seen this one before, save a placeholder
+ $this->footnotes[$fnid] = "@@FNT".($i);
+ }
+
+ // output the footnote reference and link
+ $this->doc .= '<sup><a href="#fn__'.$fnid.'" id="fnt__'.$fnid.'" class="fn_top">'.$fnid.')</a></sup>';
+ }
+
+ /**
+ * Open an unordered list
+ *
+ * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
+ */
+ public function listu_open($classes = null) {
+ $class = '';
+ if($classes !== null) {
+ if(is_array($classes)) $classes = join(' ', $classes);
+ $class = " class=\"$classes\"";
+ }
+ $this->doc .= "<ul$class>".DOKU_LF;
+ }
+
+ /**
+ * Close an unordered list
+ */
+ public function listu_close() {
+ $this->doc .= '</ul>'.DOKU_LF;
+ }
+
+ /**
+ * Open an ordered list
+ *
+ * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
+ */
+ public function listo_open($classes = null) {
+ $class = '';
+ if($classes !== null) {
+ if(is_array($classes)) $classes = join(' ', $classes);
+ $class = " class=\"$classes\"";
+ }
+ $this->doc .= "<ol$class>".DOKU_LF;
+ }
+
+ /**
+ * Close an ordered list
+ */
+ public function listo_close() {
+ $this->doc .= '</ol>'.DOKU_LF;
+ }
+
+ /**
+ * Open a list item
+ *
+ * @param int $level the nesting level
+ * @param bool $node true when a node; false when a leaf
+ */
+ public function listitem_open($level, $node=false) {
+ $branching = $node ? ' node' : '';
+ $this->doc .= '<li class="level'.$level.$branching.'">';
+ }
+
+ /**
+ * Close a list item
+ */
+ public function listitem_close() {
+ $this->doc .= '</li>'.DOKU_LF;
+ }
+
+ /**
+ * Start the content of a list item
+ */
+ public function listcontent_open() {
+ $this->doc .= '<div class="li">';
+ }
+
+ /**
+ * Stop the content of a list item
+ */
+ public function listcontent_close() {
+ $this->doc .= '</div>'.DOKU_LF;
+ }
+
+ /**
+ * Output unformatted $text
+ *
+ * Defaults to $this->cdata()
+ *
+ * @param string $text
+ */
+ public function unformatted($text) {
+ $this->doc .= $this->_xmlEntities($text);
+ }
+
+ /**
+ * Execute PHP code if allowed
+ *
+ * @param string $text PHP code that is either executed or printed
+ * @param string $wrapper html element to wrap result if $conf['phpok'] is okff
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function php($text, $wrapper = 'code') {
+ global $conf;
+
+ if($conf['phpok']) {
+ ob_start();
+ eval($text);
+ $this->doc .= ob_get_contents();
+ ob_end_clean();
+ } else {
+ $this->doc .= p_xhtml_cached_geshi($text, 'php', $wrapper);
+ }
+ }
+
+ /**
+ * Output block level PHP code
+ *
+ * If $conf['phpok'] is true this should evaluate the given code and append the result
+ * to $doc
+ *
+ * @param string $text The PHP code
+ */
+ public function phpblock($text) {
+ $this->php($text, 'pre');
+ }
+
+ /**
+ * Insert HTML if allowed
+ *
+ * @param string $text html text
+ * @param string $wrapper html element to wrap result if $conf['htmlok'] is okff
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function html($text, $wrapper = 'code') {
+ global $conf;
+
+ if($conf['htmlok']) {
+ $this->doc .= $text;
+ } else {
+ $this->doc .= p_xhtml_cached_geshi($text, 'html4strict', $wrapper);
+ }
+ }
+
+ /**
+ * Output raw block-level HTML
+ *
+ * If $conf['htmlok'] is true this should add the code as is to $doc
+ *
+ * @param string $text The HTML
+ */
+ public function htmlblock($text) {
+ $this->html($text, 'pre');
+ }
+
+ /**
+ * Start a block quote
+ */
+ public function quote_open() {
+ $this->doc .= '<blockquote><div class="no">'.DOKU_LF;
+ }
+
+ /**
+ * Stop a block quote
+ */
+ public function quote_close() {
+ $this->doc .= '</div></blockquote>'.DOKU_LF;
+ }
+
+ /**
+ * Output preformatted text
+ *
+ * @param string $text
+ */
+ public function preformatted($text) {
+ $this->doc .= '<pre class="code">'.trim($this->_xmlEntities($text), "\n\r").'</pre>'.DOKU_LF;
+ }
+
+ /**
+ * Display text as file content, optionally syntax highlighted
+ *
+ * @param string $text text to show
+ * @param string $language programming language to use for syntax highlighting
+ * @param string $filename file path label
+ * @param array $options assoziative array with additional geshi options
+ */
+ public function file($text, $language = null, $filename = null, $options=null) {
+ $this->_highlight('file', $text, $language, $filename, $options);
+ }
+
+ /**
+ * Display text as code content, optionally syntax highlighted
+ *
+ * @param string $text text to show
+ * @param string $language programming language to use for syntax highlighting
+ * @param string $filename file path label
+ * @param array $options assoziative array with additional geshi options
+ */
+ public function code($text, $language = null, $filename = null, $options=null) {
+ $this->_highlight('code', $text, $language, $filename, $options);
+ }
+
+ /**
+ * Use GeSHi to highlight language syntax in code and file blocks
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $type code|file
+ * @param string $text text to show
+ * @param string $language programming language to use for syntax highlighting
+ * @param string $filename file path label
+ * @param array $options assoziative array with additional geshi options
+ */
+ public function _highlight($type, $text, $language = null, $filename = null, $options = null) {
+ global $ID;
+ global $lang;
+ global $INPUT;
+
+ $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language);
+
+ $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language);
+
+ if($filename) {
+ // add icon
+ list($ext) = mimetype($filename, false);
+ $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
+ $class = 'mediafile mf_'.$class;
+
+ $offset = 0;
+ if ($INPUT->has('codeblockOffset')) {
+ $offset = $INPUT->str('codeblockOffset');
+ }
+ $this->doc .= '<dl class="'.$type.'">'.DOKU_LF;
+ $this->doc .= '<dt><a href="' .
+ exportlink(
+ $ID,
+ 'code',
+ array('codeblock' => $offset + $this->_codeblock)
+ ) . '" title="' . $lang['download'] . '" class="' . $class . '">';
+ $this->doc .= hsc($filename);
+ $this->doc .= '</a></dt>'.DOKU_LF.'<dd>';
+ }
+
+ if($text[0] == "\n") {
+ $text = substr($text, 1);
+ }
+ if(substr($text, -1) == "\n") {
+ $text = substr($text, 0, -1);
+ }
+
+ if(empty($language)) { // empty is faster than is_null and can prevent '' string
+ $this->doc .= '<pre class="'.$type.'">'.$this->_xmlEntities($text).'</pre>'.DOKU_LF;
+ } else {
+ $class = 'code'; //we always need the code class to make the syntax highlighting apply
+ if($type != 'code') $class .= ' '.$type;
+
+ $this->doc .= "<pre class=\"$class $language\">" .
+ p_xhtml_cached_geshi($text, $language, '', $options) .
+ '</pre>' . DOKU_LF;
+ }
+
+ if($filename) {
+ $this->doc .= '</dd></dl>'.DOKU_LF;
+ }
+
+ $this->_codeblock++;
+ }
+
+ /**
+ * Format an acronym
+ *
+ * Uses $this->acronyms
+ *
+ * @param string $acronym
+ */
+ public function acronym($acronym) {
+
+ if(array_key_exists($acronym, $this->acronyms)) {
+
+ $title = $this->_xmlEntities($this->acronyms[$acronym]);
+
+ $this->doc .= '<abbr title="'.$title
+ .'">'.$this->_xmlEntities($acronym).'</abbr>';
+
+ } else {
+ $this->doc .= $this->_xmlEntities($acronym);
+ }
+ }
+
+ /**
+ * Format a smiley
+ *
+ * Uses $this->smiley
+ *
+ * @param string $smiley
+ */
+ public function smiley($smiley) {
+ if(array_key_exists($smiley, $this->smileys)) {
+ $this->doc .= '<img src="'.DOKU_BASE.'lib/images/smileys/'.$this->smileys[$smiley].
+ '" class="icon" alt="'.
+ $this->_xmlEntities($smiley).'" />';
+ } else {
+ $this->doc .= $this->_xmlEntities($smiley);
+ }
+ }
+
+ /**
+ * Format an entity
+ *
+ * Entities are basically small text replacements
+ *
+ * Uses $this->entities
+ *
+ * @param string $entity
+ */
+ public function entity($entity) {
+ if(array_key_exists($entity, $this->entities)) {
+ $this->doc .= $this->entities[$entity];
+ } else {
+ $this->doc .= $this->_xmlEntities($entity);
+ }
+ }
+
+ /**
+ * Typographically format a multiply sign
+ *
+ * Example: ($x=640, $y=480) should result in "640×480"
+ *
+ * @param string|int $x first value
+ * @param string|int $y second value
+ */
+ public function multiplyentity($x, $y) {
+ $this->doc .= "$x&times;$y";
+ }
+
+ /**
+ * Render an opening single quote char (language specific)
+ */
+ public function singlequoteopening() {
+ global $lang;
+ $this->doc .= $lang['singlequoteopening'];
+ }
+
+ /**
+ * Render a closing single quote char (language specific)
+ */
+ public function singlequoteclosing() {
+ global $lang;
+ $this->doc .= $lang['singlequoteclosing'];
+ }
+
+ /**
+ * Render an apostrophe char (language specific)
+ */
+ public function apostrophe() {
+ global $lang;
+ $this->doc .= $lang['apostrophe'];
+ }
+
+ /**
+ * Render an opening double quote char (language specific)
+ */
+ public function doublequoteopening() {
+ global $lang;
+ $this->doc .= $lang['doublequoteopening'];
+ }
+
+ /**
+ * Render an closinging double quote char (language specific)
+ */
+ public function doublequoteclosing() {
+ global $lang;
+ $this->doc .= $lang['doublequoteclosing'];
+ }
+
+ /**
+ * Render a CamelCase link
+ *
+ * @param string $link The link name
+ * @param bool $returnonly whether to return html or write to doc attribute
+ * @return void|string writes to doc attribute or returns html depends on $returnonly
+ *
+ * @see http://en.wikipedia.org/wiki/CamelCase
+ */
+ public function camelcaselink($link, $returnonly = false) {
+ if($returnonly) {
+ return $this->internallink($link, $link, null, true);
+ } else {
+ $this->internallink($link, $link);
+ }
+ }
+
+ /**
+ * Render a page local link
+ *
+ * @param string $hash hash link identifier
+ * @param string $name name for the link
+ * @param bool $returnonly whether to return html or write to doc attribute
+ * @return void|string writes to doc attribute or returns html depends on $returnonly
+ */
+ public function locallink($hash, $name = null, $returnonly = false) {
+ global $ID;
+ $name = $this->_getLinkTitle($name, $hash, $isImage);
+ $hash = $this->_headerToLink($hash);
+ $title = $ID.' ↵';
+
+ $doc = '<a href="#'.$hash.'" title="'.$title.'" class="wikilink1">';
+ $doc .= $name;
+ $doc .= '</a>';
+
+ if($returnonly) {
+ return $doc;
+ } else {
+ $this->doc .= $doc;
+ }
+ }
+
+ /**
+ * Render an internal Wiki Link
+ *
+ * $search,$returnonly & $linktype are not for the renderer but are used
+ * elsewhere - no need to implement them in other renderers
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $id pageid
+ * @param string|null $name link name
+ * @param string|null $search adds search url param
+ * @param bool $returnonly whether to return html or write to doc attribute
+ * @param string $linktype type to set use of headings
+ * @return void|string writes to doc attribute or returns html depends on $returnonly
+ */
+ public function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content') {
+ global $conf;
+ global $ID;
+ global $INFO;
+
+ $params = '';
+ $parts = explode('?', $id, 2);
+ if(count($parts) === 2) {
+ $id = $parts[0];
+ $params = $parts[1];
+ }
+
+ // For empty $id we need to know the current $ID
+ // We need this check because _simpleTitle needs
+ // correct $id and resolve_pageid() use cleanID($id)
+ // (some things could be lost)
+ if($id === '') {
+ $id = $ID;
+ }
+
+ // default name is based on $id as given
+ $default = $this->_simpleTitle($id);
+
+ // now first resolve and clean up the $id
+ resolve_pageid(getNS($ID), $id, $exists, $this->date_at, true);
+
+ $link = array();
+ $name = $this->_getLinkTitle($name, $default, $isImage, $id, $linktype);
+ if(!$isImage) {
+ if($exists) {
+ $class = 'wikilink1';
+ } else {
+ $class = 'wikilink2';
+ $link['rel'] = 'nofollow';
+ }
+ } else {
+ $class = 'media';
+ }
+
+ //keep hash anchor
+ @list($id, $hash) = explode('#', $id, 2);
+ if(!empty($hash)) $hash = $this->_headerToLink($hash);
+
+ //prepare for formating
+ $link['target'] = $conf['target']['wiki'];
+ $link['style'] = '';
+ $link['pre'] = '';
+ $link['suf'] = '';
+ $link['more'] = 'data-wiki-id="'.$id.'"'; // id is already cleaned
+ $link['class'] = $class;
+ if($this->date_at) {
+ $params = $params.'&at='.rawurlencode($this->date_at);
+ }
+ $link['url'] = wl($id, $params);
+ $link['name'] = $name;
+ $link['title'] = $id;
+ //add search string
+ if($search) {
+ ($conf['userewrite']) ? $link['url'] .= '?' : $link['url'] .= '&amp;';
+ if(is_array($search)) {
+ $search = array_map('rawurlencode', $search);
+ $link['url'] .= 's[]='.join('&amp;s[]=', $search);
+ } else {
+ $link['url'] .= 's='.rawurlencode($search);
+ }
+ }
+
+ //keep hash
+ if($hash) $link['url'] .= '#'.$hash;
+
+ //output formatted
+ if($returnonly) {
+ return $this->_formatLink($link);
+ } else {
+ $this->doc .= $this->_formatLink($link);
+ }
+ }
+
+ /**
+ * Render an external link
+ *
+ * @param string $url full URL with scheme
+ * @param string|array $name name for the link, array for media file
+ * @param bool $returnonly whether to return html or write to doc attribute
+ * @return void|string writes to doc attribute or returns html depends on $returnonly
+ */
+ public function externallink($url, $name = null, $returnonly = false) {
+ global $conf;
+
+ $name = $this->_getLinkTitle($name, $url, $isImage);
+
+ // url might be an attack vector, only allow registered protocols
+ if(is_null($this->schemes)) $this->schemes = getSchemes();
+ list($scheme) = explode('://', $url);
+ $scheme = strtolower($scheme);
+ if(!in_array($scheme, $this->schemes)) $url = '';
+
+ // is there still an URL?
+ if(!$url) {
+ if($returnonly) {
+ return $name;
+ } else {
+ $this->doc .= $name;
+ }
+ return;
+ }
+
+ // set class
+ if(!$isImage) {
+ $class = 'urlextern';
+ } else {
+ $class = 'media';
+ }
+
+ //prepare for formating
+ $link = array();
+ $link['target'] = $conf['target']['extern'];
+ $link['style'] = '';
+ $link['pre'] = '';
+ $link['suf'] = '';
+ $link['more'] = '';
+ $link['class'] = $class;
+ $link['url'] = $url;
+ $link['rel'] = '';
+
+ $link['name'] = $name;
+ $link['title'] = $this->_xmlEntities($url);
+ if($conf['relnofollow']) $link['rel'] .= ' ugc nofollow';
+ if($conf['target']['extern']) $link['rel'] .= ' noopener';
+
+ //output formatted
+ if($returnonly) {
+ return $this->_formatLink($link);
+ } else {
+ $this->doc .= $this->_formatLink($link);
+ }
+ }
+
+ /**
+ * Render an interwiki link
+ *
+ * You may want to use $this->_resolveInterWiki() here
+ *
+ * @param string $match original link - probably not much use
+ * @param string|array $name name for the link, array for media file
+ * @param string $wikiName indentifier (shortcut) for the remote wiki
+ * @param string $wikiUri the fragment parsed from the original link
+ * @param bool $returnonly whether to return html or write to doc attribute
+ * @return void|string writes to doc attribute or returns html depends on $returnonly
+ */
+ public function interwikilink($match, $name, $wikiName, $wikiUri, $returnonly = false) {
+ global $conf;
+
+ $link = array();
+ $link['target'] = $conf['target']['interwiki'];
+ $link['pre'] = '';
+ $link['suf'] = '';
+ $link['more'] = '';
+ $link['name'] = $this->_getLinkTitle($name, $wikiUri, $isImage);
+ $link['rel'] = '';
+
+ //get interwiki URL
+ $exists = null;
+ $url = $this->_resolveInterWiki($wikiName, $wikiUri, $exists);
+
+ if(!$isImage) {
+ $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $wikiName);
+ $link['class'] = "interwiki iw_$class";
+ } else {
+ $link['class'] = 'media';
+ }
+
+ //do we stay at the same server? Use local target
+ if(strpos($url, DOKU_URL) === 0 OR strpos($url, DOKU_BASE) === 0) {
+ $link['target'] = $conf['target']['wiki'];
+ }
+ if($exists !== null && !$isImage) {
+ if($exists) {
+ $link['class'] .= ' wikilink1';
+ } else {
+ $link['class'] .= ' wikilink2';
+ $link['rel'] .= ' nofollow';
+ }
+ }
+ if($conf['target']['interwiki']) $link['rel'] .= ' noopener';
+
+ $link['url'] = $url;
+ $link['title'] = htmlspecialchars($link['url']);
+
+ // output formatted
+ if($returnonly) {
+ if($url == '') return $link['name'];
+ return $this->_formatLink($link);
+ } else {
+ if($url == '') $this->doc .= $link['name'];
+ else $this->doc .= $this->_formatLink($link);
+ }
+ }
+
+ /**
+ * Link to windows share
+ *
+ * @param string $url the link
+ * @param string|array $name name for the link, array for media file
+ * @param bool $returnonly whether to return html or write to doc attribute
+ * @return void|string writes to doc attribute or returns html depends on $returnonly
+ */
+ public function windowssharelink($url, $name = null, $returnonly = false) {
+ global $conf;
+
+ //simple setup
+ $link = array();
+ $link['target'] = $conf['target']['windows'];
+ $link['pre'] = '';
+ $link['suf'] = '';
+ $link['style'] = '';
+
+ $link['name'] = $this->_getLinkTitle($name, $url, $isImage);
+ if(!$isImage) {
+ $link['class'] = 'windows';
+ } else {
+ $link['class'] = 'media';
+ }
+
+ $link['title'] = $this->_xmlEntities($url);
+ $url = str_replace('\\', '/', $url);
+ $url = 'file:///'.$url;
+ $link['url'] = $url;
+
+ //output formatted
+ if($returnonly) {
+ return $this->_formatLink($link);
+ } else {
+ $this->doc .= $this->_formatLink($link);
+ }
+ }
+
+ /**
+ * Render a linked E-Mail Address
+ *
+ * Honors $conf['mailguard'] setting
+ *
+ * @param string $address Email-Address
+ * @param string|array $name name for the link, array for media file
+ * @param bool $returnonly whether to return html or write to doc attribute
+ * @return void|string writes to doc attribute or returns html depends on $returnonly
+ */
+ public function emaillink($address, $name = null, $returnonly = false) {
+ global $conf;
+ //simple setup
+ $link = array();
+ $link['target'] = '';
+ $link['pre'] = '';
+ $link['suf'] = '';
+ $link['style'] = '';
+ $link['more'] = '';
+
+ $name = $this->_getLinkTitle($name, '', $isImage);
+ if(!$isImage) {
+ $link['class'] = 'mail';
+ } else {
+ $link['class'] = 'media';
+ }
+
+ $address = $this->_xmlEntities($address);
+ $address = obfuscate($address);
+ $title = $address;
+
+ if(empty($name)) {
+ $name = $address;
+ }
+
+ if($conf['mailguard'] == 'visible') $address = rawurlencode($address);
+
+ $link['url'] = 'mailto:'.$address;
+ $link['name'] = $name;
+ $link['title'] = $title;
+
+ //output formatted
+ if($returnonly) {
+ return $this->_formatLink($link);
+ } else {
+ $this->doc .= $this->_formatLink($link);
+ }
+ }
+
+ /**
+ * Render an internal media file
+ *
+ * @param string $src media ID
+ * @param string $title descriptive text
+ * @param string $align left|center|right
+ * @param int $width width of media in pixel
+ * @param int $height height of media in pixel
+ * @param string $cache cache|recache|nocache
+ * @param string $linking linkonly|detail|nolink
+ * @param bool $return return HTML instead of adding to $doc
+ * @return void|string writes to doc attribute or returns html depends on $return
+ */
+ public function internalmedia($src, $title = null, $align = null, $width = null,
+ $height = null, $cache = null, $linking = null, $return = false) {
+ global $ID;
+ if (strpos($src, '#') !== false) {
+ list($src, $hash) = explode('#', $src, 2);
+ }
+ resolve_mediaid(getNS($ID), $src, $exists, $this->date_at, true);
+
+ $noLink = false;
+ $render = ($linking == 'linkonly') ? false : true;
+ $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
+
+ list($ext, $mime) = mimetype($src, false);
+ if(substr($mime, 0, 5) == 'image' && $render) {
+ $link['url'] = ml(
+ $src,
+ array(
+ 'id' => $ID,
+ 'cache' => $cache,
+ 'rev' => $this->_getLastMediaRevisionAt($src)
+ ),
+ ($linking == 'direct')
+ );
+ } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
+ // don't link movies
+ $noLink = true;
+ } else {
+ // add file icons
+ $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
+ $link['class'] .= ' mediafile mf_'.$class;
+ $link['url'] = ml(
+ $src,
+ array(
+ 'id' => $ID,
+ 'cache' => $cache,
+ 'rev' => $this->_getLastMediaRevisionAt($src)
+ ),
+ true
+ );
+ if($exists) $link['title'] .= ' ('.filesize_h(filesize(mediaFN($src))).')';
+ }
+
+ if (!empty($hash)) $link['url'] .= '#'.$hash;
+
+ //markup non existing files
+ if(!$exists) {
+ $link['class'] .= ' wikilink2';
+ }
+
+ //output formatted
+ if($return) {
+ if($linking == 'nolink' || $noLink) return $link['name'];
+ else return $this->_formatLink($link);
+ } else {
+ if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
+ else $this->doc .= $this->_formatLink($link);
+ }
+ }
+
+ /**
+ * Render an external media file
+ *
+ * @param string $src full media URL
+ * @param string $title descriptive text
+ * @param string $align left|center|right
+ * @param int $width width of media in pixel
+ * @param int $height height of media in pixel
+ * @param string $cache cache|recache|nocache
+ * @param string $linking linkonly|detail|nolink
+ * @param bool $return return HTML instead of adding to $doc
+ * @return void|string writes to doc attribute or returns html depends on $return
+ */
+ public function externalmedia($src, $title = null, $align = null, $width = null,
+ $height = null, $cache = null, $linking = null, $return = false) {
+ if(link_isinterwiki($src)){
+ list($shortcut, $reference) = explode('>', $src, 2);
+ $exists = null;
+ $src = $this->_resolveInterWiki($shortcut, $reference, $exists);
+ if($src == '' && empty($title)){
+ // make sure at least something will be shown in this case
+ $title = $reference;
+ }
+ }
+ list($src, $hash) = explode('#', $src, 2);
+ $noLink = false;
+ if($src == '') {
+ // only output plaintext without link if there is no src
+ $noLink = true;
+ }
+ $render = ($linking == 'linkonly') ? false : true;
+ $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
+
+ $link['url'] = ml($src, array('cache' => $cache));
+
+ list($ext, $mime) = mimetype($src, false);
+ if(substr($mime, 0, 5) == 'image' && $render) {
+ // link only jpeg images
+ // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true;
+ } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
+ // don't link movies
+ $noLink = true;
+ } else {
+ // add file icons
+ $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
+ $link['class'] .= ' mediafile mf_'.$class;
+ }
+
+ if($hash) $link['url'] .= '#'.$hash;
+
+ //output formatted
+ if($return) {
+ if($linking == 'nolink' || $noLink) return $link['name'];
+ else return $this->_formatLink($link);
+ } else {
+ if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
+ else $this->doc .= $this->_formatLink($link);
+ }
+ }
+
+ /**
+ * Renders an RSS feed
+ *
+ * @param string $url URL of the feed
+ * @param array $params Finetuning of the output
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function rss($url, $params) {
+ global $lang;
+ global $conf;
+
+ require_once(DOKU_INC.'inc/FeedParser.php');
+ $feed = new FeedParser();
+ $feed->set_feed_url($url);
+
+ //disable warning while fetching
+ if(!defined('DOKU_E_LEVEL')) {
+ $elvl = error_reporting(E_ERROR);
+ }
+ $rc = $feed->init();
+ if(isset($elvl)) {
+ error_reporting($elvl);
+ }
+
+ if($params['nosort']) $feed->enable_order_by_date(false);
+
+ //decide on start and end
+ if($params['reverse']) {
+ $mod = -1;
+ $start = $feed->get_item_quantity() - 1;
+ $end = $start - ($params['max']);
+ $end = ($end < -1) ? -1 : $end;
+ } else {
+ $mod = 1;
+ $start = 0;
+ $end = $feed->get_item_quantity();
+ $end = ($end > $params['max']) ? $params['max'] : $end;
+ }
+
+ $this->doc .= '<ul class="rss">';
+ if($rc) {
+ for($x = $start; $x != $end; $x += $mod) {
+ $item = $feed->get_item($x);
+ $this->doc .= '<li><div class="li">';
+ // support feeds without links
+ $lnkurl = $item->get_permalink();
+ if($lnkurl) {
+ // title is escaped by SimplePie, we unescape here because it
+ // is escaped again in externallink() FS#1705
+ $this->externallink(
+ $item->get_permalink(),
+ html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8')
+ );
+ } else {
+ $this->doc .= ' '.$item->get_title();
+ }
+ if($params['author']) {
+ $author = $item->get_author(0);
+ if($author) {
+ $name = $author->get_name();
+ if(!$name) $name = $author->get_email();
+ if($name) $this->doc .= ' '.$lang['by'].' '.hsc($name);
+ }
+ }
+ if($params['date']) {
+ $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')';
+ }
+ if($params['details']) {
+ $this->doc .= '<div class="detail">';
+ if($conf['htmlok']) {
+ $this->doc .= $item->get_description();
+ } else {
+ $this->doc .= strip_tags($item->get_description());
+ }
+ $this->doc .= '</div>';
+ }
+
+ $this->doc .= '</div></li>';
+ }
+ } else {
+ $this->doc .= '<li><div class="li">';
+ $this->doc .= '<em>'.$lang['rssfailed'].'</em>';
+ $this->externallink($url);
+ if($conf['allowdebug']) {
+ $this->doc .= '<!--'.hsc($feed->error).'-->';
+ }
+ $this->doc .= '</div></li>';
+ }
+ $this->doc .= '</ul>';
+ }
+
+ /**
+ * Start a table
+ *
+ * @param int $maxcols maximum number of columns
+ * @param int $numrows NOT IMPLEMENTED
+ * @param int $pos byte position in the original source
+ * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
+ */
+ public function table_open($maxcols = null, $numrows = null, $pos = null, $classes = null) {
+ // initialize the row counter used for classes
+ $this->_counter['row_counter'] = 0;
+ $class = 'table';
+ if($classes !== null) {
+ if(is_array($classes)) $classes = join(' ', $classes);
+ $class .= ' ' . $classes;
+ }
+ if($pos !== null) {
+ $hid = $this->_headerToLink($class, true);
+ $data = array();
+ $data['target'] = 'table';
+ $data['name'] = '';
+ $data['hid'] = $hid;
+ $class .= ' '.$this->startSectionEdit($pos, $data);
+ }
+ $this->doc .= '<div class="'.$class.'"><table class="inline">'.
+ DOKU_LF;
+ }
+
+ /**
+ * Close a table
+ *
+ * @param int $pos byte position in the original source
+ */
+ public function table_close($pos = null) {
+ $this->doc .= '</table></div>'.DOKU_LF;
+ if($pos !== null) {
+ $this->finishSectionEdit($pos);
+ }
+ }
+
+ /**
+ * Open a table header
+ */
+ public function tablethead_open() {
+ $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF;
+ }
+
+ /**
+ * Close a table header
+ */
+ public function tablethead_close() {
+ $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF;
+ }
+
+ /**
+ * Open a table body
+ */
+ public function tabletbody_open() {
+ $this->doc .= DOKU_TAB.'<tbody>'.DOKU_LF;
+ }
+
+ /**
+ * Close a table body
+ */
+ public function tabletbody_close() {
+ $this->doc .= DOKU_TAB.'</tbody>'.DOKU_LF;
+ }
+
+ /**
+ * Open a table footer
+ */
+ public function tabletfoot_open() {
+ $this->doc .= DOKU_TAB.'<tfoot>'.DOKU_LF;
+ }
+
+ /**
+ * Close a table footer
+ */
+ public function tabletfoot_close() {
+ $this->doc .= DOKU_TAB.'</tfoot>'.DOKU_LF;
+ }
+
+ /**
+ * Open a table row
+ *
+ * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
+ */
+ public function tablerow_open($classes = null) {
+ // initialize the cell counter used for classes
+ $this->_counter['cell_counter'] = 0;
+ $class = 'row'.$this->_counter['row_counter']++;
+ if($classes !== null) {
+ if(is_array($classes)) $classes = join(' ', $classes);
+ $class .= ' ' . $classes;
+ }
+ $this->doc .= DOKU_TAB.'<tr class="'.$class.'">'.DOKU_LF.DOKU_TAB.DOKU_TAB;
+ }
+
+ /**
+ * Close a table row
+ */
+ public function tablerow_close() {
+ $this->doc .= DOKU_LF.DOKU_TAB.'</tr>'.DOKU_LF;
+ }
+
+ /**
+ * Open a table header cell
+ *
+ * @param int $colspan
+ * @param string $align left|center|right
+ * @param int $rowspan
+ * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
+ */
+ public function tableheader_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
+ $class = 'class="col'.$this->_counter['cell_counter']++;
+ if(!is_null($align)) {
+ $class .= ' '.$align.'align';
+ }
+ if($classes !== null) {
+ if(is_array($classes)) $classes = join(' ', $classes);
+ $class .= ' ' . $classes;
+ }
+ $class .= '"';
+ $this->doc .= '<th '.$class;
+ if($colspan > 1) {
+ $this->_counter['cell_counter'] += $colspan - 1;
+ $this->doc .= ' colspan="'.$colspan.'"';
+ }
+ if($rowspan > 1) {
+ $this->doc .= ' rowspan="'.$rowspan.'"';
+ }
+ $this->doc .= '>';
+ }
+
+ /**
+ * Close a table header cell
+ */
+ public function tableheader_close() {
+ $this->doc .= '</th>';
+ }
+
+ /**
+ * Open a table cell
+ *
+ * @param int $colspan
+ * @param string $align left|center|right
+ * @param int $rowspan
+ * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
+ */
+ public function tablecell_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
+ $class = 'class="col'.$this->_counter['cell_counter']++;
+ if(!is_null($align)) {
+ $class .= ' '.$align.'align';
+ }
+ if($classes !== null) {
+ if(is_array($classes)) $classes = join(' ', $classes);
+ $class .= ' ' . $classes;
+ }
+ $class .= '"';
+ $this->doc .= '<td '.$class;
+ if($colspan > 1) {
+ $this->_counter['cell_counter'] += $colspan - 1;
+ $this->doc .= ' colspan="'.$colspan.'"';
+ }
+ if($rowspan > 1) {
+ $this->doc .= ' rowspan="'.$rowspan.'"';
+ }
+ $this->doc .= '>';
+ }
+
+ /**
+ * Close a table cell
+ */
+ public function tablecell_close() {
+ $this->doc .= '</td>';
+ }
+
+ /**
+ * Returns the current header level.
+ * (required e.g. by the filelist plugin)
+ *
+ * @return int The current header level
+ */
+ public function getLastlevel() {
+ return $this->lastlevel;
+ }
+
+ #region Utility functions
+
+ /**
+ * Build a link
+ *
+ * Assembles all parts defined in $link returns HTML for the link
+ *
+ * @param array $link attributes of a link
+ * @return string
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function _formatLink($link) {
+ //make sure the url is XHTML compliant (skip mailto)
+ if(substr($link['url'], 0, 7) != 'mailto:') {
+ $link['url'] = str_replace('&', '&amp;', $link['url']);
+ $link['url'] = str_replace('&amp;amp;', '&amp;', $link['url']);
+ }
+ //remove double encodings in titles
+ $link['title'] = str_replace('&amp;amp;', '&amp;', $link['title']);
+
+ // be sure there are no bad chars in url or title
+ // (we can't do this for name because it can contain an img tag)
+ $link['url'] = strtr($link['url'], array('>' => '%3E', '<' => '%3C', '"' => '%22'));
+ $link['title'] = strtr($link['title'], array('>' => '&gt;', '<' => '&lt;', '"' => '&quot;'));
+
+ $ret = '';
+ $ret .= $link['pre'];
+ $ret .= '<a href="'.$link['url'].'"';
+ if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"';
+ if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"';
+ if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"';
+ if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"';
+ if(!empty($link['rel'])) $ret .= ' rel="'.trim($link['rel']).'"';
+ if(!empty($link['more'])) $ret .= ' '.$link['more'];
+ $ret .= '>';
+ $ret .= $link['name'];
+ $ret .= '</a>';
+ $ret .= $link['suf'];
+ return $ret;
+ }
+
+ /**
+ * Renders internal and external media
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $src media ID
+ * @param string $title descriptive text
+ * @param string $align left|center|right
+ * @param int $width width of media in pixel
+ * @param int $height height of media in pixel
+ * @param string $cache cache|recache|nocache
+ * @param bool $render should the media be embedded inline or just linked
+ * @return string
+ */
+ public function _media($src, $title = null, $align = null, $width = null,
+ $height = null, $cache = null, $render = true) {
+
+ $ret = '';
+
+ list($ext, $mime) = mimetype($src);
+ if(substr($mime, 0, 5) == 'image') {
+ // first get the $title
+ if(!is_null($title)) {
+ $title = $this->_xmlEntities($title);
+ } elseif($ext == 'jpg' || $ext == 'jpeg') {
+ //try to use the caption from IPTC/EXIF
+ require_once(DOKU_INC.'inc/JpegMeta.php');
+ $jpeg = new JpegMeta(mediaFN($src));
+ if($jpeg !== false) $cap = $jpeg->getTitle();
+ if(!empty($cap)) {
+ $title = $this->_xmlEntities($cap);
+ }
+ }
+ if(!$render) {
+ // if the picture is not supposed to be rendered
+ // return the title of the picture
+ if($title === null || $title === "") {
+ // just show the sourcename
+ $title = $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($src)));
+ }
+ return $title;
+ }
+ //add image tag
+ $ret .= '<img src="' . ml(
+ $src,
+ array(
+ 'w' => $width, 'h' => $height,
+ 'cache' => $cache,
+ 'rev' => $this->_getLastMediaRevisionAt($src)
+ )
+ ) . '"';
+ $ret .= ' class="media'.$align.'"';
+
+ if($title) {
+ $ret .= ' title="'.$title.'"';
+ $ret .= ' alt="'.$title.'"';
+ } else {
+ $ret .= ' alt=""';
+ }
+
+ if(!is_null($width))
+ $ret .= ' width="'.$this->_xmlEntities($width).'"';
+
+ if(!is_null($height))
+ $ret .= ' height="'.$this->_xmlEntities($height).'"';
+
+ $ret .= ' />';
+
+ } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) {
+ // first get the $title
+ $title = !is_null($title) ? $title : false;
+ if(!$render) {
+ // if the file is not supposed to be rendered
+ // return the title of the file (just the sourcename if there is no title)
+ return $this->_xmlEntities($title ? $title : \dokuwiki\Utf8\PhpString::basename(noNS($src)));
+ }
+
+ $att = array();
+ $att['class'] = "media$align";
+ if($title) {
+ $att['title'] = $title;
+ }
+
+ if(media_supportedav($mime, 'video')) {
+ //add video
+ $ret .= $this->_video($src, $width, $height, $att);
+ }
+ if(media_supportedav($mime, 'audio')) {
+ //add audio
+ $ret .= $this->_audio($src, $att);
+ }
+
+ } elseif($mime == 'application/x-shockwave-flash') {
+ if(!$render) {
+ // if the flash is not supposed to be rendered
+ // return the title of the flash
+ if(!$title) {
+ // just show the sourcename
+ $title = \dokuwiki\Utf8\PhpString::basename(noNS($src));
+ }
+ return $this->_xmlEntities($title);
+ }
+
+ $att = array();
+ $att['class'] = "media$align";
+ if($align == 'right') $att['align'] = 'right';
+ if($align == 'left') $att['align'] = 'left';
+ $ret .= html_flashobject(
+ ml($src, array('cache' => $cache), true, '&'), $width, $height,
+ array('quality' => 'high'),
+ null,
+ $att,
+ $this->_xmlEntities($title)
+ );
+ } elseif($title) {
+ // well at least we have a title to display
+ $ret .= $this->_xmlEntities($title);
+ } else {
+ // just show the sourcename
+ $ret .= $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($src)));
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Escape string for output
+ *
+ * @param $string
+ * @return string
+ */
+ public function _xmlEntities($string) {
+ return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
+ }
+
+
+
+ /**
+ * Construct a title and handle images in titles
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @param string|array $title either string title or media array
+ * @param string $default default title if nothing else is found
+ * @param bool $isImage will be set to true if it's a media file
+ * @param null|string $id linked page id (used to extract title from first heading)
+ * @param string $linktype content|navigation
+ * @return string HTML of the title, might be full image tag or just escaped text
+ */
+ public function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') {
+ $isImage = false;
+ if(is_array($title)) {
+ $isImage = true;
+ return $this->_imageTitle($title);
+ } elseif(is_null($title) || trim($title) == '') {
+ if(useHeading($linktype) && $id) {
+ $heading = p_get_first_heading($id);
+ if(!blank($heading)) {
+ return $this->_xmlEntities($heading);
+ }
+ }
+ return $this->_xmlEntities($default);
+ } else {
+ return $this->_xmlEntities($title);
+ }
+ }
+
+ /**
+ * Returns HTML code for images used in link titles
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param array $img
+ * @return string HTML img tag or similar
+ */
+ public function _imageTitle($img) {
+ global $ID;
+
+ // some fixes on $img['src']
+ // see internalmedia() and externalmedia()
+ list($img['src']) = explode('#', $img['src'], 2);
+ if($img['type'] == 'internalmedia') {
+ resolve_mediaid(getNS($ID), $img['src'], $exists ,$this->date_at, true);
+ }
+
+ return $this->_media(
+ $img['src'],
+ $img['title'],
+ $img['align'],
+ $img['width'],
+ $img['height'],
+ $img['cache']
+ );
+ }
+
+ /**
+ * helperfunction to return a basic link to a media
+ *
+ * used in internalmedia() and externalmedia()
+ *
+ * @author Pierre Spring <pierre.spring@liip.ch>
+ * @param string $src media ID
+ * @param string $title descriptive text
+ * @param string $align left|center|right
+ * @param int $width width of media in pixel
+ * @param int $height height of media in pixel
+ * @param string $cache cache|recache|nocache
+ * @param bool $render should the media be embedded inline or just linked
+ * @return array associative array with link config
+ */
+ public function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
+ global $conf;
+
+ $link = array();
+ $link['class'] = 'media';
+ $link['style'] = '';
+ $link['pre'] = '';
+ $link['suf'] = '';
+ $link['more'] = '';
+ $link['target'] = $conf['target']['media'];
+ if($conf['target']['media']) $link['rel'] = 'noopener';
+ $link['title'] = $this->_xmlEntities($src);
+ $link['name'] = $this->_media($src, $title, $align, $width, $height, $cache, $render);
+
+ return $link;
+ }
+
+ /**
+ * Embed video(s) in HTML
+ *
+ * @author Anika Henke <anika@selfthinker.org>
+ * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
+ *
+ * @param string $src - ID of video to embed
+ * @param int $width - width of the video in pixels
+ * @param int $height - height of the video in pixels
+ * @param array $atts - additional attributes for the <video> tag
+ * @return string
+ */
+ public function _video($src, $width, $height, $atts = null) {
+ // prepare width and height
+ if(is_null($atts)) $atts = array();
+ $atts['width'] = (int) $width;
+ $atts['height'] = (int) $height;
+ if(!$atts['width']) $atts['width'] = 320;
+ if(!$atts['height']) $atts['height'] = 240;
+
+ $posterUrl = '';
+ $files = array();
+ $tracks = array();
+ $isExternal = media_isexternal($src);
+
+ if ($isExternal) {
+ // take direct source for external files
+ list(/*ext*/, $srcMime) = mimetype($src);
+ $files[$srcMime] = $src;
+ } else {
+ // prepare alternative formats
+ $extensions = array('webm', 'ogv', 'mp4');
+ $files = media_alternativefiles($src, $extensions);
+ $poster = media_alternativefiles($src, array('jpg', 'png'));
+ $tracks = media_trackfiles($src);
+ if(!empty($poster)) {
+ $posterUrl = ml(reset($poster), '', true, '&');
+ }
+ }
+
+ $out = '';
+ // open video tag
+ $out .= '<video '.buildAttributes($atts).' controls="controls"';
+ if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
+ $out .= '>'.NL;
+ $fallback = '';
+
+ // output source for each alternative video format
+ foreach($files as $mime => $file) {
+ if ($isExternal) {
+ $url = $file;
+ $linkType = 'externalmedia';
+ } else {
+ $url = ml($file, '', true, '&');
+ $linkType = 'internalmedia';
+ }
+ $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($file)));
+
+ $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
+ // alternative content (just a link to the file)
+ $fallback .= $this->$linkType(
+ $file,
+ $title,
+ null,
+ null,
+ null,
+ $cache = null,
+ $linking = 'linkonly',
+ $return = true
+ );
+ }
+
+ // output each track if any
+ foreach( $tracks as $trackid => $info ) {
+ list( $kind, $srclang ) = array_map( 'hsc', $info );
+ $out .= "<track kind=\"$kind\" srclang=\"$srclang\" ";
+ $out .= "label=\"$srclang\" ";
+ $out .= 'src="'.ml($trackid, '', true).'">'.NL;
+ }
+
+ // finish
+ $out .= $fallback;
+ $out .= '</video>'.NL;
+ return $out;
+ }
+
+ /**
+ * Embed audio in HTML
+ *
+ * @author Anika Henke <anika@selfthinker.org>
+ *
+ * @param string $src - ID of audio to embed
+ * @param array $atts - additional attributes for the <audio> tag
+ * @return string
+ */
+ public function _audio($src, $atts = array()) {
+ $files = array();
+ $isExternal = media_isexternal($src);
+
+ if ($isExternal) {
+ // take direct source for external files
+ list(/*ext*/, $srcMime) = mimetype($src);
+ $files[$srcMime] = $src;
+ } else {
+ // prepare alternative formats
+ $extensions = array('ogg', 'mp3', 'wav');
+ $files = media_alternativefiles($src, $extensions);
+ }
+
+ $out = '';
+ // open audio tag
+ $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
+ $fallback = '';
+
+ // output source for each alternative audio format
+ foreach($files as $mime => $file) {
+ if ($isExternal) {
+ $url = $file;
+ $linkType = 'externalmedia';
+ } else {
+ $url = ml($file, '', true, '&');
+ $linkType = 'internalmedia';
+ }
+ $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($file)));
+
+ $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
+ // alternative content (just a link to the file)
+ $fallback .= $this->$linkType(
+ $file,
+ $title,
+ null,
+ null,
+ null,
+ $cache = null,
+ $linking = 'linkonly',
+ $return = true
+ );
+ }
+
+ // finish
+ $out .= $fallback;
+ $out .= '</audio>'.NL;
+ return $out;
+ }
+
+ /**
+ * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
+ * which returns an existing media revision less or equal to rev or date_at
+ *
+ * @author lisps
+ * @param string $media_id
+ * @access protected
+ * @return string revision ('' for current)
+ */
+ protected function _getLastMediaRevisionAt($media_id){
+ if(!$this->date_at || media_isexternal($media_id)) return '';
+ $pagelog = new MediaChangeLog($media_id);
+ return $pagelog->getLastRevisionAt($this->date_at);
+ }
+
+ #endregion
+}
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/inc/parser/xhtmlsummary.php b/platform/www/inc/parser/xhtmlsummary.php
new file mode 100644
index 0000000..4641bf8
--- /dev/null
+++ b/platform/www/inc/parser/xhtmlsummary.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * The summary XHTML form selects either up to the first two paragraphs
+ * it find in a page or the first section (whichever comes first)
+ * It strips out the table of contents if one exists
+ * Section divs are not used - everything should be nested in a single
+ * div with CSS class "page"
+ * Headings have their a name link removed and section editing links
+ * removed
+ * It also attempts to capture the first heading in a page for
+ * use as the title of the page.
+ *
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @todo Is this currently used anywhere? Should it?
+ */
+class Doku_Renderer_xhtmlsummary extends Doku_Renderer_xhtml {
+
+ // Namespace these variables to
+ // avoid clashes with parent classes
+ protected $sum_paragraphs = 0;
+ protected $sum_capture = true;
+ protected $sum_inSection = false;
+ protected $sum_summary = '';
+ protected $sum_pageTitle = false;
+
+ /** @inheritdoc */
+ public function document_start() {
+ $this->doc .= DOKU_LF.'<div>'.DOKU_LF;
+ }
+
+ /** @inheritdoc */
+ public function document_end() {
+ $this->doc = $this->sum_summary;
+ $this->doc .= DOKU_LF.'</div>'.DOKU_LF;
+ }
+
+ /** @inheritdoc */
+ public function header($text, $level, $pos) {
+ if ( !$this->sum_pageTitle ) {
+ $this->info['sum_pagetitle'] = $text;
+ $this->sum_pageTitle = true;
+ }
+ $this->doc .= DOKU_LF.'<h'.$level.'>';
+ $this->doc .= $this->_xmlEntities($text);
+ $this->doc .= "</h$level>".DOKU_LF;
+ }
+
+ /** @inheritdoc */
+ public function section_open($level) {
+ if ( $this->sum_capture ) {
+ $this->sum_inSection = true;
+ }
+ }
+
+ /** @inheritdoc */
+ public function section_close() {
+ if ( $this->sum_capture && $this->sum_inSection ) {
+ $this->sum_summary .= $this->doc;
+ $this->sum_capture = false;
+ }
+ }
+
+ /** @inheritdoc */
+ public function p_open() {
+ if ( $this->sum_capture && $this->sum_paragraphs < 2 ) {
+ $this->sum_paragraphs++;
+ }
+ parent :: p_open();
+ }
+
+ /** @inheritdoc */
+ public function p_close() {
+ parent :: p_close();
+ if ( $this->sum_capture && $this->sum_paragraphs >= 2 ) {
+ $this->sum_summary .= $this->doc;
+ $this->sum_capture = false;
+ }
+ }
+
+}
+
+
+//Setup VIM: ex: et ts=2 :
diff --git a/platform/www/inc/parserutils.php b/platform/www/inc/parserutils.php
new file mode 100644
index 0000000..846be54
--- /dev/null
+++ b/platform/www/inc/parserutils.php
@@ -0,0 +1,809 @@
+<?php
+/**
+ * Utilities for accessing the parser
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+use dokuwiki\Cache\CacheInstructions;
+use dokuwiki\Cache\CacheRenderer;
+use dokuwiki\ChangeLog\PageChangeLog;
+use dokuwiki\Extension\PluginController;
+use dokuwiki\Extension\Event;
+use dokuwiki\Parsing\Parser;
+
+/**
+ * How many pages shall be rendered for getting metadata during one request
+ * at maximum? Note that this limit isn't respected when METADATA_RENDER_UNLIMITED
+ * is passed as render parameter to p_get_metadata.
+ */
+if (!defined('P_GET_METADATA_RENDER_LIMIT')) define('P_GET_METADATA_RENDER_LIMIT', 5);
+
+/** Don't render metadata even if it is outdated or doesn't exist */
+define('METADATA_DONT_RENDER', 0);
+/**
+ * Render metadata when the page is really newer or the metadata doesn't exist.
+ * Uses just a simple check, but should work pretty well for loading simple
+ * metadata values like the page title and avoids rendering a lot of pages in
+ * one request. The P_GET_METADATA_RENDER_LIMIT is used in this mode.
+ * Use this if it is unlikely that the metadata value you are requesting
+ * does depend e.g. on pages that are included in the current page using
+ * the include plugin (this is very likely the case for the page title, but
+ * not for relation references).
+ */
+define('METADATA_RENDER_USING_SIMPLE_CACHE', 1);
+/**
+ * Render metadata using the metadata cache logic. The P_GET_METADATA_RENDER_LIMIT
+ * is used in this mode. Use this mode when you are requesting more complex
+ * metadata. Although this will cause rendering more often it might actually have
+ * the effect that less current metadata is returned as it is more likely than in
+ * the simple cache mode that metadata needs to be rendered for all pages at once
+ * which means that when the metadata for the page is requested that actually needs
+ * to be updated the limit might have been reached already.
+ */
+define('METADATA_RENDER_USING_CACHE', 2);
+/**
+ * Render metadata without limiting the number of pages for which metadata is
+ * rendered. Use this mode with care, normally it should only be used in places
+ * like the indexer or in cli scripts where the execution time normally isn't
+ * limited. This can be combined with the simple cache using
+ * METADATA_RENDER_USING_CACHE | METADATA_RENDER_UNLIMITED.
+ */
+define('METADATA_RENDER_UNLIMITED', 4);
+
+/**
+ * Returns the parsed Wikitext in XHTML for the given id and revision.
+ *
+ * If $excuse is true an explanation is returned if the file
+ * wasn't found
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id page id
+ * @param string|int $rev revision timestamp or empty string
+ * @param bool $excuse
+ * @param string $date_at
+ *
+ * @return null|string
+ */
+function p_wiki_xhtml($id, $rev='', $excuse=true,$date_at=''){
+ $file = wikiFN($id,$rev);
+ $ret = '';
+
+ //ensure $id is in global $ID (needed for parsing)
+ global $ID;
+ $keep = $ID;
+ $ID = $id;
+
+ if($rev || $date_at){
+ if(file_exists($file)){
+ //no caching on old revisions
+ $ret = p_render('xhtml',p_get_instructions(io_readWikiPage($file,$id,$rev)),$info,$date_at);
+ }elseif($excuse){
+ $ret = p_locale_xhtml('norev');
+ }
+ }else{
+ if(file_exists($file)){
+ $ret = p_cached_output($file,'xhtml',$id);
+ }elseif($excuse){
+ //check if the page once existed
+ $changelog = new PageChangeLog($id);
+ if($changelog->hasRevisions()) {
+ $ret = p_locale_xhtml('onceexisted');
+ } else {
+ $ret = p_locale_xhtml('newpage');
+ }
+ }
+ }
+
+ //restore ID (just in case)
+ $ID = $keep;
+
+ return $ret;
+}
+
+/**
+ * Returns the specified local text in parsed format
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id page id
+ * @return null|string
+ */
+function p_locale_xhtml($id){
+ //fetch parsed locale
+ $html = p_cached_output(localeFN($id));
+ return $html;
+}
+
+/**
+ * Returns the given file parsed into the requested output format
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param string $file filename, path to file
+ * @param string $format
+ * @param string $id page id
+ * @return null|string
+ */
+function p_cached_output($file, $format='xhtml', $id='') {
+ global $conf;
+
+ $cache = new CacheRenderer($id, $file, $format);
+ if ($cache->useCache()) {
+ $parsed = $cache->retrieveCache(false);
+ if($conf['allowdebug'] && $format=='xhtml') {
+ $parsed .= "\n<!-- cachefile {$cache->cache} used -->\n";
+ }
+ } else {
+ $parsed = p_render($format, p_cached_instructions($file,false,$id), $info);
+
+ if ($info['cache'] && $cache->storeCache($parsed)) { // storeCache() attempts to save cachefile
+ if($conf['allowdebug'] && $format=='xhtml') {
+ $parsed .= "\n<!-- no cachefile used, but created {$cache->cache} -->\n";
+ }
+ }else{
+ $cache->removeCache(); //try to delete cachefile
+ if($conf['allowdebug'] && $format=='xhtml') {
+ $parsed .= "\n<!-- no cachefile used, caching forbidden -->\n";
+ }
+ }
+ }
+
+ return $parsed;
+}
+
+/**
+ * Returns the render instructions for a file
+ *
+ * Uses and creates a serialized cache file
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $file filename, path to file
+ * @param bool $cacheonly
+ * @param string $id page id
+ * @return array|null
+ */
+function p_cached_instructions($file,$cacheonly=false,$id='') {
+ static $run = null;
+ if(is_null($run)) $run = array();
+
+ $cache = new CacheInstructions($id, $file);
+
+ if ($cacheonly || $cache->useCache() || (isset($run[$file]) && !defined('DOKU_UNITTEST'))) {
+ return $cache->retrieveCache();
+ } else if (file_exists($file)) {
+ // no cache - do some work
+ $ins = p_get_instructions(io_readWikiPage($file,$id));
+ if ($cache->storeCache($ins)) {
+ $run[$file] = true; // we won't rebuild these instructions in the same run again
+ } else {
+ msg('Unable to save cache file. Hint: disk full; file permissions; safe_mode setting.',-1);
+ }
+ return $ins;
+ }
+
+ return null;
+}
+
+/**
+ * turns a page into a list of instructions
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $text raw wiki syntax text
+ * @return array a list of instruction arrays
+ */
+function p_get_instructions($text){
+
+ $modes = p_get_parsermodes();
+
+ // Create the parser and handler
+ $Parser = new Parser(new Doku_Handler());
+
+ //add modes to parser
+ foreach($modes as $mode){
+ $Parser->addMode($mode['mode'],$mode['obj']);
+ }
+
+ // Do the parsing
+ Event::createAndTrigger('PARSER_WIKITEXT_PREPROCESS', $text);
+ $p = $Parser->parse($text);
+ // dbg($p);
+ return $p;
+}
+
+/**
+ * returns the metadata of a page
+ *
+ * @param string $id The id of the page the metadata should be returned from
+ * @param string $key The key of the metdata value that shall be read (by default everything)
+ * separate hierarchies by " " like "date created"
+ * @param int $render If the page should be rendererd - possible values:
+ * METADATA_DONT_RENDER, METADATA_RENDER_USING_SIMPLE_CACHE, METADATA_RENDER_USING_CACHE
+ * METADATA_RENDER_UNLIMITED (also combined with the previous two options),
+ * default: METADATA_RENDER_USING_CACHE
+ * @return mixed The requested metadata fields
+ *
+ * @author Esther Brunner <esther@kaffeehaus.ch>
+ * @author Michael Hamann <michael@content-space.de>
+ */
+function p_get_metadata($id, $key='', $render=METADATA_RENDER_USING_CACHE){
+ global $ID;
+ static $render_count = 0;
+ // track pages that have already been rendered in order to avoid rendering the same page
+ // again
+ static $rendered_pages = array();
+
+ // cache the current page
+ // Benchmarking shows the current page's metadata is generally the only page metadata
+ // accessed several times. This may catch a few other pages, but that shouldn't be an issue.
+ $cache = ($ID == $id);
+ $meta = p_read_metadata($id, $cache);
+
+ if (!is_numeric($render)) {
+ if ($render) {
+ $render = METADATA_RENDER_USING_SIMPLE_CACHE;
+ } else {
+ $render = METADATA_DONT_RENDER;
+ }
+ }
+
+ // prevent recursive calls in the cache
+ static $recursion = false;
+ if (!$recursion && $render != METADATA_DONT_RENDER && !isset($rendered_pages[$id])&& page_exists($id)){
+ $recursion = true;
+
+ $cachefile = new CacheRenderer($id, wikiFN($id), 'metadata');
+
+ $do_render = false;
+ if ($render & METADATA_RENDER_UNLIMITED || $render_count < P_GET_METADATA_RENDER_LIMIT) {
+ if ($render & METADATA_RENDER_USING_SIMPLE_CACHE) {
+ $pagefn = wikiFN($id);
+ $metafn = metaFN($id, '.meta');
+ if (!file_exists($metafn) || @filemtime($pagefn) > @filemtime($cachefile->cache)) {
+ $do_render = true;
+ }
+ } elseif (!$cachefile->useCache()){
+ $do_render = true;
+ }
+ }
+ if ($do_render) {
+ if (!defined('DOKU_UNITTEST')) {
+ ++$render_count;
+ $rendered_pages[$id] = true;
+ }
+ $old_meta = $meta;
+ $meta = p_render_metadata($id, $meta);
+ // only update the file when the metadata has been changed
+ if ($meta == $old_meta || p_save_metadata($id, $meta)) {
+ // store a timestamp in order to make sure that the cachefile is touched
+ // this timestamp is also stored when the meta data is still the same
+ $cachefile->storeCache(time());
+ } else {
+ msg('Unable to save metadata file. Hint: disk full; file permissions; safe_mode setting.',-1);
+ }
+ }
+
+ $recursion = false;
+ }
+
+ $val = $meta['current'];
+
+ // filter by $key
+ foreach(preg_split('/\s+/', $key, 2, PREG_SPLIT_NO_EMPTY) as $cur_key) {
+ if (!isset($val[$cur_key])) {
+ return null;
+ }
+ $val = $val[$cur_key];
+ }
+ return $val;
+}
+
+/**
+ * sets metadata elements of a page
+ *
+ * @see http://www.dokuwiki.org/devel:metadata#functions_to_get_and_set_metadata
+ *
+ * @param string $id is the ID of a wiki page
+ * @param array $data is an array with key ⇒ value pairs to be set in the metadata
+ * @param boolean $render whether or not the page metadata should be generated with the renderer
+ * @param boolean $persistent indicates whether or not the particular metadata value will persist through
+ * the next metadata rendering.
+ * @return boolean true on success
+ *
+ * @author Esther Brunner <esther@kaffeehaus.ch>
+ * @author Michael Hamann <michael@content-space.de>
+ */
+function p_set_metadata($id, $data, $render=false, $persistent=true){
+ if (!is_array($data)) return false;
+
+ global $ID, $METADATA_RENDERERS;
+
+ // if there is currently a renderer change the data in the renderer instead
+ if (isset($METADATA_RENDERERS[$id])) {
+ $orig =& $METADATA_RENDERERS[$id];
+ $meta = $orig;
+ } else {
+ // cache the current page
+ $cache = ($ID == $id);
+ $orig = p_read_metadata($id, $cache);
+
+ // render metadata first?
+ $meta = $render ? p_render_metadata($id, $orig) : $orig;
+ }
+
+ // now add the passed metadata
+ $protected = array('description', 'date', 'contributor');
+ foreach ($data as $key => $value){
+
+ // be careful with sub-arrays of $meta['relation']
+ if ($key == 'relation'){
+
+ foreach ($value as $subkey => $subvalue){
+ if(isset($meta['current'][$key][$subkey]) && is_array($meta['current'][$key][$subkey])) {
+ $meta['current'][$key][$subkey] = array_replace($meta['current'][$key][$subkey], (array)$subvalue);
+ } else {
+ $meta['current'][$key][$subkey] = $subvalue;
+ }
+ if($persistent) {
+ if(isset($meta['persistent'][$key][$subkey]) && is_array($meta['persistent'][$key][$subkey])) {
+ $meta['persistent'][$key][$subkey] = array_replace(
+ $meta['persistent'][$key][$subkey],
+ (array) $subvalue
+ );
+ } else {
+ $meta['persistent'][$key][$subkey] = $subvalue;
+ }
+ }
+ }
+
+ // be careful with some senisitive arrays of $meta
+ } elseif (in_array($key, $protected)){
+
+ // these keys, must have subkeys - a legitimate value must be an array
+ if (is_array($value)) {
+ $meta['current'][$key] = !empty($meta['current'][$key]) ?
+ array_replace((array)$meta['current'][$key],$value) :
+ $value;
+
+ if ($persistent) {
+ $meta['persistent'][$key] = !empty($meta['persistent'][$key]) ?
+ array_replace((array)$meta['persistent'][$key],$value) :
+ $value;
+ }
+ }
+
+ // no special treatment for the rest
+ } else {
+ $meta['current'][$key] = $value;
+ if ($persistent) $meta['persistent'][$key] = $value;
+ }
+ }
+
+ // save only if metadata changed
+ if ($meta == $orig) return true;
+
+ if (isset($METADATA_RENDERERS[$id])) {
+ // set both keys individually as the renderer has references to the individual keys
+ $METADATA_RENDERERS[$id]['current'] = $meta['current'];
+ $METADATA_RENDERERS[$id]['persistent'] = $meta['persistent'];
+ return true;
+ } else {
+ return p_save_metadata($id, $meta);
+ }
+}
+
+/**
+ * Purges the non-persistant part of the meta data
+ * used on page deletion
+ *
+ * @author Michael Klier <chi@chimeric.de>
+ *
+ * @param string $id page id
+ * @return bool success / fail
+ */
+function p_purge_metadata($id) {
+ $meta = p_read_metadata($id);
+ foreach($meta['current'] as $key => $value) {
+ if(is_array($meta[$key])) {
+ $meta['current'][$key] = array();
+ } else {
+ $meta['current'][$key] = '';
+ }
+
+ }
+ return p_save_metadata($id, $meta);
+}
+
+/**
+ * read the metadata from source/cache for $id
+ * (internal use only - called by p_get_metadata & p_set_metadata)
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ *
+ * @param string $id absolute wiki page id
+ * @param bool $cache whether or not to cache metadata in memory
+ * (only use for metadata likely to be accessed several times)
+ *
+ * @return array metadata
+ */
+function p_read_metadata($id,$cache=false) {
+ global $cache_metadata;
+
+ if (isset($cache_metadata[(string)$id])) return $cache_metadata[(string)$id];
+
+ $file = metaFN($id, '.meta');
+ $meta = file_exists($file) ?
+ unserialize(io_readFile($file, false)) :
+ array('current'=>array(),'persistent'=>array());
+
+ if ($cache) {
+ $cache_metadata[(string)$id] = $meta;
+ }
+
+ return $meta;
+}
+
+/**
+ * This is the backend function to save a metadata array to a file
+ *
+ * @param string $id absolute wiki page id
+ * @param array $meta metadata
+ *
+ * @return bool success / fail
+ */
+function p_save_metadata($id, $meta) {
+ // sync cached copies, including $INFO metadata
+ global $cache_metadata, $INFO;
+
+ if (isset($cache_metadata[$id])) $cache_metadata[$id] = $meta;
+ if (!empty($INFO) && ($id == $INFO['id'])) { $INFO['meta'] = $meta['current']; }
+
+ return io_saveFile(metaFN($id, '.meta'), serialize($meta));
+}
+
+/**
+ * renders the metadata of a page
+ *
+ * @author Esther Brunner <esther@kaffeehaus.ch>
+ *
+ * @param string $id page id
+ * @param array $orig the original metadata
+ * @return array|null array('current'=> array,'persistent'=> array);
+ */
+function p_render_metadata($id, $orig){
+ // make sure the correct ID is in global ID
+ global $ID, $METADATA_RENDERERS;
+
+ // avoid recursive rendering processes for the same id
+ if (isset($METADATA_RENDERERS[$id])) {
+ return $orig;
+ }
+
+ // store the original metadata in the global $METADATA_RENDERERS so p_set_metadata can use it
+ $METADATA_RENDERERS[$id] =& $orig;
+
+ $keep = $ID;
+ $ID = $id;
+
+ // add an extra key for the event - to tell event handlers the page whose metadata this is
+ $orig['page'] = $id;
+ $evt = new Event('PARSER_METADATA_RENDER', $orig);
+ if ($evt->advise_before()) {
+
+ // get instructions
+ $instructions = p_cached_instructions(wikiFN($id),false,$id);
+ if(is_null($instructions)){
+ $ID = $keep;
+ unset($METADATA_RENDERERS[$id]);
+ return null; // something went wrong with the instructions
+ }
+
+ // set up the renderer
+ $renderer = new Doku_Renderer_metadata();
+ $renderer->meta =& $orig['current'];
+ $renderer->persistent =& $orig['persistent'];
+
+ // loop through the instructions
+ foreach ($instructions as $instruction){
+ // execute the callback against the renderer
+ call_user_func_array(array(&$renderer, $instruction[0]), (array) $instruction[1]);
+ }
+
+ $evt->result = array('current'=>&$renderer->meta,'persistent'=>&$renderer->persistent);
+ }
+ $evt->advise_after();
+
+ // clean up
+ $ID = $keep;
+ unset($METADATA_RENDERERS[$id]);
+ return $evt->result;
+}
+
+/**
+ * returns all available parser syntax modes in correct order
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @return array[] with for each plugin the array('sort' => sortnumber, 'mode' => mode string, 'obj' => plugin object)
+ */
+function p_get_parsermodes(){
+ global $conf;
+
+ //reuse old data
+ static $modes = null;
+ if($modes != null && !defined('DOKU_UNITTEST')){
+ return $modes;
+ }
+
+ //import parser classes and mode definitions
+ require_once DOKU_INC . 'inc/parser/parser.php';
+
+ // we now collect all syntax modes and their objects, then they will
+ // be sorted and added to the parser in correct order
+ $modes = array();
+
+ // add syntax plugins
+ $pluginlist = plugin_list('syntax');
+ if(count($pluginlist)){
+ global $PARSER_MODES;
+ $obj = null;
+ foreach($pluginlist as $p){
+ /** @var \dokuwiki\Extension\SyntaxPlugin $obj */
+ if(!$obj = plugin_load('syntax',$p)) continue; //attempt to load plugin into $obj
+ $PARSER_MODES[$obj->getType()][] = "plugin_$p"; //register mode type
+ //add to modes
+ $modes[] = array(
+ 'sort' => $obj->getSort(),
+ 'mode' => "plugin_$p",
+ 'obj' => $obj,
+ );
+ unset($obj); //remove the reference
+ }
+ }
+
+ // add default modes
+ $std_modes = array('listblock','preformatted','notoc','nocache',
+ 'header','table','linebreak','footnote','hr',
+ 'unformatted','php','html','code','file','quote',
+ 'internallink','rss','media','externallink',
+ 'emaillink','windowssharelink','eol');
+ if($conf['typography']){
+ $std_modes[] = 'quotes';
+ $std_modes[] = 'multiplyentity';
+ }
+ foreach($std_modes as $m){
+ $class = 'dokuwiki\\Parsing\\ParserMode\\'.ucfirst($m);
+ $obj = new $class();
+ $modes[] = array(
+ 'sort' => $obj->getSort(),
+ 'mode' => $m,
+ 'obj' => $obj
+ );
+ }
+
+ // add formatting modes
+ $fmt_modes = array('strong','emphasis','underline','monospace',
+ 'subscript','superscript','deleted');
+ foreach($fmt_modes as $m){
+ $obj = new \dokuwiki\Parsing\ParserMode\Formatting($m);
+ $modes[] = array(
+ 'sort' => $obj->getSort(),
+ 'mode' => $m,
+ 'obj' => $obj
+ );
+ }
+
+ // add modes which need files
+ $obj = new \dokuwiki\Parsing\ParserMode\Smiley(array_keys(getSmileys()));
+ $modes[] = array('sort' => $obj->getSort(), 'mode' => 'smiley','obj' => $obj );
+ $obj = new \dokuwiki\Parsing\ParserMode\Acronym(array_keys(getAcronyms()));
+ $modes[] = array('sort' => $obj->getSort(), 'mode' => 'acronym','obj' => $obj );
+ $obj = new \dokuwiki\Parsing\ParserMode\Entity(array_keys(getEntities()));
+ $modes[] = array('sort' => $obj->getSort(), 'mode' => 'entity','obj' => $obj );
+
+ // add optional camelcase mode
+ if($conf['camelcase']){
+ $obj = new \dokuwiki\Parsing\ParserMode\Camelcaselink();
+ $modes[] = array('sort' => $obj->getSort(), 'mode' => 'camelcaselink','obj' => $obj );
+ }
+
+ //sort modes
+ usort($modes,'p_sort_modes');
+
+ return $modes;
+}
+
+/**
+ * Callback function for usort
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $a
+ * @param array $b
+ * @return int $a is lower/equal/higher than $b
+ */
+function p_sort_modes($a, $b){
+ if($a['sort'] == $b['sort']) return 0;
+ return ($a['sort'] < $b['sort']) ? -1 : 1;
+}
+
+/**
+ * Renders a list of instruction to the specified output mode
+ *
+ * In the $info array is information from the renderer returned
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $mode
+ * @param array|null|false $instructions
+ * @param array $info returns render info like enabled toc and cache
+ * @param string $date_at
+ * @return null|string rendered output
+ */
+function p_render($mode,$instructions,&$info,$date_at=''){
+ if(is_null($instructions)) return '';
+ if($instructions === false) return '';
+
+ $Renderer = p_get_renderer($mode);
+ if (is_null($Renderer)) return null;
+
+ $Renderer->reset();
+
+ if($date_at) {
+ $Renderer->date_at = $date_at;
+ }
+
+ $Renderer->smileys = getSmileys();
+ $Renderer->entities = getEntities();
+ $Renderer->acronyms = getAcronyms();
+ $Renderer->interwiki = getInterwiki();
+
+ // Loop through the instructions
+ foreach ( $instructions as $instruction ) {
+ // Execute the callback against the Renderer
+ if(method_exists($Renderer, $instruction[0])){
+ call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1] ? $instruction[1] : array());
+ }
+ }
+
+ //set info array
+ $info = $Renderer->info;
+
+ // Post process and return the output
+ $data = array($mode,& $Renderer->doc);
+ Event::createAndTrigger('RENDERER_CONTENT_POSTPROCESS',$data);
+ return $Renderer->doc;
+}
+
+/**
+ * Figure out the correct renderer class to use for $mode,
+ * instantiate and return it
+ *
+ * @param string $mode Mode of the renderer to get
+ * @return null|Doku_Renderer The renderer
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+function p_get_renderer($mode) {
+ /** @var PluginController $plugin_controller */
+ global $conf, $plugin_controller;
+
+ $rname = !empty($conf['renderer_'.$mode]) ? $conf['renderer_'.$mode] : $mode;
+ $rclass = "Doku_Renderer_$rname";
+
+ // if requested earlier or a bundled renderer
+ if( class_exists($rclass) ) {
+ $Renderer = new $rclass();
+ return $Renderer;
+ }
+
+ // not bundled, see if its an enabled renderer plugin & when $mode is 'xhtml', the renderer can supply that format.
+ /** @var Doku_Renderer $Renderer */
+ $Renderer = $plugin_controller->load('renderer',$rname);
+ if ($Renderer && is_a($Renderer, 'Doku_Renderer') && ($mode != 'xhtml' || $mode == $Renderer->getFormat())) {
+ return $Renderer;
+ }
+
+ // there is a configuration error!
+ // not bundled, not a valid enabled plugin, use $mode to try to fallback to a bundled renderer
+ $rclass = "Doku_Renderer_$mode";
+ if ( class_exists($rclass) ) {
+ // viewers should see renderered output, so restrict the warning to admins only
+ $msg = "No renderer '$rname' found for mode '$mode', check your plugins";
+ if ($mode == 'xhtml') {
+ $msg .= " and the 'renderer_xhtml' config setting";
+ }
+ $msg .= ".<br/>Attempting to fallback to the bundled renderer.";
+ msg($msg,-1,'','',MSG_ADMINS_ONLY);
+
+ $Renderer = new $rclass;
+ $Renderer->nocache(); // fallback only (and may include admin alerts), don't cache
+ return $Renderer;
+ }
+
+ // fallback failed, alert the world
+ msg("No renderer '$rname' found for mode '$mode'",-1);
+ return null;
+}
+
+/**
+ * Gets the first heading from a file
+ *
+ * @param string $id dokuwiki page id
+ * @param int $render rerender if first heading not known
+ * default: METADATA_RENDER_USING_SIMPLE_CACHE
+ * Possible values: METADATA_DONT_RENDER,
+ * METADATA_RENDER_USING_SIMPLE_CACHE,
+ * METADATA_RENDER_USING_CACHE,
+ * METADATA_RENDER_UNLIMITED
+ * @return string|null The first heading
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Michael Hamann <michael@content-space.de>
+ */
+function p_get_first_heading($id, $render=METADATA_RENDER_USING_SIMPLE_CACHE){
+ return p_get_metadata(cleanID($id),'title',$render);
+}
+
+/**
+ * Wrapper for GeSHi Code Highlighter, provides caching of its output
+ *
+ * @param string $code source code to be highlighted
+ * @param string $language language to provide highlighting
+ * @param string $wrapper html element to wrap the returned highlighted text
+ * @return string xhtml code
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function p_xhtml_cached_geshi($code, $language, $wrapper='pre', array $options=null) {
+ global $conf, $config_cascade, $INPUT;
+ $language = strtolower($language);
+
+ // remove any leading or trailing blank lines
+ $code = preg_replace('/^\s*?\n|\s*?\n$/','',$code);
+
+ $optionsmd5 = md5(serialize($options));
+ $cache = getCacheName($language.$code.$optionsmd5,".code");
+ $ctime = @filemtime($cache);
+ if($ctime && !$INPUT->bool('purge') &&
+ $ctime > filemtime(DOKU_INC.'vendor/composer/installed.json') && // libraries changed
+ $ctime > filemtime(reset($config_cascade['main']['default']))){ // dokuwiki changed
+ $highlighted_code = io_readFile($cache, false);
+ } else {
+
+ $geshi = new GeSHi($code, $language);
+ $geshi->set_encoding('utf-8');
+ $geshi->enable_classes();
+ $geshi->set_header_type(GESHI_HEADER_PRE);
+ $geshi->set_link_target($conf['target']['extern']);
+ if($options !== null) {
+ foreach ($options as $function => $params) {
+ if(is_callable(array($geshi, $function))) {
+ $geshi->$function($params);
+ }
+ }
+ }
+
+ // remove GeSHi's wrapper element (we'll replace it with our own later)
+ // we need to use a GeSHi wrapper to avoid <BR> throughout the highlighted text
+ $highlighted_code = trim(preg_replace('!^<pre[^>]*>|</pre>$!','',$geshi->parse_code()),"\n\r");
+ io_saveFile($cache,$highlighted_code);
+ }
+
+ // add a wrapper element if required
+ if ($wrapper) {
+ return "<$wrapper class=\"code $language\">$highlighted_code</$wrapper>";
+ } else {
+ return $highlighted_code;
+ }
+}
+
diff --git a/platform/www/inc/pluginutils.php b/platform/www/inc/pluginutils.php
new file mode 100644
index 0000000..a93cd4f
--- /dev/null
+++ b/platform/www/inc/pluginutils.php
@@ -0,0 +1,151 @@
+<?php
+/**
+ * Utilities for handling plugins
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+// plugin related constants
+use dokuwiki\Extension\AdminPlugin;
+use dokuwiki\Extension\PluginController;
+use dokuwiki\Extension\PluginInterface;
+
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+// note that only [a-z0-9]+ is officially supported,
+// this is only to support plugins that don't follow these conventions, too
+if(!defined('DOKU_PLUGIN_NAME_REGEX')) define('DOKU_PLUGIN_NAME_REGEX', '[a-zA-Z0-9\x7f-\xff]+');
+
+/**
+ * Original plugin functions, remain for backwards compatibility
+ */
+
+/**
+ * Return list of available plugins
+ *
+ * @param string $type type of plugins; empty string for all
+ * @param bool $all; true to retrieve all, false to retrieve only enabled plugins
+ * @return array with plugin names or plugin component names
+ */
+function plugin_list($type='',$all=false)
+{
+ /** @var $plugin_controller PluginController */
+ global $plugin_controller;
+ $plugins = $plugin_controller->getList($type,$all);
+ sort($plugins, SORT_NATURAL|SORT_FLAG_CASE);
+ return $plugins;
+}
+
+/**
+ * Returns plugin object
+ * Returns only new instances of a plugin when $new is true or if plugin is not Singleton,
+ * otherwise an already loaded instance.
+ *
+ * @param $type string type of plugin to load
+ * @param $name string name of the plugin to load
+ * @param $new bool true to return a new instance of the plugin, false to use an already loaded instance
+ * @param $disabled bool true to load even disabled plugins
+ * @return PluginInterface|null the plugin object or null on failure
+ */
+function plugin_load($type,$name,$new=false,$disabled=false)
+{
+ /** @var $plugin_controller PluginController */
+ global $plugin_controller;
+ return $plugin_controller->load($type,$name,$new,$disabled);
+}
+
+/**
+ * Whether plugin is disabled
+ *
+ * @param string $plugin name of plugin
+ * @return bool true disabled, false enabled
+ */
+function plugin_isdisabled($plugin)
+{
+ /** @var $plugin_controller PluginController */
+ global $plugin_controller;
+ return !$plugin_controller->isEnabled($plugin);
+}
+
+/**
+ * Enable the plugin
+ *
+ * @param string $plugin name of plugin
+ * @return bool true saving succeed, false saving failed
+ */
+function plugin_enable($plugin)
+{
+ /** @var $plugin_controller PluginController */
+ global $plugin_controller;
+ return $plugin_controller->enable($plugin);
+}
+
+/**
+ * Disable the plugin
+ *
+ * @param string $plugin name of plugin
+ * @return bool true saving succeed, false saving failed
+ */
+function plugin_disable($plugin)
+{
+ /** @var $plugin_controller PluginController */
+ global $plugin_controller;
+ return $plugin_controller->disable($plugin);
+}
+
+/**
+ * Returns directory name of plugin
+ *
+ * @param string $plugin name of plugin
+ * @return string name of directory
+ * @deprecated 2018-07-20
+ */
+function plugin_directory($plugin)
+{
+ dbg_deprecated('$plugin directly');
+ return $plugin;
+}
+
+/**
+ * Returns cascade of the config files
+ *
+ * @return array with arrays of plugin configs
+ */
+function plugin_getcascade()
+{
+ /** @var $plugin_controller PluginController */
+ global $plugin_controller;
+ return $plugin_controller->getCascade();
+}
+
+
+/**
+ * Return the currently operating admin plugin or null
+ * if not on an admin plugin page
+ *
+ * @return Doku_Plugin_Admin
+ */
+function plugin_getRequestAdminPlugin()
+{
+ static $admin_plugin = false;
+ global $ACT,$INPUT,$INFO;
+
+ if ($admin_plugin === false) {
+ if (($ACT == 'admin') && ($page = $INPUT->str('page', '', true)) != '') {
+ $pluginlist = plugin_list('admin');
+ if (in_array($page, $pluginlist)) {
+ // attempt to load the plugin
+ /** @var $admin_plugin AdminPlugin */
+ $admin_plugin = plugin_load('admin', $page);
+ // verify
+ if ($admin_plugin && !$admin_plugin->isAccessibleByCurrentUser()) {
+ $admin_plugin = null;
+ $INPUT->remove('page');
+ msg('For admins only',-1);
+ }
+ }
+ }
+ }
+
+ return $admin_plugin;
+}
diff --git a/platform/www/inc/preload.php b/platform/www/inc/preload.php
new file mode 100644
index 0000000..7146344
--- /dev/null
+++ b/platform/www/inc/preload.php
@@ -0,0 +1,5 @@
+<?php
+# farm setup by farmer plugin
+if(file_exists(__DIR__ . '/../lib/plugins/farmer/DokuWikiFarmCore.php')) {
+ include(__DIR__ . '/../lib/plugins/farmer/DokuWikiFarmCore.php');
+}
diff --git a/platform/www/inc/preload.php.dist b/platform/www/inc/preload.php.dist
new file mode 100644
index 0000000..7acda0e
--- /dev/null
+++ b/platform/www/inc/preload.php.dist
@@ -0,0 +1,17 @@
+<?php
+/**
+ * This is an example for a farm setup. Simply copy this file to preload.php and
+ * uncomment what you need. See http://www.dokuwiki.org/farms for more information.
+ * You can also use preload.php for other things than farming, e.g. for moving
+ * local configuration files out of the main ./conf directory.
+ */
+
+// set this to your farm directory
+//if(!defined('DOKU_FARMDIR')) define('DOKU_FARMDIR', '/var/www/farm');
+
+// include this after DOKU_FARMDIR if you want to use farms
+//include(fullpath(dirname(__FILE__)).'/farm.php');
+
+// you can overwrite the $config_cascade to your liking
+//$config_cascade = array(
+//);
diff --git a/platform/www/inc/search.php b/platform/www/inc/search.php
new file mode 100644
index 0000000..27efc65
--- /dev/null
+++ b/platform/www/inc/search.php
@@ -0,0 +1,518 @@
+<?php
+/**
+ * DokuWiki search functions
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+/**
+ * Recurse directory
+ *
+ * This function recurses into a given base directory
+ * and calls the supplied function for each file and directory
+ *
+ * @param array &$data The results of the search are stored here
+ * @param string $base Where to start the search
+ * @param callback $func Callback (function name or array with object,method)
+ * @param array $opts option array will be given to the Callback
+ * @param string $dir Current directory beyond $base
+ * @param int $lvl Recursion Level
+ * @param mixed $sort 'natural' to use natural order sorting (default);
+ * 'date' to sort by filemtime; leave empty to skip sorting.
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function search(&$data,$base,$func,$opts,$dir='',$lvl=1,$sort='natural'){
+ $dirs = array();
+ $files = array();
+ $filepaths = array();
+
+ // safeguard against runaways #1452
+ if($base == '' || $base == '/') {
+ throw new RuntimeException('No valid $base passed to search() - possible misconfiguration or bug');
+ }
+
+ //read in directories and files
+ $dh = @opendir($base.'/'.$dir);
+ if(!$dh) return;
+ while(($file = readdir($dh)) !== false){
+ if(preg_match('/^[\._]/',$file)) continue; //skip hidden files and upper dirs
+ if(is_dir($base.'/'.$dir.'/'.$file)){
+ $dirs[] = $dir.'/'.$file;
+ continue;
+ }
+ $files[] = $dir.'/'.$file;
+ $filepaths[] = $base.'/'.$dir.'/'.$file;
+ }
+ closedir($dh);
+ if (!empty($sort)) {
+ if ($sort == 'date') {
+ @array_multisort(array_map('filemtime', $filepaths), SORT_NUMERIC, SORT_DESC, $files);
+ } else /* natural */ {
+ natsort($files);
+ }
+ natsort($dirs);
+ }
+
+ //give directories to userfunction then recurse
+ foreach($dirs as $dir){
+ if (call_user_func_array($func, array(&$data,$base,$dir,'d',$lvl,$opts))){
+ search($data,$base,$func,$opts,$dir,$lvl+1,$sort);
+ }
+ }
+ //now handle the files
+ foreach($files as $file){
+ call_user_func_array($func, array(&$data,$base,$file,'f',$lvl,$opts));
+ }
+}
+
+/**
+ * The following functions are userfunctions to use with the search
+ * function above. This function is called for every found file or
+ * directory. When a directory is given to the function it has to
+ * decide if this directory should be traversed (true) or not (false)
+ * The function has to accept the following parameters:
+ *
+ * array &$data - Reference to the result data structure
+ * string $base - Base usually $conf['datadir']
+ * string $file - current file or directory relative to $base
+ * string $type - Type either 'd' for directory or 'f' for file
+ * int $lvl - Current recursion depht
+ * array $opts - option array as given to search()
+ *
+ * return values for files are ignored
+ *
+ * All functions should check the ACL for document READ rights
+ * namespaces (directories) are NOT checked (when sneaky_index is 0) as this
+ * would break the recursion (You can have an nonreadable dir over a readable
+ * one deeper nested) also make sure to check the file type (for example
+ * in case of lockfiles).
+ */
+
+/**
+ * Searches for pages beginning with the given query
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $data
+ * @param string $base
+ * @param string $file
+ * @param string $type
+ * @param integer $lvl
+ * @param array $opts
+ *
+ * @return bool
+ */
+function search_qsearch(&$data,$base,$file,$type,$lvl,$opts){
+ $opts = array(
+ 'idmatch' => '(^|:)'.preg_quote($opts['query'],'/').'/',
+ 'listfiles' => true,
+ 'pagesonly' => true,
+ );
+ return search_universal($data,$base,$file,$type,$lvl,$opts);
+}
+
+/**
+ * Build the browsable index of pages
+ *
+ * $opts['ns'] is the currently viewed namespace
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $data
+ * @param string $base
+ * @param string $file
+ * @param string $type
+ * @param integer $lvl
+ * @param array $opts
+ *
+ * @return bool
+ */
+function search_index(&$data,$base,$file,$type,$lvl,$opts){
+ global $conf;
+ $opts = array(
+ 'pagesonly' => true,
+ 'listdirs' => true,
+ 'listfiles' => empty($opts['nofiles']),
+ 'sneakyacl' => $conf['sneaky_index'],
+ // Hacky, should rather use recmatch
+ 'depth' => preg_match('#^'.preg_quote($file, '#').'(/|$)#','/'.$opts['ns']) ? 0 : -1
+ );
+
+ return search_universal($data, $base, $file, $type, $lvl, $opts);
+}
+
+/**
+ * List all namespaces
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $data
+ * @param string $base
+ * @param string $file
+ * @param string $type
+ * @param integer $lvl
+ * @param array $opts
+ *
+ * @return bool
+ */
+function search_namespaces(&$data,$base,$file,$type,$lvl,$opts){
+ $opts = array(
+ 'listdirs' => true,
+ );
+ return search_universal($data,$base,$file,$type,$lvl,$opts);
+}
+
+/**
+ * List all mediafiles in a namespace
+ * $opts['depth'] recursion level, 0 for all
+ * $opts['showmsg'] shows message if invalid media id is used
+ * $opts['skipacl'] skip acl checking
+ * $opts['pattern'] check given pattern
+ * $opts['hash'] add hashes to result list
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $data
+ * @param string $base
+ * @param string $file
+ * @param string $type
+ * @param integer $lvl
+ * @param array $opts
+ *
+ * @return bool
+ */
+function search_media(&$data,$base,$file,$type,$lvl,$opts){
+
+ //we do nothing with directories
+ if($type == 'd') {
+ if(empty($opts['depth'])) return true; // recurse forever
+ $depth = substr_count($file,'/');
+ if($depth >= $opts['depth']) return false; // depth reached
+ return true;
+ }
+
+ $info = array();
+ $info['id'] = pathID($file,true);
+ if($info['id'] != cleanID($info['id'])){
+ if($opts['showmsg'])
+ msg(hsc($info['id']).' is not a valid file name for DokuWiki - skipped',-1);
+ return false; // skip non-valid files
+ }
+
+ //check ACL for namespace (we have no ACL for mediafiles)
+ $info['perm'] = auth_quickaclcheck(getNS($info['id']).':*');
+ if(empty($opts['skipacl']) && $info['perm'] < AUTH_READ){
+ return false;
+ }
+
+ //check pattern filter
+ if(!empty($opts['pattern']) && !@preg_match($opts['pattern'], $info['id'])){
+ return false;
+ }
+
+ $info['file'] = \dokuwiki\Utf8\PhpString::basename($file);
+ $info['size'] = filesize($base.'/'.$file);
+ $info['mtime'] = filemtime($base.'/'.$file);
+ $info['writable'] = is_writable($base.'/'.$file);
+ if(preg_match("/\.(jpe?g|gif|png)$/",$file)){
+ $info['isimg'] = true;
+ $info['meta'] = new JpegMeta($base.'/'.$file);
+ }else{
+ $info['isimg'] = false;
+ }
+ if(!empty($opts['hash'])){
+ $info['hash'] = md5(io_readFile(mediaFN($info['id']),false));
+ }
+
+ $data[] = $info;
+
+ return false;
+}
+
+/**
+ * This function just lists documents (for RSS namespace export)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $data
+ * @param string $base
+ * @param string $file
+ * @param string $type
+ * @param integer $lvl
+ * @param array $opts
+ *
+ * @return bool
+ */
+function search_list(&$data,$base,$file,$type,$lvl,$opts){
+ //we do nothing with directories
+ if($type == 'd') return false;
+ //only search txt files
+ if(substr($file,-4) == '.txt'){
+ //check ACL
+ $id = pathID($file);
+ if(auth_quickaclcheck($id) < AUTH_READ){
+ return false;
+ }
+ $data[]['id'] = $id;
+ }
+ return false;
+}
+
+/**
+ * Quicksearch for searching matching pagenames
+ *
+ * $opts['query'] is the search query
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $data
+ * @param string $base
+ * @param string $file
+ * @param string $type
+ * @param integer $lvl
+ * @param array $opts
+ *
+ * @return bool
+ */
+function search_pagename(&$data,$base,$file,$type,$lvl,$opts){
+ //we do nothing with directories
+ if($type == 'd') return true;
+ //only search txt files
+ if(substr($file,-4) != '.txt') return true;
+
+ //simple stringmatching
+ if (!empty($opts['query'])){
+ if(strpos($file,$opts['query']) !== false){
+ //check ACL
+ $id = pathID($file);
+ if(auth_quickaclcheck($id) < AUTH_READ){
+ return false;
+ }
+ $data[]['id'] = $id;
+ }
+ }
+ return true;
+}
+
+/**
+ * Just lists all documents
+ *
+ * $opts['depth'] recursion level, 0 for all
+ * $opts['hash'] do md5 sum of content?
+ * $opts['skipacl'] list everything regardless of ACL
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $data
+ * @param string $base
+ * @param string $file
+ * @param string $type
+ * @param integer $lvl
+ * @param array $opts
+ *
+ * @return bool
+ */
+function search_allpages(&$data,$base,$file,$type,$lvl,$opts){
+ if(isset($opts['depth']) && $opts['depth']){
+ $parts = explode('/',ltrim($file,'/'));
+ if(($type == 'd' && count($parts) >= $opts['depth'])
+ || ($type != 'd' && count($parts) > $opts['depth'])){
+ return false; // depth reached
+ }
+ }
+
+ //we do nothing with directories
+ if($type == 'd'){
+ return true;
+ }
+
+ //only search txt files
+ if(substr($file,-4) != '.txt') return true;
+
+ $item = array();
+ $item['id'] = pathID($file);
+ if(empty($opts['skipacl']) && auth_quickaclcheck($item['id']) < AUTH_READ){
+ return false;
+ }
+
+ $item['rev'] = filemtime($base.'/'.$file);
+ $item['mtime'] = $item['rev'];
+ $item['size'] = filesize($base.'/'.$file);
+ if(!empty($opts['hash'])){
+ $item['hash'] = md5(trim(rawWiki($item['id'])));
+ }
+
+ $data[] = $item;
+ return true;
+}
+
+/* ------------- helper functions below -------------- */
+
+/**
+ * fulltext sort
+ *
+ * Callback sort function for use with usort to sort the data
+ * structure created by search_fulltext. Sorts descending by count
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $a
+ * @param array $b
+ *
+ * @return int
+ */
+function sort_search_fulltext($a,$b){
+ if($a['count'] > $b['count']){
+ return -1;
+ }elseif($a['count'] < $b['count']){
+ return 1;
+ }else{
+ return strcmp($a['id'],$b['id']);
+ }
+}
+
+/**
+ * translates a document path to an ID
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @todo move to pageutils
+ *
+ * @param string $path
+ * @param bool $keeptxt
+ *
+ * @return mixed|string
+ */
+function pathID($path,$keeptxt=false){
+ $id = utf8_decodeFN($path);
+ $id = str_replace('/',':',$id);
+ if(!$keeptxt) $id = preg_replace('#\.txt$#','',$id);
+ $id = trim($id, ':');
+ return $id;
+}
+
+
+/**
+ * This is a very universal callback for the search() function, replacing
+ * many of the former individual functions at the cost of a more complex
+ * setup.
+ *
+ * How the function behaves, depends on the options passed in the $opts
+ * array, where the following settings can be used.
+ *
+ * depth int recursion depth. 0 for unlimited (default: 0)
+ * keeptxt bool keep .txt extension for IDs (default: false)
+ * listfiles bool include files in listing (default: false)
+ * listdirs bool include namespaces in listing (default: false)
+ * pagesonly bool restrict files to pages (default: false)
+ * skipacl bool do not check for READ permission (default: false)
+ * sneakyacl bool don't recurse into nonreadable dirs (default: false)
+ * hash bool create MD5 hash for files (default: false)
+ * meta bool return file metadata (default: false)
+ * filematch string match files against this regexp (default: '', so accept everything)
+ * idmatch string match full ID against this regexp (default: '', so accept everything)
+ * dirmatch string match directory against this regexp when adding (default: '', so accept everything)
+ * nsmatch string match namespace against this regexp when adding (default: '', so accept everything)
+ * recmatch string match directory against this regexp when recursing (default: '', so accept everything)
+ * showmsg bool warn about non-ID files (default: false)
+ * showhidden bool show hidden files(e.g. by hidepages config) too (default: false)
+ * firsthead bool return first heading for pages (default: false)
+ *
+ * @param array &$data - Reference to the result data structure
+ * @param string $base - Base usually $conf['datadir']
+ * @param string $file - current file or directory relative to $base
+ * @param string $type - Type either 'd' for directory or 'f' for file
+ * @param int $lvl - Current recursion depht
+ * @param array $opts - option array as given to search()
+ * @return bool if this directory should be traversed (true) or not (false)
+ * return value is ignored for files
+ *
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+function search_universal(&$data,$base,$file,$type,$lvl,$opts){
+ $item = array();
+ $return = true;
+
+ // get ID and check if it is a valid one
+ $item['id'] = pathID($file,($type == 'd' || !empty($opts['keeptxt'])));
+ if($item['id'] != cleanID($item['id'])){
+ if(!empty($opts['showmsg'])){
+ msg(hsc($item['id']).' is not a valid file name for DokuWiki - skipped',-1);
+ }
+ return false; // skip non-valid files
+ }
+ $item['ns'] = getNS($item['id']);
+
+ if($type == 'd') {
+ // decide if to recursion into this directory is wanted
+ if(empty($opts['depth'])){
+ $return = true; // recurse forever
+ }else{
+ $depth = substr_count($file,'/');
+ if($depth >= $opts['depth']){
+ $return = false; // depth reached
+ }else{
+ $return = true;
+ }
+ }
+
+ if ($return) {
+ $match = empty($opts['recmatch']) || preg_match('/'.$opts['recmatch'].'/',$file);
+ if (!$match) {
+ return false; // doesn't match
+ }
+ }
+ }
+
+ // check ACL
+ if(empty($opts['skipacl'])){
+ if($type == 'd'){
+ $item['perm'] = auth_quickaclcheck($item['id'].':*');
+ }else{
+ $item['perm'] = auth_quickaclcheck($item['id']); //FIXME check namespace for media files
+ }
+ }else{
+ $item['perm'] = AUTH_DELETE;
+ }
+
+ // are we done here maybe?
+ if($type == 'd'){
+ if(empty($opts['listdirs'])) return $return;
+ //neither list nor recurse forbidden items:
+ if(empty($opts['skipacl']) && !empty($opts['sneakyacl']) && $item['perm'] < AUTH_READ) return false;
+ if(!empty($opts['dirmatch']) && !preg_match('/'.$opts['dirmatch'].'/',$file)) return $return;
+ if(!empty($opts['nsmatch']) && !preg_match('/'.$opts['nsmatch'].'/',$item['ns'])) return $return;
+ }else{
+ if(empty($opts['listfiles'])) return $return;
+ if(empty($opts['skipacl']) && $item['perm'] < AUTH_READ) return $return;
+ if(!empty($opts['pagesonly']) && (substr($file,-4) != '.txt')) return $return;
+ if(empty($opts['showhidden']) && isHiddenPage($item['id'])) return $return;
+ if(!empty($opts['filematch']) && !preg_match('/'.$opts['filematch'].'/',$file)) return $return;
+ if(!empty($opts['idmatch']) && !preg_match('/'.$opts['idmatch'].'/',$item['id'])) return $return;
+ }
+
+ // still here? prepare the item
+ $item['type'] = $type;
+ $item['level'] = $lvl;
+ $item['open'] = $return;
+
+ if(!empty($opts['meta'])){
+ $item['file'] = \dokuwiki\Utf8\PhpString::basename($file);
+ $item['size'] = filesize($base.'/'.$file);
+ $item['mtime'] = filemtime($base.'/'.$file);
+ $item['rev'] = $item['mtime'];
+ $item['writable'] = is_writable($base.'/'.$file);
+ $item['executable'] = is_executable($base.'/'.$file);
+ }
+
+ if($type == 'f'){
+ if(!empty($opts['hash'])) $item['hash'] = md5(io_readFile($base.'/'.$file,false));
+ if(!empty($opts['firsthead'])) $item['title'] = p_get_first_heading($item['id'],METADATA_DONT_RENDER);
+ }
+
+ // finally add the item
+ $data[] = $item;
+ return $return;
+}
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/inc/template.php b/platform/www/inc/template.php
new file mode 100644
index 0000000..cb8f560
--- /dev/null
+++ b/platform/www/inc/template.php
@@ -0,0 +1,1895 @@
+<?php
+/**
+ * DokuWiki template functions
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+use dokuwiki\Extension\AdminPlugin;
+use dokuwiki\Extension\Event;
+
+/**
+ * Access a template file
+ *
+ * Returns the path to the given file inside the current template, uses
+ * default template if the custom version doesn't exist.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $file
+ * @return string
+ */
+function template($file) {
+ global $conf;
+
+ if(@is_readable(DOKU_INC.'lib/tpl/'.$conf['template'].'/'.$file))
+ return DOKU_INC.'lib/tpl/'.$conf['template'].'/'.$file;
+
+ return DOKU_INC.'lib/tpl/dokuwiki/'.$file;
+}
+
+/**
+ * Convenience function to access template dir from local FS
+ *
+ * This replaces the deprecated DOKU_TPLINC constant
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $tpl The template to use, default to current one
+ * @return string
+ */
+function tpl_incdir($tpl='') {
+ global $conf;
+ if(!$tpl) $tpl = $conf['template'];
+ return DOKU_INC.'lib/tpl/'.$tpl.'/';
+}
+
+/**
+ * Convenience function to access template dir from web
+ *
+ * This replaces the deprecated DOKU_TPL constant
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $tpl The template to use, default to current one
+ * @return string
+ */
+function tpl_basedir($tpl='') {
+ global $conf;
+ if(!$tpl) $tpl = $conf['template'];
+ return DOKU_BASE.'lib/tpl/'.$tpl.'/';
+}
+
+/**
+ * Print the content
+ *
+ * This function is used for printing all the usual content
+ * (defined by the global $ACT var) by calling the appropriate
+ * outputfunction(s) from html.php
+ *
+ * Everything that doesn't use the main template file isn't
+ * handled by this function. ACL stuff is not done here either.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @triggers TPL_ACT_RENDER
+ * @triggers TPL_CONTENT_DISPLAY
+ * @param bool $prependTOC should the TOC be displayed here?
+ * @return bool true if any output
+ */
+function tpl_content($prependTOC = true) {
+ global $ACT;
+ global $INFO;
+ $INFO['prependTOC'] = $prependTOC;
+
+ ob_start();
+ Event::createAndTrigger('TPL_ACT_RENDER', $ACT, 'tpl_content_core');
+ $html_output = ob_get_clean();
+ Event::createAndTrigger('TPL_CONTENT_DISPLAY', $html_output, 'ptln');
+
+ return !empty($html_output);
+}
+
+/**
+ * Default Action of TPL_ACT_RENDER
+ *
+ * @return bool
+ */
+function tpl_content_core() {
+ $router = \dokuwiki\ActionRouter::getInstance();
+ try {
+ $router->getAction()->tplContent();
+ } catch(\dokuwiki\Action\Exception\FatalException $e) {
+ // there was no content for the action
+ msg(hsc($e->getMessage()), -1);
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Places the TOC where the function is called
+ *
+ * If you use this you most probably want to call tpl_content with
+ * a false argument
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param bool $return Should the TOC be returned instead to be printed?
+ * @return string
+ */
+function tpl_toc($return = false) {
+ global $TOC;
+ global $ACT;
+ global $ID;
+ global $REV;
+ global $INFO;
+ global $conf;
+ global $INPUT;
+ $toc = array();
+
+ if(is_array($TOC)) {
+ // if a TOC was prepared in global scope, always use it
+ $toc = $TOC;
+ } elseif(($ACT == 'show' || substr($ACT, 0, 6) == 'export') && !$REV && $INFO['exists']) {
+ // get TOC from metadata, render if neccessary
+ $meta = p_get_metadata($ID, '', METADATA_RENDER_USING_CACHE);
+ if(isset($meta['internal']['toc'])) {
+ $tocok = $meta['internal']['toc'];
+ } else {
+ $tocok = true;
+ }
+ $toc = isset($meta['description']['tableofcontents']) ? $meta['description']['tableofcontents'] : null;
+ if(!$tocok || !is_array($toc) || !$conf['tocminheads'] || count($toc) < $conf['tocminheads']) {
+ $toc = array();
+ }
+ } elseif($ACT == 'admin') {
+ // try to load admin plugin TOC
+ /** @var $plugin AdminPlugin */
+ if ($plugin = plugin_getRequestAdminPlugin()) {
+ $toc = $plugin->getTOC();
+ $TOC = $toc; // avoid later rebuild
+ }
+ }
+
+ Event::createAndTrigger('TPL_TOC_RENDER', $toc, null, false);
+ $html = html_TOC($toc);
+ if($return) return $html;
+ echo $html;
+ return '';
+}
+
+/**
+ * Handle the admin page contents
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @return bool
+ */
+function tpl_admin() {
+ global $INFO;
+ global $TOC;
+ global $INPUT;
+
+ $plugin = null;
+ $class = $INPUT->str('page');
+ if(!empty($class)) {
+ $pluginlist = plugin_list('admin');
+
+ if(in_array($class, $pluginlist)) {
+ // attempt to load the plugin
+ /** @var $plugin AdminPlugin */
+ $plugin = plugin_load('admin', $class);
+ }
+ }
+
+ if($plugin !== null) {
+ if(!is_array($TOC)) $TOC = $plugin->getTOC(); //if TOC wasn't requested yet
+ if($INFO['prependTOC']) tpl_toc();
+ $plugin->html();
+ } else {
+ $admin = new dokuwiki\Ui\Admin();
+ $admin->show();
+ }
+ return true;
+}
+
+/**
+ * Print the correct HTML meta headers
+ *
+ * This has to go into the head section of your template.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @triggers TPL_METAHEADER_OUTPUT
+ * @param bool $alt Should feeds and alternative format links be added?
+ * @return bool
+ */
+function tpl_metaheaders($alt = true) {
+ global $ID;
+ global $REV;
+ global $INFO;
+ global $JSINFO;
+ global $ACT;
+ global $QUERY;
+ global $lang;
+ global $conf;
+ global $updateVersion;
+ /** @var Input $INPUT */
+ global $INPUT;
+
+ // prepare the head array
+ $head = array();
+
+ // prepare seed for js and css
+ $tseed = $updateVersion;
+ $depends = getConfigFiles('main');
+ $depends[] = DOKU_CONF."tpl/".$conf['template']."/style.ini";
+ foreach($depends as $f) $tseed .= @filemtime($f);
+ $tseed = md5($tseed);
+
+ // the usual stuff
+ $head['meta'][] = array('name'=> 'generator', 'content'=> 'DokuWiki');
+ if(actionOK('search')) {
+ $head['link'][] = array(
+ 'rel' => 'search', 'type'=> 'application/opensearchdescription+xml',
+ 'href'=> DOKU_BASE.'lib/exe/opensearch.php', 'title'=> $conf['title']
+ );
+ }
+
+ $head['link'][] = array('rel'=> 'start', 'href'=> DOKU_BASE);
+ if(actionOK('index')) {
+ $head['link'][] = array(
+ 'rel' => 'contents', 'href'=> wl($ID, 'do=index', false, '&'),
+ 'title'=> $lang['btn_index']
+ );
+ }
+
+ if (actionOK('manifest')) {
+ $head['link'][] = array('rel'=> 'manifest', 'href'=> DOKU_BASE.'lib/exe/manifest.php');
+ }
+
+ $styleUtil = new \dokuwiki\StyleUtils();
+ $styleIni = $styleUtil->cssStyleini();
+ $replacements = $styleIni['replacements'];
+ if (!empty($replacements['__theme_color__'])) {
+ $head['meta'][] = array('name' => 'theme-color', 'content' => $replacements['__theme_color__']);
+ }
+
+ if($alt) {
+ if(actionOK('rss')) {
+ $head['link'][] = array(
+ 'rel' => 'alternate', 'type'=> 'application/rss+xml',
+ 'title'=> $lang['btn_recent'], 'href'=> DOKU_BASE.'feed.php'
+ );
+ $head['link'][] = array(
+ 'rel' => 'alternate', 'type'=> 'application/rss+xml',
+ 'title'=> $lang['currentns'],
+ 'href' => DOKU_BASE.'feed.php?mode=list&ns='.(isset($INFO) ? $INFO['namespace'] : '')
+ );
+ }
+ if(($ACT == 'show' || $ACT == 'search') && $INFO['writable']) {
+ $head['link'][] = array(
+ 'rel' => 'edit',
+ 'title'=> $lang['btn_edit'],
+ 'href' => wl($ID, 'do=edit', false, '&')
+ );
+ }
+
+ if(actionOK('rss') && $ACT == 'search') {
+ $head['link'][] = array(
+ 'rel' => 'alternate', 'type'=> 'application/rss+xml',
+ 'title'=> $lang['searchresult'],
+ 'href' => DOKU_BASE.'feed.php?mode=search&q='.$QUERY
+ );
+ }
+
+ if(actionOK('export_xhtml')) {
+ $head['link'][] = array(
+ 'rel' => 'alternate', 'type'=> 'text/html', 'title'=> $lang['plainhtml'],
+ 'href'=> exportlink($ID, 'xhtml', '', false, '&')
+ );
+ }
+
+ if(actionOK('export_raw')) {
+ $head['link'][] = array(
+ 'rel' => 'alternate', 'type'=> 'text/plain', 'title'=> $lang['wikimarkup'],
+ 'href'=> exportlink($ID, 'raw', '', false, '&')
+ );
+ }
+ }
+
+ // setup robot tags apropriate for different modes
+ if(($ACT == 'show' || $ACT == 'export_xhtml') && !$REV) {
+ if($INFO['exists']) {
+ //delay indexing:
+ if((time() - $INFO['lastmod']) >= $conf['indexdelay'] && !isHiddenPage($ID) ) {
+ $head['meta'][] = array('name'=> 'robots', 'content'=> 'index,follow');
+ } else {
+ $head['meta'][] = array('name'=> 'robots', 'content'=> 'noindex,nofollow');
+ }
+ $canonicalUrl = wl($ID, '', true, '&');
+ if ($ID == $conf['start']) {
+ $canonicalUrl = DOKU_URL;
+ }
+ $head['link'][] = array('rel'=> 'canonical', 'href'=> $canonicalUrl);
+ } else {
+ $head['meta'][] = array('name'=> 'robots', 'content'=> 'noindex,follow');
+ }
+ } elseif(defined('DOKU_MEDIADETAIL')) {
+ $head['meta'][] = array('name'=> 'robots', 'content'=> 'index,follow');
+ } else {
+ $head['meta'][] = array('name'=> 'robots', 'content'=> 'noindex,nofollow');
+ }
+
+ // set metadata
+ if($ACT == 'show' || $ACT == 'export_xhtml') {
+ // keywords (explicit or implicit)
+ if(!empty($INFO['meta']['subject'])) {
+ $head['meta'][] = array('name'=> 'keywords', 'content'=> join(',', $INFO['meta']['subject']));
+ } else {
+ $head['meta'][] = array('name'=> 'keywords', 'content'=> str_replace(':', ',', $ID));
+ }
+ }
+
+ // load stylesheets
+ $head['link'][] = array(
+ 'rel' => 'stylesheet',
+ 'href'=> DOKU_BASE.'lib/exe/css.php?t='.rawurlencode($conf['template']).'&tseed='.$tseed
+ );
+
+ $script = "var NS='".(isset($INFO)?$INFO['namespace']:'')."';";
+ if($conf['useacl'] && $INPUT->server->str('REMOTE_USER')) {
+ $script .= "var SIG=".toolbar_signature().";";
+ }
+ jsinfo();
+ $script .= 'var JSINFO = ' . json_encode($JSINFO).';';
+ $head['script'][] = array('_data'=> $script);
+
+ // load jquery
+ $jquery = getCdnUrls();
+ foreach($jquery as $src) {
+ $head['script'][] = array(
+ 'charset' => 'utf-8',
+ '_data' => '',
+ 'src' => $src,
+ ) + ($conf['defer_js'] ? [ 'defer' => 'defer'] : []);
+ }
+
+ // load our javascript dispatcher
+ $head['script'][] = array(
+ 'charset'=> 'utf-8', '_data'=> '',
+ 'src' => DOKU_BASE.'lib/exe/js.php'.'?t='.rawurlencode($conf['template']).'&tseed='.$tseed,
+ ) + ($conf['defer_js'] ? [ 'defer' => 'defer'] : []);
+
+ // trigger event here
+ Event::createAndTrigger('TPL_METAHEADER_OUTPUT', $head, '_tpl_metaheaders_action', true);
+ return true;
+}
+
+/**
+ * prints the array build by tpl_metaheaders
+ *
+ * $data is an array of different header tags. Each tag can have multiple
+ * instances. Attributes are given as key value pairs. Values will be HTML
+ * encoded automatically so they should be provided as is in the $data array.
+ *
+ * For tags having a body attribute specify the body data in the special
+ * attribute '_data'. This field will NOT BE ESCAPED automatically.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array $data
+ */
+function _tpl_metaheaders_action($data) {
+ foreach($data as $tag => $inst) {
+ if($tag == 'script') {
+ echo "<!--[if gte IE 9]><!-->\n"; // no scripts for old IE
+ }
+ foreach($inst as $attr) {
+ if ( empty($attr) ) { continue; }
+ echo '<', $tag, ' ', buildAttributes($attr);
+ if(isset($attr['_data']) || $tag == 'script') {
+ if($tag == 'script' && $attr['_data'])
+ $attr['_data'] = "/*<![CDATA[*/".
+ $attr['_data'].
+ "\n/*!]]>*/";
+
+ echo '>', $attr['_data'], '</', $tag, '>';
+ } else {
+ echo '/>';
+ }
+ echo "\n";
+ }
+ if($tag == 'script') {
+ echo "<!--<![endif]-->\n";
+ }
+ }
+}
+
+/**
+ * Print a link
+ *
+ * Just builds a link.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $url
+ * @param string $name
+ * @param string $more
+ * @param bool $return if true return the link html, otherwise print
+ * @return bool|string html of the link, or true if printed
+ */
+function tpl_link($url, $name, $more = '', $return = false) {
+ $out = '<a href="'.$url.'" ';
+ if($more) $out .= ' '.$more;
+ $out .= ">$name</a>";
+ if($return) return $out;
+ print $out;
+ return true;
+}
+
+/**
+ * Prints a link to a WikiPage
+ *
+ * Wrapper around html_wikilink
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id page id
+ * @param string|null $name the name of the link
+ * @param bool $return
+ * @return true|string
+ */
+function tpl_pagelink($id, $name = null, $return = false) {
+ $out = '<bdi>'.html_wikilink($id, $name).'</bdi>';
+ if($return) return $out;
+ print $out;
+ return true;
+}
+
+/**
+ * get the parent page
+ *
+ * Tries to find out which page is parent.
+ * returns false if none is available
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id page id
+ * @return false|string
+ */
+function tpl_getparent($id) {
+ $parent = getNS($id).':';
+ resolve_pageid('', $parent, $exists);
+ if($parent == $id) {
+ $pos = strrpos(getNS($id), ':');
+ $parent = substr($parent, 0, $pos).':';
+ resolve_pageid('', $parent, $exists);
+ if($parent == $id) return false;
+ }
+ return $parent;
+}
+
+/**
+ * Print one of the buttons
+ *
+ * @author Adrian Lang <mail@adrianlang.de>
+ * @see tpl_get_action
+ *
+ * @param string $type
+ * @param bool $return
+ * @return bool|string html, or false if no data, true if printed
+ * @deprecated 2017-09-01 see devel:menus
+ */
+function tpl_button($type, $return = false) {
+ dbg_deprecated('see devel:menus');
+ $data = tpl_get_action($type);
+ if($data === false) {
+ return false;
+ } elseif(!is_array($data)) {
+ $out = sprintf($data, 'button');
+ } else {
+ /**
+ * @var string $accesskey
+ * @var string $id
+ * @var string $method
+ * @var array $params
+ */
+ extract($data);
+ if($id === '#dokuwiki__top') {
+ $out = html_topbtn();
+ } else {
+ $out = html_btn($type, $id, $accesskey, $params, $method);
+ }
+ }
+ if($return) return $out;
+ echo $out;
+ return true;
+}
+
+/**
+ * Like the action buttons but links
+ *
+ * @author Adrian Lang <mail@adrianlang.de>
+ * @see tpl_get_action
+ *
+ * @param string $type action command
+ * @param string $pre prefix of link
+ * @param string $suf suffix of link
+ * @param string $inner innerHML of link
+ * @param bool $return if true it returns html, otherwise prints
+ * @return bool|string html or false if no data, true if printed
+ * @deprecated 2017-09-01 see devel:menus
+ */
+function tpl_actionlink($type, $pre = '', $suf = '', $inner = '', $return = false) {
+ dbg_deprecated('see devel:menus');
+ global $lang;
+ $data = tpl_get_action($type);
+ if($data === false) {
+ return false;
+ } elseif(!is_array($data)) {
+ $out = sprintf($data, 'link');
+ } else {
+ /**
+ * @var string $accesskey
+ * @var string $id
+ * @var string $method
+ * @var bool $nofollow
+ * @var array $params
+ * @var string $replacement
+ */
+ extract($data);
+ if(strpos($id, '#') === 0) {
+ $linktarget = $id;
+ } else {
+ $linktarget = wl($id, $params);
+ }
+ $caption = $lang['btn_'.$type];
+ if(strpos($caption, '%s')){
+ $caption = sprintf($caption, $replacement);
+ }
+ $akey = $addTitle = '';
+ if($accesskey) {
+ $akey = 'accesskey="'.$accesskey.'" ';
+ $addTitle = ' ['.strtoupper($accesskey).']';
+ }
+ $rel = $nofollow ? 'rel="nofollow" ' : '';
+ $out = tpl_link(
+ $linktarget, $pre.(($inner) ? $inner : $caption).$suf,
+ 'class="action '.$type.'" '.
+ $akey.$rel.
+ 'title="'.hsc($caption).$addTitle.'"', true
+ );
+ }
+ if($return) return $out;
+ echo $out;
+ return true;
+}
+
+/**
+ * Check the actions and get data for buttons and links
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
+ * @author Adrian Lang <mail@adrianlang.de>
+ *
+ * @param string $type
+ * @return array|bool|string
+ * @deprecated 2017-09-01 see devel:menus
+ */
+function tpl_get_action($type) {
+ dbg_deprecated('see devel:menus');
+ if($type == 'history') $type = 'revisions';
+ if($type == 'subscription') $type = 'subscribe';
+ if($type == 'img_backto') $type = 'imgBackto';
+
+ $class = '\\dokuwiki\\Menu\\Item\\' . ucfirst($type);
+ if(class_exists($class)) {
+ try {
+ /** @var \dokuwiki\Menu\Item\AbstractItem $item */
+ $item = new $class;
+ $data = $item->getLegacyData();
+ $unknown = false;
+ } catch(\RuntimeException $ignored) {
+ return false;
+ }
+ } else {
+ global $ID;
+ $data = array(
+ 'accesskey' => null,
+ 'type' => $type,
+ 'id' => $ID,
+ 'method' => 'get',
+ 'params' => array('do' => $type),
+ 'nofollow' => true,
+ 'replacement' => '',
+ );
+ $unknown = true;
+ }
+
+ $evt = new Event('TPL_ACTION_GET', $data);
+ if($evt->advise_before()) {
+ //handle unknown types
+ if($unknown) {
+ $data = '[unknown %s type]';
+ }
+ }
+ $evt->advise_after();
+ unset($evt);
+
+ return $data;
+}
+
+/**
+ * Wrapper around tpl_button() and tpl_actionlink()
+ *
+ * @author Anika Henke <anika@selfthinker.org>
+ *
+ * @param string $type action command
+ * @param bool $link link or form button?
+ * @param string|bool $wrapper HTML element wrapper
+ * @param bool $return return or print
+ * @param string $pre prefix for links
+ * @param string $suf suffix for links
+ * @param string $inner inner HTML for links
+ * @return bool|string
+ * @deprecated 2017-09-01 see devel:menus
+ */
+function tpl_action($type, $link = false, $wrapper = false, $return = false, $pre = '', $suf = '', $inner = '') {
+ dbg_deprecated('see devel:menus');
+ $out = '';
+ if($link) {
+ $out .= tpl_actionlink($type, $pre, $suf, $inner, true);
+ } else {
+ $out .= tpl_button($type, true);
+ }
+ if($out && $wrapper) $out = "<$wrapper>$out</$wrapper>";
+
+ if($return) return $out;
+ print $out;
+ return $out ? true : false;
+}
+
+/**
+ * Print the search form
+ *
+ * If the first parameter is given a div with the ID 'qsearch_out' will
+ * be added which instructs the ajax pagequicksearch to kick in and place
+ * its output into this div. The second parameter controls the propritary
+ * attribute autocomplete. If set to false this attribute will be set with an
+ * value of "off" to instruct the browser to disable it's own built in
+ * autocompletion feature (MSIE and Firefox)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param bool $ajax
+ * @param bool $autocomplete
+ * @return bool
+ */
+function tpl_searchform($ajax = true, $autocomplete = true) {
+ global $lang;
+ global $ACT;
+ global $QUERY;
+ global $ID;
+
+ // don't print the search form if search action has been disabled
+ if(!actionOK('search')) return false;
+
+ $searchForm = new dokuwiki\Form\Form([
+ 'action' => wl(),
+ 'method' => 'get',
+ 'role' => 'search',
+ 'class' => 'search',
+ 'id' => 'dw__search',
+ ], true);
+ $searchForm->addTagOpen('div')->addClass('no');
+ $searchForm->setHiddenField('do', 'search');
+ $searchForm->setHiddenField('id', $ID);
+ $searchForm->addTextInput('q')
+ ->addClass('edit')
+ ->attrs([
+ 'title' => '[F]',
+ 'accesskey' => 'f',
+ 'placeholder' => $lang['btn_search'],
+ 'autocomplete' => $autocomplete ? 'on' : 'off',
+ ])
+ ->id('qsearch__in')
+ ->val($ACT === 'search' ? $QUERY : '')
+ ->useInput(false)
+ ;
+ $searchForm->addButton('', $lang['btn_search'])->attrs([
+ 'type' => 'submit',
+ 'title' => $lang['btn_search'],
+ ]);
+ if ($ajax) {
+ $searchForm->addTagOpen('div')->id('qsearch__out')->addClass('ajax_qsearch JSpopup');
+ $searchForm->addTagClose('div');
+ }
+ $searchForm->addTagClose('div');
+ Event::createAndTrigger('FORM_QUICKSEARCH_OUTPUT', $searchForm);
+
+ echo $searchForm->toHTML();
+
+ return true;
+}
+
+/**
+ * Print the breadcrumbs trace
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $sep Separator between entries
+ * @param bool $return return or print
+ * @return bool|string
+ */
+function tpl_breadcrumbs($sep = null, $return = false) {
+ global $lang;
+ global $conf;
+
+ //check if enabled
+ if(!$conf['breadcrumbs']) return false;
+
+ //set default
+ if(is_null($sep)) $sep = '•';
+
+ $out='';
+
+ $crumbs = breadcrumbs(); //setup crumb trace
+
+ $crumbs_sep = ' <span class="bcsep">'.$sep.'</span> ';
+
+ //render crumbs, highlight the last one
+ $out .= '<span class="bchead">'.$lang['breadcrumb'].'</span>';
+ $last = count($crumbs);
+ $i = 0;
+ foreach($crumbs as $id => $name) {
+ $i++;
+ $out .= $crumbs_sep;
+ if($i == $last) $out .= '<span class="curid">';
+ $out .= '<bdi>' . tpl_link(wl($id), hsc($name), 'class="breadcrumbs" title="'.$id.'"', true) . '</bdi>';
+ if($i == $last) $out .= '</span>';
+ }
+ if($return) return $out;
+ print $out;
+ return $out ? true : false;
+}
+
+/**
+ * Hierarchical breadcrumbs
+ *
+ * This code was suggested as replacement for the usual breadcrumbs.
+ * It only makes sense with a deep site structure.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Nigel McNie <oracle.shinoda@gmail.com>
+ * @author Sean Coates <sean@caedmon.net>
+ * @author <fredrik@averpil.com>
+ * @todo May behave strangely in RTL languages
+ *
+ * @param string $sep Separator between entries
+ * @param bool $return return or print
+ * @return bool|string
+ */
+function tpl_youarehere($sep = null, $return = false) {
+ global $conf;
+ global $ID;
+ global $lang;
+
+ // check if enabled
+ if(!$conf['youarehere']) return false;
+
+ //set default
+ if(is_null($sep)) $sep = ' » ';
+
+ $out = '';
+
+ $parts = explode(':', $ID);
+ $count = count($parts);
+
+ $out .= '<span class="bchead">'.$lang['youarehere'].' </span>';
+
+ // always print the startpage
+ $out .= '<span class="home">' . tpl_pagelink(':'.$conf['start'], null, true) . '</span>';
+
+ // print intermediate namespace links
+ $part = '';
+ for($i = 0; $i < $count - 1; $i++) {
+ $part .= $parts[$i].':';
+ $page = $part;
+ if($page == $conf['start']) continue; // Skip startpage
+
+ // output
+ $out .= $sep . tpl_pagelink($page, null, true);
+ }
+
+ // print current page, skipping start page, skipping for namespace index
+ resolve_pageid('', $page, $exists);
+ if (isset($page) && $page == $part.$parts[$i]) {
+ if($return) return $out;
+ print $out;
+ return true;
+ }
+ $page = $part.$parts[$i];
+ if($page == $conf['start']) {
+ if($return) return $out;
+ print $out;
+ return true;
+ }
+ $out .= $sep;
+ $out .= tpl_pagelink($page, null, true);
+ if($return) return $out;
+ print $out;
+ return $out ? true : false;
+}
+
+/**
+ * Print info if the user is logged in
+ * and show full name in that case
+ *
+ * Could be enhanced with a profile link in future?
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @return bool
+ */
+function tpl_userinfo() {
+ global $lang;
+ /** @var Input $INPUT */
+ global $INPUT;
+
+ if($INPUT->server->str('REMOTE_USER')) {
+ print $lang['loggedinas'].' '.userlink();
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Print some info about the current page
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param bool $ret return content instead of printing it
+ * @return bool|string
+ */
+function tpl_pageinfo($ret = false) {
+ global $conf;
+ global $lang;
+ global $INFO;
+ global $ID;
+
+ // return if we are not allowed to view the page
+ if(!auth_quickaclcheck($ID)) {
+ return false;
+ }
+
+ // prepare date and path
+ $fn = $INFO['filepath'];
+ if(!$conf['fullpath']) {
+ if($INFO['rev']) {
+ $fn = str_replace($conf['olddir'].'/', '', $fn);
+ } else {
+ $fn = str_replace($conf['datadir'].'/', '', $fn);
+ }
+ }
+ $fn = utf8_decodeFN($fn);
+ $date = dformat($INFO['lastmod']);
+
+ // print it
+ if($INFO['exists']) {
+ $out = '';
+ $out .= '<bdi>'.$fn.'</bdi>';
+ $out .= ' · ';
+ $out .= $lang['lastmod'];
+ $out .= ' ';
+ $out .= $date;
+ if($INFO['editor']) {
+ $out .= ' '.$lang['by'].' ';
+ $out .= '<bdi>'.editorinfo($INFO['editor']).'</bdi>';
+ } else {
+ $out .= ' ('.$lang['external_edit'].')';
+ }
+ if($INFO['locked']) {
+ $out .= ' · ';
+ $out .= $lang['lockedby'];
+ $out .= ' ';
+ $out .= '<bdi>'.editorinfo($INFO['locked']).'</bdi>';
+ }
+ if($ret) {
+ return $out;
+ } else {
+ echo $out;
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Prints or returns the name of the given page (current one if none given).
+ *
+ * If useheading is enabled this will use the first headline else
+ * the given ID is used.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $id page id
+ * @param bool $ret return content instead of printing
+ * @return bool|string
+ */
+function tpl_pagetitle($id = null, $ret = false) {
+ global $ACT, $INPUT, $conf, $lang;
+
+ if(is_null($id)) {
+ global $ID;
+ $id = $ID;
+ }
+
+ $name = $id;
+ if(useHeading('navigation')) {
+ $first_heading = p_get_first_heading($id);
+ if($first_heading) $name = $first_heading;
+ }
+
+ // default page title is the page name, modify with the current action
+ switch ($ACT) {
+ // admin functions
+ case 'admin' :
+ $page_title = $lang['btn_admin'];
+ // try to get the plugin name
+ /** @var $plugin AdminPlugin */
+ if ($plugin = plugin_getRequestAdminPlugin()){
+ $plugin_title = $plugin->getMenuText($conf['lang']);
+ $page_title = $plugin_title ? $plugin_title : $plugin->getPluginName();
+ }
+ break;
+
+ // user functions
+ case 'login' :
+ case 'profile' :
+ case 'register' :
+ case 'resendpwd' :
+ $page_title = $lang['btn_'.$ACT];
+ break;
+
+ // wiki functions
+ case 'search' :
+ case 'index' :
+ $page_title = $lang['btn_'.$ACT];
+ break;
+
+ // page functions
+ case 'edit' :
+ case 'preview' :
+ $page_title = "✎ ".$name;
+ break;
+
+ case 'revisions' :
+ $page_title = $name . ' - ' . $lang['btn_revs'];
+ break;
+
+ case 'backlink' :
+ case 'recent' :
+ case 'subscribe' :
+ $page_title = $name . ' - ' . $lang['btn_'.$ACT];
+ break;
+
+ default : // SHOW and anything else not included
+ $page_title = $name;
+ }
+
+ if($ret) {
+ return hsc($page_title);
+ } else {
+ print hsc($page_title);
+ return true;
+ }
+}
+
+/**
+ * Returns the requested EXIF/IPTC tag from the current image
+ *
+ * If $tags is an array all given tags are tried until a
+ * value is found. If no value is found $alt is returned.
+ *
+ * Which texts are known is defined in the functions _exifTagNames
+ * and _iptcTagNames() in inc/jpeg.php (You need to prepend IPTC
+ * to the names of the latter one)
+ *
+ * Only allowed in: detail.php
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param array|string $tags tag or array of tags to try
+ * @param string $alt alternative output if no data was found
+ * @param null|string $src the image src, uses global $SRC if not given
+ * @return string
+ */
+function tpl_img_getTag($tags, $alt = '', $src = null) {
+ // Init Exif Reader
+ global $SRC;
+
+ if(is_null($src)) $src = $SRC;
+
+ static $meta = null;
+ if(is_null($meta)) $meta = new JpegMeta($src);
+ if($meta === false) return $alt;
+ $info = cleanText($meta->getField($tags));
+ if($info == false) return $alt;
+ return $info;
+}
+
+/**
+ * Returns a description list of the metatags of the current image
+ *
+ * @return string html of description list
+ */
+function tpl_img_meta() {
+ global $lang;
+
+ $tags = tpl_get_img_meta();
+
+ echo '<dl>';
+ foreach($tags as $tag) {
+ $label = $lang[$tag['langkey']];
+ if(!$label) $label = $tag['langkey'] . ':';
+
+ echo '<dt>'.$label.'</dt><dd>';
+ if ($tag['type'] == 'date') {
+ echo dformat($tag['value']);
+ } else {
+ echo hsc($tag['value']);
+ }
+ echo '</dd>';
+ }
+ echo '</dl>';
+}
+
+/**
+ * Returns metadata as configured in mediameta config file, ready for creating html
+ *
+ * @return array with arrays containing the entries:
+ * - string langkey key to lookup in the $lang var, if not found printed as is
+ * - string type type of value
+ * - string value tag value (unescaped)
+ */
+function tpl_get_img_meta() {
+
+ $config_files = getConfigFiles('mediameta');
+ foreach ($config_files as $config_file) {
+ if(file_exists($config_file)) {
+ include($config_file);
+ }
+ }
+ /** @var array $fields the included array with metadata */
+
+ $tags = array();
+ foreach($fields as $tag){
+ $t = array();
+ if (!empty($tag[0])) {
+ $t = array($tag[0]);
+ }
+ if(is_array($tag[3])) {
+ $t = array_merge($t,$tag[3]);
+ }
+ $value = tpl_img_getTag($t);
+ if ($value) {
+ $tags[] = array('langkey' => $tag[1], 'type' => $tag[2], 'value' => $value);
+ }
+ }
+ return $tags;
+}
+
+/**
+ * Prints the image with a link to the full sized version
+ *
+ * Only allowed in: detail.php
+ *
+ * @triggers TPL_IMG_DISPLAY
+ * @param $maxwidth int - maximal width of the image
+ * @param $maxheight int - maximal height of the image
+ * @param $link bool - link to the orginal size?
+ * @param $params array - additional image attributes
+ * @return bool Result of TPL_IMG_DISPLAY
+ */
+function tpl_img($maxwidth = 0, $maxheight = 0, $link = true, $params = null) {
+ global $IMG;
+ /** @var Input $INPUT */
+ global $INPUT;
+ global $REV;
+ $w = (int) tpl_img_getTag('File.Width');
+ $h = (int) tpl_img_getTag('File.Height');
+
+ //resize to given max values
+ $ratio = 1;
+ if($w >= $h) {
+ if($maxwidth && $w >= $maxwidth) {
+ $ratio = $maxwidth / $w;
+ } elseif($maxheight && $h > $maxheight) {
+ $ratio = $maxheight / $h;
+ }
+ } else {
+ if($maxheight && $h >= $maxheight) {
+ $ratio = $maxheight / $h;
+ } elseif($maxwidth && $w > $maxwidth) {
+ $ratio = $maxwidth / $w;
+ }
+ }
+ if($ratio) {
+ $w = floor($ratio * $w);
+ $h = floor($ratio * $h);
+ }
+
+ //prepare URLs
+ $url = ml($IMG, array('cache'=> $INPUT->str('cache'),'rev'=>$REV), true, '&');
+ $src = ml($IMG, array('cache'=> $INPUT->str('cache'),'rev'=>$REV, 'w'=> $w, 'h'=> $h), true, '&');
+
+ //prepare attributes
+ $alt = tpl_img_getTag('Simple.Title');
+ if(is_null($params)) {
+ $p = array();
+ } else {
+ $p = $params;
+ }
+ if($w) $p['width'] = $w;
+ if($h) $p['height'] = $h;
+ $p['class'] = 'img_detail';
+ if($alt) {
+ $p['alt'] = $alt;
+ $p['title'] = $alt;
+ } else {
+ $p['alt'] = '';
+ }
+ $p['src'] = $src;
+
+ $data = array('url'=> ($link ? $url : null), 'params'=> $p);
+ return Event::createAndTrigger('TPL_IMG_DISPLAY', $data, '_tpl_img_action', true);
+}
+
+/**
+ * Default action for TPL_IMG_DISPLAY
+ *
+ * @param array $data
+ * @return bool
+ */
+function _tpl_img_action($data) {
+ global $lang;
+ $p = buildAttributes($data['params']);
+
+ if($data['url']) print '<a href="'.hsc($data['url']).'" title="'.$lang['mediaview'].'">';
+ print '<img '.$p.'/>';
+ if($data['url']) print '</a>';
+ return true;
+}
+
+/**
+ * This function inserts a small gif which in reality is the indexer function.
+ *
+ * Should be called somewhere at the very end of the main.php
+ * template
+ *
+ * @return bool
+ */
+function tpl_indexerWebBug() {
+ global $ID;
+
+ $p = array();
+ $p['src'] = DOKU_BASE.'lib/exe/taskrunner.php?id='.rawurlencode($ID).
+ '&'.time();
+ $p['width'] = 2; //no more 1x1 px image because we live in times of ad blockers...
+ $p['height'] = 1;
+ $p['alt'] = '';
+ $att = buildAttributes($p);
+ print "<img $att />";
+ return true;
+}
+
+/**
+ * tpl_getConf($id)
+ *
+ * use this function to access template configuration variables
+ *
+ * @param string $id name of the value to access
+ * @param mixed $notset what to return if the setting is not available
+ * @return mixed
+ */
+function tpl_getConf($id, $notset=false) {
+ global $conf;
+ static $tpl_configloaded = false;
+
+ $tpl = $conf['template'];
+
+ if(!$tpl_configloaded) {
+ $tconf = tpl_loadConfig();
+ if($tconf !== false) {
+ foreach($tconf as $key => $value) {
+ if(isset($conf['tpl'][$tpl][$key])) continue;
+ $conf['tpl'][$tpl][$key] = $value;
+ }
+ $tpl_configloaded = true;
+ }
+ }
+
+ if(isset($conf['tpl'][$tpl][$id])){
+ return $conf['tpl'][$tpl][$id];
+ }
+
+ return $notset;
+}
+
+/**
+ * tpl_loadConfig()
+ *
+ * reads all template configuration variables
+ * this function is automatically called by tpl_getConf()
+ *
+ * @return array
+ */
+function tpl_loadConfig() {
+
+ $file = tpl_incdir().'/conf/default.php';
+ $conf = array();
+
+ if(!file_exists($file)) return false;
+
+ // load default config file
+ include($file);
+
+ return $conf;
+}
+
+// language methods
+/**
+ * tpl_getLang($id)
+ *
+ * use this function to access template language variables
+ *
+ * @param string $id key of language string
+ * @return string
+ */
+function tpl_getLang($id) {
+ static $lang = array();
+
+ if(count($lang) === 0) {
+ global $conf, $config_cascade; // definitely don't invoke "global $lang"
+
+ $path = tpl_incdir() . 'lang/';
+
+ $lang = array();
+
+ // don't include once
+ @include($path . 'en/lang.php');
+ foreach($config_cascade['lang']['template'] as $config_file) {
+ if(file_exists($config_file . $conf['template'] . '/en/lang.php')) {
+ include($config_file . $conf['template'] . '/en/lang.php');
+ }
+ }
+
+ if($conf['lang'] != 'en') {
+ @include($path . $conf['lang'] . '/lang.php');
+ foreach($config_cascade['lang']['template'] as $config_file) {
+ if(file_exists($config_file . $conf['template'] . '/' . $conf['lang'] . '/lang.php')) {
+ include($config_file . $conf['template'] . '/' . $conf['lang'] . '/lang.php');
+ }
+ }
+ }
+ }
+ return $lang[$id];
+}
+
+/**
+ * Retrieve a language dependent file and pass to xhtml renderer for display
+ * template equivalent of p_locale_xhtml()
+ *
+ * @param string $id id of language dependent wiki page
+ * @return string parsed contents of the wiki page in xhtml format
+ */
+function tpl_locale_xhtml($id) {
+ return p_cached_output(tpl_localeFN($id));
+}
+
+/**
+ * Prepends appropriate path for a language dependent filename
+ *
+ * @param string $id id of localized text
+ * @return string wiki text
+ */
+function tpl_localeFN($id) {
+ $path = tpl_incdir().'lang/';
+ global $conf;
+ $file = DOKU_CONF.'template_lang/'.$conf['template'].'/'.$conf['lang'].'/'.$id.'.txt';
+ if (!file_exists($file)){
+ $file = $path.$conf['lang'].'/'.$id.'.txt';
+ if(!file_exists($file)){
+ //fall back to english
+ $file = $path.'en/'.$id.'.txt';
+ }
+ }
+ return $file;
+}
+
+/**
+ * prints the "main content" in the mediamanager popup
+ *
+ * Depending on the user's actions this may be a list of
+ * files in a namespace, the meta editing dialog or
+ * a message of referencing pages
+ *
+ * Only allowed in mediamanager.php
+ *
+ * @triggers MEDIAMANAGER_CONTENT_OUTPUT
+ * @param bool $fromajax - set true when calling this function via ajax
+ * @param string $sort
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function tpl_mediaContent($fromajax = false, $sort='natural') {
+ global $IMG;
+ global $AUTH;
+ global $INUSE;
+ global $NS;
+ global $JUMPTO;
+ /** @var Input $INPUT */
+ global $INPUT;
+
+ $do = $INPUT->extract('do')->str('do');
+ if(in_array($do, array('save', 'cancel'))) $do = '';
+
+ if(!$do) {
+ if($INPUT->bool('edit')) {
+ $do = 'metaform';
+ } elseif(is_array($INUSE)) {
+ $do = 'filesinuse';
+ } else {
+ $do = 'filelist';
+ }
+ }
+
+ // output the content pane, wrapped in an event.
+ if(!$fromajax) ptln('<div id="media__content">');
+ $data = array('do' => $do);
+ $evt = new Event('MEDIAMANAGER_CONTENT_OUTPUT', $data);
+ if($evt->advise_before()) {
+ $do = $data['do'];
+ if($do == 'filesinuse') {
+ media_filesinuse($INUSE, $IMG);
+ } elseif($do == 'filelist') {
+ media_filelist($NS, $AUTH, $JUMPTO,false,$sort);
+ } elseif($do == 'searchlist') {
+ media_searchlist($INPUT->str('q'), $NS, $AUTH);
+ } else {
+ msg('Unknown action '.hsc($do), -1);
+ }
+ }
+ $evt->advise_after();
+ unset($evt);
+ if(!$fromajax) ptln('</div>');
+
+}
+
+/**
+ * Prints the central column in full-screen media manager
+ * Depending on the opened tab this may be a list of
+ * files in a namespace, upload form or search form
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ */
+function tpl_mediaFileList() {
+ global $AUTH;
+ global $NS;
+ global $JUMPTO;
+ global $lang;
+ /** @var Input $INPUT */
+ global $INPUT;
+
+ $opened_tab = $INPUT->str('tab_files');
+ if(!$opened_tab || !in_array($opened_tab, array('files', 'upload', 'search'))) $opened_tab = 'files';
+ if($INPUT->str('mediado') == 'update') $opened_tab = 'upload';
+
+ echo '<h2 class="a11y">'.$lang['mediaselect'].'</h2>'.NL;
+
+ media_tabs_files($opened_tab);
+
+ echo '<div class="panelHeader">'.NL;
+ echo '<h3>';
+ $tabTitle = ($NS) ? $NS : '['.$lang['mediaroot'].']';
+ printf($lang['media_'.$opened_tab], '<strong>'.hsc($tabTitle).'</strong>');
+ echo '</h3>'.NL;
+ if($opened_tab === 'search' || $opened_tab === 'files') {
+ media_tab_files_options();
+ }
+ echo '</div>'.NL;
+
+ echo '<div class="panelContent">'.NL;
+ if($opened_tab == 'files') {
+ media_tab_files($NS, $AUTH, $JUMPTO);
+ } elseif($opened_tab == 'upload') {
+ media_tab_upload($NS, $AUTH, $JUMPTO);
+ } elseif($opened_tab == 'search') {
+ media_tab_search($NS, $AUTH);
+ }
+ echo '</div>'.NL;
+}
+
+/**
+ * Prints the third column in full-screen media manager
+ * Depending on the opened tab this may be details of the
+ * selected file, the meta editing dialog or
+ * list of file revisions
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ *
+ * @param string $image
+ * @param boolean $rev
+ */
+function tpl_mediaFileDetails($image, $rev) {
+ global $conf, $DEL, $lang;
+ /** @var Input $INPUT */
+ global $INPUT;
+
+ $removed = (
+ !file_exists(mediaFN($image)) &&
+ file_exists(mediaMetaFN($image, '.changes')) &&
+ $conf['mediarevisions']
+ );
+ if(!$image || (!file_exists(mediaFN($image)) && !$removed) || $DEL) return;
+ if($rev && !file_exists(mediaFN($image, $rev))) $rev = false;
+ $ns = getNS($image);
+ $do = $INPUT->str('mediado');
+
+ $opened_tab = $INPUT->str('tab_details');
+
+ $tab_array = array('view');
+ list(, $mime) = mimetype($image);
+ if($mime == 'image/jpeg') {
+ $tab_array[] = 'edit';
+ }
+ if($conf['mediarevisions']) {
+ $tab_array[] = 'history';
+ }
+
+ if(!$opened_tab || !in_array($opened_tab, $tab_array)) $opened_tab = 'view';
+ if($INPUT->bool('edit')) $opened_tab = 'edit';
+ if($do == 'restore') $opened_tab = 'view';
+
+ media_tabs_details($image, $opened_tab);
+
+ echo '<div class="panelHeader"><h3>';
+ list($ext) = mimetype($image, false);
+ $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
+ $class = 'select mediafile mf_'.$class;
+ $attributes = $rev ? ['rev' => $rev] : [];
+ $tabTitle = '<strong><a href="'.ml($image, $attributes).'" class="'.$class.'" title="'.$lang['mediaview'].'">'.
+ $image.'</a>'.'</strong>';
+ if($opened_tab === 'view' && $rev) {
+ printf($lang['media_viewold'], $tabTitle, dformat($rev));
+ } else {
+ printf($lang['media_'.$opened_tab], $tabTitle);
+ }
+
+ echo '</h3></div>'.NL;
+
+ echo '<div class="panelContent">'.NL;
+
+ if($opened_tab == 'view') {
+ media_tab_view($image, $ns, null, $rev);
+
+ } elseif($opened_tab == 'edit' && !$removed) {
+ media_tab_edit($image, $ns);
+
+ } elseif($opened_tab == 'history' && $conf['mediarevisions']) {
+ media_tab_history($image, $ns);
+ }
+
+ echo '</div>'.NL;
+}
+
+/**
+ * prints the namespace tree in the mediamanager popup
+ *
+ * Only allowed in mediamanager.php
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function tpl_mediaTree() {
+ global $NS;
+ ptln('<div id="media__tree">');
+ media_nstree($NS);
+ ptln('</div>');
+}
+
+/**
+ * Print a dropdown menu with all DokuWiki actions
+ *
+ * Note: this will not use any pretty URLs
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $empty empty option label
+ * @param string $button submit button label
+ * @deprecated 2017-09-01 see devel:menus
+ */
+function tpl_actiondropdown($empty = '', $button = '&gt;') {
+ dbg_deprecated('see devel:menus');
+ $menu = new \dokuwiki\Menu\MobileMenu();
+ echo $menu->getDropdown($empty, $button);
+}
+
+/**
+ * Print a informational line about the used license
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $img print image? (|button|badge)
+ * @param bool $imgonly skip the textual description?
+ * @param bool $return when true don't print, but return HTML
+ * @param bool $wrap wrap in div with class="license"?
+ * @return string
+ */
+function tpl_license($img = 'badge', $imgonly = false, $return = false, $wrap = true) {
+ global $license;
+ global $conf;
+ global $lang;
+ if(!$conf['license']) return '';
+ if(!is_array($license[$conf['license']])) return '';
+ $lic = $license[$conf['license']];
+ $target = ($conf['target']['extern']) ? ' target="'.$conf['target']['extern'].'"' : '';
+
+ $out = '';
+ if($wrap) $out .= '<div class="license">';
+ if($img) {
+ $src = license_img($img);
+ if($src) {
+ $out .= '<a href="'.$lic['url'].'" rel="license"'.$target;
+ $out .= '><img src="'.DOKU_BASE.$src.'" alt="'.$lic['name'].'" /></a>';
+ if(!$imgonly) $out .= ' ';
+ }
+ }
+ if(!$imgonly) {
+ $out .= $lang['license'].' ';
+ $out .= '<bdi><a href="'.$lic['url'].'" rel="license" class="urlextern"'.$target;
+ $out .= '>'.$lic['name'].'</a></bdi>';
+ }
+ if($wrap) $out .= '</div>';
+
+ if($return) return $out;
+ echo $out;
+ return '';
+}
+
+/**
+ * Includes the rendered HTML of a given page
+ *
+ * This function is useful to populate sidebars or similar features in a
+ * template
+ *
+ * @param string $pageid The page name you want to include
+ * @param bool $print Should the content be printed or returned only
+ * @param bool $propagate Search higher namespaces, too?
+ * @param bool $useacl Include the page only if the ACLs check out?
+ * @return bool|null|string
+ */
+function tpl_include_page($pageid, $print = true, $propagate = false, $useacl = true) {
+ if($propagate) {
+ $pageid = page_findnearest($pageid, $useacl);
+ } elseif($useacl && auth_quickaclcheck($pageid) == AUTH_NONE) {
+ return false;
+ }
+ if(!$pageid) return false;
+
+ global $TOC;
+ $oldtoc = $TOC;
+ $html = p_wiki_xhtml($pageid, '', false);
+ $TOC = $oldtoc;
+
+ if($print) echo $html;
+ return $html;
+}
+
+/**
+ * Display the subscribe form
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+function tpl_subscribe() {
+ global $INFO;
+ global $ID;
+ global $lang;
+ global $conf;
+ $stime_days = $conf['subscribe_time'] / 60 / 60 / 24;
+
+ echo p_locale_xhtml('subscr_form');
+ echo '<h2>'.$lang['subscr_m_current_header'].'</h2>';
+ echo '<div class="level2">';
+ if($INFO['subscribed'] === false) {
+ echo '<p>'.$lang['subscr_m_not_subscribed'].'</p>';
+ } else {
+ echo '<ul>';
+ foreach($INFO['subscribed'] as $sub) {
+ echo '<li><div class="li">';
+ if($sub['target'] !== $ID) {
+ echo '<code class="ns">'.hsc(prettyprint_id($sub['target'])).'</code>';
+ } else {
+ echo '<code class="page">'.hsc(prettyprint_id($sub['target'])).'</code>';
+ }
+ $sstl = sprintf($lang['subscr_style_'.$sub['style']], $stime_days);
+ if(!$sstl) $sstl = hsc($sub['style']);
+ echo ' ('.$sstl.') ';
+
+ echo '<a href="'.wl(
+ $ID,
+ array(
+ 'do' => 'subscribe',
+ 'sub_target'=> $sub['target'],
+ 'sub_style' => $sub['style'],
+ 'sub_action'=> 'unsubscribe',
+ 'sectok' => getSecurityToken()
+ )
+ ).
+ '" class="unsubscribe">'.$lang['subscr_m_unsubscribe'].
+ '</a></div></li>';
+ }
+ echo '</ul>';
+ }
+ echo '</div>';
+
+ // Add new subscription form
+ echo '<h2>'.$lang['subscr_m_new_header'].'</h2>';
+ echo '<div class="level2">';
+ $ns = getNS($ID).':';
+ $targets = array(
+ $ID => '<code class="page">'.prettyprint_id($ID).'</code>',
+ $ns => '<code class="ns">'.prettyprint_id($ns).'</code>',
+ );
+ $styles = array(
+ 'every' => $lang['subscr_style_every'],
+ 'digest' => sprintf($lang['subscr_style_digest'], $stime_days),
+ 'list' => sprintf($lang['subscr_style_list'], $stime_days),
+ );
+
+ $form = new Doku_Form(array('id' => 'subscribe__form'));
+ $form->startFieldset($lang['subscr_m_subscribe']);
+ $form->addRadioSet('sub_target', $targets);
+ $form->startFieldset($lang['subscr_m_receive']);
+ $form->addRadioSet('sub_style', $styles);
+ $form->addHidden('sub_action', 'subscribe');
+ $form->addHidden('do', 'subscribe');
+ $form->addHidden('id', $ID);
+ $form->endFieldset();
+ $form->addElement(form_makeButton('submit', 'subscribe', $lang['subscr_m_subscribe']));
+ html_form('SUBSCRIBE', $form);
+ echo '</div>';
+}
+
+/**
+ * Tries to send already created content right to the browser
+ *
+ * Wraps around ob_flush() and flush()
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function tpl_flush() {
+ if( ob_get_level() > 0 ) ob_flush();
+ flush();
+}
+
+/**
+ * Tries to find a ressource file in the given locations.
+ *
+ * If a given location starts with a colon it is assumed to be a media
+ * file, otherwise it is assumed to be relative to the current template
+ *
+ * @param string[] $search locations to look at
+ * @param bool $abs if to use absolute URL
+ * @param array &$imginfo filled with getimagesize()
+ * @param bool $fallback use fallback image if target isn't found or return 'false' if potential
+ * false result is required
+ * @return string
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function tpl_getMediaFile($search, $abs = false, &$imginfo = null, $fallback = true) {
+ $img = '';
+ $file = '';
+ $ismedia = false;
+ // loop through candidates until a match was found:
+ foreach($search as $img) {
+ if(substr($img, 0, 1) == ':') {
+ $file = mediaFN($img);
+ $ismedia = true;
+ } else {
+ $file = tpl_incdir().$img;
+ $ismedia = false;
+ }
+
+ if(file_exists($file)) break;
+ }
+
+ // manage non existing target
+ if (!file_exists($file)) {
+ // give result for fallback image
+ if ($fallback === true) {
+ $file = DOKU_INC . 'lib/images/blank.gif';
+ // stop process if false result is required (if $fallback is false)
+ } else {
+ return false;
+ }
+ }
+
+ // fetch image data if requested
+ if(!is_null($imginfo)) {
+ $imginfo = getimagesize($file);
+ }
+
+ // build URL
+ if($ismedia) {
+ $url = ml($img, '', true, '', $abs);
+ } else {
+ $url = tpl_basedir().$img;
+ if($abs) $url = DOKU_URL.substr($url, strlen(DOKU_REL));
+ }
+
+ return $url;
+}
+
+/**
+ * PHP include a file
+ *
+ * either from the conf directory if it exists, otherwise use
+ * file in the template's root directory.
+ *
+ * The function honours config cascade settings and looks for the given
+ * file next to the ´main´ config files, in the order protected, local,
+ * default.
+ *
+ * Note: no escaping or sanity checking is done here. Never pass user input
+ * to this function!
+ *
+ * @author Anika Henke <anika@selfthinker.org>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ *
+ * @param string $file
+ */
+function tpl_includeFile($file) {
+ global $config_cascade;
+ foreach(array('protected', 'local', 'default') as $config_group) {
+ if(empty($config_cascade['main'][$config_group])) continue;
+ foreach($config_cascade['main'][$config_group] as $conf_file) {
+ $dir = dirname($conf_file);
+ if(file_exists("$dir/$file")) {
+ include("$dir/$file");
+ return;
+ }
+ }
+ }
+
+ // still here? try the template dir
+ $file = tpl_incdir().$file;
+ if(file_exists($file)) {
+ include($file);
+ }
+}
+
+/**
+ * Returns <link> tag for various icon types (favicon|mobile|generic)
+ *
+ * @author Anika Henke <anika@selfthinker.org>
+ *
+ * @param array $types - list of icon types to display (favicon|mobile|generic)
+ * @return string
+ */
+function tpl_favicon($types = array('favicon')) {
+
+ $return = '';
+
+ foreach($types as $type) {
+ switch($type) {
+ case 'favicon':
+ $look = array(':wiki:favicon.ico', ':favicon.ico', 'images/favicon.ico');
+ $return .= '<link rel="shortcut icon" href="'.tpl_getMediaFile($look).'" />'.NL;
+ break;
+ case 'mobile':
+ $look = array(':wiki:apple-touch-icon.png', ':apple-touch-icon.png', 'images/apple-touch-icon.png');
+ $return .= '<link rel="apple-touch-icon" href="'.tpl_getMediaFile($look).'" />'.NL;
+ break;
+ case 'generic':
+ // ideal world solution, which doesn't work in any browser yet
+ $look = array(':wiki:favicon.svg', ':favicon.svg', 'images/favicon.svg');
+ $return .= '<link rel="icon" href="'.tpl_getMediaFile($look).'" type="image/svg+xml" />'.NL;
+ break;
+ }
+ }
+
+ return $return;
+}
+
+/**
+ * Prints full-screen media manager
+ *
+ * @author Kate Arzamastseva <pshns@ukr.net>
+ */
+function tpl_media() {
+ global $NS, $IMG, $JUMPTO, $REV, $lang, $fullscreen, $INPUT;
+ $fullscreen = true;
+ require_once DOKU_INC.'lib/exe/mediamanager.php';
+
+ $rev = '';
+ $image = cleanID($INPUT->str('image'));
+ if(isset($IMG)) $image = $IMG;
+ if(isset($JUMPTO)) $image = $JUMPTO;
+ if(isset($REV) && !$JUMPTO) $rev = $REV;
+
+ echo '<div id="mediamanager__page">'.NL;
+ echo '<h1>'.$lang['btn_media'].'</h1>'.NL;
+ html_msgarea();
+
+ echo '<div class="panel namespaces">'.NL;
+ echo '<h2>'.$lang['namespaces'].'</h2>'.NL;
+ echo '<div class="panelHeader">';
+ echo $lang['media_namespaces'];
+ echo '</div>'.NL;
+
+ echo '<div class="panelContent" id="media__tree">'.NL;
+ media_nstree($NS);
+ echo '</div>'.NL;
+ echo '</div>'.NL;
+
+ echo '<div class="panel filelist">'.NL;
+ tpl_mediaFileList();
+ echo '</div>'.NL;
+
+ echo '<div class="panel file">'.NL;
+ echo '<h2 class="a11y">'.$lang['media_file'].'</h2>'.NL;
+ tpl_mediaFileDetails($image, $rev);
+ echo '</div>'.NL;
+
+ echo '</div>'.NL;
+}
+
+/**
+ * Return useful layout classes
+ *
+ * @author Anika Henke <anika@selfthinker.org>
+ *
+ * @return string
+ */
+function tpl_classes() {
+ global $ACT, $conf, $ID, $INFO;
+ /** @var Input $INPUT */
+ global $INPUT;
+
+ $classes = array(
+ 'dokuwiki',
+ 'mode_'.$ACT,
+ 'tpl_'.$conf['template'],
+ $INPUT->server->bool('REMOTE_USER') ? 'loggedIn' : '',
+ (isset($INFO) && $INFO['exists']) ? '' : 'notFound',
+ ($ID == $conf['start']) ? 'home' : '',
+ );
+ return join(' ', $classes);
+}
+
+/**
+ * Create event for tools menues
+ *
+ * @author Anika Henke <anika@selfthinker.org>
+ * @param string $toolsname name of menu
+ * @param array $items
+ * @param string $view e.g. 'main', 'detail', ...
+ * @deprecated 2017-09-01 see devel:menus
+ */
+function tpl_toolsevent($toolsname, $items, $view = 'main') {
+ dbg_deprecated('see devel:menus');
+ $data = array(
+ 'view' => $view,
+ 'items' => $items
+ );
+
+ $hook = 'TEMPLATE_' . strtoupper($toolsname) . '_DISPLAY';
+ $evt = new Event($hook, $data);
+ if($evt->advise_before()) {
+ foreach($evt->data['items'] as $k => $html) echo $html;
+ }
+ $evt->advise_after();
+}
+
+//Setup VIM: ex: et ts=4 :
+
diff --git a/platform/www/inc/toolbar.php b/platform/www/inc/toolbar.php
new file mode 100644
index 0000000..7151202
--- /dev/null
+++ b/platform/www/inc/toolbar.php
@@ -0,0 +1,277 @@
+<?php
+/**
+ * Editing toolbar functions
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */use dokuwiki\Extension\Event;
+
+/**
+ * Prepares and prints an JavaScript array with all toolbar buttons
+ *
+ * @emits TOOLBAR_DEFINE
+ * @param string $varname Name of the JS variable to fill
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function toolbar_JSdefines($varname){
+ global $lang;
+
+ $menu = array();
+
+ $evt = new Event('TOOLBAR_DEFINE', $menu);
+ if ($evt->advise_before()){
+
+ // build button array
+ $menu = array_merge($menu, array(
+ array(
+ 'type' => 'format',
+ 'title' => $lang['qb_bold'],
+ 'icon' => 'bold.png',
+ 'key' => 'b',
+ 'open' => '**',
+ 'close' => '**',
+ 'block' => false
+ ),
+ array(
+ 'type' => 'format',
+ 'title' => $lang['qb_italic'],
+ 'icon' => 'italic.png',
+ 'key' => 'i',
+ 'open' => '//',
+ 'close' => '//',
+ 'block' => false
+ ),
+ array(
+ 'type' => 'format',
+ 'title' => $lang['qb_underl'],
+ 'icon' => 'underline.png',
+ 'key' => 'u',
+ 'open' => '__',
+ 'close' => '__',
+ 'block' => false
+ ),
+ array(
+ 'type' => 'format',
+ 'title' => $lang['qb_code'],
+ 'icon' => 'mono.png',
+ 'key' => 'm',
+ 'open' => "''",
+ 'close' => "''",
+ 'block' => false
+ ),
+ array(
+ 'type' => 'format',
+ 'title' => $lang['qb_strike'],
+ 'icon' => 'strike.png',
+ 'key' => 'd',
+ 'open' => '<del>',
+ 'close' => '</del>',
+ 'block' => false
+ ),
+
+ array(
+ 'type' => 'autohead',
+ 'title' => $lang['qb_hequal'],
+ 'icon' => 'hequal.png',
+ 'key' => '8',
+ 'text' => $lang['qb_h'],
+ 'mod' => 0,
+ 'block' => true
+ ),
+ array(
+ 'type' => 'autohead',
+ 'title' => $lang['qb_hminus'],
+ 'icon' => 'hminus.png',
+ 'key' => '9',
+ 'text' => $lang['qb_h'],
+ 'mod' => 1,
+ 'block' => true
+ ),
+ array(
+ 'type' => 'autohead',
+ 'title' => $lang['qb_hplus'],
+ 'icon' => 'hplus.png',
+ 'key' => '0',
+ 'text' => $lang['qb_h'],
+ 'mod' => -1,
+ 'block' => true
+ ),
+
+ array(
+ 'type' => 'picker',
+ 'title' => $lang['qb_hs'],
+ 'icon' => 'h.png',
+ 'class' => 'pk_hl',
+ 'list' => array(
+ array(
+ 'type' => 'format',
+ 'title' => $lang['qb_h1'],
+ 'icon' => 'h1.png',
+ 'key' => '1',
+ 'open' => '====== ',
+ 'close' => ' ======\n',
+ ),
+ array(
+ 'type' => 'format',
+ 'title' => $lang['qb_h2'],
+ 'icon' => 'h2.png',
+ 'key' => '2',
+ 'open' => '===== ',
+ 'close' => ' =====\n',
+ ),
+ array(
+ 'type' => 'format',
+ 'title' => $lang['qb_h3'],
+ 'icon' => 'h3.png',
+ 'key' => '3',
+ 'open' => '==== ',
+ 'close' => ' ====\n',
+ ),
+ array(
+ 'type' => 'format',
+ 'title' => $lang['qb_h4'],
+ 'icon' => 'h4.png',
+ 'key' => '4',
+ 'open' => '=== ',
+ 'close' => ' ===\n',
+ ),
+ array(
+ 'type' => 'format',
+ 'title' => $lang['qb_h5'],
+ 'icon' => 'h5.png',
+ 'key' => '5',
+ 'open' => '== ',
+ 'close' => ' ==\n',
+ ),
+ ),
+ 'block' => true
+ ),
+
+ array(
+ 'type' => 'linkwiz',
+ 'title' => $lang['qb_link'],
+ 'icon' => 'link.png',
+ 'key' => 'l',
+ 'open' => '[[',
+ 'close' => ']]',
+ 'block' => false
+ ),
+ array(
+ 'type' => 'format',
+ 'title' => $lang['qb_extlink'],
+ 'icon' => 'linkextern.png',
+ 'open' => '[[',
+ 'close' => ']]',
+ 'sample' => 'http://example.com|'.$lang['qb_extlink'],
+ 'block' => false
+ ),
+ array(
+ 'type' => 'formatln',
+ 'title' => $lang['qb_ol'],
+ 'icon' => 'ol.png',
+ 'open' => ' - ',
+ 'close' => '',
+ 'key' => '-',
+ 'block' => true
+ ),
+ array(
+ 'type' => 'formatln',
+ 'title' => $lang['qb_ul'],
+ 'icon' => 'ul.png',
+ 'open' => ' * ',
+ 'close' => '',
+ 'key' => '.',
+ 'block' => true
+ ),
+ array(
+ 'type' => 'insert',
+ 'title' => $lang['qb_hr'],
+ 'icon' => 'hr.png',
+ 'insert' => '\n----\n',
+ 'block' => true
+ ),
+ array(
+ 'type' => 'mediapopup',
+ 'title' => $lang['qb_media'],
+ 'icon' => 'image.png',
+ 'url' => 'lib/exe/mediamanager.php?ns=',
+ 'name' => 'mediaselect',
+ 'options'=> 'width=750,height=500,left=20,top=20,scrollbars=yes,resizable=yes',
+ 'block' => false
+ ),
+ array(
+ 'type' => 'picker',
+ 'title' => $lang['qb_smileys'],
+ 'icon' => 'smiley.png',
+ 'list' => getSmileys(),
+ 'icobase'=> 'smileys',
+ 'block' => false
+ ),
+ array(
+ 'type' => 'picker',
+ 'title' => $lang['qb_chars'],
+ 'icon' => 'chars.png',
+ 'list' => [
+ 'À', 'à', 'Á', 'á', 'Â', 'â', 'Ã', 'ã', 'Ä', 'ä', 'Ǎ', 'ǎ', 'Ă', 'ă', 'Å', 'å',
+ 'Ā', 'ā', 'Ą', 'ą', 'Æ', 'æ', 'Ć', 'ć', 'Ç', 'ç', 'Č', 'č', 'Ĉ', 'ĉ', 'Ċ', 'ċ',
+ 'Ð', 'đ', 'ð', 'Ď', 'ď', 'È', 'è', 'É', 'é', 'Ê', 'ê', 'Ë', 'ë', 'Ě', 'ě', 'Ē',
+ 'ē', 'Ė', 'ė', 'Ę', 'ę', 'Ģ', 'ģ', 'Ĝ', 'ĝ', 'Ğ', 'ğ', 'Ġ', 'ġ', 'Ĥ', 'ĥ', 'Ì',
+ 'ì', 'Í', 'í', 'Î', 'î', 'Ï', 'ï', 'Ǐ', 'ǐ', 'Ī', 'ī', 'İ', 'ı', 'Į', 'į', 'Ĵ',
+ 'ĵ', 'Ķ', 'ķ', 'Ĺ', 'ĺ', 'Ļ', 'ļ', 'Ľ', 'ľ', 'Ł', 'ł', 'Ŀ', 'ŀ', 'Ń', 'ń', 'Ñ',
+ 'ñ', 'Ņ', 'ņ', 'Ň', 'ň', 'Ò', 'ò', 'Ó', 'ó', 'Ô', 'ô', 'Õ', 'õ', 'Ö', 'ö', 'Ǒ',
+ 'ǒ', 'Ō', 'ō', 'Ő', 'ő', 'Œ', 'œ', 'Ø', 'ø', 'Ŕ', 'ŕ', 'Ŗ', 'ŗ', 'Ř', 'ř', 'Ś',
+ 'ś', 'Ş', 'ş', 'Š', 'š', 'Ŝ', 'ŝ', 'Ţ', 'ţ', 'Ť', 'ť', 'Ù', 'ù', 'Ú', 'ú', 'Û',
+ 'û', 'Ü', 'ü', 'Ǔ', 'ǔ', 'Ŭ', 'ŭ', 'Ū', 'ū', 'Ů', 'ů', 'ǖ', 'ǘ', 'ǚ', 'ǜ', 'Ų',
+ 'ų', 'Ű', 'ű', 'Ŵ', 'ŵ', 'Ý', 'ý', 'Ÿ', 'ÿ', 'Ŷ', 'ŷ', 'Ź', 'ź', 'Ž', 'ž', 'Ż',
+ 'ż', 'Þ', 'þ', 'ß', 'Ħ', 'ħ', '¿', '¡', '¢', '£', '¤', '¥', '€', '¦', '§', 'ª',
+ '¬', '¯', '°', '±', '÷', '‰', '¼', '½', '¾', '¹', '²', '³', 'µ', '¶', '†', '‡',
+ '·', '•', 'º', '∀', '∂', '∃', 'Ə', 'ə', '∅', '∇', '∈', '∉', '∋', '∏', '∑', '‾',
+ '−', '∗', '×', '⁄', '√', '∝', '∞', '∠', '∧', '∨', '∩', '∪', '∫', '∴', '∼', '≅',
+ '≈', '≠', '≡', '≤', '≥', '⊂', '⊃', '⊄', '⊆', '⊇', '⊕', '⊗', '⊥', '⋅', '◊', '℘',
+ 'ℑ', 'ℜ', 'ℵ', '♠', '♣', '♥', '♦', 'α', 'β', 'Γ', 'γ', 'Δ', 'δ', 'ε', 'ζ', 'η',
+ 'Θ', 'θ', 'ι', 'κ', 'Λ', 'λ', 'μ', 'Ξ', 'ξ', 'Π', 'π', 'ρ', 'Σ', 'σ', 'Τ', 'τ',
+ 'υ', 'Φ', 'φ', 'χ', 'Ψ', 'ψ', 'Ω', 'ω', '★', '☆', '☎', '☚', '☛', '☜', '☝', '☞',
+ '☟', '☹', '☺', '✔', '✘', '„', '“', '”', '‚', '‘', '’', '«', '»', '‹', '›', '—',
+ '–', '…', '←', '↑', '→', '↓', '↔', '⇐', '⇑', '⇒', '⇓', '⇔', '©', '™', '®', '′',
+ '″', '[', ']', '{', '}', '~', '(', ')', '%', '§', '$', '#', '|', '@'
+ ],
+ 'block' => false
+ ),
+ array(
+ 'type' => 'signature',
+ 'title' => $lang['qb_sig'],
+ 'icon' => 'sig.png',
+ 'key' => 'y',
+ 'block' => false
+ ),
+ ));
+ } // end event TOOLBAR_DEFINE default action
+ $evt->advise_after();
+ unset($evt);
+
+ // use JSON to build the JavaScript array
+ print "var $varname = ".json_encode($menu).";\n";
+}
+
+/**
+ * prepares the signature string as configured in the config
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function toolbar_signature(){
+ global $conf;
+ global $INFO;
+ /** @var Input $INPUT */
+ global $INPUT;
+
+ $sig = $conf['signature'];
+ $sig = dformat(null,$sig);
+ $sig = str_replace('@USER@',$INPUT->server->str('REMOTE_USER'),$sig);
+ $sig = str_replace('@NAME@',$INFO['userinfo']['name'],$sig);
+ $sig = str_replace('@MAIL@',$INFO['userinfo']['mail'],$sig);
+ $sig = str_replace('@DATE@',dformat(),$sig);
+ $sig = str_replace('\\\\n','\\n',$sig);
+ return json_encode($sig);
+}
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/inc/utf8.php b/platform/www/inc/utf8.php
new file mode 100644
index 0000000..1227407
--- /dev/null
+++ b/platform/www/inc/utf8.php
@@ -0,0 +1,284 @@
+<?php
+/**
+ * UTF8 helper functions
+ *
+ * This file now only intitializes the UTF-8 capability detection and defines helper
+ * functions if needed. All actual code is in the \dokuwiki\Utf8 classes
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+use dokuwiki\Utf8\Clean;
+use dokuwiki\Utf8\Conversion;
+use dokuwiki\Utf8\PhpString;
+use dokuwiki\Utf8\Unicode;
+
+/**
+ * check for mb_string support
+ */
+if (!defined('UTF8_MBSTRING')) {
+ if (function_exists('mb_substr') && !defined('UTF8_NOMBSTRING')) {
+ define('UTF8_MBSTRING', 1);
+ } else {
+ define('UTF8_MBSTRING', 0);
+ }
+}
+
+/**
+ * Check if PREG was compiled with UTF-8 support
+ *
+ * Without this many of the functions below will not work, so this is a minimal requirement
+ */
+if (!defined('UTF8_PREGSUPPORT')) {
+ define('UTF8_PREGSUPPORT', (bool)@preg_match('/^.$/u', 'ñ'));
+}
+
+/**
+ * Check if PREG was compiled with Unicode Property support
+ *
+ * This is not required for the functions below, but might be needed in a UTF-8 aware application
+ */
+if (!defined('UTF8_PROPERTYSUPPORT')) {
+ define('UTF8_PROPERTYSUPPORT', (bool)@preg_match('/^\pL$/u', 'ñ'));
+}
+
+
+if (UTF8_MBSTRING) {
+ mb_internal_encoding('UTF-8');
+}
+
+
+if (!function_exists('utf8_isASCII')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_isASCII($str)
+ {
+ dbg_deprecated(Clean::class . '::isASCII()');
+ return Clean::isASCII($str);
+ }
+}
+
+
+if (!function_exists('utf8_strip')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_strip($str)
+ {
+ dbg_deprecated(Clean::class . '::strip()');
+ return Clean::strip($str);
+ }
+}
+
+if (!function_exists('utf8_check')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_check($str)
+ {
+ dbg_deprecated(Clean::class . '::isUtf8()');
+ return Clean::isUtf8($str);
+ }
+}
+
+if (!function_exists('utf8_basename')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_basename($path, $suffix = '')
+ {
+ dbg_deprecated(PhpString::class . '::basename()');
+ return PhpString::basename($path, $suffix);
+ }
+}
+
+if (!function_exists('utf8_strlen')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_strlen($str)
+ {
+ dbg_deprecated(PhpString::class . '::strlen()');
+ return PhpString::strlen($str);
+ }
+}
+
+if (!function_exists('utf8_substr')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_substr($str, $offset, $length = null)
+ {
+ dbg_deprecated(PhpString::class . '::substr()');
+ return PhpString::substr($str, $offset, $length);
+ }
+}
+
+if (!function_exists('utf8_substr_replace')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_substr_replace($string, $replacement, $start, $length = 0)
+ {
+ dbg_deprecated(PhpString::class . '::substr_replace()');
+ return PhpString::substr_replace($string, $replacement, $start, $length);
+ }
+}
+
+if (!function_exists('utf8_ltrim')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_ltrim($str, $charlist = '')
+ {
+ dbg_deprecated(PhpString::class . '::ltrim()');
+ return PhpString::ltrim($str, $charlist);
+ }
+}
+
+if (!function_exists('utf8_rtrim')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_rtrim($str, $charlist = '')
+ {
+ dbg_deprecated(PhpString::class . '::rtrim()');
+ return PhpString::rtrim($str, $charlist);
+ }
+}
+
+if (!function_exists('utf8_trim')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_trim($str, $charlist = '')
+ {
+ dbg_deprecated(PhpString::class . '::trim()');
+ return PhpString::trim($str, $charlist);
+ }
+}
+
+if (!function_exists('utf8_strtolower')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_strtolower($str)
+ {
+ dbg_deprecated(PhpString::class . '::strtolower()');
+ return PhpString::strtolower($str);
+ }
+}
+
+if (!function_exists('utf8_strtoupper')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_strtoupper($str)
+ {
+ dbg_deprecated(PhpString::class . '::strtoupper()');
+ return PhpString::strtoupper($str);
+ }
+}
+
+if (!function_exists('utf8_ucfirst')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_ucfirst($str)
+ {
+ dbg_deprecated(PhpString::class . '::ucfirst()');
+ return PhpString::ucfirst($str);
+ }
+}
+
+if (!function_exists('utf8_ucwords')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_ucwords($str)
+ {
+ dbg_deprecated(PhpString::class . '::ucwords()');
+ return PhpString::ucwords($str);
+ }
+}
+
+if (!function_exists('utf8_deaccent')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_deaccent($str, $case = 0)
+ {
+ dbg_deprecated(Clean::class . '::deaccent()');
+ return Clean::deaccent($str, $case);
+ }
+}
+
+if (!function_exists('utf8_romanize')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_romanize($str)
+ {
+ dbg_deprecated(Clean::class . '::romanize()');
+ return Clean::romanize($str);
+ }
+}
+
+if (!function_exists('utf8_stripspecials')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_stripspecials($str, $repl = '', $additional = '')
+ {
+ dbg_deprecated(Clean::class . '::stripspecials()');
+ return Clean::stripspecials($str, $repl, $additional);
+ }
+}
+
+if (!function_exists('utf8_strpos')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_strpos($haystack, $needle, $offset = 0)
+ {
+ dbg_deprecated(PhpString::class . '::strpos()');
+ return PhpString::strpos($haystack, $needle, $offset);
+ }
+}
+
+if (!function_exists('utf8_tohtml')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_tohtml($str, $all = false)
+ {
+ dbg_deprecated(Conversion::class . '::toHtml()');
+ return Conversion::toHtml($str, $all);
+ }
+}
+
+if (!function_exists('utf8_unhtml')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_unhtml($str, $enties = false)
+ {
+ dbg_deprecated(Conversion::class . '::fromHtml()');
+ return Conversion::fromHtml($str, $enties);
+ }
+}
+
+if (!function_exists('utf8_to_unicode')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_to_unicode($str, $strict = false)
+ {
+ dbg_deprecated(Unicode::class . '::fromUtf8()');
+ return Unicode::fromUtf8($str, $strict);
+ }
+}
+
+if (!function_exists('unicode_to_utf8')) {
+ /** @deprecated 2019-06-09 */
+ function unicode_to_utf8($arr, $strict = false)
+ {
+ dbg_deprecated(Unicode::class . '::toUtf8()');
+ return Unicode::toUtf8($arr, $strict);
+ }
+}
+
+if (!function_exists('utf8_to_utf16be')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_to_utf16be($str, $bom = false)
+ {
+ dbg_deprecated(Conversion::class . '::toUtf16be()');
+ return Conversion::toUtf16be($str, $bom);
+ }
+}
+
+if (!function_exists('utf16be_to_utf8')) {
+ /** @deprecated 2019-06-09 */
+ function utf16be_to_utf8($str)
+ {
+ dbg_deprecated(Conversion::class . '::fromUtf16be()');
+ return Conversion::fromUtf16be($str);
+ }
+}
+
+if (!function_exists('utf8_bad_replace')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_bad_replace($str, $replace = '')
+ {
+ dbg_deprecated(Clean::class . '::replaceBadBytes()');
+ return Clean::replaceBadBytes($str, $replace);
+ }
+}
+
+if (!function_exists('utf8_correctIdx')) {
+ /** @deprecated 2019-06-09 */
+ function utf8_correctIdx($str, $i, $next = false)
+ {
+ dbg_deprecated(Clean::class . '::correctIdx()');
+ return Clean::correctIdx($str, $i, $next);
+ }
+}