ajout de la partie slam dans le dossier web

This commit is contained in:
root
2022-03-10 11:56:26 +01:00
parent 31d3052792
commit e375c4f088
4847 changed files with 325719 additions and 0 deletions

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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');
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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');
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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');
}
}

View File

@@ -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();
}
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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;
}
}

View File

@@ -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 {
}

View File

@@ -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);
}
}

View File

@@ -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 {
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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');
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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');
}
}
}

View File

@@ -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);
}
}

View File

@@ -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');
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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
}
}

View File

@@ -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');
}
}

View File

@@ -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'));
}
}

View File

@@ -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');
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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?');
}
}

View File

@@ -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();
}
}

View File

@@ -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');
}
}

View File

@@ -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;
}
}

438
ap23/web/doku/inc/Ajax.php Normal file
View File

@@ -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>';
}
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

File diff suppressed because it is too large Load Diff

165
ap23/web/doku/inc/Draft.php Normal file
View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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'));
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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']);
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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>';
}
}

View File

@@ -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');
}
}
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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>';
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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>';
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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().'>';
}
}

View File

@@ -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()).' />';
}
}

View File

@@ -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()).'>';
}
}

View File

@@ -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>';
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace dokuwiki\HTTP;
use Exception;
class HTTPClientException extends Exception
{
}

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}
}

View File

@@ -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);
}
}

Some files were not shown because too many files have changed in this diff Show More