@@ -0,0 +1,23 @@ | |||
# Another repository | |||
/data | |||
# やばいの | |||
/conf/users.auth.php | |||
/conf/local.php | |||
# Ignore vendor directory | |||
vendor/ | |||
# DokuWiki upgrade/install files | |||
install.php | |||
VERSION | |||
CHANGES | |||
README | |||
COPYING | |||
SECURITY.md | |||
# Temporary files | |||
*.tmp | |||
*.bak | |||
*.swp | |||
*~ |
@@ -0,0 +1,45 @@ | |||
## You should disable Indexes and MultiViews either here or in the | |||
## global config. Symlinks maybe needed for URL rewriting. | |||
#Options -Indexes -MultiViews +FollowSymLinks | |||
## make sure nobody gets the htaccess, README, COPYING or VERSION files | |||
<Files ~ "^([\._]ht|README$|VERSION$|COPYING$)"> | |||
<IfModule mod_authz_core.c> | |||
Require all denied | |||
</IfModule> | |||
<IfModule !mod_authz_core.c> | |||
Order allow,deny | |||
Deny from all | |||
</IfModule> | |||
</Files> | |||
## Don't allow access to git directories | |||
<IfModule alias_module> | |||
RedirectMatch 404 /\.git | |||
</IfModule> | |||
## Uncomment these rules if you want to have nice URLs using | |||
## $conf['userewrite'] = 1 - not needed for rewrite mode 2 | |||
RewriteEngine on | |||
RewriteRule ^_media/(.*) lib/exe/fetch.php?media=$1 [QSA,L] | |||
RewriteRule ^_detail/(.*) lib/exe/detail.php?media=$1 [QSA,L] | |||
RewriteRule ^_export/([^/]+)/(.*) doku.php?do=export_$1&id=$2 [QSA,L] | |||
RewriteRule ^$ doku.php [L] | |||
RewriteCond %{REQUEST_FILENAME} !-f | |||
RewriteCond %{REQUEST_FILENAME} !-d | |||
RewriteRule (.*) doku.php?id=$1 [QSA,L] | |||
RewriteRule ^index.php$ doku.php | |||
## Not all installations will require the following line. If you do, | |||
## change "/dokuwiki" to the path to your dokuwiki directory relative | |||
## to your document root. | |||
#RewriteBase /dokuwiki | |||
# | |||
## If you enable DokuWikis XML-RPC interface, you should consider to | |||
## restrict access to it over HTTPS only! Uncomment the following two | |||
## rules if your server setup allows HTTPS. | |||
#RewriteCond %{HTTPS} !=on | |||
#RewriteRule ^lib/exe/xmlrpc.php$ https://%{SERVER_NAME}%{REQUEST_URI} [L,R=301] | |||
# vim: set tabstop=2 softtabstop=2 expandtab : |
@@ -0,0 +1,7 @@ | |||
<IfModule mod_authz_core.c> | |||
Require all denied | |||
</IfModule> | |||
<IfModule !mod_authz_core.c> | |||
Order allow,deny | |||
Deny from all | |||
</IfModule> |
@@ -0,0 +1,359 @@ | |||
#!/usr/bin/env php | |||
<?php | |||
use splitbrain\phpcli\CLI; | |||
use splitbrain\phpcli\Options; | |||
use dokuwiki\Utf8\PhpString; | |||
if (!defined('DOKU_INC')) define('DOKU_INC', realpath(__DIR__ . '/../') . '/'); | |||
define('NOSESSION', 1); | |||
require_once(DOKU_INC . 'inc/init.php'); | |||
/** | |||
* Checkout and commit pages from the command line while maintaining the history | |||
*/ | |||
class PageCLI extends CLI | |||
{ | |||
protected $force = false; | |||
protected $username = ''; | |||
/** | |||
* Register options and arguments on the given $options object | |||
* | |||
* @param Options $options | |||
* @return void | |||
*/ | |||
protected function setup(Options $options) | |||
{ | |||
/* global */ | |||
$options->registerOption( | |||
'force', | |||
'force obtaining a lock for the page (generally bad idea)', | |||
'f' | |||
); | |||
$options->registerOption( | |||
'user', | |||
'work as this user. defaults to current CLI user', | |||
'u', | |||
'username' | |||
); | |||
$options->setHelp( | |||
'Utility to help command line Dokuwiki page editing, allow ' . | |||
'pages to be checked out for editing then committed after changes' | |||
); | |||
/* checkout command */ | |||
$options->registerCommand( | |||
'checkout', | |||
'Checks out a file from the repository, using the wiki id and obtaining ' . | |||
'a lock for the page. ' . "\n" . | |||
'If a working_file is specified, this is where the page is copied to. ' . | |||
'Otherwise defaults to the same as the wiki page in the current ' . | |||
'working directory.' | |||
); | |||
$options->registerArgument( | |||
'wikipage', | |||
'The wiki page to checkout', | |||
true, | |||
'checkout' | |||
); | |||
$options->registerArgument( | |||
'workingfile', | |||
'How to name the local checkout', | |||
false, | |||
'checkout' | |||
); | |||
/* commit command */ | |||
$options->registerCommand( | |||
'commit', | |||
'Checks in the working_file into the repository using the specified ' . | |||
'wiki id, archiving the previous version.' | |||
); | |||
$options->registerArgument( | |||
'workingfile', | |||
'The local file to commit', | |||
true, | |||
'commit' | |||
); | |||
$options->registerArgument( | |||
'wikipage', | |||
'The wiki page to create or update', | |||
true, | |||
'commit' | |||
); | |||
$options->registerOption( | |||
'message', | |||
'Summary describing the change (required)', | |||
'm', | |||
'summary', | |||
'commit' | |||
); | |||
$options->registerOption( | |||
'trivial', | |||
'minor change', | |||
't', | |||
false, | |||
'commit' | |||
); | |||
/* lock command */ | |||
$options->registerCommand( | |||
'lock', | |||
'Obtains or updates a lock for a wiki page' | |||
); | |||
$options->registerArgument( | |||
'wikipage', | |||
'The wiki page to lock', | |||
true, | |||
'lock' | |||
); | |||
/* unlock command */ | |||
$options->registerCommand( | |||
'unlock', | |||
'Removes a lock for a wiki page.' | |||
); | |||
$options->registerArgument( | |||
'wikipage', | |||
'The wiki page to unlock', | |||
true, | |||
'unlock' | |||
); | |||
/* gmeta command */ | |||
$options->registerCommand( | |||
'getmeta', | |||
'Prints metadata value for a page to stdout.' | |||
); | |||
$options->registerArgument( | |||
'wikipage', | |||
'The wiki page to get the metadata for', | |||
true, | |||
'getmeta' | |||
); | |||
$options->registerArgument( | |||
'key', | |||
'The name of the metadata item to be retrieved.' . "\n" . | |||
'If empty, an array of all the metadata items is returned.' . "\n" . | |||
'For retrieving items that are stored in sub-arrays, separate the ' . | |||
'keys of the different levels by spaces, in quotes, eg "date modified".', | |||
false, | |||
'getmeta' | |||
); | |||
} | |||
/** | |||
* Your main program | |||
* | |||
* Arguments and options have been parsed when this is run | |||
* | |||
* @param Options $options | |||
* @return void | |||
*/ | |||
protected function main(Options $options) | |||
{ | |||
$this->force = $options->getOpt('force', false); | |||
$this->username = $options->getOpt('user', $this->getUser()); | |||
$command = $options->getCmd(); | |||
$args = $options->getArgs(); | |||
switch ($command) { | |||
case 'checkout': | |||
$wiki_id = array_shift($args); | |||
$localfile = array_shift($args); | |||
$this->commandCheckout($wiki_id, $localfile); | |||
break; | |||
case 'commit': | |||
$localfile = array_shift($args); | |||
$wiki_id = array_shift($args); | |||
$this->commandCommit( | |||
$localfile, | |||
$wiki_id, | |||
$options->getOpt('message', ''), | |||
$options->getOpt('trivial', false) | |||
); | |||
break; | |||
case 'lock': | |||
$wiki_id = array_shift($args); | |||
$this->obtainLock($wiki_id); | |||
$this->success("$wiki_id locked"); | |||
break; | |||
case 'unlock': | |||
$wiki_id = array_shift($args); | |||
$this->clearLock($wiki_id); | |||
$this->success("$wiki_id unlocked"); | |||
break; | |||
case 'getmeta': | |||
$wiki_id = array_shift($args); | |||
$key = trim(array_shift($args)); | |||
$meta = p_get_metadata($wiki_id, $key, METADATA_RENDER_UNLIMITED); | |||
echo trim(json_encode($meta, JSON_PRETTY_PRINT)); | |||
echo "\n"; | |||
break; | |||
default: | |||
echo $options->help(); | |||
} | |||
} | |||
/** | |||
* Check out a file | |||
* | |||
* @param string $wiki_id | |||
* @param string $localfile | |||
*/ | |||
protected function commandCheckout($wiki_id, $localfile) | |||
{ | |||
global $conf; | |||
$wiki_id = cleanID($wiki_id); | |||
$wiki_fn = wikiFN($wiki_id); | |||
if (!file_exists($wiki_fn)) { | |||
$this->fatal("$wiki_id does not yet exist"); | |||
} | |||
if (empty($localfile)) { | |||
$localfile = getcwd() . '/' . PhpString::basename($wiki_fn); | |||
} | |||
if (!file_exists(dirname($localfile))) { | |||
$this->fatal("Directory " . dirname($localfile) . " does not exist"); | |||
} | |||
if (stristr(realpath(dirname($localfile)), (string) realpath($conf['datadir'])) !== false) { | |||
$this->fatal("Attempt to check out file into data directory - not allowed"); | |||
} | |||
$this->obtainLock($wiki_id); | |||
if (!copy($wiki_fn, $localfile)) { | |||
$this->clearLock($wiki_id); | |||
$this->fatal("Unable to copy $wiki_fn to $localfile"); | |||
} | |||
$this->success("$wiki_id > $localfile"); | |||
} | |||
/** | |||
* Save a file as a new page revision | |||
* | |||
* @param string $localfile | |||
* @param string $wiki_id | |||
* @param string $message | |||
* @param bool $minor | |||
*/ | |||
protected function commandCommit($localfile, $wiki_id, $message, $minor) | |||
{ | |||
$wiki_id = cleanID($wiki_id); | |||
$message = trim($message); | |||
if (!file_exists($localfile)) { | |||
$this->fatal("$localfile does not exist"); | |||
} | |||
if (!is_readable($localfile)) { | |||
$this->fatal("Cannot read from $localfile"); | |||
} | |||
if (!$message) { | |||
$this->fatal("Summary message required"); | |||
} | |||
$this->obtainLock($wiki_id); | |||
saveWikiText($wiki_id, file_get_contents($localfile), $message, $minor); | |||
$this->clearLock($wiki_id); | |||
$this->success("$localfile > $wiki_id"); | |||
} | |||
/** | |||
* Lock the given page or exit | |||
* | |||
* @param string $wiki_id | |||
*/ | |||
protected function obtainLock($wiki_id) | |||
{ | |||
if ($this->force) $this->deleteLock($wiki_id); | |||
$_SERVER['REMOTE_USER'] = $this->username; | |||
if (checklock($wiki_id)) { | |||
$this->error("Page $wiki_id is already locked by another user"); | |||
exit(1); | |||
} | |||
lock($wiki_id); | |||
if (checklock($wiki_id)) { | |||
$this->error("Unable to obtain lock for $wiki_id "); | |||
var_dump(checklock($wiki_id)); | |||
exit(1); | |||
} | |||
} | |||
/** | |||
* Clear the lock on the given page | |||
* | |||
* @param string $wiki_id | |||
*/ | |||
protected function clearLock($wiki_id) | |||
{ | |||
if ($this->force) $this->deleteLock($wiki_id); | |||
$_SERVER['REMOTE_USER'] = $this->username; | |||
if (checklock($wiki_id)) { | |||
$this->error("Page $wiki_id is locked by another user"); | |||
exit(1); | |||
} | |||
unlock($wiki_id); | |||
if (file_exists(wikiLockFN($wiki_id))) { | |||
$this->error("Unable to clear lock for $wiki_id"); | |||
exit(1); | |||
} | |||
} | |||
/** | |||
* Forcefully remove a lock on the page given | |||
* | |||
* @param string $wiki_id | |||
*/ | |||
protected function deleteLock($wiki_id) | |||
{ | |||
$wikiLockFN = wikiLockFN($wiki_id); | |||
if (file_exists($wikiLockFN)) { | |||
if (!unlink($wikiLockFN)) { | |||
$this->error("Unable to delete $wikiLockFN"); | |||
exit(1); | |||
} | |||
} | |||
} | |||
/** | |||
* Get the current user's username from the environment | |||
* | |||
* @return string | |||
*/ | |||
protected function getUser() | |||
{ | |||
$user = getenv('USER'); | |||
if (empty($user)) { | |||
$user = getenv('USERNAME'); | |||
} else { | |||
return $user; | |||
} | |||
if (empty($user)) { | |||
$user = 'admin'; | |||
} | |||
return $user; | |||
} | |||
} | |||
// Main | |||
$cli = new PageCLI(); | |||
$cli->run(); |
@@ -0,0 +1,346 @@ | |||
#!/usr/bin/env php | |||
<?php | |||
use splitbrain\phpcli\CLI; | |||
use splitbrain\phpcli\Options; | |||
if (!defined('DOKU_INC')) define('DOKU_INC', realpath(__DIR__ . '/../') . '/'); | |||
define('NOSESSION', 1); | |||
require_once(DOKU_INC . 'inc/init.php'); | |||
/** | |||
* Easily manage DokuWiki git repositories | |||
* | |||
* @author Andreas Gohr <andi@splitbrain.org> | |||
*/ | |||
class GitToolCLI extends CLI | |||
{ | |||
/** | |||
* Register options and arguments on the given $options object | |||
* | |||
* @param Options $options | |||
* @return void | |||
*/ | |||
protected function setup(Options $options) | |||
{ | |||
$options->setHelp( | |||
"Manage git repositories for DokuWiki and its plugins and templates.\n\n" . | |||
"$> ./bin/gittool.php clone gallery template:ach\n" . | |||
"$> ./bin/gittool.php repos\n" . | |||
"$> ./bin/gittool.php origin -v" | |||
); | |||
$options->registerArgument( | |||
'command', | |||
'Command to execute. See below', | |||
true | |||
); | |||
$options->registerCommand( | |||
'clone', | |||
'Tries to install a known plugin or template (prefix with template:) via git. Uses the DokuWiki.org ' . | |||
'plugin repository to find the proper git repository. Multiple extensions can be given as parameters' | |||
); | |||
$options->registerArgument( | |||
'extension', | |||
'name of the extension to install, prefix with \'template:\' for templates', | |||
true, | |||
'clone' | |||
); | |||
$options->registerCommand( | |||
'install', | |||
'The same as clone, but when no git source repository can be found, the extension is installed via ' . | |||
'download' | |||
); | |||
$options->registerArgument( | |||
'extension', | |||
'name of the extension to install, prefix with \'template:\' for templates', | |||
true, | |||
'install' | |||
); | |||
$options->registerCommand( | |||
'repos', | |||
'Lists all git repositories found in this DokuWiki installation' | |||
); | |||
$options->registerCommand( | |||
'*', | |||
'Any unknown commands are assumed to be arguments to git and will be executed in all repositories ' . | |||
'found within this DokuWiki installation' | |||
); | |||
} | |||
/** | |||
* Your main program | |||
* | |||
* Arguments and options have been parsed when this is run | |||
* | |||
* @param Options $options | |||
* @return void | |||
*/ | |||
protected function main(Options $options) | |||
{ | |||
$command = $options->getCmd(); | |||
$args = $options->getArgs(); | |||
if (!$command) $command = array_shift($args); | |||
switch ($command) { | |||
case '': | |||
echo $options->help(); | |||
break; | |||
case 'clone': | |||
$this->cmdClone($args); | |||
break; | |||
case 'install': | |||
$this->cmdInstall($args); | |||
break; | |||
case 'repo': | |||
case 'repos': | |||
$this->cmdRepos(); | |||
break; | |||
default: | |||
$this->cmdGit($command, $args); | |||
} | |||
} | |||
/** | |||
* Tries to install the given extensions using git clone | |||
* | |||
* @param array $extensions | |||
*/ | |||
public function cmdClone($extensions) | |||
{ | |||
$errors = []; | |||
$succeeded = []; | |||
foreach ($extensions as $ext) { | |||
$repo = $this->getSourceRepo($ext); | |||
if (!$repo) { | |||
$this->error("could not find a repository for $ext"); | |||
$errors[] = $ext; | |||
} elseif ($this->cloneExtension($ext, $repo)) { | |||
$succeeded[] = $ext; | |||
} else { | |||
$errors[] = $ext; | |||
} | |||
} | |||
echo "\n"; | |||
if ($succeeded) $this->success('successfully cloned the following extensions: ' . implode(', ', $succeeded)); | |||
if ($errors) $this->error('failed to clone the following extensions: ' . implode(', ', $errors)); | |||
} | |||
/** | |||
* Tries to install the given extensions using git clone with fallback to install | |||
* | |||
* @param array $extensions | |||
*/ | |||
public function cmdInstall($extensions) | |||
{ | |||
$errors = []; | |||
$succeeded = []; | |||
foreach ($extensions as $ext) { | |||
$repo = $this->getSourceRepo($ext); | |||
if (!$repo) { | |||
$this->info("could not find a repository for $ext"); | |||
if ($this->downloadExtension($ext)) { | |||
$succeeded[] = $ext; | |||
} else { | |||
$errors[] = $ext; | |||
} | |||
} elseif ($this->cloneExtension($ext, $repo)) { | |||
$succeeded[] = $ext; | |||
} else { | |||
$errors[] = $ext; | |||
} | |||
} | |||
echo "\n"; | |||
if ($succeeded) $this->success('successfully installed the following extensions: ' . implode(', ', $succeeded)); | |||
if ($errors) $this->error('failed to install the following extensions: ' . implode(', ', $errors)); | |||
} | |||
/** | |||
* Executes the given git command in every repository | |||
* | |||
* @param $cmd | |||
* @param $arg | |||
*/ | |||
public function cmdGit($cmd, $arg) | |||
{ | |||
$repos = $this->findRepos(); | |||
$shell = array_merge(['git', $cmd], $arg); | |||
$shell = array_map('escapeshellarg', $shell); | |||
$shell = implode(' ', $shell); | |||
foreach ($repos as $repo) { | |||
if (!@chdir($repo)) { | |||
$this->error("Could not change into $repo"); | |||
continue; | |||
} | |||
$this->info("executing $shell in $repo"); | |||
$ret = 0; | |||
system($shell, $ret); | |||
if ($ret == 0) { | |||
$this->success("git succeeded in $repo"); | |||
} else { | |||
$this->error("git failed in $repo"); | |||
} | |||
} | |||
} | |||
/** | |||
* Simply lists the repositories | |||
*/ | |||
public function cmdRepos() | |||
{ | |||
$repos = $this->findRepos(); | |||
foreach ($repos as $repo) { | |||
echo "$repo\n"; | |||
} | |||
} | |||
/** | |||
* Install extension from the given download URL | |||
* | |||
* @param string $ext | |||
* @return bool|null | |||
*/ | |||
private function downloadExtension($ext) | |||
{ | |||
/** @var helper_plugin_extension_extension $plugin */ | |||
$plugin = plugin_load('helper', 'extension_extension'); | |||
if (!$ext) die("extension plugin not available, can't continue"); | |||
$plugin->setExtension($ext); | |||
$url = $plugin->getDownloadURL(); | |||
if (!$url) { | |||
$this->error("no download URL for $ext"); | |||
return false; | |||
} | |||
$ok = false; | |||
try { | |||
$this->info("installing $ext via download from $url"); | |||
$ok = $plugin->installFromURL($url); | |||
} catch (Exception $e) { | |||
$this->error($e->getMessage()); | |||
} | |||
if ($ok) { | |||
$this->success("installed $ext via download"); | |||
return true; | |||
} else { | |||
$this->success("failed to install $ext via download"); | |||
return false; | |||
} | |||
} | |||
/** | |||
* Clones the extension from the given repository | |||
* | |||
* @param string $ext | |||
* @param string $repo | |||
* @return bool | |||
*/ | |||
private function cloneExtension($ext, $repo) | |||
{ | |||
if (str_starts_with($ext, 'template:')) { | |||
$target = fullpath(tpl_incdir() . '../' . substr($ext, 9)); | |||
} else { | |||
$target = DOKU_PLUGIN . $ext; | |||
} | |||
$this->info("cloning $ext from $repo to $target"); | |||
$ret = 0; | |||
system("git clone $repo $target", $ret); | |||
if ($ret === 0) { | |||
$this->success("cloning of $ext succeeded"); | |||
return true; | |||
} else { | |||
$this->error("cloning of $ext failed"); | |||
return false; | |||
} | |||
} | |||
/** | |||
* Returns all git repositories in this DokuWiki install | |||
* | |||
* Looks in root, template and plugin directories only. | |||
* | |||
* @return array | |||
*/ | |||
private function findRepos() | |||
{ | |||
$this->info('Looking for .git directories'); | |||
$data = array_merge( | |||
glob(DOKU_INC . '.git', GLOB_ONLYDIR), | |||
glob(DOKU_PLUGIN . '*/.git', GLOB_ONLYDIR), | |||
glob(fullpath(tpl_incdir() . '../') . '/*/.git', GLOB_ONLYDIR) | |||
); | |||
if (!$data) { | |||
$this->error('Found no .git directories'); | |||
} else { | |||
$this->success('Found ' . count($data) . ' .git directories'); | |||
} | |||
$data = array_map('fullpath', array_map('dirname', $data)); | |||
return $data; | |||
} | |||
/** | |||
* Returns the repository for the given extension | |||
* | |||
* @param $extension | |||
* @return false|string | |||
*/ | |||
private function getSourceRepo($extension) | |||
{ | |||
/** @var helper_plugin_extension_extension $ext */ | |||
$ext = plugin_load('helper', 'extension_extension'); | |||
if (!$ext) die("extension plugin not available, can't continue"); | |||
$ext->setExtension($extension); | |||
$repourl = $ext->getSourcerepoURL(); | |||
if (!$repourl) return false; | |||
// match github repos | |||
if (preg_match('/github\.com\/([^\/]+)\/([^\/]+)/i', $repourl, $m)) { | |||
$user = $m[1]; | |||
$repo = $m[2]; | |||
return 'https://github.com/' . $user . '/' . $repo . '.git'; | |||
} | |||
// match gitorious repos | |||
if (preg_match('/gitorious.org\/([^\/]+)\/([^\/]+)?/i', $repourl, $m)) { | |||
$user = $m[1]; | |||
$repo = $m[2]; | |||
if (!$repo) $repo = $user; | |||
return 'https://git.gitorious.org/' . $user . '/' . $repo . '.git'; | |||
} | |||
// match bitbucket repos - most people seem to use mercurial there though | |||
if (preg_match('/bitbucket\.org\/([^\/]+)\/([^\/]+)/i', $repourl, $m)) { | |||
$user = $m[1]; | |||
$repo = $m[2]; | |||
return 'https://bitbucket.org/' . $user . '/' . $repo . '.git'; | |||
} | |||
return false; | |||
} | |||
} | |||
// Main | |||
$cli = new GitToolCLI(); | |||
$cli->run(); |
@@ -0,0 +1,113 @@ | |||
#!/usr/bin/env php | |||
<?php | |||
use splitbrain\phpcli\CLI; | |||
use splitbrain\phpcli\Options; | |||
if (!defined('DOKU_INC')) define('DOKU_INC', realpath(__DIR__ . '/../') . '/'); | |||
define('NOSESSION', 1); | |||
require_once(DOKU_INC . 'inc/init.php'); | |||
/** | |||
* Update the Search Index from command line | |||
*/ | |||
class IndexerCLI extends CLI | |||
{ | |||
private $quiet = false; | |||
private $clear = false; | |||
/** | |||
* Register options and arguments on the given $options object | |||
* | |||
* @param Options $options | |||
* @return void | |||
*/ | |||
protected function setup(Options $options) | |||
{ | |||
$options->setHelp( | |||
'Updates the searchindex by indexing all new or changed pages. When the -c option is ' . | |||
'given the index is cleared first.' | |||
); | |||
$options->registerOption( | |||
'clear', | |||
'clear the index before updating', | |||
'c' | |||
); | |||
$options->registerOption( | |||
'quiet', | |||
'don\'t produce any output', | |||
'q' | |||
); | |||
} | |||
/** | |||
* Your main program | |||
* | |||
* Arguments and options have been parsed when this is run | |||
* | |||
* @param Options $options | |||
* @return void | |||
*/ | |||
protected function main(Options $options) | |||
{ | |||
$this->clear = $options->getOpt('clear'); | |||
$this->quiet = $options->getOpt('quiet'); | |||
if ($this->clear) $this->clearindex(); | |||
$this->update(); | |||
} | |||
/** | |||
* Update the index | |||
*/ | |||
protected function update() | |||
{ | |||
global $conf; | |||
$data = []; | |||
$this->quietecho("Searching pages... "); | |||
search($data, $conf['datadir'], 'search_allpages', ['skipacl' => true]); | |||
$this->quietecho(count($data) . " pages found.\n"); | |||
foreach ($data as $val) { | |||
$this->index($val['id']); | |||
} | |||
} | |||
/** | |||
* Index the given page | |||
* | |||
* @param string $id | |||
*/ | |||
protected function index($id) | |||
{ | |||
$this->quietecho("$id... "); | |||
idx_addPage($id, !$this->quiet, $this->clear); | |||
$this->quietecho("done.\n"); | |||
} | |||
/** | |||
* Clear all index files | |||
*/ | |||
protected function clearindex() | |||
{ | |||
$this->quietecho("Clearing index... "); | |||
idx_get_indexer()->clear(); | |||
$this->quietecho("done.\n"); | |||
} | |||
/** | |||
* Print message if not supressed | |||
* | |||
* @param string $msg | |||
*/ | |||
protected function quietecho($msg) | |||
{ | |||
if (!$this->quiet) echo $msg; | |||
} | |||
} | |||
// Main | |||
$cli = new IndexerCLI(); | |||
$cli->run(); |
@@ -0,0 +1,110 @@ | |||
#!/usr/bin/env php | |||
<?php | |||
use dokuwiki\Extension\PluginController; | |||
use splitbrain\phpcli\CLI; | |||
use splitbrain\phpcli\Colors; | |||
use splitbrain\phpcli\Options; | |||
use dokuwiki\Extension\CLIPlugin; | |||
use splitbrain\phpcli\TableFormatter; | |||
if (!defined('DOKU_INC')) define('DOKU_INC', realpath(__DIR__ . '/../') . '/'); | |||
define('NOSESSION', 1); | |||
require_once(DOKU_INC . 'inc/init.php'); | |||
class PluginCLI extends CLI | |||
{ | |||
/** | |||
* Register options and arguments on the given $options object | |||
* | |||
* @param Options $options | |||
* @return void | |||
*/ | |||
protected function setup(Options $options) | |||
{ | |||
$options->setHelp('Excecutes Plugin command line tools'); | |||
$options->registerArgument('plugin', 'The plugin CLI you want to run. Leave off to see list', false); | |||
} | |||
/** | |||
* Your main program | |||
* | |||
* Arguments and options have been parsed when this is run | |||
* | |||
* @param Options $options | |||
* @return void | |||
*/ | |||
protected function main(Options $options) | |||
{ | |||
global $argv; | |||
$argv = $options->getArgs(); | |||
if ($argv) { | |||
$plugin = $this->loadPlugin($argv[0]); | |||
if ($plugin instanceof CLIPlugin) { | |||
$plugin->run(); | |||
} else { | |||
$this->fatal('Command {cmd} not found.', ['cmd' => $argv[0]]); | |||
} | |||
} else { | |||
echo $options->help(); | |||
$this->listPlugins(); | |||
} | |||
} | |||
/** | |||
* List available plugins | |||
*/ | |||
protected function listPlugins() | |||
{ | |||
/** @var PluginController $plugin_controller */ | |||
global $plugin_controller; | |||
echo "\n"; | |||
echo "\n"; | |||
echo $this->colors->wrap('AVAILABLE PLUGINS:', Colors::C_BROWN); | |||
echo "\n"; | |||
$list = $plugin_controller->getList('cli'); | |||
sort($list); | |||
if ($list === []) { | |||
echo $this->colors->wrap(" No plugins providing CLI components available\n", Colors::C_RED); | |||
} else { | |||
$tf = new TableFormatter($this->colors); | |||
foreach ($list as $name) { | |||
$plugin = $this->loadPlugin($name); | |||
if (!$plugin instanceof CLIPlugin) continue; | |||
$info = $plugin->getInfo(); | |||
echo $tf->format( | |||
[2, '30%', '*'], | |||
['', $name, $info['desc']], | |||
['', Colors::C_CYAN, ''] | |||
); | |||
} | |||
} | |||
} | |||
/** | |||
* Instantiate a CLI plugin | |||
* | |||
* @param string $name | |||
* @return CLIPlugin|null | |||
*/ | |||
protected function loadPlugin($name) | |||
{ | |||
if (plugin_isdisabled($name)) return null; | |||
// execute the plugin CLI | |||
$class = "cli_plugin_$name"; | |||
if (class_exists($class)) { | |||
return new $class(); | |||
} | |||
return null; | |||
} | |||
} | |||
// Main | |||
$cli = new PluginCLI(); | |||
$cli->run(); |
@@ -0,0 +1,66 @@ | |||
#!/usr/bin/env php | |||
<?php | |||
use splitbrain\phpcli\CLI; | |||
use splitbrain\phpcli\Options; | |||
if (!defined('DOKU_INC')) define('DOKU_INC', realpath(__DIR__ . '/../') . '/'); | |||
define('NOSESSION', 1); | |||
require_once(DOKU_INC . 'inc/init.php'); | |||
/** | |||
* A simple commandline tool to render some DokuWiki syntax with a given | |||
* renderer. | |||
* | |||
* This may not work for plugins that expect a certain environment to be | |||
* set up before rendering, but should work for most or even all standard | |||
* DokuWiki markup | |||
* | |||
* @license GPL2 | |||
* @author Andreas Gohr <andi@splitbrain.org> | |||
*/ | |||
class RenderCLI extends CLI | |||
{ | |||
/** | |||
* Register options and arguments on the given $options object | |||
* | |||
* @param Options $options | |||
* @return void | |||
*/ | |||
protected function setup(Options $options) | |||
{ | |||
$options->setHelp( | |||
'A simple commandline tool to render some DokuWiki syntax with a given renderer.' . | |||
"\n\n" . | |||
'This may not work for plugins that expect a certain environment to be ' . | |||
'set up before rendering, but should work for most or even all standard ' . | |||
'DokuWiki markup' | |||
); | |||
$options->registerOption('renderer', 'The renderer mode to use. Defaults to xhtml', 'r', 'mode'); | |||
} | |||
/** | |||
* Your main program | |||
* | |||
* Arguments and options have been parsed when this is run | |||
* | |||
* @param Options $options | |||
* @throws DokuCLI_Exception | |||
* @return void | |||
*/ | |||
protected function main(Options $options) | |||
{ | |||
$renderer = $options->getOpt('renderer', 'xhtml'); | |||
// do the action | |||
$source = stream_get_contents(STDIN); | |||
$info = []; | |||
$result = p_render($renderer, p_get_instructions($source), $info); | |||
if (is_null($result)) throw new DokuCLI_Exception("No such renderer $renderer"); | |||
echo $result; | |||
} | |||
} | |||
// Main | |||
$cli = new RenderCLI(); | |||
$cli->run(); |
@@ -0,0 +1,116 @@ | |||
#!/usr/bin/env php | |||
<?php | |||
use splitbrain\phpcli\CLI; | |||
use splitbrain\phpcli\Options; | |||
if (!defined('DOKU_INC')) define('DOKU_INC', realpath(__DIR__ . '/../') . '/'); | |||
define('NOSESSION', 1); | |||
require_once(DOKU_INC . 'inc/init.php'); | |||
/** | |||
* Remove unwanted languages from a DokuWiki install | |||
*/ | |||
class StripLangsCLI extends CLI | |||
{ | |||
/** | |||
* Register options and arguments on the given $options object | |||
* | |||
* @param Options $options | |||
* @return void | |||
*/ | |||
protected function setup(Options $options) | |||
{ | |||
$options->setHelp( | |||
'Remove all languages from the installation, besides the ones specified. English language ' . | |||
'is never removed!' | |||
); | |||
$options->registerOption( | |||
'keep', | |||
'Comma separated list of languages to keep in addition to English.', | |||
'k', | |||
'langcodes' | |||
); | |||
$options->registerOption( | |||
'english-only', | |||
'Remove all languages except English', | |||
'e' | |||
); | |||
} | |||
/** | |||
* Your main program | |||
* | |||
* Arguments and options have been parsed when this is run | |||
* | |||
* @param Options $options | |||
* @return void | |||
*/ | |||
protected function main(Options $options) | |||
{ | |||
if ($options->getOpt('keep')) { | |||
$keep = explode(',', $options->getOpt('keep')); | |||
if (!in_array('en', $keep)) $keep[] = 'en'; | |||
} elseif ($options->getOpt('english-only')) { | |||
$keep = ['en']; | |||
} else { | |||
echo $options->help(); | |||
exit(0); | |||
} | |||
// Kill all language directories in /inc/lang and /lib/plugins besides those in $langs array | |||
$this->stripDirLangs(realpath(__DIR__ . '/../inc/lang'), $keep); | |||
$this->processExtensions(realpath(__DIR__ . '/../lib/plugins'), $keep); | |||
$this->processExtensions(realpath(__DIR__ . '/../lib/tpl'), $keep); | |||
} | |||
/** | |||
* Strip languages from extensions | |||
* | |||
* @param string $path path to plugin or template dir | |||
* @param array $keep_langs languages to keep | |||
*/ | |||
protected function processExtensions($path, $keep_langs) | |||
{ | |||
if (is_dir($path)) { | |||
$entries = scandir($path); | |||
foreach ($entries as $entry) { | |||
if ($entry != "." && $entry != "..") { | |||
if (is_dir($path . '/' . $entry)) { | |||
$plugin_langs = $path . '/' . $entry . '/lang'; | |||
if (is_dir($plugin_langs)) { | |||
$this->stripDirLangs($plugin_langs, $keep_langs); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
/** | |||
* Strip languages from path | |||
* | |||
* @param string $path path to lang dir | |||
* @param array $keep_langs languages to keep | |||
*/ | |||
protected function stripDirLangs($path, $keep_langs) | |||
{ | |||
$dir = dir($path); | |||
while (($cur_dir = $dir->read()) !== false) { | |||
if ($cur_dir != '.' && $cur_dir != '..' && is_dir($path . '/' . $cur_dir)) { | |||
if (!in_array($cur_dir, $keep_langs, true)) { | |||
io_rmdir($path . '/' . $cur_dir, true); | |||
} | |||
} | |||
} | |||
$dir->close(); | |||
} | |||
} | |||
$cli = new StripLangsCLI(); | |||
$cli->run(); |
@@ -0,0 +1,188 @@ | |||
#!/usr/bin/env php | |||
<?php | |||
use dokuwiki\Utf8\Sort; | |||
use dokuwiki\File\PageResolver; | |||
use splitbrain\phpcli\CLI; | |||
use splitbrain\phpcli\Options; | |||
if (!defined('DOKU_INC')) define('DOKU_INC', realpath(__DIR__ . '/../') . '/'); | |||
define('NOSESSION', 1); | |||
require_once(DOKU_INC . 'inc/init.php'); | |||
/** | |||
* Find wanted pages | |||
*/ | |||
class WantedPagesCLI extends CLI | |||
{ | |||
protected const DIR_CONTINUE = 1; | |||
protected const DIR_NS = 2; | |||
protected const DIR_PAGE = 3; | |||
private $skip = false; | |||
private $sort = 'wanted'; | |||
private $result = []; | |||
/** | |||
* Register options and arguments on the given $options object | |||
* | |||
* @param Options $options | |||
* @return void | |||
*/ | |||
protected function setup(Options $options) | |||
{ | |||
$options->setHelp( | |||
'Outputs a list of wanted pages (pages that do not exist yet) and their origin pages ' . | |||
' (the pages that are linkin to these missing pages).' | |||
); | |||
$options->registerArgument( | |||
'namespace', | |||
'The namespace to lookup. Defaults to root namespace', | |||
false | |||
); | |||
$options->registerOption( | |||
'sort', | |||
'Sort by wanted or origin page', | |||
's', | |||
'(wanted|origin)' | |||
); | |||
$options->registerOption( | |||
'skip', | |||
'Do not show the second dimension', | |||
'k' | |||
); | |||
} | |||
/** | |||
* Your main program | |||
* | |||
* Arguments and options have been parsed when this is run | |||
* | |||
* @param Options $options | |||
* @return void | |||
*/ | |||
protected function main(Options $options) | |||
{ | |||
$args = $options->getArgs(); | |||
if ($args) { | |||
$startdir = dirname(wikiFN($args[0] . ':xxx')); | |||
} else { | |||
$startdir = dirname(wikiFN('xxx')); | |||
} | |||
$this->skip = $options->getOpt('skip'); | |||
$this->sort = $options->getOpt('sort'); | |||
$this->info("searching $startdir"); | |||
foreach ($this->getPages($startdir) as $page) { | |||
$this->internalLinks($page); | |||
} | |||
Sort::ksort($this->result); | |||
foreach ($this->result as $main => $subs) { | |||
if ($this->skip) { | |||
echo "$main\n"; | |||
} else { | |||
$subs = array_unique($subs); | |||
Sort::sort($subs); | |||
foreach ($subs as $sub) { | |||
printf("%-40s %s\n", $main, $sub); | |||
} | |||
} | |||
} | |||
} | |||
/** | |||
* Determine directions of the search loop | |||
* | |||
* @param string $entry | |||
* @param string $basepath | |||
* @return int | |||
*/ | |||
protected function dirFilter($entry, $basepath) | |||
{ | |||
if ($entry == '.' || $entry == '..') { | |||
return WantedPagesCLI::DIR_CONTINUE; | |||
} | |||
if (is_dir($basepath . '/' . $entry)) { | |||
if (strpos($entry, '_') === 0) { | |||
return WantedPagesCLI::DIR_CONTINUE; | |||
} | |||
return WantedPagesCLI::DIR_NS; | |||
} | |||
if (preg_match('/\.txt$/', $entry)) { | |||
return WantedPagesCLI::DIR_PAGE; | |||
} | |||
return WantedPagesCLI::DIR_CONTINUE; | |||
} | |||
/** | |||
* Collects recursively the pages in a namespace | |||
* | |||
* @param string $dir | |||
* @return array | |||
* @throws DokuCLI_Exception | |||
*/ | |||
protected function getPages($dir) | |||
{ | |||
static $trunclen = null; | |||
if (!$trunclen) { | |||
global $conf; | |||
$trunclen = strlen($conf['datadir'] . ':'); | |||
} | |||
if (!is_dir($dir)) { | |||
throw new DokuCLI_Exception("Unable to read directory $dir"); | |||
} | |||
$pages = []; | |||
$dh = opendir($dir); | |||
while (false !== ($entry = readdir($dh))) { | |||
$status = $this->dirFilter($entry, $dir); | |||
if ($status == WantedPagesCLI::DIR_CONTINUE) { | |||
continue; | |||
} elseif ($status == WantedPagesCLI::DIR_NS) { | |||
$pages = array_merge($pages, $this->getPages($dir . '/' . $entry)); | |||
} else { | |||
$page = ['id' => pathID(substr($dir . '/' . $entry, $trunclen)), 'file' => $dir . '/' . $entry]; | |||
$pages[] = $page; | |||
} | |||
} | |||
closedir($dh); | |||
return $pages; | |||
} | |||
/** | |||
* Parse instructions and add the non-existing links to the result array | |||
* | |||
* @param array $page array with page id and file path | |||
*/ | |||
protected function internalLinks($page) | |||
{ | |||
global $conf; | |||
$instructions = p_get_instructions(file_get_contents($page['file'])); | |||
$resolver = new PageResolver($page['id']); | |||
$pid = $page['id']; | |||
foreach ($instructions as $ins) { | |||
if ($ins[0] == 'internallink' || ($conf['camelcase'] && $ins[0] == 'camelcaselink')) { | |||
$mid = $resolver->resolveId($ins[1][0]); | |||
if (!page_exists($mid)) { | |||
[$mid] = explode('#', $mid); //record pages without hashes | |||
if ($this->sort == 'origin') { | |||
$this->result[$pid][] = $mid; | |||
} else { | |||
$this->result[$mid][] = $pid; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
// Main | |||
$cli = new WantedPagesCLI(); | |||
$cli->run(); |
@@ -0,0 +1,8 @@ | |||
## no access to the conf directory | |||
<IfModule mod_authz_core.c> | |||
Require all denied | |||
</IfModule> | |||
<IfModule !mod_authz_core.c> | |||
Order allow,deny | |||
Deny from all | |||
</IfModule> |
@@ -0,0 +1,9 @@ | |||
# acl.auth.php | |||
# <?php exit()?> | |||
# Don't modify the lines above | |||
# | |||
# Access Control Lists | |||
# | |||
# Auto-generated by install script | |||
# Date: Sun, 07 Jul 2024 09:48:21 +0000 | |||
* @ALL 8 |
@@ -0,0 +1,21 @@ | |||
# acl.auth.php | |||
# <?php exit()?> | |||
# Don't modify the lines above | |||
# | |||
# Access Control Lists | |||
# | |||
# Editing this file by hand shouldn't be necessary. Use the ACL | |||
# Manager interface instead. | |||
# | |||
# If your auth backend allows special char like spaces in groups | |||
# or user names you need to urlencode them (only chars <128, leave | |||
# UTF-8 multibyte chars as is) | |||
# | |||
# none 0 | |||
# read 1 | |||
# edit 2 | |||
# create 4 | |||
# upload 8 | |||
# delete 16 | |||
* @ALL 8 |
@@ -0,0 +1,62 @@ | |||
# Acronyms. | |||
ACL Access Control List | |||
AFAICS As far as I can see | |||
AFAIK As far as I know | |||
AFAIR As far as I remember | |||
API Application Programming Interface | |||
ASAP As soon as possible | |||
ASCII American Standard Code for Information Interchange | |||
BTW By the way | |||
CMS Content Management System | |||
CSS Cascading Style Sheets | |||
DNS Domain Name System | |||
EOF End of file | |||
EOL End of line | |||
EOM End of message | |||
EOT End of text | |||
FAQ Frequently Asked Questions | |||
FTP File Transfer Protocol | |||
FOSS Free & Open-Source Software | |||
FLOSS Free/Libre and Open Source Software | |||
FUD Fear, Uncertainty, and Doubt | |||
FYI For your information | |||
GB Gigabyte | |||
GHz Gigahertz | |||
GPL GNU General Public License | |||
GUI Graphical User Interface | |||
HTML HyperText Markup Language | |||
IANAL I am not a lawyer (but) | |||
IE Internet Explorer | |||
IIRC If I remember correctly | |||
IMHO In my humble opinion | |||
IMO In my opinion | |||
IOW In other words | |||
IRC Internet Relay Chat | |||
IRL In real life | |||
KISS Keep it simple stupid | |||
LAN Local Area Network | |||
LGPL GNU Lesser General Public License | |||
LOL Laughing out loud | |||
MathML Mathematical Markup Language | |||
MB Megabyte | |||
MHz Megahertz | |||
MSIE Microsoft Internet Explorer | |||
OMG Oh my God | |||
OS Operating System | |||
OSS Open Source Software | |||
OTOH On the other hand | |||
PITA Pain in the Ass | |||
RFC Request for Comments | |||
ROTFL Rolling on the floor laughing | |||
RTFM Read The Fine Manual | |||
spec specification | |||
TIA Thanks in advance | |||
TL;DR Too long; didn't read | |||
TOC Table of Contents | |||
URI Uniform Resource Identifier | |||
URL Uniform Resource Locator | |||
W3C World Wide Web Consortium | |||
WTF? What the f*** | |||
WYSIWYG What You See Is What You Get | |||
YMMV Your mileage may vary |
@@ -0,0 +1,181 @@ | |||
<?php | |||
/** | |||
* This is DokuWiki's Main Configuration file | |||
* | |||
* All the default values are kept here, you should not modify it but use | |||
* a local.php file instead to override the settings from here. | |||
* | |||
* This is a piece of PHP code so PHP syntax applies! | |||
* | |||
* For help with the configuration and a more detailed explanation of the various options | |||
* see https://www.dokuwiki.org/config | |||
*/ | |||
/* Basic Settings */ | |||
$conf['title'] = 'DokuWiki'; //what to show in the title | |||
$conf['start'] = 'start'; //name of start page | |||
$conf['lang'] = 'en'; //your language | |||
$conf['template'] = 'dokuwiki'; //see lib/tpl directory | |||
$conf['tagline'] = ''; //tagline in header (if template supports it) | |||
$conf['sidebar'] = 'sidebar'; //name of sidebar in root namespace (if template supports it) | |||
$conf['license'] = 'cc-by-nc-sa'; //see conf/license.php | |||
$conf['savedir'] = './data'; //where to store all the files | |||
$conf['basedir'] = ''; //absolute dir from serveroot - blank for autodetection | |||
$conf['baseurl'] = ''; //URL to server including protocol - blank for autodetect | |||
$conf['cookiedir'] = ''; //path to use in cookies - blank for basedir | |||
$conf['dmode'] = 0755; //set directory creation mode | |||
$conf['fmode'] = 0644; //set file creation mode | |||
$conf['allowdebug'] = 0; //allow debug output, enable if needed 0|1 | |||
/* Display Settings */ | |||
$conf['recent'] = 20; //how many entries to show in recent | |||
$conf['recent_days'] = 7; //How many days of recent changes to keep. (days) | |||
$conf['breadcrumbs'] = 10; //how many recent visited pages to show | |||
$conf['youarehere'] = 0; //show "You are here" navigation? 0|1 | |||
$conf['fullpath'] = 0; //show full path of the document or relative to datadir only? 0|1 | |||
$conf['typography'] = 1; //smartquote conversion 0=off, 1=doublequotes, 2=all quotes | |||
$conf['dformat'] = '%Y/%m/%d %H:%M'; //dateformat accepted by PHPs strftime() function | |||
$conf['signature'] = ' --- //[[@MAIL@|@NAME@]] @DATE@//'; //signature see wiki page for details | |||
$conf['showuseras'] = 'loginname'; // 'loginname' users login name | |||
// 'username' users full name | |||
// 'email' e-mail address (will be obfuscated as per mailguard) | |||
// 'email_link' e-mail address as a mailto: link (obfuscated) | |||
$conf['toptoclevel'] = 1; //Level starting with and below to include in AutoTOC (max. 5) | |||
$conf['tocminheads'] = 3; //Minimum amount of headlines that determines if a TOC is built | |||
$conf['maxtoclevel'] = 3; //Up to which level include into AutoTOC (max. 5) | |||
$conf['maxseclevel'] = 3; //Up to which level create editable sections (max. 5) | |||
$conf['camelcase'] = 0; //Use CamelCase for linking? (I don't like it) 0|1 | |||
$conf['deaccent'] = 1; //deaccented chars in pagenames (1) or romanize (2) or keep (0)? | |||
$conf['useheading'] = 0; //use the first heading in a page as its name | |||
$conf['sneaky_index']= 0; //check for namespace read permission in index view (0|1) (1 might cause unexpected behavior) | |||
$conf['hidepages'] = ''; //Regexp for pages to be skipped from RSS, Search and Recent Changes | |||
/* Authentication Settings */ | |||
$conf['useacl'] = 0; //Use Access Control Lists to restrict access? | |||
$conf['autopasswd'] = 1; //autogenerate passwords and email them to user | |||
$conf['authtype'] = 'authplain'; //which authentication backend should be used | |||
$conf['passcrypt'] = 'bcrypt'; //Used crypt method (smd5,md5,sha1,ssha,crypt,mysql,my411,bcrypt) | |||
$conf['defaultgroup']= 'user'; //Default groups new Users are added to | |||
$conf['superuser'] = '!!not set!!'; //The admin can be user or @group or comma separated list user1,@group1,user2 | |||
$conf['manager'] = '!!not set!!'; //The manager can be user or @group or comma separated list user1,@group1,user2 | |||
$conf['profileconfirm'] = 1; //Require current password to confirm changes to user profile | |||
$conf['rememberme'] = 1; //Enable/disable remember me on login | |||
$conf['disableactions'] = ''; //comma separated list of actions to disable | |||
$conf['auth_security_timeout'] = 900; //time (seconds) auth data is considered valid, set to 0 to recheck on every page view | |||
$conf['securecookie'] = 1; //never send HTTPS cookies via HTTP | |||
$conf['samesitecookie'] = 'Lax'; //SameSite attribute for cookies (Lax|Strict|None|Empty) | |||
$conf['remote'] = 0; //Enable/disable remote interfaces | |||
$conf['remoteuser'] = '!!not set!!'; //user/groups that have access to remote interface (comma separated). leave empty to allow all users | |||
$conf['remotecors'] = ''; //enable Cross-Origin Resource Sharing (CORS) for the remote interfaces. Asterisk (*) to allow all origins. leave empty to deny. | |||
/* Antispam Features */ | |||
$conf['usewordblock']= 1; //block spam based on words? 0|1 | |||
$conf['relnofollow'] = 1; //use rel="ugc nofollow" for external links? | |||
$conf['indexdelay'] = 60*60*24*5; //allow indexing after this time (seconds) default is 5 days | |||
$conf['mailguard'] = 'hex'; //obfuscate email addresses against spam harvesters? | |||
//valid entries are: | |||
// 'visible' - replace @ with [at], . with [dot] and - with [dash] | |||
// 'hex' - use hex entities to encode the mail address | |||
// 'none' - do not obfuscate addresses | |||
$conf['iexssprotect']= 1; // check for JavaScript and HTML in uploaded files 0|1 | |||
/* Editing Settings */ | |||
$conf['usedraft'] = 1; //automatically save a draft while editing (0|1) | |||
$conf['locktime'] = 15*60; //maximum age for lockfiles (defaults to 15 minutes) | |||
$conf['cachetime'] = 60*60*24; //maximum age for cachefile in seconds (defaults to a day) | |||
/* Link Settings */ | |||
// Set target to use when creating links - leave empty for same window | |||
$conf['target']['wiki'] = ''; | |||
$conf['target']['interwiki'] = ''; | |||
$conf['target']['extern'] = ''; | |||
$conf['target']['media'] = ''; | |||
$conf['target']['windows'] = ''; | |||
/* Media Settings */ | |||
$conf['mediarevisions'] = 1; //enable/disable media revisions | |||
$conf['refcheck'] = 1; //check for references before deleting media files | |||
$conf['gdlib'] = 2; //the GDlib version (0, 1 or 2) 2 tries to autodetect | |||
$conf['im_convert'] = ''; //path to ImageMagicks convert (will be used instead of GD) | |||
$conf['jpg_quality'] = '70'; //quality of compression when scaling jpg images (0-100) | |||
$conf['fetchsize'] = 0; //maximum size (bytes) fetch.php may download from extern, disabled by default | |||
/* Notification Settings */ | |||
$conf['subscribers'] = 0; //enable change notice subscription support | |||
$conf['subscribe_time'] = 24*60*60; //Time after which digests / lists are sent (in sec, default 1 day) | |||
//Should be smaller than the time specified in recent_days | |||
$conf['notify'] = ''; //send change info to this email (leave blank for nobody) | |||
$conf['registernotify'] = ''; //send info about newly registered users to this email (leave blank for nobody) | |||
$conf['mailfrom'] = ''; //use this email when sending mails | |||
$conf['mailreturnpath'] = ''; //use this email as returnpath for bounce mails | |||
$conf['mailprefix'] = ''; //use this as prefix of outgoing mails | |||
$conf['htmlmail'] = 1; //send HTML multipart mails | |||
$conf['dontlog'] = 'debug'; //logging facilities that should be disabled | |||
$conf['logretain'] = 3; //how many days of logs to keep | |||
/* Syndication Settings */ | |||
$conf['sitemap'] = 0; //Create a Google sitemap? How often? In days. | |||
$conf['rss_type'] = 'rss1'; //type of RSS feed to provide, by default: | |||
// 'rss' - RSS 0.91 | |||
// 'rss1' - RSS 1.0 | |||
// 'rss2' - RSS 2.0 | |||
// 'atom' - Atom 0.3 | |||
// 'atom1' - Atom 1.0 | |||
$conf['rss_linkto'] = 'diff'; //what page RSS entries link to: | |||
// 'diff' - page showing revision differences | |||
// 'page' - the revised page itself | |||
// 'rev' - page showing all revisions | |||
// 'current' - most recent revision of page | |||
$conf['rss_content'] = 'abstract'; //what to put in the items by default? | |||
// 'abstract' - plain text, first paragraph or so | |||
// 'diff' - plain text unified diff wrapped in <pre> tags | |||
// 'htmldiff' - diff as HTML table | |||
// 'html' - the full page rendered in XHTML | |||
$conf['rss_media'] = 'both'; //what should be listed? | |||
// 'both' - page and media changes | |||
// 'pages' - page changes only | |||
// 'media' - media changes only | |||
$conf['rss_update'] = 5*60; //Update the RSS feed every n seconds (defaults to 5 minutes) | |||
$conf['rss_show_summary'] = 1; //Add revision summary to title? 0|1 | |||
$conf['rss_show_deleted'] = 1; //Show deleted items 0|1 | |||
/* Advanced Settings */ | |||
$conf['updatecheck'] = 1; //automatically check for new releases? | |||
$conf['userewrite'] = 0; //this makes nice URLs: 0: off 1: .htaccess 2: internal | |||
$conf['useslash'] = 0; //use slash instead of colon? only when rewrite is on | |||
$conf['sepchar'] = '_'; //word separator character in page names; may be a | |||
// letter, a digit, '_', '-', or '.'. | |||
$conf['canonical'] = 0; //Should all URLs use full canonical http://... style? | |||
$conf['fnencode'] = 'url'; //encode filenames (url|safe|utf-8) | |||
$conf['autoplural'] = 0; //try (non)plural form of nonexistent files? | |||
$conf['compression'] = 'gz'; //compress old revisions: (0: off) ('gz': gnuzip) ('bz2': bzip) | |||
// bz2 generates smaller files, but needs more cpu-power | |||
$conf['gzip_output'] = 0; //use gzip content encoding for the output xhtml (if allowed by browser) | |||
$conf['compress'] = 1; //Strip whitespaces and comments from Styles and JavaScript? 1|0 | |||
$conf['cssdatauri'] = 512; //Maximum byte size of small images to embed into CSS, won't work on IE<8 | |||
$conf['send404'] = 0; //Send an HTTP 404 status for nonexistent pages? | |||
$conf['broken_iua'] = 0; //Platform with broken ignore_user_abort (IIS+CGI) 0|1 | |||
$conf['xsendfile'] = 0; //Use X-Sendfile (1 = lighttpd, 2 = standard) | |||
$conf['renderer_xhtml'] = 'xhtml'; //renderer to use for main page generation | |||
$conf['readdircache'] = 0; //time cache in second for the readdir operation, 0 to deactivate. | |||
$conf['search_nslimit'] = 0; //limit the search to the current X namespaces | |||
$conf['search_fragment'] = 'exact'; //specify the default fragment search behavior | |||
$conf['trustedproxy'] = '^(::1|[fF][eE]80:|127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)'; | |||
//Regexp of trusted proxy address when reading IP using HTTP header | |||
// if blank, do not trust any proxy (including local IP) | |||
/* Feature Flags */ | |||
$conf['defer_js'] = 1; // Defer javascript to be executed after the page's HTML has been parsed. Setting will be removed in the next release. | |||
$conf['hidewarnings'] = 0; // Hide warnings | |||
/* Network Settings */ | |||
$conf['dnslookups'] = 1; //disable to disallow IP to hostname lookups | |||
$conf['jquerycdn'] = 0; //use a CDN for delivering jQuery? | |||
// Proxy setup - if your Server needs a proxy to access the web set these | |||
$conf['proxy']['host'] = ''; | |||
$conf['proxy']['port'] = ''; | |||
$conf['proxy']['user'] = ''; | |||
$conf['proxy']['pass'] = ''; | |||
$conf['proxy']['ssl'] = 0; | |||
$conf['proxy']['except'] = ''; |
@@ -0,0 +1,22 @@ | |||
# Typography replacements | |||
# | |||
# Order does matter! | |||
# | |||
# You can use HTML entities here, but it is not recommended because it may break | |||
# non-HTML renderers. Use UTF-8 chars directly instead. | |||
<-> ↔ | |||
-> → | |||
<- ← | |||
<=> ⇔ | |||
=> ⇒ | |||
<= ⇐ | |||
>> » | |||
<< « | |||
--- — | |||
-- – | |||
(c) © | |||
(tm) ™ | |||
(r) ® | |||
... … | |||
@@ -0,0 +1,43 @@ | |||
# Each URL may contain one of these placeholders | |||
# {URL} is replaced by the URL encoded representation of the wikiname | |||
# this is the right thing to do in most cases | |||
# {NAME} this is replaced by the wikiname as given in the document | |||
# only mandatory encoded is done, urlencoding if the link | |||
# is an external URL, or encoding as a wikiname if it is an | |||
# internal link (begins with a colon) | |||
# {SCHEME} | |||
# {HOST} | |||
# {PORT} | |||
# {PATH} | |||
# {QUERY} these placeholders will be replaced with the appropriate part | |||
# of the link when parsed as a URL | |||
# If no placeholder is defined the urlencoded name is appended to the URL | |||
# To prevent losing your added InterWiki shortcuts after an upgrade, | |||
# you should add new ones to interwiki.local.conf | |||
wp https://en.wikipedia.org/wiki/{NAME} | |||
wpfr https://fr.wikipedia.org/wiki/{NAME} | |||
wpde https://de.wikipedia.org/wiki/{NAME} | |||
wpes https://es.wikipedia.org/wiki/{NAME} | |||
wppl https://pl.wikipedia.org/wiki/{NAME} | |||
wpjp https://ja.wikipedia.org/wiki/{NAME} | |||
wpru https://ru.wikipedia.org/wiki/{NAME} | |||
wpmeta https://meta.wikipedia.org/wiki/{NAME} | |||
doku https://www.dokuwiki.org/ | |||
rfc https://tools.ietf.org/html/rfc | |||
man http://man.cx/ | |||
amazon https://www.amazon.com/dp/{URL}?tag=splitbrain-20 | |||
amazon.de https://www.amazon.de/dp/{URL}?tag=splitbrain-21 | |||
amazon.uk https://www.amazon.co.uk/dp/{URL} | |||
paypal https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business= | |||
phpfn https://secure.php.net/{NAME} | |||
skype skype:{NAME} | |||
google https://www.google.com/search?q= | |||
google.de https://www.google.de/search?q= | |||
go https://www.google.com/search?q={URL}&btnI=lucky | |||
user :user:{NAME} | |||
# To support VoIP/SIP/TEL links | |||
callto callto://{NAME} | |||
tel tel:{NAME} |
@@ -0,0 +1,38 @@ | |||
<?php | |||
/** | |||
* This file defines multiple available licenses you can license your | |||
* wiki contents under. Do not change this file, but create a | |||
* license.local.php instead. | |||
*/ | |||
if(empty($LC)) $LC = empty($conf['lang']) ? 'en' : $conf['lang']; | |||
$license['cc-zero'] = array( | |||
'name' => 'CC0 1.0 Universal', | |||
'url' => 'https://creativecommons.org/publicdomain/zero/1.0/deed.'.$LC, | |||
); | |||
$license['publicdomain'] = array( | |||
'name' => 'Public Domain', | |||
'url' => 'https://creativecommons.org/licenses/publicdomain/deed.'.$LC, | |||
); | |||
$license['cc-by'] = array( | |||
'name' => 'CC Attribution 4.0 International', | |||
'url' => 'https://creativecommons.org/licenses/by/4.0/deed.'.$LC, | |||
); | |||
$license['cc-by-sa'] = array( | |||
'name' => 'CC Attribution-Share Alike 4.0 International', | |||
'url' => 'https://creativecommons.org/licenses/by-sa/4.0/deed.'.$LC, | |||
); | |||
$license['gnufdl'] = array( | |||
'name' => 'GNU Free Documentation License 1.3', | |||
'url' => 'https://www.gnu.org/licenses/fdl-1.3.html', | |||
); | |||
$license['cc-by-nc'] = array( | |||
'name' => 'CC Attribution-Noncommercial 4.0 International', | |||
'url' => 'https://creativecommons.org/licenses/by-nc/4.0/deed.'.$LC, | |||
); | |||
$license['cc-by-nc-sa'] = array( | |||
'name' => 'CC Attribution-Noncommercial-Share Alike 4.0 International', | |||
'url' => 'https://creativecommons.org/licenses/by-nc-sa/4.0/deed.'.$LC, | |||
); | |||
@@ -0,0 +1,21 @@ | |||
<?php | |||
/* | |||
* Dokuwiki's Main Configuration File - Local Settings | |||
* Auto-generated by config plugin | |||
* Run for user: miteruzo | |||
* Date: Mon, 08 Jul 2024 01:50:32 +0900 | |||
*/ | |||
$conf['title'] = 'Mr.伝説 Wiki'; | |||
$conf['lang'] = 'ja'; | |||
$conf['template'] = 'vector'; | |||
$conf['license'] = 'cc-zero'; | |||
$conf['useacl'] = 1; | |||
$conf['superuser'] = '@admin'; | |||
$conf['target']['interwiki'] = '_blank'; | |||
$conf['target']['extern'] = '_blank'; | |||
$conf['userewrite'] = '1'; | |||
$conf['plugin']['ckgedit']['scayt_lang'] = 'British English/en_GB'; | |||
$conf['plugin']['ckgedit']['other_lang'] = 'ja'; | |||
$conf['tpl']['bootstrap3']['socialShareProviders'] = 'facebook,linkedin,microsoft-teams,pinterest,whatsapp,reddit,twitter,telegram,yammer,google-plus'; | |||
$conf['tpl']['flat']['topSidebar'] = 'sidebar'; |
@@ -0,0 +1,16 @@ | |||
<?php | |||
/** | |||
* This is an example of how a local.php could look like. | |||
* Simply copy the options you want to change from dokuwiki.php | |||
* to this file and change them. | |||
* | |||
* When using the installer, a correct local.php file be generated for | |||
* you automatically. | |||
*/ | |||
//$conf['title'] = 'My Wiki'; //what to show in the title | |||
//$conf['useacl'] = 1; //Use Access Control Lists to restrict access? | |||
//$conf['superuser'] = 'joe'; | |||
@@ -0,0 +1,3 @@ | |||
{ | |||
"display": "standalone" | |||
} |
@@ -0,0 +1,91 @@ | |||
<?php | |||
/** | |||
* This configures which metadata will be editable through | |||
* the media manager. Each field of the array is an array with the | |||
* following contents: | |||
* fieldname - Where data will be saved (EXIF or IPTC field) | |||
* label - key to lookup in the $lang var, if not found printed as is | |||
* htmltype - 'text', 'textarea' or 'date' | |||
* lookups - array additional fields to look up the data (EXIF or IPTC fields) | |||
* | |||
* The fields are not ordered continuously to make inserting additional items | |||
* in between simpler. | |||
* | |||
* This is a PHP snippet, so PHP syntax applies. | |||
* | |||
* Note: $fields is not a global variable and will not be available to any | |||
* other functions or templates later | |||
* | |||
* You may extend or overwrite this variable in an optional | |||
* conf/mediameta.local.php file | |||
* | |||
* For a list of available EXIF/IPTC fields refer to | |||
* http://www.dokuwiki.org/devel:templates:detail.php | |||
*/ | |||
$fields = array( | |||
10 => array('Iptc.Headline', | |||
'img_title', | |||
'text'), | |||
20 => array('', | |||
'img_date', | |||
'date', | |||
array('Date.EarliestTime')), | |||
30 => array('', | |||
'img_fname', | |||
'text', | |||
array('File.Name')), | |||
40 => array('Iptc.Caption', | |||
'img_caption', | |||
'textarea', | |||
array('Exif.UserComment', | |||
'Exif.TIFFImageDescription', | |||
'Exif.TIFFUserComment')), | |||
50 => array('Iptc.Byline', | |||
'img_artist', | |||
'text', | |||
array('Exif.TIFFArtist', | |||
'Exif.Artist', | |||
'Iptc.Credit')), | |||
60 => array('Iptc.CopyrightNotice', | |||
'img_copyr', | |||
'text', | |||
array('Exif.TIFFCopyright', | |||
'Exif.Copyright')), | |||
70 => array('', | |||
'img_format', | |||
'text', | |||
array('File.Format')), | |||
80 => array('', | |||
'img_fsize', | |||
'text', | |||
array('File.NiceSize')), | |||
90 => array('', | |||
'img_width', | |||
'text', | |||
array('File.Width')), | |||
100 => array('', | |||
'img_height', | |||
'text', | |||
array('File.Height')), | |||
110 => array('', | |||
'img_camera', | |||
'text', | |||
array('Simple.Camera')), | |||
120 => array('Iptc.Keywords', | |||
'img_keywords', | |||
'text', | |||
array('Exif.Category')), | |||
); |
@@ -0,0 +1,75 @@ | |||
# Allowed uploadable file extensions and mimetypes are defined here. | |||
# To extend this file it is recommended to create a mime.local.conf | |||
# file. Mimetypes that should be downloadable and not be opened in the | |||
# should be prefixed with a ! | |||
jpg image/jpeg | |||
jpeg image/jpeg | |||
gif image/gif | |||
png image/png | |||
webp image/webp | |||
ico image/vnd.microsoft.icon | |||
mp3 audio/mpeg | |||
ogg audio/ogg | |||
wav audio/wav | |||
webm video/webm | |||
ogv video/ogg | |||
mp4 video/mp4 | |||
vtt text/vtt | |||
tgz !application/octet-stream | |||
tar !application/x-gtar | |||
gz !application/octet-stream | |||
bz2 !application/octet-stream | |||
zip !application/zip | |||
rar !application/rar | |||
7z !application/x-7z-compressed | |||
pdf application/pdf | |||
ps !application/postscript | |||
rpm !application/octet-stream | |||
deb !application/octet-stream | |||
doc !application/msword | |||
xls !application/msexcel | |||
ppt !application/mspowerpoint | |||
rtf !application/msword | |||
docx !application/vnd.openxmlformats-officedocument.wordprocessingml.document | |||
xlsx !application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | |||
pptx !application/vnd.openxmlformats-officedocument.presentationml.presentation | |||
sxw !application/soffice | |||
sxc !application/soffice | |||
sxi !application/soffice | |||
sxd !application/soffice | |||
odc !application/vnd.oasis.opendocument.chart | |||
odf !application/vnd.oasis.opendocument.formula | |||
odg !application/vnd.oasis.opendocument.graphics | |||
odi !application/vnd.oasis.opendocument.image | |||
odp !application/vnd.oasis.opendocument.presentation | |||
ods !application/vnd.oasis.opendocument.spreadsheet | |||
odt !application/vnd.oasis.opendocument.text | |||
svg image/svg+xml | |||
# You should enable HTML and Text uploads only for restricted Wikis. | |||
# Spammers are known to upload spam pages through unprotected Wikis. | |||
# Note: Enabling HTML opens Cross Site Scripting vulnerabilities | |||
# through JavaScript. Only enable this with trusted users. You | |||
# need to disable the iexssprotect option additionally to | |||
# adding the mime type here | |||
#html text/html | |||
#htm text/html | |||
#txt text/plain | |||
#conf text/plain | |||
#xml text/xml | |||
#csv text/csv | |||
# Also flash may be able to execute arbitrary scripts in the website's | |||
# context | |||
#swf application/x-shockwave-flash | |||
@@ -0,0 +1,253 @@ | |||
<?php | |||
/* | |||
* This is an example configuration for the mysql auth plugin. | |||
* | |||
* This SQL statements are optimized for following table structure. | |||
* If you use a different one you have to change them accordingly. | |||
* See comments of every statement for details. | |||
* | |||
* TABLE users | |||
* uid login pass firstname lastname email | |||
* | |||
* TABLE groups | |||
* gid name | |||
* | |||
* TABLE usergroup | |||
* uid gid | |||
* | |||
* To use this configuration you have to copy them to local.protected.php | |||
* or at least include this file in local.protected.php. | |||
*/ | |||
/* Options to configure database access. You need to set up this | |||
* options carefully, otherwise you won't be able to access you | |||
* database. | |||
*/ | |||
$conf['plugin']['authmysql']['server'] = ''; | |||
$conf['plugin']['authmysql']['user'] = ''; | |||
$conf['plugin']['authmysql']['password'] = ''; | |||
$conf['plugin']['authmysql']['database'] = ''; | |||
/* This option enables debug messages in the mysql plugin. It is | |||
* mostly useful for system admins. | |||
*/ | |||
$conf['plugin']['authmysql']['debug'] = 0; | |||
/* Normally password encryption is done by DokuWiki (recommended) but for | |||
* some reasons it might be useful to let the database do the encryption. | |||
* Set 'forwardClearPass' to '1' and the cleartext password is forwarded to | |||
* the database, otherwise the encrypted one. | |||
*/ | |||
$conf['plugin']['authmysql']['forwardClearPass'] = 0; | |||
/* Multiple table operations will be protected by locks. This array tells | |||
* the plugin which tables to lock. If you use any aliases for table names | |||
* these array must also contain these aliases. Any unnamed alias will cause | |||
* a warning during operation. See the example below. | |||
*/ | |||
$conf['plugin']['authmysql']['TablesToLock']= array("users", "users AS u","groups", "groups AS g", "usergroup", "usergroup AS ug"); | |||
/***********************************************************************/ | |||
/* Basic SQL statements for user authentication (required) */ | |||
/***********************************************************************/ | |||
/* This statement is used to grant or deny access to the wiki. The result | |||
* should be a table with exact one line containing at least the password | |||
* of the user. If the result table is empty or contains more than one | |||
* row, access will be denied. | |||
* | |||
* The plugin accesses the password as 'pass' so an alias might be necessary. | |||
* | |||
* Following patters will be replaced: | |||
* %{user} user name | |||
* %{pass} encrypted or clear text password (depends on 'encryptPass') | |||
* %{dgroup} default group name | |||
*/ | |||
$conf['plugin']['authmysql']['checkPass'] = "SELECT pass | |||
FROM usergroup AS ug | |||
JOIN users AS u ON u.uid=ug.uid | |||
JOIN groups AS g ON g.gid=ug.gid | |||
WHERE login='%{user}' | |||
AND name='%{dgroup}'"; | |||
/* This statement should return a table with exact one row containing | |||
* information about one user. The field needed are: | |||
* 'pass' containing the encrypted or clear text password | |||
* 'name' the user's full name | |||
* 'mail' the user's email address | |||
* | |||
* Keep in mind that Dokuwiki will access this information through the | |||
* names listed above so aliases might be necessary. | |||
* | |||
* Following patters will be replaced: | |||
* %{user} user name | |||
*/ | |||
$conf['plugin']['authmysql']['getUserInfo'] = "SELECT pass, CONCAT(firstname,' ',lastname) AS name, email AS mail | |||
FROM users | |||
WHERE login='%{user}'"; | |||
/* This statement is used to get all groups a user is member of. The | |||
* result should be a table containing all groups the given user is | |||
* member of. The plugin accesses the group name as 'group' so an alias | |||
* might be necessary. | |||
* | |||
* Following patters will be replaced: | |||
* %{user} user name | |||
*/ | |||
$conf['plugin']['authmysql']['getGroups'] = "SELECT name as `group` | |||
FROM groups g, users u, usergroup ug | |||
WHERE u.uid = ug.uid | |||
AND g.gid = ug.gid | |||
AND u.login='%{user}'"; | |||
/***********************************************************************/ | |||
/* Additional minimum SQL statements to use the user manager */ | |||
/***********************************************************************/ | |||
/* This statement should return a table containing all user login names | |||
* that meet certain filter criteria. The filter expressions will be added | |||
* case dependent by the plugin. At the end a sort expression will be added. | |||
* Important is that this list contains no double entries for a user. Each | |||
* user name is only allowed once in the table. | |||
* | |||
* The login name will be accessed as 'user' to an alias might be necessary. | |||
* No patterns will be replaced in this statement but following patters | |||
* will be replaced in the filter expressions: | |||
* %{user} in FilterLogin user's login name | |||
* %{name} in FilterName user's full name | |||
* %{email} in FilterEmail user's email address | |||
* %{group} in FilterGroup group name | |||
*/ | |||
$conf['plugin']['authmysql']['getUsers'] = "SELECT DISTINCT login AS user | |||
FROM users AS u | |||
LEFT JOIN usergroup AS ug ON u.uid=ug.uid | |||
LEFT JOIN groups AS g ON ug.gid=g.gid"; | |||
$conf['plugin']['authmysql']['FilterLogin'] = "login LIKE '%{user}'"; | |||
$conf['plugin']['authmysql']['FilterName'] = "CONCAT(firstname,' ',lastname) LIKE '%{name}'"; | |||
$conf['plugin']['authmysql']['FilterEmail'] = "email LIKE '%{email}'"; | |||
$conf['plugin']['authmysql']['FilterGroup'] = "name LIKE '%{group}'"; | |||
$conf['plugin']['authmysql']['SortOrder'] = "ORDER BY login"; | |||
/***********************************************************************/ | |||
/* Additional SQL statements to add new users with the user manager */ | |||
/***********************************************************************/ | |||
/* This statement should add a user to the database. Minimum information | |||
* to store are: login name, password, email address and full name. | |||
* | |||
* Following patterns will be replaced: | |||
* %{user} user's login name | |||
* %{pass} password (encrypted or clear text, depends on 'encryptPass') | |||
* %{email} email address | |||
* %{name} user's full name | |||
*/ | |||
$conf['plugin']['authmysql']['addUser'] = "INSERT INTO users | |||
(login, pass, email, firstname, lastname) | |||
VALUES ('%{user}', '%{pass}', '%{email}', | |||
SUBSTRING_INDEX('%{name}',' ', 1), | |||
SUBSTRING_INDEX('%{name}',' ', -1))"; | |||
/* This statement should add a group to the database. | |||
* Following patterns will be replaced: | |||
* %{group} group name | |||
*/ | |||
$conf['plugin']['authmysql']['addGroup'] = "INSERT INTO groups (name) | |||
VALUES ('%{group}')"; | |||
/* This statement should connect a user to a group (a user become member | |||
* of that group). | |||
* Following patterns will be replaced: | |||
* %{user} user's login name | |||
* %{uid} id of a user dataset | |||
* %{group} group name | |||
* %{gid} id of a group dataset | |||
*/ | |||
$conf['plugin']['authmysql']['addUserGroup']= "INSERT INTO usergroup (uid, gid) | |||
VALUES ('%{uid}', '%{gid}')"; | |||
/* This statement should remove a group fom the database. | |||
* Following patterns will be replaced: | |||
* %{group} group name | |||
* %{gid} id of a group dataset | |||
*/ | |||
$conf['plugin']['authmysql']['delGroup'] = "DELETE FROM groups | |||
WHERE gid='%{gid}'"; | |||
/* This statement should return the database index of a given user name. | |||
* The plugin will access the index with the name 'id' so an alias might be | |||
* necessary. | |||
* following patters will be replaced: | |||
* %{user} user name | |||
*/ | |||
$conf['plugin']['authmysql']['getUserID'] = "SELECT uid AS id | |||
FROM users | |||
WHERE login='%{user}'"; | |||
/***********************************************************************/ | |||
/* Additional SQL statements to delete users with the user manager */ | |||
/***********************************************************************/ | |||
/* This statement should remove a user fom the database. | |||
* Following patterns will be replaced: | |||
* %{user} user's login name | |||
* %{uid} id of a user dataset | |||
*/ | |||
$conf['plugin']['authmysql']['delUser'] = "DELETE FROM users | |||
WHERE uid='%{uid}'"; | |||
/* This statement should remove all connections from a user to any group | |||
* (a user quits membership of all groups). | |||
* Following patterns will be replaced: | |||
* %{uid} id of a user dataset | |||
*/ | |||
$conf['plugin']['authmysql']['delUserRefs'] = "DELETE FROM usergroup | |||
WHERE uid='%{uid}'"; | |||
/***********************************************************************/ | |||
/* Additional SQL statements to modify users with the user manager */ | |||
/***********************************************************************/ | |||
/* This statements should modify a user entry in the database. The | |||
* statements UpdateLogin, UpdatePass, UpdateEmail and UpdateName will be | |||
* added to updateUser on demand. Only changed parameters will be used. | |||
* | |||
* Following patterns will be replaced: | |||
* %{user} user's login name | |||
* %{pass} password (encrypted or clear text, depends on 'encryptPass') | |||
* %{email} email address | |||
* %{name} user's full name | |||
* %{uid} user id that should be updated | |||
*/ | |||
$conf['plugin']['authmysql']['updateUser'] = "UPDATE users SET"; | |||
$conf['plugin']['authmysql']['UpdateLogin'] = "login='%{user}'"; | |||
$conf['plugin']['authmysql']['UpdatePass'] = "pass='%{pass}'"; | |||
$conf['plugin']['authmysql']['UpdateEmail'] = "email='%{email}'"; | |||
$conf['plugin']['authmysql']['UpdateName'] = "firstname=SUBSTRING_INDEX('%{name}',' ', 1), | |||
lastname=SUBSTRING_INDEX('%{name}',' ', -1)"; | |||
$conf['plugin']['authmysql']['UpdateTarget']= "WHERE uid=%{uid}"; | |||
/* This statement should remove a single connection from a user to a | |||
* group (a user quits membership of that group). | |||
* | |||
* Following patterns will be replaced: | |||
* %{user} user's login name | |||
* %{uid} id of a user dataset | |||
* %{group} group name | |||
* %{gid} id of a group dataset | |||
*/ | |||
$conf['plugin']['authmysql']['delUserGroup']= "DELETE FROM usergroup | |||
WHERE uid='%{uid}' | |||
AND gid='%{gid}'"; | |||
/* This statement should return the database index of a given group name. | |||
* The plugin will access the index with the name 'id' so an alias might | |||
* be necessary. | |||
* | |||
* Following patters will be replaced: | |||
* %{group} group name | |||
*/ | |||
$conf['plugin']['authmysql']['getGroupID'] = "SELECT gid AS id | |||
FROM groups | |||
WHERE name='%{group}'"; | |||
@@ -0,0 +1,12 @@ | |||
<?php | |||
/* | |||
* Local plugin enable/disable settings | |||
* | |||
* Auto-generated by install script | |||
* Date: Sun, 07 Jul 2024 09:48:21 +0000 | |||
*/ | |||
$plugins['authad'] = 0; | |||
$plugins['authldap'] = 0; | |||
$plugins['authmysql'] = 0; | |||
$plugins['authpgsql'] = 0; |
@@ -0,0 +1,6 @@ | |||
<?php | |||
/** | |||
* This file configures the default states of available plugins. All settings in | |||
* the plugins.*.php files will override those here. | |||
*/ | |||
$plugins['testing'] = 0; |
@@ -0,0 +1,12 @@ | |||
<?php | |||
/** | |||
* This file configures the enabled/disabled status of plugins, which are also protected | |||
* from changes by the extension manager. These settings will override any local settings. | |||
* It is not recommended to change this file, as it is overwritten on DokuWiki upgrades. | |||
*/ | |||
$plugins['acl'] = 1; | |||
$plugins['authplain'] = 1; | |||
$plugins['extension'] = 1; | |||
$plugins['config'] = 1; | |||
$plugins['usermanager'] = 1; | |||
$plugins['template:dokuwiki'] = 1; // not a plugin, but this should not be uninstalled either |
@@ -0,0 +1,11 @@ | |||
#Add URL schemes you want to be recognized as links here | |||
http | |||
https | |||
telnet | |||
gopher | |||
wais | |||
ftp | |||
ed2k | |||
irc | |||
ldap |
@@ -0,0 +1,28 @@ | |||
# Smileys configured here will be replaced by the | |||
# configured images in the smiley directory | |||
8-) cool.svg | |||
8-O eek.svg | |||
8-o eek.svg | |||
:-( sad.svg | |||
:-) smile.svg | |||
=) smile2.svg | |||
:-/ doubt.svg | |||
:-\ doubt2.svg | |||
:-? confused.svg | |||
:-D biggrin.svg | |||
:-P razz.svg | |||
:-o surprised.svg | |||
:-O surprised.svg | |||
:-x silenced.svg | |||
:-X silenced.svg | |||
:-| neutral.svg | |||
;-) wink.svg | |||
m( facepalm.svg | |||
^_^ fun.svg | |||
:?: question.svg | |||
:!: exclaim.svg | |||
LOL lol.svg | |||
FIXME fixme.svg | |||
DELETEME deleteme.svg | |||
@@ -0,0 +1,10 @@ | |||
# users.auth.php | |||
# <?php exit()?> | |||
# Don't modify the lines above | |||
# | |||
# Userfile | |||
# | |||
# Format: | |||
# | |||
# login:passwordhash:Real Name:email:groups,comma,separated | |||
@@ -0,0 +1,29 @@ | |||
# This blacklist is maintained by the DokuWiki community | |||
# patches welcome | |||
# | |||
https?:\/\/(\S*?)(-side-effects|top|pharm|pill|discount|discount-|deal|price|order|now|best|cheap|cheap-|online|buy|buy-|sale|sell)(\S*?)(cialis|viagra|prazolam|xanax|zanax|soma|vicodin|zenical|xenical|meridia|paxil|prozac|claritin|allegra|lexapro|wellbutrin|zoloft|retin|valium|levitra|phentermine) | |||
https?:\/\/(\S*?)(bi\s*sex|gay\s*sex|fetish|incest|penis|\brape\b) | |||
zoosex | |||
gang\s*bang | |||
facials | |||
ladyboy | |||
\btits\b | |||
bolea\.com | |||
52crystal | |||
baida\.org | |||
web-directory\.awardspace\.us | |||
korsan-team\.com | |||
BUDA TAMAMDIR | |||
wow-powerleveling-wow\.com | |||
wow gold | |||
wow-gold\.dinmo\.cn | |||
downgrade-vista\.com | |||
downgradetowindowsxp\.com | |||
elegantugg\.com | |||
classicedhardy\.com | |||
research-service\.com | |||
https?:\/\/(\S*?)(2-pay-secure|911essay|academia-research|anypapers|applicationessay|bestbuyessay|bestdissertation|bestessay|bestresume|besttermpaper|businessessay|college-paper|customessay|custom-made-paper|custom-writing|degree-?result|dissertationblog|dissertation-service|dissertations?expert|essaybank|essay-?blog|essaycapital|essaylogic|essaymill|essayontime|essaypaper|essays?land|essaytownsucks|essay-?writ|fastessays|freelancercareers|genuinecontent|genuineessay|genuinepaper|goessay|grandresume|killer-content|ma-dissertation|managementessay|masterpaper|mightystudent|needessay|researchedge|researchpaper-blog|resumecvservice|resumesexperts|resumesplanet|rushessay|samedayessay|superiorcontent|superiorpaper|superiorthesis|term-paper|termpaper-blog|term-paper-research|thesisblog|universalresearch|valwriting|vdwriters|wisetranslation|writersassembly|writers\.com\.ph|writers\.ph) | |||
flatsinmumbai\.co\.in | |||
https?:\/\/(\S*?)penny-?stock | |||
mattressreview\.biz | |||
(just|simply) (my|a) profile (site|webpage|page) |
@@ -0,0 +1,136 @@ | |||
<?php | |||
/** | |||
* DokuWiki mainscript | |||
* | |||
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html) | |||
* @author Andreas Gohr <andi@splitbrain.org> | |||
* | |||
* @global Input $INPUT | |||
*/ | |||
use dokuwiki\ChangeLog\PageChangeLog; | |||
use dokuwiki\Extension\Event; | |||
// update message version - always use a string to avoid localized floats! | |||
$updateVersion = "55.1"; | |||
// xdebug_start_profiling(); | |||
if (!defined('DOKU_INC')) define('DOKU_INC', __DIR__ . '/'); | |||
// define all DokuWiki globals here (needed within test requests but also helps to keep track) | |||
global $ACT, $INPUT, $QUERY, $ID, $REV, $DATE_AT, $IDX, | |||
$DATE, $RANGE, $HIGH, $TEXT, $PRE, $SUF, $SUM, $INFO, $JSINFO; | |||
if (isset($_SERVER['HTTP_X_DOKUWIKI_DO'])) { | |||
$ACT = trim(strtolower($_SERVER['HTTP_X_DOKUWIKI_DO'])); | |||
} elseif (!empty($_REQUEST['idx'])) { | |||
$ACT = 'index'; | |||
} elseif (isset($_REQUEST['do'])) { | |||
$ACT = $_REQUEST['do']; | |||
} else { | |||
$ACT = 'show'; | |||
} | |||
// load and initialize the core system | |||
require_once(DOKU_INC . 'inc/init.php'); | |||
//import variables | |||
$INPUT->set('id', str_replace("\xC2\xAD", '', $INPUT->str('id'))); //soft-hyphen | |||
$QUERY = trim($INPUT->str('q')); | |||
$ID = getID(); | |||
$REV = $INPUT->int('rev'); | |||
$DATE_AT = $INPUT->str('at'); | |||
$IDX = $INPUT->str('idx'); | |||
$DATE = $INPUT->int('date'); | |||
$RANGE = $INPUT->str('range'); | |||
$HIGH = $INPUT->param('s'); | |||
if (empty($HIGH)) $HIGH = getGoogleQuery(); | |||
if ($INPUT->post->has('wikitext')) { | |||
$TEXT = cleanText($INPUT->post->str('wikitext')); | |||
} | |||
$PRE = cleanText(substr($INPUT->post->str('prefix'), 0, -1)); | |||
$SUF = cleanText($INPUT->post->str('suffix')); | |||
$SUM = $INPUT->post->str('summary'); | |||
//parse DATE_AT | |||
if ($DATE_AT) { | |||
$date_parse = strtotime($DATE_AT); | |||
if ($date_parse) { | |||
$DATE_AT = $date_parse; | |||
} else { // check for UNIX Timestamp | |||
$date_parse = @date('Ymd', $DATE_AT); | |||
if (!$date_parse || $date_parse === '19700101') { | |||
msg(sprintf($lang['unable_to_parse_date'], hsc($DATE_AT))); | |||
$DATE_AT = null; | |||
} | |||
} | |||
} | |||
//check for existing $REV related to $DATE_AT | |||
if ($DATE_AT) { | |||
$pagelog = new PageChangeLog($ID); | |||
$rev_t = $pagelog->getLastRevisionAt($DATE_AT); | |||
if ($rev_t === '') { | |||
//current revision | |||
$REV = null; | |||
$DATE_AT = null; | |||
} elseif ($rev_t === false) { | |||
//page did not exist | |||
$rev_n = $pagelog->getRelativeRevision($DATE_AT, +1); | |||
msg( | |||
sprintf( | |||
$lang['page_nonexist_rev'], | |||
dformat($DATE_AT), | |||
wl($ID, ['rev' => $rev_n]), | |||
dformat($rev_n) | |||
) | |||
); | |||
$REV = $DATE_AT; //will result in a page not exists message | |||
} else { | |||
$REV = $rev_t; | |||
} | |||
} | |||
//make infos about the selected page available | |||
$INFO = pageinfo(); | |||
// handle debugging | |||
if ($conf['allowdebug'] && $ACT == 'debug') { | |||
html_debug(); | |||
exit; | |||
} | |||
//send 404 for missing pages if configured or ID has special meaning to bots | |||
if ( | |||
!$INFO['exists'] && | |||
($conf['send404'] || preg_match('/^(robots\.txt|sitemap\.xml(\.gz)?|favicon\.ico|crossdomain\.xml)$/', $ID)) && | |||
($ACT == 'show' || (!is_array($ACT) && str_starts_with($ACT, 'export_'))) | |||
) { | |||
header('HTTP/1.0 404 Not Found'); | |||
} | |||
//prepare breadcrumbs (initialize a static var) | |||
if ($conf['breadcrumbs']) breadcrumbs(); | |||
// check upstream | |||
checkUpdateMessages(); | |||
$tmp = []; // No event data | |||
Event::createAndTrigger('DOKUWIKI_STARTED', $tmp); | |||
//close session | |||
session_write_close(); | |||
//do the work (picks up what to do from global env) | |||
act_dispatch(); | |||
$tmp = []; // No event data | |||
Event::createAndTrigger('DOKUWIKI_DONE', $tmp); | |||
// xdebug_dump_function_profile(1); |
@@ -0,0 +1,75 @@ | |||
<?php | |||
/** | |||
* XML feed export | |||
* | |||
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html) | |||
* @author Andreas Gohr <andi@splitbrain.org> | |||
* | |||
* @global array $conf | |||
* @global Input $INPUT | |||
*/ | |||
use dokuwiki\Feed\FeedCreator; | |||
use dokuwiki\Feed\FeedCreatorOptions; | |||
use dokuwiki\Cache\Cache; | |||
use dokuwiki\ChangeLog\MediaChangeLog; | |||
use dokuwiki\ChangeLog\PageChangeLog; | |||
use dokuwiki\Extension\AuthPlugin; | |||
use dokuwiki\Extension\Event; | |||
if (!defined('DOKU_INC')) define('DOKU_INC', __DIR__ . '/'); | |||
require_once(DOKU_INC . 'inc/init.php'); | |||
//close session | |||
session_write_close(); | |||
//feed disabled? | |||
if (!actionOK('rss')) { | |||
http_status(404); | |||
echo '<error>RSS feed is disabled.</error>'; | |||
exit; | |||
} | |||
$options = new FeedCreatorOptions(); | |||
// the feed is dynamic - we need a cache for each combo | |||
// (but most people just use the default feed so it's still effective) | |||
$key = implode('$', [ | |||
$options->getCacheKey(), | |||
$INPUT->server->str('REMOTE_USER'), | |||
$INPUT->server->str('HTTP_HOST'), | |||
$INPUT->server->str('SERVER_PORT') | |||
]); | |||
$cache = new Cache($key, '.feed'); | |||
// prepare cache depends | |||
$depends['files'] = getConfigFiles('main'); | |||
$depends['age'] = $conf['rss_update']; | |||
$depends['purge'] = $INPUT->bool('purge'); | |||
// check cacheage and deliver if nothing has changed since last | |||
// time or the update interval has not passed, also handles conditional requests | |||
header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); | |||
header('Pragma: public'); | |||
header('Content-Type: ' . $options->getMimeType()); | |||
header('X-Robots-Tag: noindex'); | |||
if ($cache->useCache($depends)) { | |||
http_conditionalRequest($cache->getTime()); | |||
if ($conf['allowdebug']) header("X-CacheUsed: $cache->cache"); | |||
echo $cache->retrieveCache(); | |||
exit; | |||
} else { | |||
http_conditionalRequest(time()); | |||
} | |||
// create new feed | |||
try { | |||
$feed = (new FeedCreator($options))->build(); | |||
$cache->storeCache($feed); | |||
echo $feed; | |||
} catch (Exception $e) { | |||
http_status(500); | |||
echo '<error>' . hsc($e->getMessage()) . '</error>'; | |||
exit; | |||
} |
@@ -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> |
@@ -0,0 +1,26 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Action\Exception\ActionAclRequiredException; | |||
use dokuwiki\Extension\AuthPlugin; | |||
/** | |||
* 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 instanceof AuthPlugin) throw new ActionAclRequiredException(); | |||
} | |||
} |
@@ -0,0 +1,93 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
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 | |||
* @throws FatalException | |||
*/ | |||
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; | |||
} | |||
} |
@@ -0,0 +1,32 @@ | |||
<?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; | |||
} | |||
/** | |||
* @throws FatalException | |||
*/ | |||
public function preProcess() | |||
{ | |||
throw new FatalException('Alias Actions need to implement preProcess to load the aliased action'); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,45 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Action\Exception\ActionException; | |||
use dokuwiki\Extension\AdminPlugin; | |||
/** | |||
* 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 | |||
} | |||
/** @inheritDoc */ | |||
public function preProcess() | |||
{ | |||
global $INPUT; | |||
// retrieve admin plugin name from $_REQUEST['page'] | |||
if ($INPUT->str('page', '', true) != '') { | |||
/** @var AdminPlugin $plugin */ | |||
if ($plugin = plugin_getRequestAdminPlugin()) { // FIXME this method does also permission checking | |||
if (!$plugin->isAccessibleByCurrentUser()) { | |||
throw new ActionException('denied'); | |||
} | |||
$plugin->handle(); | |||
} | |||
} | |||
} | |||
/** @inheritDoc */ | |||
public function tplContent() | |||
{ | |||
tpl_admin(); | |||
} | |||
} |
@@ -0,0 +1,34 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Action\Exception\ActionAbort; | |||
use dokuwiki\Action\Exception\ActionException; | |||
use dokuwiki\JWT; | |||
class Authtoken extends AbstractUserAction | |||
{ | |||
/** @inheritdoc */ | |||
public function minimumPermission() | |||
{ | |||
return AUTH_NONE; | |||
} | |||
/** @inheritdoc */ | |||
public function checkPreconditions() | |||
{ | |||
parent::checkPreconditions(); | |||
if (!checkSecurityToken()) throw new ActionException('profile'); | |||
} | |||
/** @inheritdoc */ | |||
public function preProcess() | |||
{ | |||
global $INPUT; | |||
parent::preProcess(); | |||
$token = JWT::fromUser($INPUT->server->str('REMOTE_USER')); | |||
$token->save(); | |||
throw new ActionAbort('profile'); | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui\Backlinks; | |||
use dokuwiki\Ui; | |||
/** | |||
* 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() | |||
{ | |||
(new Backlinks())->show(); | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Action\Exception\ActionAbort; | |||
/** | |||
* Class Cancel | |||
* | |||
* Alias for show. Aborts editing | |||
* | |||
* @package dokuwiki\Action | |||
*/ | |||
class Cancel extends AbstractAliasAction | |||
{ | |||
/** | |||
* @inheritdoc | |||
* @throws ActionAbort | |||
*/ | |||
public function preProcess() | |||
{ | |||
global $ID; | |||
unlock($ID); | |||
// continue with draftdel -> redirect -> show | |||
throw new ActionAbort('draftdel'); | |||
} | |||
} |
@@ -0,0 +1,27 @@ | |||
<?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(); | |||
} | |||
} |
@@ -0,0 +1,39 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui\PageConflict; | |||
use dokuwiki\Ui; | |||
/** | |||
* 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; | |||
} | |||
} | |||
/** @inheritdoc */ | |||
public function tplContent() | |||
{ | |||
global $PRE; | |||
global $TEXT; | |||
global $SUF; | |||
global $SUM; | |||
$text = con($PRE, $TEXT, $SUF); | |||
(new PageConflict($text, $SUM))->show(); | |||
} | |||
} |
@@ -0,0 +1,52 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui\Login; | |||
use dokuwiki\Extension\Event; | |||
use dokuwiki\Ui; | |||
/** | |||
* Class Denied | |||
* | |||
* Show the access denied screen | |||
* | |||
* @package dokuwiki\Action | |||
*/ | |||
class Denied extends AbstractAction | |||
{ | |||
/** @inheritdoc */ | |||
public function minimumPermission() | |||
{ | |||
return AUTH_NONE; | |||
} | |||
/** @inheritdoc */ | |||
public function tplContent() | |||
{ | |||
$this->showBanner(); | |||
$data = null; | |||
$event = new Event('ACTION_DENIED_TPLCONTENT', $data); | |||
if ($event->advise_before()) { | |||
global $INPUT; | |||
if (empty($INPUT->server->str('REMOTE_USER')) && actionOK('login')) { | |||
(new Login())->show(); | |||
} | |||
} | |||
$event->advise_after(); | |||
} | |||
/** | |||
* Display error on denied pages | |||
* | |||
* @author Andreas Gohr <andi@splitbrain.org> | |||
* | |||
* @return void | |||
*/ | |||
public function showBanner() | |||
{ | |||
// print intro | |||
echo p_locale_xhtml('denied'); | |||
} | |||
} |
@@ -0,0 +1,41 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui\PageDiff; | |||
use dokuwiki\Ui; | |||
/** | |||
* 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() | |||
{ | |||
global $INFO; | |||
(new PageDiff($INFO['id']))->preference('showIntro', true)->show(); | |||
} | |||
} |
@@ -0,0 +1,43 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui\PageDraft; | |||
use dokuwiki\Action\Exception\ActionException; | |||
use dokuwiki\Ui; | |||
/** | |||
* 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 (!isset($INFO['draft']) || !file_exists($INFO['draft'])) throw new ActionException('edit'); | |||
} | |||
/** @inheritdoc */ | |||
public function tplContent() | |||
{ | |||
(new PageDraft())->show(); | |||
} | |||
} |
@@ -0,0 +1,40 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Draft; | |||
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 Draft($ID, $INFO['client']); | |||
if ($draft->isDraftAvailable() && checkSecurityToken()) { | |||
$draft->deleteDraft(); | |||
} | |||
throw new ActionAbort('redirect'); | |||
} | |||
} |
@@ -0,0 +1,96 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui\Editor; | |||
use dokuwiki\Action\Exception\ActionAbort; | |||
use dokuwiki\Ui; | |||
/** | |||
* 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) { | |||
[$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() | |||
{ | |||
(new Editor())->show(); | |||
} | |||
} |
@@ -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 | |||
{ | |||
} |
@@ -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 | |||
{ | |||
} |
@@ -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 | |||
{ | |||
} |
@@ -0,0 +1,69 @@ | |||
<?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; | |||
} | |||
} |
@@ -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 | |||
{ | |||
} |
@@ -0,0 +1,28 @@ | |||
<?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); | |||
} | |||
} |
@@ -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 | |||
{ | |||
} |
@@ -0,0 +1,114 @@ | |||
<?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 = []; | |||
// 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 = []; | |||
$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"); | |||
} | |||
echo $pre . $data['output'] . $post; | |||
exit; | |||
} | |||
throw new ActionAbort(); | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui; | |||
/** | |||
* 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; | |||
(new Ui\Index($IDX))->show(); | |||
} | |||
} |
@@ -0,0 +1,57 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui\Editor; | |||
/** | |||
* 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() | |||
{ | |||
$this->showBanner(); | |||
(new Editor())->show(); | |||
} | |||
/** | |||
* Display error on locked pages | |||
* | |||
* @return void | |||
* @author Andreas Gohr <andi@splitbrain.org> | |||
* | |||
*/ | |||
public function showBanner() | |||
{ | |||
global $ID; | |||
global $conf; | |||
global $lang; | |||
global $INFO; | |||
$locktime = filemtime(wikiLockFN($ID)); | |||
$expire = dformat($locktime + $conf['locktime']); | |||
$min = round(($conf['locktime'] - (time() - $locktime)) / 60); | |||
// print intro | |||
echo p_locale_xhtml('locked'); | |||
echo '<ul>'; | |||
echo '<li><div class="li"><strong>' . $lang['lockedby'] . '</strong> ' . | |||
editorinfo($INFO['locked']) . '</div></li>'; | |||
echo '<li><div class="li"><strong>' . $lang['lockexpire'] . '</strong> ' . | |||
$expire . ' (' . $min . ' min)</div></li>'; | |||
echo '</ul>' . DOKU_LF; | |||
} | |||
} |
@@ -0,0 +1,39 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Action\Exception\ActionException; | |||
use dokuwiki\Ui; | |||
/** | |||
* 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() | |||
{ | |||
(new Ui\Login())->show(); | |||
} | |||
} |
@@ -0,0 +1,55 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Action\Exception\ActionDisabledException; | |||
use dokuwiki\Action\Exception\ActionException; | |||
use dokuwiki\Extension\AuthPlugin; | |||
/** | |||
* 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 AuthPlugin $auth */ | |||
global $auth; | |||
if (!$auth->canDo('logout')) throw new ActionDisabledException(); | |||
} | |||
/** @inheritdoc */ | |||
public function preProcess() | |||
{ | |||
global $ID; | |||
global $INPUT; | |||
if (!checkSecurityToken()) throw new ActionException(); | |||
// 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, ['do' => 'login'], true, '&')); | |||
// should never be reached | |||
throw new ActionException('login'); | |||
} | |||
} |
@@ -0,0 +1,25 @@ | |||
<?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(); | |||
} | |||
} |
@@ -0,0 +1,36 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Extension\Event; | |||
/** | |||
* 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 Event('TPL_ACT_UNKNOWN', $this->actionname); | |||
if ($evt->advise_before()) { | |||
msg('Failed to handle action: ' . hsc($this->actionname), -1); | |||
} | |||
$evt->advise_after(); | |||
} | |||
} |
@@ -0,0 +1,49 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui\Editor; | |||
use dokuwiki\Ui\PageView; | |||
use dokuwiki\Draft; | |||
use dokuwiki\Ui; | |||
/** | |||
* 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; | |||
(new Editor())->show(); | |||
(new PageView($TEXT))->show(); | |||
} | |||
/** | |||
* Saves a draft on preview | |||
*/ | |||
protected function savedraft() | |||
{ | |||
global $ID, $INFO; | |||
$draft = new Draft($ID, $INFO['client']); | |||
if (!$draft->saveDraft()) { | |||
$errors = $draft->getErrors(); | |||
foreach ($errors as $error) { | |||
msg(hsc($error), -1); | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,51 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui\UserProfile; | |||
use dokuwiki\Action\Exception\ActionAbort; | |||
use dokuwiki\Action\Exception\ActionDisabledException; | |||
use dokuwiki\Extension\AuthPlugin; | |||
use dokuwiki\Ui; | |||
/** | |||
* 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 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() | |||
{ | |||
(new UserProfile())->show(); | |||
} | |||
} |
@@ -0,0 +1,45 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Action\Exception\ActionAbort; | |||
use dokuwiki\Action\Exception\ActionDisabledException; | |||
use dokuwiki\Extension\AuthPlugin; | |||
/** | |||
* 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 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'); | |||
} | |||
} | |||
} |
@@ -0,0 +1,44 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui; | |||
/** | |||
* 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; | |||
(new Ui\Recent($INPUT->extract('first')->int('first'), $this->showType))->show(); | |||
} | |||
} |
@@ -0,0 +1,24 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Action\Exception\ActionAbort; | |||
/** | |||
* Class Recover | |||
* | |||
* Recover a draft | |||
* | |||
* @package dokuwiki\Action | |||
*/ | |||
class Recover extends AbstractAliasAction | |||
{ | |||
/** | |||
* @inheritdoc | |||
* @throws ActionAbort | |||
*/ | |||
public function preProcess() | |||
{ | |||
throw new ActionAbort('edit'); | |||
} | |||
} |
@@ -0,0 +1,64 @@ | |||
<?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 = ['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'); | |||
} elseif ($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, [$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); | |||
} | |||
} |
@@ -0,0 +1,51 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui\UserRegister; | |||
use dokuwiki\Action\Exception\ActionAbort; | |||
use dokuwiki\Action\Exception\ActionDisabledException; | |||
use dokuwiki\Extension\AuthPlugin; | |||
use dokuwiki\Ui; | |||
/** | |||
* 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 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() | |||
{ | |||
(new UserRegister())->show(); | |||
} | |||
} |
@@ -0,0 +1,182 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui\UserResendPwd; | |||
use dokuwiki\Action\Exception\ActionAbort; | |||
use dokuwiki\Action\Exception\ActionDisabledException; | |||
use dokuwiki\Extension\AuthPlugin; | |||
use dokuwiki\Ui; | |||
/** | |||
* 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 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() | |||
{ | |||
(new UserResendPwd())->show(); | |||
} | |||
/** | |||
* 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 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 (empty($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', [$user, ['pass' => $pass]])) { | |||
msg($lang['proffail'], -1); | |||
return false; | |||
} | |||
} else { // autogenerate the password and send by mail | |||
$pass = auth_pwgen($user); | |||
if (!$auth->triggerUserMod('modify', [$user, ['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 (empty($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('', ['do' => 'resendpwd', 'pwauth' => $token], true, '&'); | |||
io_saveFile($tfile, $user); | |||
$text = rawLocale('pwconfirm'); | |||
$trep = [ | |||
'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 | |||
} | |||
} |
@@ -0,0 +1,61 @@ | |||
<?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 AbstractUserAction | |||
{ | |||
/** @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'); | |||
} | |||
} |
@@ -0,0 +1,29 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui\PageRevisions; | |||
use dokuwiki\Ui; | |||
/** | |||
* 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 $INFO, $INPUT; | |||
(new PageRevisions($INFO['id']))->show($INPUT->int('first', -1)); | |||
} | |||
} |
@@ -0,0 +1,65 @@ | |||
<?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 | |||
&& isset($INFO['meta']['date']['modified']) | |||
&& $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'); | |||
} | |||
} |
@@ -0,0 +1,136 @@ | |||
<?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 = []; | |||
protected $fullTextResults = []; | |||
protected $highlight = []; | |||
/** @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); | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,42 @@ | |||
<?php | |||
/** | |||
* Created by IntelliJ IDEA. | |||
* User: andi | |||
* Date: 2/10/17 | |||
* Time: 4:32 PM | |||
*/ | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui\PageView; | |||
use dokuwiki\Ui; | |||
/** | |||
* 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() | |||
{ | |||
(new PageView())->show(); | |||
} | |||
} |
@@ -0,0 +1,68 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Action\Exception\FatalException; | |||
use dokuwiki\Sitemap\Mapper; | |||
use dokuwiki\Utf8\PhpString; | |||
/** | |||
* 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=' . 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?'); | |||
} | |||
} |
@@ -0,0 +1,41 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Ui\Editor; | |||
use dokuwiki\Ui; | |||
/** | |||
* 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() | |||
{ | |||
(new Editor())->show(); | |||
} | |||
} |
@@ -0,0 +1,181 @@ | |||
<?php | |||
namespace dokuwiki\Action; | |||
use dokuwiki\Action\Exception\ActionAbort; | |||
use dokuwiki\Action\Exception\ActionDisabledException; | |||
use dokuwiki\Subscriptions\SubscriberManager; | |||
use dokuwiki\Extension\Event; | |||
use dokuwiki\Ui; | |||
use Exception; | |||
/** | |||
* 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() | |||
{ | |||
(new Ui\Subscribe())->show(); | |||
} | |||
/** | |||
* 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 = []; | |||
foreach (['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, [$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 = ['every', 'digest']; | |||
if (str_ends_with($target, ':')) { | |||
// 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', | |||
['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 = ['target' => $target, 'style' => $style, 'action' => $action]; | |||
} | |||
} |
@@ -0,0 +1,235 @@ | |||
<?php | |||
namespace dokuwiki; | |||
use dokuwiki\Extension\Event; | |||
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; | |||
/** @var int transition counter */ | |||
protected $transitions = 0; | |||
/** maximum loop */ | |||
protected 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); | |||
$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 instanceof \dokuwiki\ActionRouter) || $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 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 ($e instanceof ActionDisabledException) { | |||
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(\Throwable $e) | |||
{ | |||
if ($e instanceof FatalException) { | |||
http_status($e->getCode()); | |||
} else { | |||
http_status(500); | |||
} | |||
if (defined('DOKU_UNITTEST')) { | |||
throw $e; | |||
} | |||
ErrorHandler::logException($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 ($parts !== []) { | |||
$load = implode('_', $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; | |||
} | |||
} |
@@ -0,0 +1,447 @@ | |||
<?php | |||
namespace dokuwiki; | |||
use dokuwiki\Extension\Event; | |||
use dokuwiki\Ui\MediaDiff; | |||
use dokuwiki\Ui\Index; | |||
use dokuwiki\Ui; | |||
use dokuwiki\Utf8\Sort; | |||
/** | |||
* 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 Event('AJAX_CALL_UNKNOWN', $call); | |||
if ($evt->advise_before()) { | |||
echo "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 ($data === []) return; | |||
echo '<strong>' . $lang['quickhits'] . '</strong>'; | |||
echo '<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; | |||
} | |||
} | |||
echo '</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 ($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::sort($data); | |||
/* now construct a json */ | |||
$suggestions = [ | |||
$query, // the original query | |||
$data, // some suggestions | |||
[], // no description | |||
[], // no urls | |||
]; | |||
header('Content-Type: application/x-suggestions+json'); | |||
echo json_encode($suggestions, JSON_THROW_ON_ERROR); | |||
} | |||
/** | |||
* 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, JSON_THROW_ON_ERROR); | |||
} | |||
/** | |||
* Delete a draft | |||
* | |||
* @author Andreas Gohr <andi@splitbrain.org> | |||
*/ | |||
protected function callDraftdel() | |||
{ | |||
global $INPUT; | |||
$id = cleanID($INPUT->str('id')); | |||
if (empty($id)) return; | |||
$client = $INPUT->server->str('REMOTE_USER'); | |||
if (!$client) $client = clientIP(true); | |||
$draft = new Draft($id, $client); | |||
if ($draft->isDraftAvailable() && checkSecurityToken()) { | |||
$draft->deleteDraft(); | |||
} | |||
} | |||
/** | |||
* 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 = []; | |||
search($data, $conf['mediadir'], 'search_index', ['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 $INPUT; | |||
$image = ''; | |||
if ($INPUT->has('image')) $image = cleanID($INPUT->str('image')); | |||
(new MediaDiff($image))->preference('fromAjax', true)->show(); | |||
} | |||
/** | |||
* 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 = [ | |||
'success' => true, | |||
'link' => media_managerURL(['ns' => $ns, 'image' => $NS . ':' . $id], '&'), | |||
'id' => $NS . ':' . $id, | |||
'ns' => $NS | |||
]; | |||
} else { | |||
$error = ''; | |||
if (isset($MSG)) { | |||
foreach ($MSG as $msg) { | |||
$error .= $msg['msg']; | |||
} | |||
} | |||
$result = ['error' => $error, 'ns' => $NS]; | |||
} | |||
header('Content-Type: application/json'); | |||
echo json_encode($result, JSON_THROW_ON_ERROR); | |||
} | |||
/** | |||
* 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 = []; | |||
search($data, $conf['datadir'], 'search_index', ['ns' => $ns], $dir); | |||
foreach (array_keys($data) as $item) { | |||
$data[$item]['level'] = $lvl + 1; | |||
} | |||
$idx = new Index(); | |||
echo html_buildlist($data, 'idx', [$idx,'formatListItem'], [$idx,'tagListItem']); | |||
} | |||
/** | |||
* 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 = []; | |||
if ($q !== '' && $ns === '') { | |||
// use index to lookup matching pages | |||
$pages = ft_pageLookup($id, true); | |||
// If 'useheading' option is 'always' or 'content', | |||
// search page titles with original query as well. | |||
if ($conf['useheading'] == '1' || $conf['useheading'] == 'content') { | |||
$pages = array_merge($pages, ft_pageLookup($q, true, true)); | |||
asort($pages, SORT_STRING); | |||
} | |||
// result contains matches in pages and namespaces | |||
// we now extract the matching namespaces to show | |||
// them seperately | |||
$dirs = []; | |||
foreach ($pages as $pid => $title) { | |||
if (strpos(getNS($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[] = ['id' => $pid, 'title' => $title, 'type' => 'f']; | |||
} | |||
unset($pages[$pid]); | |||
} | |||
foreach (array_keys($dirs) as $dir) { | |||
$data[] = ['id' => $dir, 'type' => 'd']; | |||
} | |||
} else { | |||
$opts = [ | |||
'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, | |||
['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>'; | |||
} | |||
} | |||
} |
@@ -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 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 = []) | |||
{ | |||
$this->depends = $depends; | |||
$this->addDependencies(); | |||
if ($this->getEvent()) { | |||
return $this->stats( | |||
Event::createAndTrigger( | |||
$this->getEvent(), | |||
$this, | |||
[$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])) { | |||
[$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, implode("\n", $stats)); | |||
return $success; | |||
} | |||
/** | |||
* @return bool | |||
*/ | |||
public function isNoCache() | |||
{ | |||
return $this->_nocache; | |||
} | |||
} |
@@ -0,0 +1,54 @@ | |||
<?php | |||
namespace dokuwiki\Cache; | |||
/** | |||
* Handle the caching of modified (resized/cropped) images | |||
*/ | |||
class CacheImageMod extends Cache | |||
{ | |||
/** @var string source file */ | |||
protected $file; | |||
/** | |||
* @param string $file Original source file | |||
* @param int $w new width in pixel | |||
* @param int $h new height in pixel | |||
* @param string $ext Image extension - no leading dot | |||
* @param bool $crop Is this a crop? | |||
*/ | |||
public function __construct($file, $w, $h, $ext, $crop) | |||
{ | |||
$fullext = '.media.' . $w . 'x' . $h; | |||
$fullext .= $crop ? '.crop' : ''; | |||
$fullext .= ".$ext"; | |||
$this->file = $file; | |||
$this->setEvent('IMAGEMOD_CACHE_USE'); | |||
parent::__construct($file, $fullext); | |||
} | |||
/** @inheritdoc */ | |||
public function makeDefaultCacheDecision() | |||
{ | |||
if (!file_exists($this->file)) { | |||
return false; | |||
} | |||
return parent::makeDefaultCacheDecision(); | |||
} | |||
/** | |||
* Caching depends on the source and the wiki config | |||
* @inheritdoc | |||
*/ | |||
protected function addDependencies() | |||
{ | |||
parent::addDependencies(); | |||
$this->depends['files'] = array_merge( | |||
[$this->file], | |||
getConfigFiles('main') | |||
); | |||
} | |||
} |
@@ -0,0 +1,45 @@ | |||
<?php | |||
namespace dokuwiki\Cache; | |||
/** | |||
* Caching of parser instructions | |||
*/ | |||
class CacheInstructions extends 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); | |||
} | |||
/** | |||
* 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)); | |||
} | |||
} |
@@ -0,0 +1,63 @@ | |||
<?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) | |||
{ | |||
global $INPUT; | |||
if ($id) { | |||
$this->page = $id; | |||
} | |||
$this->file = $file; | |||
$this->mode = $mode; | |||
$this->setEvent('PARSER_CACHE_USE'); | |||
parent::__construct($file . $INPUT->server->str('HTTP_HOST') . $INPUT->server->str('SERVER_PORT'), '.' . $mode); | |||
} | |||
/** | |||
* method contains cache use decision logic | |||
* | |||
* @return bool see useCache() | |||
*/ | |||
public function makeDefaultCacheDecision() | |||
{ | |||
if (!file_exists($this->file)) { | |||
// source doesn't exist | |||
return false; | |||
} | |||
return parent::makeDefaultCacheDecision(); | |||
} | |||
protected function addDependencies() | |||
{ | |||
// parser cache file dependencies ... | |||
$files = [ | |||
$this->file, // source | |||
DOKU_INC . 'inc/Parsing/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']) ? | |||
$files : | |||
array_merge($files, $this->depends['files']); | |||
parent::addDependencies(); | |||
} | |||
} |
@@ -0,0 +1,92 @@ | |||
<?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 = [DOKU_INC . 'inc/parser/' . $this->mode . '.php']; | |||
// 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']) ? | |||
$files : | |||
array_merge($files, $this->depends['files']); | |||
parent::addDependencies(); | |||
} | |||
} |
@@ -0,0 +1,700 @@ | |||
<?php | |||
namespace dokuwiki\ChangeLog; | |||
use dokuwiki\Logger; | |||
/** | |||
* ChangeLog Prototype; methods for handling changelog | |||
*/ | |||
abstract class ChangeLog | |||
{ | |||
use ChangeLogTrait; | |||
/** @var string */ | |||
protected $id; | |||
/** @var false|int */ | |||
protected $currentRevision; | |||
/** @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] = []; | |||
} | |||
$this->id = $id; | |||
$this->setChunkSize($chunk_size); | |||
} | |||
/** | |||
* Returns path to current page/media | |||
* | |||
* @param string|int $rev empty string or revision timestamp | |||
* @return string path to file | |||
*/ | |||
abstract protected function getFilename($rev = ''); | |||
/** | |||
* Returns mode | |||
* | |||
* @return string RevisionInfo::MODE_MEDIA or RevisionInfo::MODE_PAGE | |||
*/ | |||
abstract protected function getMode(); | |||
/** | |||
* 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 == $this->currentRevision(); | |||
} | |||
/** | |||
* Checks if the revision is last revision | |||
* | |||
* @param int $rev revision timestamp | |||
* @return bool true if $rev is last revision, otherwise false | |||
*/ | |||
public function isLastRevision($rev = null) | |||
{ | |||
return $rev === $this->lastRevision(); | |||
} | |||
/** | |||
* Return the current revision identifier | |||
* | |||
* The "current" revision means current version of the page or media file. It is either | |||
* identical with or newer than the "last" revision, that depends on whether the file | |||
* has modified, created or deleted outside of DokuWiki. | |||
* The value of identifier can be determined by timestamp as far as the file exists, | |||
* otherwise it must be assigned larger than any other revisions to keep them sortable. | |||
* | |||
* @return int|false revision timestamp | |||
*/ | |||
public function currentRevision() | |||
{ | |||
if (!isset($this->currentRevision)) { | |||
// set ChangeLog::currentRevision property | |||
$this->getCurrentRevisionInfo(); | |||
} | |||
return $this->currentRevision; | |||
} | |||
/** | |||
* Return the last revision identifier, date value of the last entry of the changelog | |||
* | |||
* @return int|false revision timestamp | |||
*/ | |||
public function lastRevision() | |||
{ | |||
$revs = $this->getRevisions(-1, 1); | |||
return empty($revs) ? false : $revs[0]; | |||
} | |||
/** | |||
* Parses a changelog line into its components and save revision info to the cache pool | |||
* | |||
* @param string $value changelog line | |||
* @return array|bool parsed line or false | |||
*/ | |||
protected function parseAndCacheLogLine($value) | |||
{ | |||
$info = static::parseLogLine($value); | |||
if (is_array($info)) { | |||
$info['mode'] = $this->getMode(); | |||
$this->cache[$this->id][$info['date']] ??= $info; | |||
return $info; | |||
} | |||
return false; | |||
} | |||
/** | |||
* Get the changelog information for a specific 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 | |||
* @param bool $retrieveCurrentRevInfo allows to skip for getting other revision info in the | |||
* getCurrentRevisionInfo() where $currentRevision is not yet determined | |||
* @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) | |||
* - sizechange: change of filesize | |||
* additional: | |||
* - mode: page or media | |||
* | |||
* @author Ben Coburn <btcoburn@silicodon.net> | |||
* @author Kate Arzamastseva <pshns@ukr.net> | |||
*/ | |||
public function getRevisionInfo($rev, $retrieveCurrentRevInfo = true) | |||
{ | |||
$rev = max(0, $rev); | |||
if (!$rev) return false; | |||
//ensure the external edits are cached as well | |||
if (!isset($this->currentRevision) && $retrieveCurrentRevInfo) { | |||
$this->getCurrentRevisionInfo(); | |||
} | |||
// check if it's already in the memory cache | |||
if (isset($this->cache[$this->id][$rev])) { | |||
return $this->cache[$this->id][$rev]; | |||
} | |||
//read lines from changelog | |||
[$fp, $lines] = $this->readloglines($rev); | |||
if ($fp) { | |||
fclose($fp); | |||
} | |||
if (empty($lines)) return false; | |||
// parse and cache changelog lines | |||
foreach ($lines as $line) { | |||
$this->parseAndCacheLogLine($line); | |||
} | |||
return $this->cache[$this->id][$rev] ?? false; | |||
} | |||
/** | |||
* 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 received. | |||
* | |||
* @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 = []; | |||
$lines = []; | |||
$count = 0; | |||
$logfile = $this->getChangelogFilename(); | |||
if (!file_exists($logfile)) return $revs; | |||
$num = max($num, 0); | |||
if ($num == 0) { | |||
return $revs; | |||
} | |||
if ($first < 0) { | |||
$first = 0; | |||
} else { | |||
$fileLastMod = $this->getFilename(); | |||
if (file_exists($fileLastMod) && $this->isLastRevision(filemtime($fileLastMod))) { | |||
// skip last revision if the page exists | |||
$first = max($first + 1, 0); | |||
} | |||
} | |||
if (filesize($logfile) < $this->chunk_size || $this->chunk_size == 0) { | |||
// read whole file | |||
$lines = file($logfile); | |||
if ($lines === false) { | |||
return $revs; | |||
} | |||
} else { | |||
// read chunks backwards | |||
$fp = fopen($logfile, '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 = [...$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); | |||
} elseif ($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--) { | |||
$info = $this->parseAndCacheLogLine($lines[$i]); | |||
if (is_array($info)) { | |||
$revs[] = $info['date']; | |||
} | |||
} | |||
return $revs; | |||
} | |||
/** | |||
* Get the nth revision left or right-hand side 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 chunk. | |||
* | |||
* Adjacent changelog lines are optimistically parsed and cached to speed up | |||
* consecutive calls to getRevisionInfo. | |||
* | |||
* @param int $rev revision timestamp used as start date | |||
* (doesn't need to be exact revision number) | |||
* @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 | |||
[$fp, $lines, $head, $tail, $eof] = $this->readloglines($rev); | |||
if (empty($lines)) return false; | |||
// look for revisions later/earlier than $rev, when founded count till the wanted revision is reached | |||
// also parse and cache changelog lines for getRevisionInfo(). | |||
$revCounter = 0; | |||
$relativeRev = false; | |||
$checkOtherChunk = true; //always runs once | |||
while (!$relativeRev && $checkOtherChunk) { | |||
$info = []; | |||
//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 += $step) { | |||
$info = $this->parseAndCacheLogLine($lines[$i]); | |||
if (is_array($info)) { | |||
//look for revs older/earlier then reference $rev and select $direction-th one | |||
if (($direction > 0 && $info['date'] > $rev) || ($direction < 0 && $info['date'] < $rev)) { | |||
$revCounter++; | |||
if ($revCounter == abs($direction)) { | |||
$relativeRev = $info['date']; | |||
} | |||
} | |||
} | |||
} | |||
//true when $rev is found, but not the wanted follow-up. | |||
$checkOtherChunk = $fp | |||
&& ($info['date'] == $rev || ($revCounter > 0 && !$relativeRev)) | |||
&& (!($tail == $eof && $direction > 0) && !($head == 0 && $direction < 0)); | |||
if ($checkOtherChunk) { | |||
[$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 = (int) (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. | |||
$rev2 = $this->currentRevision(); | |||
} | |||
//collect revisions around rev2 | |||
[$revs2, $allRevs, $fp, $lines, $head, $tail] = $this->retrieveRevisionsAround($rev2, $max); | |||
if (empty($revs2)) return [[], []]; | |||
//collect revisions around rev1 | |||
$index = array_search($rev1, $allRevs); | |||
if ($index === false) { | |||
//no overlapping revisions | |||
[$revs1, , , , , ] = $this->retrieveRevisionsAround($rev1, $max); | |||
if (empty($revs1)) $revs1 = []; | |||
} else { | |||
//revisions overlaps, reuse revisions around rev2 | |||
$lastRev = array_pop($allRevs); //keep last entry that could be external edit | |||
$revs1 = $allRevs; | |||
while ($head > 0) { | |||
for ($i = count($lines) - 1; $i >= 0; $i--) { | |||
$info = $this->parseAndCacheLogLine($lines[$i]); | |||
if (is_array($info)) { | |||
$revs1[] = $info['date']; | |||
$index++; | |||
if ($index > (int) ($max / 2)) { | |||
break 2; | |||
} | |||
} | |||
} | |||
[$lines, $head, $tail] = $this->readAdjacentChunk($fp, $head, $tail, -1); | |||
} | |||
sort($revs1); | |||
$revs1[] = $lastRev; //push back last entry | |||
//return wanted selection | |||
$revs1 = array_slice($revs1, max($index - (int) ($max / 2), 0), $max); | |||
} | |||
return [array_reverse($revs1), array_reverse($revs2)]; | |||
} | |||
/** | |||
* 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) | |||
{ | |||
$fileLastMod = $this->getFilename(); | |||
//requested date_at(timestamp) younger or equal then modified_time($this->id) => load current | |||
if (file_exists($fileLastMod) && $date_at >= @filemtime($fileLastMod)) { | |||
return ''; | |||
} elseif ($rev = $this->getRelativeRevision($date_at + 1, -1)) { | |||
//+1 to get also the requested date revision | |||
return $rev; | |||
} else { | |||
return false; | |||
} | |||
} | |||
/** | |||
* Collect the $max revisions near to the timestamp $rev | |||
* | |||
* Ideally, half of retrieved timestamps are older than $rev, another half are newer. | |||
* The returned array $requestedRevs may not contain the reference timestamp $rev | |||
* when it does not match any revision value recorded in changelog. | |||
* | |||
* @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: file pointer only defined for chuck reading, needs closing. | |||
* - $lines: non-parsed changelog lines before the parsed revisions | |||
* - $head: position of first read changelog line | |||
* - $lastTail: position of end of last read changelog line | |||
* otherwise false | |||
*/ | |||
protected function retrieveRevisionsAround($rev, $max) | |||
{ | |||
$revs = []; | |||
$afterCount = 0; | |||
$beforeCount = 0; | |||
//get lines from changelog | |||
[$fp, $lines, $startHead, $startTail, $eof] = $this->readloglines($rev); | |||
if (empty($lines)) return false; | |||
//parse changelog lines in chunk, and read forward more chunks until $max/2 is reached | |||
$head = $startHead; | |||
$tail = $startTail; | |||
while (count($lines) > 0) { | |||
foreach ($lines as $line) { | |||
$info = $this->parseAndCacheLogLine($line); | |||
if (is_array($info)) { | |||
$revs[] = $info['date']; | |||
if ($info['date'] >= $rev) { | |||
//count revs after reference $rev | |||
$afterCount++; | |||
if ($afterCount == 1) { | |||
$beforeCount = count($revs); | |||
} | |||
} | |||
//enough revs after reference $rev? | |||
if ($afterCount > (int) ($max / 2)) { | |||
break 2; | |||
} | |||
} | |||
} | |||
//retrieve next chunk | |||
[$lines, $head, $tail] = $this->readAdjacentChunk($fp, $head, $tail, 1); | |||
} | |||
$lastTail = $tail; | |||
// add a possible revision of external edit, create or deletion | |||
if ( | |||
$lastTail == $eof && $afterCount <= (int) ($max / 2) && | |||
count($revs) && !$this->isCurrentRevision($revs[count($revs) - 1]) | |||
) { | |||
$revs[] = $this->currentRevision; | |||
$afterCount++; | |||
} | |||
if ($afterCount == 0) { | |||
//given timestamp $rev is newer than the most recent line in chunk | |||
return false; //FIXME: or proceed to collect older revisions? | |||
} | |||
//read more chunks backward until $max/2 is reached and total number of revs is equal to $max | |||
$lines = []; | |||
$i = 0; | |||
$head = $startHead; | |||
$tail = $startTail; | |||
while ($head > 0) { | |||
[$lines, $head, $tail] = $this->readAdjacentChunk($fp, $head, $tail, -1); | |||
for ($i = count($lines) - 1; $i >= 0; $i--) { | |||
$info = $this->parseAndCacheLogLine($lines[$i]); | |||
if (is_array($info)) { | |||
$revs[] = $info['date']; | |||
$beforeCount++; | |||
//enough revs before reference $rev? | |||
if ($beforeCount > max((int) ($max / 2), $max - $afterCount)) { | |||
break 2; | |||
} | |||
} | |||
} | |||
} | |||
//keep only non-parsed lines | |||
$lines = array_slice($lines, 0, $i); | |||
sort($revs); | |||
//trunk desired selection | |||
$requestedRevs = array_slice($revs, -$max, $max); | |||
return [$requestedRevs, $revs, $fp, $lines, $head, $lastTail]; | |||
} | |||
/** | |||
* Get the current revision information, considering external edit, create or deletion | |||
* | |||
* When the file has not modified since its last revision, the information of the last | |||
* change that had already recorded in the changelog is returned as current change info. | |||
* Otherwise, the change information since the last revision caused outside DokuWiki | |||
* should be returned, which is referred as "external revision". | |||
* | |||
* The change date of the file can be determined by timestamp as far as the file exists, | |||
* however this is not possible when the file has already deleted outside of DokuWiki. | |||
* In such case we assign 1 sec before current time() for the external deletion. | |||
* As a result, the value of current revision identifier may change each time because: | |||
* 1) the file has again modified outside of DokuWiki, or | |||
* 2) the value is essentially volatile for deleted but once existed files. | |||
* | |||
* @return bool|array false when page had never existed or array with entries: | |||
* - date: revision identifier (timestamp or last revision +1) | |||
* - ip: IPv4 address (127.0.0.1) | |||
* - type: log line type | |||
* - id: id of page or media | |||
* - user: user name | |||
* - sum: edit summary (or action reason) | |||
* - extra: extra data (varies by line type) | |||
* - sizechange: change of filesize | |||
* - timestamp: unix timestamp or false (key set only for external edit occurred) | |||
* additional: | |||
* - mode: page or media | |||
* | |||
* @author Satoshi Sahara <sahara.satoshi@gmail.com> | |||
*/ | |||
public function getCurrentRevisionInfo() | |||
{ | |||
global $lang; | |||
if (isset($this->currentRevision)) { | |||
return $this->getRevisionInfo($this->currentRevision); | |||
} | |||
// get revision id from the item file timestamp and changelog | |||
$fileLastMod = $this->getFilename(); | |||
$fileRev = @filemtime($fileLastMod); // false when the file not exist | |||
$lastRev = $this->lastRevision(); // false when no changelog | |||
if (!$fileRev && !$lastRev) { // has never existed | |||
$this->currentRevision = false; | |||
return false; | |||
} elseif ($fileRev === $lastRev) { // not external edit | |||
$this->currentRevision = $lastRev; | |||
return $this->getRevisionInfo($lastRev); | |||
} | |||
if (!$fileRev && $lastRev) { // item file does not exist | |||
// check consistency against changelog | |||
$revInfo = $this->getRevisionInfo($lastRev, false); | |||
if ($revInfo['type'] == DOKU_CHANGE_TYPE_DELETE) { | |||
$this->currentRevision = $lastRev; | |||
return $revInfo; | |||
} | |||
// externally deleted, set revision date as late as possible | |||
$revInfo = [ | |||
'date' => max($lastRev + 1, time() - 1), // 1 sec before now or new page save | |||
'ip' => '127.0.0.1', | |||
'type' => DOKU_CHANGE_TYPE_DELETE, | |||
'id' => $this->id, | |||
'user' => '', | |||
'sum' => $lang['deleted'] . ' - ' . $lang['external_edit'] . ' (' . $lang['unknowndate'] . ')', | |||
'extra' => '', | |||
'sizechange' => -io_getSizeFile($this->getFilename($lastRev)), | |||
'timestamp' => false, | |||
'mode' => $this->getMode() | |||
]; | |||
} else { // item file exists, with timestamp $fileRev | |||
// here, file timestamp $fileRev is different with last revision timestamp $lastRev in changelog | |||
$isJustCreated = $lastRev === false || ( | |||
$fileRev > $lastRev && | |||
$this->getRevisionInfo($lastRev, false)['type'] == DOKU_CHANGE_TYPE_DELETE | |||
); | |||
$filesize_new = filesize($this->getFilename()); | |||
$filesize_old = $isJustCreated ? 0 : io_getSizeFile($this->getFilename($lastRev)); | |||
$sizechange = $filesize_new - $filesize_old; | |||
if ($isJustCreated) { | |||
$timestamp = $fileRev; | |||
$sum = $lang['created'] . ' - ' . $lang['external_edit']; | |||
} elseif ($fileRev > $lastRev) { | |||
$timestamp = $fileRev; | |||
$sum = $lang['external_edit']; | |||
} else { | |||
// $fileRev is older than $lastRev, that is erroneous/incorrect occurrence. | |||
$msg = "Warning: current file modification time is older than last revision date"; | |||
$details = 'File revision: ' . $fileRev . ' ' . dformat($fileRev, "%Y-%m-%d %H:%M:%S") . "\n" | |||
. 'Last revision: ' . $lastRev . ' ' . dformat($lastRev, "%Y-%m-%d %H:%M:%S"); | |||
Logger::error($msg, $details, $this->getFilename()); | |||
$timestamp = false; | |||
$sum = $lang['external_edit'] . ' (' . $lang['unknowndate'] . ')'; | |||
} | |||
// externally created or edited | |||
$revInfo = [ | |||
'date' => $timestamp ?: $lastRev + 1, | |||
'ip' => '127.0.0.1', | |||
'type' => $isJustCreated ? DOKU_CHANGE_TYPE_CREATE : DOKU_CHANGE_TYPE_EDIT, | |||
'id' => $this->id, | |||
'user' => '', | |||
'sum' => $sum, | |||
'extra' => '', | |||
'sizechange' => $sizechange, | |||
'timestamp' => $timestamp, | |||
'mode' => $this->getMode() | |||
]; | |||
} | |||
// cache current revision information of external edition | |||
$this->currentRevision = $revInfo['date']; | |||
$this->cache[$this->id][$this->currentRevision] = $revInfo; | |||
return $this->getRevisionInfo($this->currentRevision); | |||
} | |||
/** | |||
* Mechanism to trace no-actual external current revision | |||
* @param int $rev | |||
*/ | |||
public function traceCurrentRevision($rev) | |||
{ | |||
if ($rev > $this->lastRevision()) { | |||
$rev = $this->currentRevision(); | |||
} | |||
return $rev; | |||
} | |||
} |
@@ -0,0 +1,262 @@ | |||
<?php | |||
namespace dokuwiki\ChangeLog; | |||
use dokuwiki\Utf8\PhpString; | |||
/** | |||
* Provides methods for handling of changelog | |||
*/ | |||
trait ChangeLogTrait | |||
{ | |||
/** | |||
* Adds an entry to the changelog file | |||
* | |||
* @return array added log line as revision info | |||
*/ | |||
abstract public function addLogEntry(array $info, $timestamp = null); | |||
/** | |||
* Parses a changelog line into its components | |||
* | |||
* @param string $line changelog line | |||
* @return array|bool parsed line or false | |||
* @author Ben Coburn <btcoburn@silicodon.net> | |||
* | |||
*/ | |||
public static function parseLogLine($line) | |||
{ | |||
$info = sexplode("\t", rtrim($line, "\n"), 8); | |||
if ($info[3]) { // we need at least the page id to consider it a valid line | |||
return [ | |||
'date' => (int)$info[0], // unix timestamp | |||
'ip' => $info[1], // IP address (127.0.0.1) | |||
'type' => $info[2], // log line type | |||
'id' => $info[3], // page id | |||
'user' => $info[4], // user name | |||
'sum' => $info[5], // edit summary (or action reason) | |||
'extra' => $info[6], // extra data (varies by line type) | |||
'sizechange' => ($info[7] != '') ? (int)$info[7] : null, // size difference in bytes | |||
]; | |||
} else { | |||
return false; | |||
} | |||
} | |||
/** | |||
* Build a changelog line from its components | |||
* | |||
* @param array $info Revision info structure | |||
* @param int $timestamp log line date (optional) | |||
* @return string changelog line | |||
*/ | |||
public static function buildLogLine(array &$info, $timestamp = null) | |||
{ | |||
$strip = ["\t", "\n"]; | |||
$entry = [ | |||
'date' => $timestamp ?? $info['date'], | |||
'ip' => $info['ip'], | |||
'type' => str_replace($strip, '', $info['type']), | |||
'id' => $info['id'], | |||
'user' => $info['user'], | |||
'sum' => PhpString::substr(str_replace($strip, '', $info['sum'] ?? ''), 0, 255), | |||
'extra' => str_replace($strip, '', $info['extra']), | |||
'sizechange' => $info['sizechange'] | |||
]; | |||
$info = $entry; | |||
return implode("\t", $entry) . "\n"; | |||
} | |||
/** | |||
* Returns path to changelog | |||
* | |||
* @return string path to file | |||
*/ | |||
abstract protected function getChangelogFilename(); | |||
/** | |||
* Checks if the ID has old revisions | |||
* @return boolean | |||
*/ | |||
public function hasRevisions() | |||
{ | |||
$logfile = $this->getChangelogFilename(); | |||
return file_exists($logfile); | |||
} | |||
/** @var int */ | |||
protected $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 = max($chunk_size, 0); | |||
} | |||
/** | |||
* Returns lines from changelog. | |||
* If file larger than $chunk_size, only chunk is read that could contain $rev. | |||
* | |||
* When reference timestamp $rev is outside time range of changelog, readloglines() will return | |||
* lines in first or last chunk, but they obviously does not 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; | |||
} | |||
fseek($fp, 0, SEEK_END); | |||
$eof = ftell($fp); | |||
$tail = $eof; | |||
// find chunk | |||
while ($tail - $head > $this->chunk_size) { | |||
$finger = $head + (int)(($tail - $head) / 2); | |||
$finger = $this->getNewlinepointer($fp, $finger); | |||
$tmp = fgets($fp); | |||
if ($finger == $head || $finger == $tail) { | |||
break; | |||
} | |||
$info = $this->parseLogLine($tmp); | |||
$finger_rev = $info['date']; | |||
if ($finger_rev > $rev) { | |||
$tail = $finger; | |||
} else { | |||
$head = $finger; | |||
} | |||
} | |||
if ($tail - $head < 1) { | |||
// could not find chunk, assume requested rev is missing | |||
fclose($fp); | |||
return false; | |||
} | |||
$lines = $this->readChunk($fp, $head, $tail); | |||
} | |||
return [$fp, $lines, $head, $tail, $eof]; | |||
} | |||
/** | |||
* Read chunk and return array with lines of given chunk. | |||
* Has no check if $head and $tail are really at a new line | |||
* | |||
* @param resource $fp resource file pointer | |||
* @param int $head start point chunk | |||
* @param int $tail end point chunk | |||
* @return array lines read from chunk | |||
*/ | |||
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 file pointer | |||
* @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; | |||
} | |||
/** | |||
* Returns the next lines of the changelog of the chunk before head or after tail | |||
* | |||
* @param resource $fp file pointer | |||
* @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 read chunk | |||
* - $head: head of chunk | |||
* - $tail: tail of chunk | |||
*/ | |||
protected function readAdjacentChunk($fp, $head, $tail, $direction) | |||
{ | |||
if (!$fp) return [[], $head, $tail]; | |||
if ($direction > 0) { | |||
//read forward | |||
$head = $tail; | |||
$tail = $head + (int)($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 chunk | |||
$lines = $this->readChunk($fp, $head, $tail); | |||
return [$lines, $head, $tail]; | |||
} | |||
} |
@@ -0,0 +1,68 @@ | |||
<?php | |||
namespace dokuwiki\ChangeLog; | |||
/** | |||
* Class MediaChangeLog; 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 | |||
* | |||
* @param string|int $rev empty string or revision timestamp | |||
* @return string path to file | |||
*/ | |||
protected function getFilename($rev = '') | |||
{ | |||
return mediaFN($this->id, $rev); | |||
} | |||
/** | |||
* Returns mode | |||
* | |||
* @return string RevisionInfo::MODE_PAGE | |||
*/ | |||
protected function getMode() | |||
{ | |||
return RevisionInfo::MODE_MEDIA; | |||
} | |||
/** | |||
* Adds an entry to the changelog | |||
* | |||
* @param array $info Revision info structure of a media file | |||
* @param int $timestamp log line date (optional) | |||
* @return array revision info of added log line | |||
* | |||
* @see also addMediaLogEntry() in inc/changelog.php file | |||
*/ | |||
public function addLogEntry(array $info, $timestamp = null) | |||
{ | |||
global $conf; | |||
if (isset($timestamp)) unset($this->cache[$this->id][$info['date']]); | |||
// add changelog lines | |||
$logline = static::buildLogLine($info, $timestamp); | |||
io_saveFile(mediaMetaFN($this->id, '.changes'), $logline, $append = true); | |||
io_saveFile($conf['media_changelog'], $logline, $append = true); //global changelog cache | |||
// update cache | |||
$this->currentRevision = $info['date']; | |||
$info['mode'] = $this->getMode(); | |||
$this->cache[$this->id][$this->currentRevision] = $info; | |||
return $info; | |||
} | |||
} |
@@ -0,0 +1,68 @@ | |||
<?php | |||
namespace dokuwiki\ChangeLog; | |||
/** | |||
* Class PageChangeLog; 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 | |||
* | |||
* @param string|int $rev empty string or revision timestamp | |||
* @return string path to file | |||
*/ | |||
protected function getFilename($rev = '') | |||
{ | |||
return wikiFN($this->id, $rev); | |||
} | |||
/** | |||
* Returns mode | |||
* | |||
* @return string RevisionInfo::MODE_PAGE | |||
*/ | |||
protected function getMode() | |||
{ | |||
return RevisionInfo::MODE_PAGE; | |||
} | |||
/** | |||
* Adds an entry to the changelog | |||
* | |||
* @param array $info Revision info structure of a page | |||
* @param int $timestamp log line date (optional) | |||
* @return array revision info of added log line | |||
* | |||
* @see also addLogEntry() in inc/changelog.php file | |||
*/ | |||
public function addLogEntry(array $info, $timestamp = null) | |||
{ | |||
global $conf; | |||
if (isset($timestamp)) unset($this->cache[$this->id][$info['date']]); | |||
// add changelog lines | |||
$logline = static::buildLogLine($info, $timestamp); | |||
io_saveFile(metaFN($this->id, '.changes'), $logline, true); | |||
io_saveFile($conf['changelog'], $logline, true); //global changelog cache | |||
// update cache | |||
$this->currentRevision = $info['date']; | |||
$info['mode'] = $this->getMode(); | |||
$this->cache[$this->id][$this->currentRevision] = $info; | |||
return $info; | |||
} | |||
} |
@@ -0,0 +1,395 @@ | |||
<?php | |||
namespace dokuwiki\ChangeLog; | |||
/** | |||
* Class RevisionInfo | |||
* | |||
* Provides methods to show Revision Information in DokuWiki Ui components: | |||
* - Ui\Recent | |||
* - Ui\PageRevisions | |||
* - Ui\MediaRevisions | |||
* - Ui\PageDiff | |||
* - Ui\MediaDiff | |||
*/ | |||
class RevisionInfo | |||
{ | |||
public const MODE_PAGE = 'page'; | |||
public const MODE_MEDIA = 'media'; | |||
/* @var array */ | |||
protected $info; | |||
/** | |||
* Constructor | |||
* | |||
* @param array $info Revision Information structure with entries: | |||
* - date: unix timestamp | |||
* - ip: IPv4 or IPv6 address | |||
* - type: change type (log line type) | |||
* - id: page id | |||
* - user: user name | |||
* - sum: edit summary (or action reason) | |||
* - extra: extra data (varies by line type) | |||
* - sizechange: change of filesize | |||
* additionally, | |||
* - current: (optional) whether current revision or not | |||
* - timestamp: (optional) set only when external edits occurred | |||
* - mode: (internal use) ether "media" or "page" | |||
*/ | |||
public function __construct($info = null) | |||
{ | |||
if (!is_array($info) || !isset($info['id'])) { | |||
$info = [ | |||
'mode' => self::MODE_PAGE, | |||
'date' => false, | |||
]; | |||
} | |||
$this->info = $info; | |||
} | |||
/** | |||
* Set or return whether this revision is current page or media file | |||
* | |||
* This method does not check exactly whether the revision is current or not. Instead, | |||
* set value of associated "current" key for internal use. Some UI element like diff | |||
* link button depend on relation to current page or media file. A changelog line does | |||
* not indicate whether it corresponds to current page or media file. | |||
* | |||
* @param bool $value true if the revision is current, otherwise false | |||
* @return bool | |||
*/ | |||
public function isCurrent($value = null) | |||
{ | |||
return (bool) $this->val('current', $value); | |||
} | |||
/** | |||
* Return or set a value of associated key of revision information | |||
* but does not allow to change values of existing keys | |||
* | |||
* @param string $key | |||
* @param mixed $value | |||
* @return string|null | |||
*/ | |||
public function val($key, $value = null) | |||
{ | |||
if (isset($value) && !array_key_exists($key, $this->info)) { | |||
// setter, only for new keys | |||
$this->info[$key] = $value; | |||
} | |||
if (array_key_exists($key, $this->info)) { | |||
// getter | |||
return $this->info[$key]; | |||
} | |||
return null; | |||
} | |||
/** | |||
* Set extra key-value to the revision information | |||
* but does not allow to change values of existing keys | |||
* @param array $info | |||
* @return void | |||
*/ | |||
public function append(array $info) | |||
{ | |||
foreach ($info as $key => $value) { | |||
$this->val($key, $value); | |||
} | |||
} | |||
/** | |||
* file icon of the page or media file | |||
* used in [Ui\recent] | |||
* | |||
* @return string | |||
*/ | |||
public function showFileIcon() | |||
{ | |||
$id = $this->val('id'); | |||
if ($this->val('mode') == self::MODE_MEDIA) { | |||
// media file revision | |||
return media_printicon($id); | |||
} elseif ($this->val('mode') == self::MODE_PAGE) { | |||
// page revision | |||
return '<img class="icon" src="' . DOKU_BASE . 'lib/images/fileicons/file.png" alt="' . $id . '" />'; | |||
} | |||
} | |||
/** | |||
* edit date and time of the page or media file | |||
* used in [Ui\recent, Ui\Revisions] | |||
* | |||
* @param bool $checkTimestamp enable timestamp check, alter formatted string when timestamp is false | |||
* @return string | |||
*/ | |||
public function showEditDate($checkTimestamp = false) | |||
{ | |||
$formatted = dformat($this->val('date')); | |||
if ($checkTimestamp && $this->val('timestamp') === false) { | |||
// exact date is unknown for externally deleted file | |||
// when unknown, alter formatted string "YYYY-mm-DD HH:MM" to "____-__-__ __:__" | |||
$formatted = preg_replace('/[0-9a-zA-Z]/', '_', $formatted); | |||
} | |||
return '<span class="date">' . $formatted . '</span>'; | |||
} | |||
/** | |||
* edit summary | |||
* used in [Ui\recent, Ui\Revisions] | |||
* | |||
* @return string | |||
*/ | |||
public function showEditSummary() | |||
{ | |||
return '<span class="sum">' . ' – ' . hsc($this->val('sum')) . '</span>'; | |||
} | |||
/** | |||
* editor of the page or media file | |||
* used in [Ui\recent, Ui\Revisions] | |||
* | |||
* @return string | |||
*/ | |||
public function showEditor() | |||
{ | |||
if ($this->val('user')) { | |||
$html = '<bdi>' . editorinfo($this->val('user')) . '</bdi>'; | |||
if (auth_ismanager()) { | |||
$html .= ' <bdo dir="ltr">(' . $this->val('ip') . ')</bdo>'; | |||
} | |||
} else { | |||
$html = '<bdo dir="ltr">' . $this->val('ip') . '</bdo>'; | |||
} | |||
return '<span class="user">' . $html . '</span>'; | |||
} | |||
/** | |||
* name of the page or media file | |||
* used in [Ui\recent, Ui\Revisions] | |||
* | |||
* @return string | |||
*/ | |||
public function showFileName() | |||
{ | |||
$id = $this->val('id'); | |||
$rev = $this->isCurrent() ? '' : $this->val('date'); | |||
if ($this->val('mode') == self::MODE_MEDIA) { | |||
// media file revision | |||
$params = ['tab_details' => 'view', 'ns' => getNS($id), 'image' => $id]; | |||
if ($rev) $params += ['rev' => $rev]; | |||
$href = media_managerURL($params, '&'); | |||
$display_name = $id; | |||
$exists = file_exists(mediaFN($id, $rev)); | |||
} elseif ($this->val('mode') == self::MODE_PAGE) { | |||
// page revision | |||
$params = $rev ? ['rev' => $rev] : []; | |||
$href = wl($id, $params, false, '&'); | |||
$display_name = useHeading('navigation') ? hsc(p_get_first_heading($id)) : $id; | |||
if (!$display_name) $display_name = $id; | |||
$exists = page_exists($id, $rev); | |||
} | |||
if ($exists) { | |||
$class = 'wikilink1'; | |||
} elseif ($this->isCurrent()) { | |||
//show only not-existing link for current page, which allows for directly create a new page/upload | |||
$class = 'wikilink2'; | |||
} else { | |||
//revision is not in attic | |||
return $display_name; | |||
} | |||
if ($this->val('type') == DOKU_CHANGE_TYPE_DELETE) { | |||
$class = 'wikilink2'; | |||
} | |||
return '<a href="' . $href . '" class="' . $class . '">' . $display_name . '</a>'; | |||
} | |||
/** | |||
* Revision Title for PageDiff table headline | |||
* | |||
* @return string | |||
*/ | |||
public function showRevisionTitle() | |||
{ | |||
global $lang; | |||
if (!$this->val('date')) return '—'; | |||
$id = $this->val('id'); | |||
$rev = $this->isCurrent() ? '' : $this->val('date'); | |||
$params = ($rev) ? ['rev' => $rev] : []; | |||
// revision info may have timestamp key when external edits occurred | |||
$date = ($this->val('timestamp') === false) | |||
? $lang['unknowndate'] | |||
: dformat($this->val('date')); | |||
if ($this->val('mode') == self::MODE_MEDIA) { | |||
// media file revision | |||
$href = ml($id, $params, false, '&'); | |||
$exists = file_exists(mediaFN($id, $rev)); | |||
} elseif ($this->val('mode') == self::MODE_PAGE) { | |||
// page revision | |||
$href = wl($id, $params, false, '&'); | |||
$exists = page_exists($id, $rev); | |||
} | |||
if ($exists) { | |||
$class = 'wikilink1'; | |||
} elseif ($this->isCurrent()) { | |||
//show only not-existing link for current page, which allows for directly create a new page/upload | |||
$class = 'wikilink2'; | |||
} else { | |||
//revision is not in attic | |||
return $id . ' [' . $date . ']'; | |||
} | |||
if ($this->val('type') == DOKU_CHANGE_TYPE_DELETE) { | |||
$class = 'wikilink2'; | |||
} | |||
return '<bdi><a class="' . $class . '" href="' . $href . '">' . $id . ' [' . $date . ']' . '</a></bdi>'; | |||
} | |||
/** | |||
* diff link icon in recent changes list, to compare (this) current revision with previous one | |||
* all items in "recent changes" are current revision of the page or media | |||
* | |||
* @return string | |||
*/ | |||
public function showIconCompareWithPrevious() | |||
{ | |||
global $lang; | |||
$id = $this->val('id'); | |||
$href = ''; | |||
if ($this->val('mode') == self::MODE_MEDIA) { | |||
// media file revision | |||
// unlike page, media file does not copied to media_attic when uploaded. | |||
// diff icon will not be shown when external edit occurred | |||
// because no attic file to be compared with current. | |||
$revs = (new MediaChangeLog($id))->getRevisions(0, 1); | |||
$showLink = (count($revs) && file_exists(mediaFN($id, $revs[0])) && file_exists(mediaFN($id))); | |||
if ($showLink) { | |||
$param = ['tab_details' => 'history', 'mediado' => 'diff', 'ns' => getNS($id), 'image' => $id]; | |||
$href = media_managerURL($param, '&'); | |||
} | |||
} elseif ($this->val('mode') == self::MODE_PAGE) { | |||
// page revision | |||
// when a page just created anyway, it is natural to expect no older revisions | |||
// even if it had once existed but deleted before. Simply ignore to check changelog. | |||
if ($this->val('type') !== DOKU_CHANGE_TYPE_CREATE) { | |||
$href = wl($id, ['do' => 'diff'], false, '&'); | |||
} | |||
} | |||
if ($href) { | |||
return '<a href="' . $href . '" class="diff_link">' | |||
. '<img src="' . DOKU_BASE . 'lib/images/diff.png" width="15" height="11"' | |||
. ' title="' . $lang['diff'] . '" alt="' . $lang['diff'] . '" />' | |||
. '</a>'; | |||
} else { | |||
return '<img src="' . DOKU_BASE . 'lib/images/blank.gif" width="15" height="11" alt="" />'; | |||
} | |||
} | |||
/** | |||
* diff link icon in revisions list, compare this revision with current one | |||
* the icon does not displayed for the current revision | |||
* | |||
* @return string | |||
*/ | |||
public function showIconCompareWithCurrent() | |||
{ | |||
global $lang; | |||
$id = $this->val('id'); | |||
$rev = $this->isCurrent() ? '' : $this->val('date'); | |||
$href = ''; | |||
if ($this->val('mode') == self::MODE_MEDIA) { | |||
// media file revision | |||
if (!$this->isCurrent() && file_exists(mediaFN($id, $rev))) { | |||
$param = ['mediado' => 'diff', 'image' => $id, 'rev' => $rev]; | |||
$href = media_managerURL($param, '&'); | |||
} | |||
} elseif ($this->val('mode') == self::MODE_PAGE) { | |||
// page revision | |||
if (!$this->isCurrent()) { | |||
$href = wl($id, ['rev' => $rev, 'do' => 'diff'], false, '&'); | |||
} | |||
} | |||
if ($href) { | |||
return '<a href="' . $href . '" class="diff_link">' | |||
. '<img src="' . DOKU_BASE . 'lib/images/diff.png" width="15" height="11"' | |||
. ' title="' . $lang['diff'] . '" alt="' . $lang['diff'] . '" />' | |||
. '</a>'; | |||
} else { | |||
return '<img src="' . DOKU_BASE . 'lib/images/blank.gif" width="15" height="11" alt="" />'; | |||
} | |||
} | |||
/** | |||
* icon for revision action | |||
* used in [Ui\recent] | |||
* | |||
* @return string | |||
*/ | |||
public function showIconRevisions() | |||
{ | |||
global $lang; | |||
if (!actionOK('revisions')) { | |||
return ''; | |||
} | |||
$id = $this->val('id'); | |||
if ($this->val('mode') == self::MODE_MEDIA) { | |||
// media file revision | |||
$param = ['tab_details' => 'history', 'ns' => getNS($id), 'image' => $id]; | |||
$href = media_managerURL($param, '&'); | |||
} elseif ($this->val('mode') == self::MODE_PAGE) { | |||
// page revision | |||
$href = wl($id, ['do' => 'revisions'], false, '&'); | |||
} | |||
return '<a href="' . $href . '" class="revisions_link">' | |||
. '<img src="' . DOKU_BASE . 'lib/images/history.png" width="12" height="14"' | |||
. ' title="' . $lang['btn_revs'] . '" alt="' . $lang['btn_revs'] . '" />' | |||
. '</a>'; | |||
} | |||
/** | |||
* size change | |||
* used in [Ui\recent, Ui\Revisions] | |||
* | |||
* @return string | |||
*/ | |||
public function showSizeChange() | |||
{ | |||
$class = 'sizechange'; | |||
$value = filesize_h(abs($this->val('sizechange'))); | |||
if ($this->val('sizechange') > 0) { | |||
$class .= ' positive'; | |||
$value = '+' . $value; | |||
} elseif ($this->val('sizechange') < 0) { | |||
$class .= ' negative'; | |||
$value = '-' . $value; | |||
} else { | |||
$value = '±' . $value; | |||
} | |||
return '<span class="' . $class . '">' . $value . '</span>'; | |||
} | |||
/** | |||
* current indicator, used in revision list | |||
* not used in Ui\Recent because recent files are always current one | |||
* | |||
* @return string | |||
*/ | |||
public function showCurrentIndicator() | |||
{ | |||
global $lang; | |||
return $this->isCurrent() ? '(' . $lang['current'] . ')' : ''; | |||
} | |||
} |
@@ -0,0 +1,178 @@ | |||
<?php | |||
namespace dokuwiki\Debug; | |||
use dokuwiki\Extension\Event; | |||
use dokuwiki\Extension\EventHandler; | |||
use dokuwiki\Logger; | |||
class DebugHelper | |||
{ | |||
protected const INFO_DEPRECATION_LOG_EVENT = 'INFO_DEPRECATION_LOG'; | |||
/** | |||
* Check if deprecation messages shall be handled | |||
* | |||
* This is either because its logging is not disabled or a deprecation handler was registered | |||
* | |||
* @return bool | |||
*/ | |||
public static function isEnabled() | |||
{ | |||
/** @var EventHandler $EVENT_HANDLER */ | |||
global $EVENT_HANDLER; | |||
if ( | |||
!Logger::getInstance(Logger::LOG_DEPRECATED)->isLogging() && | |||
(!$EVENT_HANDLER instanceof EventHandler || !$EVENT_HANDLER->hasHandlerForEvent('INFO_DEPRECATION_LOG')) | |||
) { | |||
// avoid any work if no one cares | |||
return false; | |||
} | |||
return true; | |||
} | |||
/** | |||
* 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 | |||
* @param string $thing (optional) The deprecated thing, defaults to the calling method | |||
* @triggers \dokuwiki\Debug::INFO_DEPRECATION_LOG_EVENT | |||
*/ | |||
public static function dbgDeprecatedFunction($alternative = '', $callerOffset = 1, $thing = '') | |||
{ | |||
if (!self::isEnabled()) return; | |||
$backtrace = debug_backtrace(); | |||
for ($i = 0; $i < $callerOffset; ++$i) { | |||
if (count($backtrace) > 1) array_shift($backtrace); | |||
} | |||
[$self, $call] = $backtrace; | |||
self::triggerDeprecationEvent( | |||
$backtrace, | |||
$alternative, | |||
self::formatCall($self), | |||
self::formatCall($call), | |||
$self['file'] ?? $call['file'] ?? '', | |||
$self['line'] ?? $call['line'] ?? 0 | |||
); | |||
} | |||
/** | |||
* Format the given backtrace info into a proper function/method call string | |||
* @param array $call | |||
* @return string | |||
*/ | |||
protected static function formatCall($call) | |||
{ | |||
$thing = ''; | |||
if (!empty($call['class'])) { | |||
$thing .= $call['class'] . '::'; | |||
} | |||
$thing .= $call['function'] . '()'; | |||
return trim($thing, ':'); | |||
} | |||
/** | |||
* 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) | |||
{ | |||
if (!self::isEnabled()) 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 | |||
) { | |||
if (!self::isEnabled()) 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 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!'; | |||
} | |||
Logger::getInstance(Logger::LOG_DEPRECATED)->log($msg); | |||
} | |||
$event->advise_after(); | |||
} | |||
} |
@@ -0,0 +1,133 @@ | |||
<?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($this); | |||
} | |||
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 self::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 = self::class; | |||
} | |||
return $classname; | |||
} | |||
} | |||
return false; | |||
} | |||
} |
@@ -0,0 +1,168 @@ | |||
<?php | |||
namespace dokuwiki; | |||
use dokuwiki\Extension\Event; | |||
/** | |||
* 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\n$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 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); | |||
} | |||
} |
@@ -0,0 +1,205 @@ | |||
<?php | |||
namespace dokuwiki; | |||
use dokuwiki\Exception\FatalException; | |||
/** | |||
* Manage the global handling of errors and exceptions | |||
* | |||
* Developer may use this to log and display exceptions themselves | |||
*/ | |||
class ErrorHandler | |||
{ | |||
/** | |||
* Standard error codes used in PHP errors | |||
* @see https://www.php.net/manual/en/errorfunc.constants.php | |||
*/ | |||
protected const ERRORCODES = [ | |||
1 => 'E_ERROR', | |||
2 => 'E_WARNING', | |||
4 => 'E_PARSE', | |||
8 => 'E_NOTICE', | |||
16 => 'E_CORE_ERROR', | |||
32 => 'E_CORE_WARNING', | |||
64 => 'E_COMPILE_ERROR', | |||
128 => 'E_COMPILE_WARNING', | |||
256 => 'E_USER_ERROR', | |||
512 => 'E_USER_WARNING', | |||
1024 => 'E_USER_NOTICE', | |||
2048 => 'E_STRICT', | |||
4096 => 'E_RECOVERABLE_ERROR', | |||
8192 => 'E_DEPRECATED', | |||
16384 => 'E_USER_DEPRECATED', | |||
]; | |||
/** | |||
* Register the default error handling | |||
*/ | |||
public static function register() | |||
{ | |||
if (!defined('DOKU_UNITTEST')) { | |||
set_exception_handler([ErrorHandler::class, 'fatalException']); | |||
register_shutdown_function([ErrorHandler::class, 'fatalShutdown']); | |||
set_error_handler( | |||
[ErrorHandler::class, 'errorHandler'], | |||
E_WARNING | E_USER_ERROR | E_USER_WARNING | E_RECOVERABLE_ERROR | |||
); | |||
} | |||
} | |||
/** | |||
* Default Exception handler to show a nice user message before dieing | |||
* | |||
* The exception is logged to the error log | |||
* | |||
* @param \Throwable $e | |||
*/ | |||
public static function fatalException($e) | |||
{ | |||
$plugin = self::guessPlugin($e); | |||
$title = hsc(get_class($e) . ': ' . $e->getMessage()); | |||
$msg = 'An unforeseen error has occured. This is most likely a bug somewhere.'; | |||
if ($plugin) $msg .= ' It might be a problem in the ' . $plugin . ' plugin.'; | |||
$logged = self::logException($e) | |||
? 'More info has been written to the DokuWiki error log.' | |||
: $e->getFile() . ':' . $e->getLine(); | |||
echo <<<EOT | |||
<!DOCTYPE html> | |||
<html> | |||
<head><title>$title</title></head> | |||
<body style="font-family: Arial, sans-serif"> | |||
<div style="width:60%; margin: auto; background-color: #fcc; | |||
border: 1px solid #faa; padding: 0.5em 1em;"> | |||
<h1 style="font-size: 120%">$title</h1> | |||
<p>$msg</p> | |||
<p>$logged</p> | |||
</div> | |||
</body> | |||
</html> | |||
EOT; | |||
} | |||
/** | |||
* Convenience method to display an error message for the given Exception | |||
* | |||
* @param \Throwable $e | |||
* @param string $intro | |||
*/ | |||
public static function showExceptionMsg($e, $intro = 'Error!') | |||
{ | |||
$msg = hsc($intro) . '<br />' . hsc(get_class($e) . ': ' . $e->getMessage()); | |||
if (self::logException($e)) $msg .= '<br />More info is available in the error log.'; | |||
msg($msg, -1); | |||
} | |||
/** | |||
* Last resort to handle fatal errors that still can't be caught | |||
*/ | |||
public static function fatalShutdown() | |||
{ | |||
$error = error_get_last(); | |||
// Check if it's a core/fatal error, otherwise it's a normal shutdown | |||
if ( | |||
$error !== null && | |||
in_array( | |||
$error['type'], | |||
[ | |||
E_ERROR, | |||
E_CORE_ERROR, | |||
E_COMPILE_ERROR, | |||
] | |||
) | |||
) { | |||
self::fatalException( | |||
new FatalException($error['message'], 0, $error['type'], $error['file'], $error['line']) | |||
); | |||
} | |||
} | |||
/** | |||
* Log the given exception to the error log | |||
* | |||
* @param \Throwable $e | |||
* @return bool false if the logging failed | |||
*/ | |||
public static function logException($e) | |||
{ | |||
if ($e instanceof \ErrorException) { | |||
$prefix = self::ERRORCODES[$e->getSeverity()]; | |||
} else { | |||
$prefix = get_class($e); | |||
} | |||
return Logger::getInstance()->log( | |||
$prefix . ': ' . $e->getMessage(), | |||
$e->getTraceAsString(), | |||
$e->getFile(), | |||
$e->getLine() | |||
); | |||
} | |||
/** | |||
* Error handler to log non-exception errors | |||
* | |||
* @param int $errno | |||
* @param string $errstr | |||
* @param string $errfile | |||
* @param int $errline | |||
* @return bool | |||
*/ | |||
public static function errorHandler($errno, $errstr, $errfile, $errline) | |||
{ | |||
global $conf; | |||
// ignore supressed warnings | |||
if (!(error_reporting() & $errno)) return false; | |||
$ex = new \ErrorException( | |||
$errstr, | |||
0, | |||
$errno, | |||
$errfile, | |||
$errline | |||
); | |||
self::logException($ex); | |||
if ($ex->getSeverity() === E_WARNING && $conf['hidewarnings']) { | |||
return true; | |||
} | |||
return false; | |||
} | |||
/** | |||
* Checks the the stacktrace for plugin files | |||
* | |||
* @param \Throwable $e | |||
* @return false|string | |||
*/ | |||
protected static function guessPlugin($e) | |||
{ | |||
if (preg_match('/lib\/plugins\/(\w+)\//', str_replace('\\', '/', $e->getFile()), $match)) { | |||
return $match[1]; | |||
} | |||
foreach ($e->getTrace() as $line) { | |||
if ( | |||
isset($line['class']) && | |||
preg_match('/\w+?_plugin_(\w+)/', $line['class'], $match) | |||
) { | |||
return $match[1]; | |||
} | |||
if ( | |||
isset($line['file']) && | |||
preg_match('/lib\/plugins\/(\w+)\//', str_replace('\\', '/', $line['file']), $match) | |||
) { | |||
return $match[1]; | |||
} | |||
} | |||
return false; | |||
} | |||
} |
@@ -0,0 +1,10 @@ | |||
<?php | |||
namespace dokuwiki\Exception; | |||
/** | |||
* Fatal Errors are converted into this Exception in out Shutdown handler | |||
*/ | |||
class FatalException extends \ErrorException | |||
{ | |||
} |
@@ -0,0 +1,21 @@ | |||
<?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 EventHandler $controller | |||
*/ | |||
abstract public function register(EventHandler $controller); | |||
} |
@@ -0,0 +1,121 @@ | |||
<?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 []; | |||
} | |||
} |
@@ -0,0 +1,459 @@ | |||
<?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 = [ | |||
'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']); | |||
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']); | |||
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 = [ | |||
'create' => 'createUser', | |||
'modify' => 'modifyUser', | |||
'delete' => 'deleteUsers' | |||
]; | |||
if (empty($validTypes[$type])) { | |||
return false; | |||
} | |||
$result = false; | |||
$eventdata = ['type' => $type, 'params' => $params, 'modification_result' => null]; | |||
$evt = new Event('AUTH_USER_CHANGE', $eventdata); | |||
if ($evt->advise_before(true)) { | |||
$result = call_user_func_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 = []) | |||
{ | |||
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 []; | |||
} | |||
/** | |||
* 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 []; | |||
} | |||
/** | |||
* 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')); | |||
} | |||
} |