Browse Source

はじまりの大地

main
miteruzo 1 year ago
commit
c616a96f53
100 changed files with 10394 additions and 0 deletions
  1. +23
    -0
      .gitignore
  2. +45
    -0
      .htaccess
  3. +7
    -0
      bin/.htaccess
  4. +359
    -0
      bin/dwpage.php
  5. +346
    -0
      bin/gittool.php
  6. +113
    -0
      bin/indexer.php
  7. +110
    -0
      bin/plugin.php
  8. +66
    -0
      bin/render.php
  9. +116
    -0
      bin/striplangs.php
  10. +188
    -0
      bin/wantedpages.php
  11. +8
    -0
      conf/.htaccess
  12. +9
    -0
      conf/acl.auth.php
  13. +21
    -0
      conf/acl.auth.php.dist
  14. +62
    -0
      conf/acronyms.conf
  15. +181
    -0
      conf/dokuwiki.php
  16. +22
    -0
      conf/entities.conf
  17. +43
    -0
      conf/interwiki.conf
  18. +38
    -0
      conf/license.php
  19. +21
    -0
      conf/local.php.bak.php
  20. +16
    -0
      conf/local.php.dist
  21. +3
    -0
      conf/manifest.json
  22. +91
    -0
      conf/mediameta.php
  23. +75
    -0
      conf/mime.conf
  24. +253
    -0
      conf/mysql.conf.php.example
  25. +12
    -0
      conf/plugins.local.php
  26. +6
    -0
      conf/plugins.php
  27. +12
    -0
      conf/plugins.required.php
  28. +11
    -0
      conf/scheme.conf
  29. +28
    -0
      conf/smileys.conf
  30. +10
    -0
      conf/users.auth.php.dist
  31. +29
    -0
      conf/wordblock.conf
  32. +136
    -0
      doku.php
  33. +75
    -0
      feed.php
  34. +8
    -0
      inc/.htaccess
  35. +26
    -0
      inc/Action/AbstractAclAction.php
  36. +93
    -0
      inc/Action/AbstractAction.php
  37. +32
    -0
      inc/Action/AbstractAliasAction.php
  38. +25
    -0
      inc/Action/AbstractUserAction.php
  39. +45
    -0
      inc/Action/Admin.php
  40. +34
    -0
      inc/Action/Authtoken.php
  41. +28
    -0
      inc/Action/Backlink.php
  42. +28
    -0
      inc/Action/Cancel.php
  43. +27
    -0
      inc/Action/Check.php
  44. +39
    -0
      inc/Action/Conflict.php
  45. +52
    -0
      inc/Action/Denied.php
  46. +41
    -0
      inc/Action/Diff.php
  47. +43
    -0
      inc/Action/Draft.php
  48. +40
    -0
      inc/Action/Draftdel.php
  49. +96
    -0
      inc/Action/Edit.php
  50. +20
    -0
      inc/Action/Exception/ActionAbort.php
  51. +17
    -0
      inc/Action/Exception/ActionAclRequiredException.php
  52. +17
    -0
      inc/Action/Exception/ActionDisabledException.php
  53. +69
    -0
      inc/Action/Exception/ActionException.php
  54. +17
    -0
      inc/Action/Exception/ActionUserRequiredException.php
  55. +28
    -0
      inc/Action/Exception/FatalException.php
  56. +15
    -0
      inc/Action/Exception/NoActionException.php
  57. +114
    -0
      inc/Action/Export.php
  58. +28
    -0
      inc/Action/Index.php
  59. +57
    -0
      inc/Action/Locked.php
  60. +39
    -0
      inc/Action/Login.php
  61. +55
    -0
      inc/Action/Logout.php
  62. +25
    -0
      inc/Action/Media.php
  63. +36
    -0
      inc/Action/Plugin.php
  64. +49
    -0
      inc/Action/Preview.php
  65. +51
    -0
      inc/Action/Profile.php
  66. +45
    -0
      inc/Action/ProfileDelete.php
  67. +44
    -0
      inc/Action/Recent.php
  68. +24
    -0
      inc/Action/Recover.php
  69. +64
    -0
      inc/Action/Redirect.php
  70. +51
    -0
      inc/Action/Register.php
  71. +182
    -0
      inc/Action/Resendpwd.php
  72. +61
    -0
      inc/Action/Revert.php
  73. +29
    -0
      inc/Action/Revisions.php
  74. +65
    -0
      inc/Action/Save.php
  75. +136
    -0
      inc/Action/Search.php
  76. +42
    -0
      inc/Action/Show.php
  77. +68
    -0
      inc/Action/Sitemap.php
  78. +41
    -0
      inc/Action/Source.php
  79. +181
    -0
      inc/Action/Subscribe.php
  80. +235
    -0
      inc/ActionRouter.php
  81. +447
    -0
      inc/Ajax.php
  82. +240
    -0
      inc/Cache/Cache.php
  83. +54
    -0
      inc/Cache/CacheImageMod.php
  84. +45
    -0
      inc/Cache/CacheInstructions.php
  85. +63
    -0
      inc/Cache/CacheParser.php
  86. +92
    -0
      inc/Cache/CacheRenderer.php
  87. +700
    -0
      inc/ChangeLog/ChangeLog.php
  88. +262
    -0
      inc/ChangeLog/ChangeLogTrait.php
  89. +68
    -0
      inc/ChangeLog/MediaChangeLog.php
  90. +68
    -0
      inc/ChangeLog/PageChangeLog.php
  91. +395
    -0
      inc/ChangeLog/RevisionInfo.php
  92. +178
    -0
      inc/Debug/DebugHelper.php
  93. +133
    -0
      inc/Debug/PropertyDeprecationHelper.php
  94. +1568
    -0
      inc/DifferenceEngine.php
  95. +168
    -0
      inc/Draft.php
  96. +205
    -0
      inc/ErrorHandler.php
  97. +10
    -0
      inc/Exception/FatalException.php
  98. +21
    -0
      inc/Extension/ActionPlugin.php
  99. +121
    -0
      inc/Extension/AdminPlugin.php
  100. +459
    -0
      inc/Extension/AuthPlugin.php

+ 23
- 0
.gitignore View File

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

+ 45
- 0
.htaccess View File

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

+ 7
- 0
bin/.htaccess View File

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

+ 359
- 0
bin/dwpage.php View File

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

+ 346
- 0
bin/gittool.php View File

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

+ 113
- 0
bin/indexer.php View File

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

+ 110
- 0
bin/plugin.php View File

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

+ 66
- 0
bin/render.php View File

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

+ 116
- 0
bin/striplangs.php View File

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

+ 188
- 0
bin/wantedpages.php View File

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

+ 8
- 0
conf/.htaccess View File

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

+ 9
- 0
conf/acl.auth.php View File

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

+ 21
- 0
conf/acl.auth.php.dist View File

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

+ 62
- 0
conf/acronyms.conf View File

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

+ 181
- 0
conf/dokuwiki.php View File

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

+ 22
- 0
conf/entities.conf View File

@@ -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) ®
... …


+ 43
- 0
conf/interwiki.conf View File

@@ -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&amp;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}&amp;btnI=lucky
user :user:{NAME}

# To support VoIP/SIP/TEL links
callto callto://{NAME}
tel tel:{NAME}

+ 38
- 0
conf/license.php View File

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


+ 21
- 0
conf/local.php.bak.php View File

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

+ 16
- 0
conf/local.php.dist View File

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


+ 3
- 0
conf/manifest.json View File

@@ -0,0 +1,3 @@
{
"display": "standalone"
}

+ 91
- 0
conf/mediameta.php View File

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

+ 75
- 0
conf/mime.conf View File

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


+ 253
- 0
conf/mysql.conf.php.example View File

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



+ 12
- 0
conf/plugins.local.php View File

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

+ 6
- 0
conf/plugins.php View File

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

+ 12
- 0
conf/plugins.required.php View File

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

+ 11
- 0
conf/scheme.conf View File

@@ -0,0 +1,11 @@
#Add URL schemes you want to be recognized as links here

http
https
telnet
gopher
wais
ftp
ed2k
irc
ldap

+ 28
- 0
conf/smileys.conf View File

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


+ 10
- 0
conf/users.auth.php.dist View File

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


+ 29
- 0
conf/wordblock.conf View File

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

+ 136
- 0
doku.php View File

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

+ 75
- 0
feed.php View File

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

+ 8
- 0
inc/.htaccess View File

@@ -0,0 +1,8 @@
## no access to the inc directory
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
</IfModule>

+ 26
- 0
inc/Action/AbstractAclAction.php View File

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

+ 93
- 0
inc/Action/AbstractAction.php View File

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

+ 32
- 0
inc/Action/AbstractAliasAction.php View File

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

+ 25
- 0
inc/Action/AbstractUserAction.php View File

@@ -0,0 +1,25 @@
<?php

namespace dokuwiki\Action;

use dokuwiki\Action\Exception\ActionUserRequiredException;

/**
* Class AbstractUserAction
*
* An action that requires a logged in user
*
* @package dokuwiki\Action
*/
abstract class AbstractUserAction extends AbstractAclAction
{
/** @inheritdoc */
public function checkPreconditions()
{
parent::checkPreconditions();
global $INPUT;
if ($INPUT->server->str('REMOTE_USER') === '') {
throw new ActionUserRequiredException();
}
}
}

+ 45
- 0
inc/Action/Admin.php View File

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

+ 34
- 0
inc/Action/Authtoken.php View File

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

+ 28
- 0
inc/Action/Backlink.php View File

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

+ 28
- 0
inc/Action/Cancel.php View File

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

+ 27
- 0
inc/Action/Check.php View File

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

+ 39
- 0
inc/Action/Conflict.php View File

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

+ 52
- 0
inc/Action/Denied.php View File

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

+ 41
- 0
inc/Action/Diff.php View File

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

+ 43
- 0
inc/Action/Draft.php View File

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

+ 40
- 0
inc/Action/Draftdel.php View File

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

+ 96
- 0
inc/Action/Edit.php View File

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

+ 20
- 0
inc/Action/Exception/ActionAbort.php View File

@@ -0,0 +1,20 @@
<?php

namespace dokuwiki\Action\Exception;

/**
* Class ActionAbort
*
* Strictly speaking not an Exception but an expected execution path. Used to
* signal when one action is done and another should take over.
*
* If you want to signal the same but under some error condition use ActionException
* or one of it's decendants.
*
* The message will NOT be shown to the enduser
*
* @package dokuwiki\Action\Exception
*/
class ActionAbort extends ActionException
{
}

+ 17
- 0
inc/Action/Exception/ActionAclRequiredException.php View File

@@ -0,0 +1,17 @@
<?php

namespace dokuwiki\Action\Exception;

/**
* Class ActionAclRequiredException
*
* Thrown by AbstractACLAction when an action requires that the ACL subsystem is
* enabled but it isn't. You should not use it
*
* The message will NOT be shown to the enduser
*
* @package dokuwiki\Action\Exception
*/
class ActionAclRequiredException extends ActionException
{
}

+ 17
- 0
inc/Action/Exception/ActionDisabledException.php View File

@@ -0,0 +1,17 @@
<?php

namespace dokuwiki\Action\Exception;

/**
* Class ActionDisabledException
*
* Thrown when the requested action has been disabled. Eg. through the 'disableactions'
* config setting. You should probably not use it.
*
* The message will NOT be shown to the enduser, but a generic information will be shown.
*
* @package dokuwiki\Action\Exception
*/
class ActionDisabledException extends ActionException
{
}

+ 69
- 0
inc/Action/Exception/ActionException.php View File

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

+ 17
- 0
inc/Action/Exception/ActionUserRequiredException.php View File

@@ -0,0 +1,17 @@
<?php

namespace dokuwiki\Action\Exception;

/**
* Class ActionUserRequiredException
*
* Thrown by AbstractUserAction when an action requires that a user is logged
* in but it isn't. You should not use it.
*
* The message will NOT be shown to the enduser
*
* @package dokuwiki\Action\Exception
*/
class ActionUserRequiredException extends ActionException
{
}

+ 28
- 0
inc/Action/Exception/FatalException.php View File

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

+ 15
- 0
inc/Action/Exception/NoActionException.php View File

@@ -0,0 +1,15 @@
<?php

namespace dokuwiki\Action\Exception;

/**
* Class NoActionException
*
* Thrown in the ActionRouter when a wanted action can not be found. Triggers
* the unknown action event
*
* @package dokuwiki\Action\Exception
*/
class NoActionException extends \Exception
{
}

+ 114
- 0
inc/Action/Export.php View File

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

+ 28
- 0
inc/Action/Index.php View File

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

+ 57
- 0
inc/Action/Locked.php View File

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

+ 39
- 0
inc/Action/Login.php View File

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

+ 55
- 0
inc/Action/Logout.php View File

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

+ 25
- 0
inc/Action/Media.php View File

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

+ 36
- 0
inc/Action/Plugin.php View File

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

+ 49
- 0
inc/Action/Preview.php View File

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

+ 51
- 0
inc/Action/Profile.php View File

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

+ 45
- 0
inc/Action/ProfileDelete.php View File

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

+ 44
- 0
inc/Action/Recent.php View File

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

+ 24
- 0
inc/Action/Recover.php View File

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

+ 64
- 0
inc/Action/Redirect.php View File

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

+ 51
- 0
inc/Action/Register.php View File

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

+ 182
- 0
inc/Action/Resendpwd.php View File

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

+ 61
- 0
inc/Action/Revert.php View File

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

+ 29
- 0
inc/Action/Revisions.php View File

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

+ 65
- 0
inc/Action/Save.php View File

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

+ 136
- 0
inc/Action/Search.php View File

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

+ 42
- 0
inc/Action/Show.php View File

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

+ 68
- 0
inc/Action/Sitemap.php View File

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

+ 41
- 0
inc/Action/Source.php View File

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

+ 181
- 0
inc/Action/Subscribe.php View File

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

+ 235
- 0
inc/ActionRouter.php View File

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

+ 447
- 0
inc/Ajax.php View File

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

+ 240
- 0
inc/Cache/Cache.php View File

@@ -0,0 +1,240 @@
<?php

namespace dokuwiki\Cache;

use dokuwiki\Debug\PropertyDeprecationHelper;
use dokuwiki\Extension\Event;

/**
* Generic handling of caching
*/
class Cache
{
use PropertyDeprecationHelper;

public $key = ''; // primary identifier for this item
public $ext = ''; // file ext for cache data, secondary identifier for this item
public $cache = ''; // cache file name
public $depends = []; // array 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;
}
}

+ 54
- 0
inc/Cache/CacheImageMod.php View File

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

+ 45
- 0
inc/Cache/CacheInstructions.php View File

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

+ 63
- 0
inc/Cache/CacheParser.php View File

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

+ 92
- 0
inc/Cache/CacheRenderer.php View File

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

+ 700
- 0
inc/ChangeLog/ChangeLog.php View File

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

+ 262
- 0
inc/ChangeLog/ChangeLogTrait.php View File

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

+ 68
- 0
inc/ChangeLog/MediaChangeLog.php View File

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

+ 68
- 0
inc/ChangeLog/PageChangeLog.php View File

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

+ 395
- 0
inc/ChangeLog/RevisionInfo.php View File

@@ -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 '&mdash;';

$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'] . ')' : '';
}
}

+ 178
- 0
inc/Debug/DebugHelper.php View File

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

+ 133
- 0
inc/Debug/PropertyDeprecationHelper.php View File

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

+ 1568
- 0
inc/DifferenceEngine.php
File diff suppressed because it is too large
View File


+ 168
- 0
inc/Draft.php View File

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

+ 205
- 0
inc/ErrorHandler.php View File

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

+ 10
- 0
inc/Exception/FatalException.php View File

@@ -0,0 +1,10 @@
<?php

namespace dokuwiki\Exception;

/**
* Fatal Errors are converted into this Exception in out Shutdown handler
*/
class FatalException extends \ErrorException
{
}

+ 21
- 0
inc/Extension/ActionPlugin.php View File

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

+ 121
- 0
inc/Extension/AdminPlugin.php View File

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

+ 459
- 0
inc/Extension/AuthPlugin.php View File

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

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

Loading…
Cancel
Save