[packages/dokuwiki-plugin-editx] Rel 2. Fix DokuWiki 2026 compatibility
arekm
arekm at pld-linux.org
Wed Oct 21 20:23:49 CEST 2026
commit f4f3a28958d14e10f0e7964790e6e6bc3c6b023e
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date: Wed Oct 21 20:22:41 2026 +0200
Rel 2. Fix DokuWiki 2026 compatibility
dokuwiki-plugin-editx-compat.patch | 119 +++++++++++++
dokuwiki-plugin-editx-safety.patch | 354 +++++++++++++++++++++++++++++++++++++
dokuwiki-plugin-editx.spec | 13 +-
regression.php | 184 +++++++++++++++++++
4 files changed, 666 insertions(+), 4 deletions(-)
---
diff --git a/dokuwiki-plugin-editx.spec b/dokuwiki-plugin-editx.spec
index c44bda1..84812e7 100644
--- a/dokuwiki-plugin-editx.spec
+++ b/dokuwiki-plugin-editx.spec
@@ -3,16 +3,19 @@
Summary: DokuWiki editx plugin
Name: dokuwiki-plugin-%{plugin}
Version: 20160207
-Release: 1
+Release: 2
License: GPL v2
Group: Applications/WWW
-Source0: https://github.com/danny0838/dw-editx/tarball/master/%{plugin}-%{version}.tar.gz
+Source0: https://github.com/danny0838/dokuwiki-plugin-editx/tarball/644a3148c5aa2079e6df7ed62baa48d545de073e/%{plugin}-%{version}.tar.gz
# Source0-md5: dbc337cf034cefb1ecd9a686c0de97d4
+Source1: regression.php
+Patch0: dokuwiki-plugin-editx-compat.patch
+Patch1: dokuwiki-plugin-editx-safety.patch
URL: https://www.dokuwiki.org/plugin:editx
BuildRequires: rpm-php-pearprov >= 4.4.2-11
BuildRequires: rpmbuild(macros) >= 1.553
-BuildRequires: unzip
-Requires: dokuwiki >= 20061106
+# checkSecurityToken() and formSecurityToken()
+Requires: dokuwiki >= 20080505
Requires: php(core) >= %{php_min_version}
Requires: php(date)
Requires: php(pcre)
@@ -31,6 +34,8 @@ Extended edit functions such as renaming or deleting a page.
%setup -qc
mv *%{plugin}*/* .
%undos -f php
+%patch -P0 -p1
+%patch -P1 -p1
# using date from latest commit
#version=$(awk '/^date/{print $2}' plugin.info.txt)
diff --git a/dokuwiki-plugin-editx-compat.patch b/dokuwiki-plugin-editx-compat.patch
new file mode 100644
index 0000000..d8c07a7
--- /dev/null
+++ b/dokuwiki-plugin-editx-compat.patch
@@ -0,0 +1,119 @@
+# PHP 8 rejects curly string offsets and bare array keys; DokuWiki loads fulltext via compatibility functions.
+--- a/action.php
++++ b/action.php
+@@ -8,9 +8,13 @@
+ if(!defined('DOKU_INC')) die();
+ if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
+ require_once (DOKU_PLUGIN . 'action.php');
+-require_once(DOKU_INC.'inc/fulltext.php');
++if (!function_exists('ft_backlinks')) {
++ require_once(DOKU_INC.'inc/fulltext.php');
++}
+
+ class action_plugin_editx extends DokuWiki_Action_Plugin {
++
++ var $errors = array();
+
+ /**
+ * register the eventhandlers
+@@ -81,8 +85,8 @@
+
+ if(!count($allowed)) return true; //no restrictions
+
+- $user = $_SERVER['REMOTE_USER'];
+- $groups = (array) $USERINFO['grps'];
++ $user = isset($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'] : '';
++ $groups = isset($USERINFO['grps']) ? (array) $USERINFO['grps'] : array();
+
+ if(in_array($user,$allowed)) return true; //user explicitly mentioned
+
+@@ -101,8 +105,8 @@
+ }
+
+ function _auth_can_rename($id) {
+- static $cache = null;
+- if (!$cache[$id]) {
++ static $cache = array();
++ if (!isset($cache[$id])) {
+ $cache[$id] = auth_quickaclcheck($id)>=AUTH_EDIT &&
+ $this->_auth_check_list($this->getConf('user_rename'));
+ }
+@@ -110,8 +114,8 @@
+ }
+
+ function _auth_can_rename_nr($id) {
+- static $cache = null;
+- if (!$cache[$id]) {
++ static $cache = array();
++ if (!isset($cache[$id])) {
+ $cache[$id] = auth_quickaclcheck($id)>=AUTH_DELETE &&
+ $this->_auth_check_list($this->getConf('user_rename_nr'));
+ }
+@@ -119,8 +123,8 @@
+ }
+
+ function _auth_can_delete($id) {
+- static $cache = null;
+- if (!$cache[$id]) {
++ static $cache = array();
++ if (!isset($cache[$id])) {
+ $cache[$id] = auth_quickaclcheck($id)>=AUTH_DELETE &&
+ $this->_auth_check_list($this->getConf('user_delete'));
+ }
+@@ -130,17 +134,17 @@
+ function _locate_filepairs(&$opts, $dir, $regex ){
+ global $conf;
+ $oldpath = $conf[$dir].'/'.str_replace(':','/',$opts['oldns']);
+- $newpath = $conf[$dir].'/'.str_replace(':','/',$opts['newns']);
+- if (!$opts['oldfiles']) $opts['oldfiles'] = array();
+- if (!$opts['newfiles']) $opts['newfiles'] = array();
++ $newpath = empty($opts['move']) ? '' : $conf[$dir].'/'.str_replace(':','/',$opts['newns']);
++ if (!isset($opts['oldfiles'])) $opts['oldfiles'] = array();
++ if (!isset($opts['newfiles'])) $opts['newfiles'] = array();
+ $dh = @opendir($oldpath);
+ if($dh) {
+ while(($file = readdir($dh)) !== false){
+- if ($file{0}=='.') continue;
++ if ($file[0]=='.') continue;
+ $oldfile = $oldpath.$file;
+ if (is_file($oldfile) && preg_match($regex,$file)){
+ $opts['oldfiles'][] = $oldfile;
+- if ($opts['move']) {
++ if (!empty($opts['move'])) {
+ $newfilebase = str_replace($opts['oldname'], $opts['newname'], $file);
+ $newfile = $newpath.$newfilebase;
+ if (@file_exists($newfile)) {
+@@ -173,7 +177,7 @@
+ }
+
+ function _FN($id) {
+- $id = str_replace(':','/',$id);
++ $id = str_replace(':','/',(string) $id);
+ $id = utf8_encodeFN($id);
+ return $id;
+ }
+@@ -291,7 +295,7 @@
+ msg( $msg, 1 );
+ }
+ // display form and table
+- $data = array( rp_newpage => $opts['newpage'], rp_summary => $opts['summary'], rp_nr => $opts['rp_nr'] );
++ $data = array( 'rp_newpage' => $opts['newpage'], 'rp_summary' => $opts['summary'], 'rp_nr' => $opts['rp_nr'] );
+ $this->_print_form($data);
+ }
+
+@@ -325,11 +329,13 @@
+ msg( $msg, 1 );
+ }
+ // display form and table
+- $data = array( dp_purge => $opts['purge'], dp_summary => $opts['summary'] );
++ $data = array( 'dp_purge' => $opts['purge'], 'dp_summary' => $opts['summary'] );
+ $this->_print_form($data);
+ }
+
+- function _print_form($data=null) {
++ function _print_form($data=array()) {
++ $data = (array) $data + array('rp_newpage' => '', 'rp_summary' => '',
++ 'rp_nr' => false, 'dp_purge' => false, 'dp_summary' => '');
+ global $ID, $lang;
+ $chk = ' checked="checked"';
+ ?>
diff --git a/dokuwiki-plugin-editx-safety.patch b/dokuwiki-plugin-editx-safety.patch
new file mode 100644
index 0000000..4c9064c
--- /dev/null
+++ b/dokuwiki-plugin-editx-safety.patch
@@ -0,0 +1,354 @@
+# Require CSRF-protected POSTs, escape form values, enforce create ACLs and handle file operation failures.
+# Match history filenames literally and rename only their page-name prefix.
+--- a/action.php
++++ b/action.php
+@@ -47,20 +47,25 @@
+ if($event->data != 'editx') return;
+ $event->preventDefault();
+
+- switch ($_REQUEST['work']) {
++ if ($this->_post('work') && !checkSecurityToken($this->_post('sectok'))) {
++ $this->_print_form();
++ return;
++ }
++
++ switch ($this->_post('work')) {
+ case 'rename':
+- $opts['oldpage'] = cleanID($_REQUEST['oldpage']);
+- $opts['newpage'] = cleanID($_REQUEST['newpage']);
+- $opts['summary'] = $_REQUEST['summary'];
+- $opts['rp_nr'] = $_REQUEST['rp_nr'];
+- $opts['confirm'] = $_REQUEST['rp_confirm'];
++ $opts['oldpage'] = cleanID($this->_post('oldpage'));
++ $opts['newpage'] = cleanID($this->_post('newpage'));
++ $opts['summary'] = $this->_post('summary');
++ $opts['rp_nr'] = $this->_post('rp_nr');
++ $opts['confirm'] = $this->_post('rp_confirm');
+ $this->_rename_page($opts);
+ break;
+ case 'delete':
+- $opts['oldpage'] = cleanID($_REQUEST['oldpage']);
+- $opts['summary'] = $_REQUEST['summary'];
+- $opts['purge'] = $_REQUEST['dp_purge'];
+- $opts['confirm'] = $_REQUEST['dp_confirm'];
++ $opts['oldpage'] = cleanID($this->_post('oldpage'));
++ $opts['summary'] = $this->_post('summary');
++ $opts['purge'] = $this->_post('dp_purge');
++ $opts['confirm'] = $this->_post('dp_confirm');
+ $this->_delete_page($opts);
+ break;
+ default:
+@@ -72,6 +77,10 @@
+ /**
+ * helper functions
+ */
++ function _post($key) {
++ return isset($_POST[$key]) && is_string($_POST[$key]) ? $_POST[$key] : '';
++ }
++
+ function _auth_check_list($list) {
+ global $conf;
+ global $USERINFO;
+@@ -145,10 +154,11 @@
+ if (is_file($oldfile) && preg_match($regex,$file)){
+ $opts['oldfiles'][] = $oldfile;
+ if (!empty($opts['move'])) {
+- $newfilebase = str_replace($opts['oldname'], $opts['newname'], $file);
++ $newfilebase = $opts['newname'].substr($file, strlen($opts['oldname']));
+ $newfile = $newpath.$newfilebase;
+ if (@file_exists($newfile)) {
+- $this->errors[] = sprintf( $this->getLang('rp_msg_file_conflict'), '<a href="'. wl($opts['newpage']) . '">'.$opts['newpage'].'</a>', $newfilebase );
++ $this->errors[] = sprintf( $this->getLang('rp_msg_file_conflict'), '<a href="'. wl($opts['newpage']) . '">'.hsc($opts['newpage']).'</a>', hsc($newfilebase) );
++ closedir($dh);
+ return false;
+ }
+ $opts['newfiles'][] = $newfile;
+@@ -158,22 +168,42 @@
+ closedir($dh);
+ return true;
+ }
+- return false;
++ if (is_dir($oldpath)) {
++ $this->errors[] = sprintf($this->getLang('msg_io_error'), hsc($oldpath));
++ return false;
++ }
++ return true;
++ }
++
++ function _rollback_moves(&$opts, $count) {
++ for ($i = $count - 1; $i >= 0; $i--) {
++ if (!io_rename($opts['newfiles'][$i], $opts['oldfiles'][$i])) {
++ $this->errors[] = sprintf($this->getLang('msg_io_error'), hsc($opts['oldfiles'][$i]));
++ }
++ }
+ }
+
+ function _apply_moves(&$opts) {
+ foreach ($opts['oldfiles'] as $i => $oldfile) {
+ $newfile = $opts['newfiles'][$i];
+ $newdir = dirname($newfile);
+- if (!io_mkdir_p($newdir)) continue;
+- io_rename($oldfile, $newfile);
+- }
++ if (!io_mkdir_p($newdir) || !io_rename($oldfile, $newfile)) {
++ $this->errors[] = sprintf($this->getLang('msg_io_error'), hsc($newfile));
++ $this->_rollback_moves($opts, $i);
++ return false;
++ }
++ }
++ return true;
+ }
+
+ function _apply_deletes(&$opts) {
+ foreach ($opts['oldfiles'] as $oldfile) {
+- unlink($oldfile);
+- }
++ if (!@unlink($oldfile)) {
++ $this->errors[] = sprintf($this->getLang('msg_io_error'), hsc($oldfile));
++ return false;
++ }
++ }
++ return true;
+ }
+
+ function _FN($id) {
+@@ -184,18 +214,27 @@
+
+ function _custom_delete_page($id, $summary) {
+ global $ID, $INFO, $conf;
+- // mark as nonexist to prevent indexerWebBug
+- if ($id==$ID) $INFO['exists'] = 0;
+ // delete page, meta and attic
+ $file = wikiFN($id);
+ $old = @filemtime($file); // from page
+- if (file_exists($file)) unlink($file);
++
+ $opts['oldname'] = $this->_FN(noNS($id));
+ $opts['oldns'] = $this->_FN(getNS($id));
+ if ($opts['oldns']) $opts['oldns'] .= '/';
+- $this->_locate_filepairs( $opts, 'metadir', '/^'.$opts['oldname'].'\.(?!mlist)\w*?$/' );
+- $this->_locate_filepairs( $opts, 'olddir', '/^'.$opts['oldname'].'\.\d{10}\.txt(\.gz|\.bz2)?$/' );
+- $this->_apply_deletes($opts);
++ $this->_locate_filepairs( $opts, 'metadir', '/^'.preg_quote($opts['oldname'], '/').'\.(?!mlist)\w*?$/' );
++ $this->_locate_filepairs( $opts, 'olddir', '/^'.preg_quote($opts['oldname'], '/').'\.\d{10}\.txt(\.gz|\.bz2)?$/' );
++ if ($this->errors) return false;
++ if (file_exists($file)) $opts['oldfiles'][] = $file;
++ // Check all parent directories before removing any history.
++ foreach ($opts['oldfiles'] as $oldfile) {
++ if (!is_writable(dirname($oldfile))) {
++ $this->errors[] = sprintf($this->getLang('msg_io_error'), hsc($oldfile));
++ return false;
++ }
++ }
++ if (!$this->_apply_deletes($opts)) return false;
++ // mark as nonexist to prevent indexerWebBug
++ if ($id==$ID) $INFO['exists'] = 0;
+ io_sweepNS($id, 'datadir');
+ io_sweepNS($id, 'metadir');
+ io_sweepNS($id, 'olddir');
+@@ -212,12 +251,14 @@
+ $cache->removeCache();
+ }
+ }
++ return true;
+ }
+
+ /**
+ * main functions
+ */
+ function _rename_page(&$opts) {
++ $this->errors = array();
+ // check confirmation
+ if (!$opts['confirm']) {
+ $this->errors[] = $this->getLang('rp_msg_unconfirmed');
+@@ -226,11 +267,11 @@
+ if (!$opts['oldpage']) {
+ $this->errors[] = $this->getLang('rp_msg_old_empty');
+ } else if (!page_exists($opts['oldpage'])) {
+- $this->errors[] = sprintf( $this->getLang('rp_msg_old_noexist'), $opts['oldpage'] );
++ $this->errors[] = sprintf( $this->getLang('rp_msg_old_noexist'), hsc($opts['oldpage']) );
+ } else if (!$this->_auth_can_rename($opts['oldpage'])) {
+- $this->errors[] = sprintf( $this->getLang('rp_msg_auth'), $opts['oldpage'] );
++ $this->errors[] = sprintf( $this->getLang('rp_msg_auth'), hsc($opts['oldpage']) );
+ } else if (checklock($opts['oldpage'])) {
+- $this->errors[] = sprintf( $this->getLang('rp_msg_locked'), $opts['oldpage'] );
++ $this->errors[] = sprintf( $this->getLang('rp_msg_locked'), hsc($opts['oldpage']) );
+ }
+ // check noredirect
+ if ($opts['rp_nr'] && !$this->_auth_can_rename_nr($opts['oldpage']))
+@@ -239,11 +280,12 @@
+ if (!$opts['newpage']) {
+ $this->errors[] = $this->getLang('rp_msg_new_empty');
+ } else if (page_exists($opts['newpage'])) {
+- $this->errors[] = sprintf( $this->getLang('rp_msg_new_exist'), '<a href="'. wl($opts['newpage']) . '">'.$opts['newpage'].'</a>' );
+- } else if (!$this->_auth_can_rename($opts['newpage'])) {
+- $this->errors[] = sprintf( $this->getLang('rp_msg_auth'), $opts['newpage'] );
++ $this->errors[] = sprintf( $this->getLang('rp_msg_new_exist'), '<a href="'. wl($opts['newpage']) . '">'.hsc($opts['newpage']).'</a>' );
++ } else if (!$this->_auth_can_rename($opts['newpage']) ||
++ auth_quickaclcheck($opts['newpage']) < AUTH_CREATE) {
++ $this->errors[] = sprintf( $this->getLang('rp_msg_auth'), hsc($opts['newpage']) );
+ } else if (checklock($opts['newpage'])) {
+- $this->errors[] = sprintf( $this->getLang('rp_msg_locked'), $opts['newpage'] );
++ $this->errors[] = sprintf( $this->getLang('rp_msg_locked'), hsc($opts['newpage']) );
+ }
+ // try to locate moves
+ if (!$this->errors) {
+@@ -254,13 +296,11 @@
+ $opts['newns'] = $this->_FN(getNS($opts['newpage']));
+ if ($opts['oldns']) $opts['oldns'] .= '/';
+ if ($opts['newns']) $opts['newns'] .= '/';
+- $this->_locate_filepairs( $opts, 'metadir', '/^'.$opts['oldname'].'\.(?!mlist|meta|indexed)\w*?$/' );
+- $this->_locate_filepairs( $opts, 'olddir', '/^'.$opts['oldname'].'\.\d{10}\.txt(\.gz|\.bz2)?$/' );
++ $this->_locate_filepairs( $opts, 'metadir', '/^'.preg_quote($opts['oldname'], '/').'\.(?!mlist|meta|indexed)\w*?$/' );
++ $this->_locate_filepairs( $opts, 'olddir', '/^'.preg_quote($opts['oldname'], '/').'\.\d{10}\.txt(\.gz|\.bz2)?$/' );
+ }
+ // if no error do rename
+- if (!$this->errors) {
+- // move meta and attic
+- $this->_apply_moves($opts);
++ if (!$this->errors && $this->_apply_moves($opts)) {
+ // save to newpage
+ $text = rawWiki($opts['oldpage']);
+ if ($opts['summary'])
+@@ -268,17 +308,22 @@
+ else
+ $summary = sprintf( $this->getLang('rp_newsummary'), $opts['oldpage'], $opts['newpage'] );
+ saveWikiText($opts['newpage'],$text,$summary);
++ if (!page_exists($opts['newpage']) || rawWiki($opts['newpage']) !== $text) {
++ $this->errors[] = sprintf($this->getLang('msg_io_error'), hsc(wikiFN($opts['newpage'])));
++ $this->_rollback_moves($opts, count($opts['oldfiles']));
++ }
+ // purge or recreate old page
+ $summary = $opts['summary'] ?
+ sprintf( $this->getLang('rp_oldsummaryx'), $opts['oldpage'], $opts['newpage'], $opts['summary'] ) :
+ sprintf( $this->getLang('rp_oldsummary'), $opts['oldpage'], $opts['newpage'] );
+- if ($opts['rp_nr']) {
+- $this->_custom_delete_page( $opts['oldpage'], $summary );
+- // write change log afterwards, or it would be deleted
+- addLogEntry( null, $opts['oldpage'], DOKU_CHANGE_TYPE_DELETE, $summary ); // also writes to global changes
+- unlink(metaFN($opts['oldpage'],'.changes')); // purge page changes
+- }
+- else {
++ if (!$this->errors && $opts['rp_nr']) {
++ if ($this->_custom_delete_page( $opts['oldpage'], $summary )) {
++ // write change log afterwards, or it would be deleted
++ addLogEntry( null, $opts['oldpage'], DOKU_CHANGE_TYPE_DELETE, $summary ); // also writes to global changes
++ unlink(metaFN($opts['oldpage'],'.changes')); // purge page changes
++ }
++ }
++ else if (!$this->errors) {
+ $text = $this->getConf('redirecttext');
+ if (!$text) $text = $this->getLang('redirecttext');
+ $text = str_replace( '@ID@', $opts['newpage'], $text );
+@@ -291,7 +336,7 @@
+ foreach ($this->errors as $error) msg( $error, -1 );
+ }
+ else {
+- $msg = sprintf( $this->getLang('rp_msg_success'), $opts['oldpage'], '<a href="'. wl($opts['newpage']) . '">'.$opts['newpage'].'</a>' );
++ $msg = sprintf( $this->getLang('rp_msg_success'), hsc($opts['oldpage']), '<a href="'. wl($opts['newpage']) . '">'.hsc($opts['newpage']).'</a>' );
+ msg( $msg, 1 );
+ }
+ // display form and table
+@@ -300,6 +345,7 @@
+ }
+
+ function _delete_page(&$opts) {
++ $this->errors = array();
+ // check confirm
+ if (!$opts['confirm']) {
+ $this->errors[] = $this->getLang('dp_msg_unconfirmed');
+@@ -308,24 +354,27 @@
+ if (!$opts['oldpage']) {
+ $this->errors[] = $this->getLang('dp_msg_old_empty');
+ } else if (!$this->_auth_can_delete($opts['oldpage'])) {
+- $this->errors[] = sprintf( $this->getLang('dp_msg_auth'), $opts['oldpage'] );
++ $this->errors[] = sprintf( $this->getLang('dp_msg_auth'), hsc($opts['oldpage']) );
++ } else if (checklock($opts['oldpage'])) {
++ $this->errors[] = sprintf( $this->getLang('rp_msg_locked'), hsc($opts['oldpage']) );
+ }
+ // if no error do delete
+ if (!$this->errors) {
+ $summary = $opts['summary'] ?
+ sprintf( $this->getLang('dp_oldsummaryx'), $opts['summary'] ) :
+ $this->getLang('dp_oldsummary');
+- $this->_custom_delete_page( $opts['oldpage'], $summary );
+- // write change log afterwards, or it would be deleted
+- addLogEntry( null, $opts['oldpage'], DOKU_CHANGE_TYPE_DELETE, $summary ); // also writes to global changes
+- if ($opts['purge']) unlink(metaFN($opts['oldpage'],'.changes')); // purge page changes
++ if ($this->_custom_delete_page( $opts['oldpage'], $summary )) {
++ // write change log afterwards, or it would be deleted
++ addLogEntry( null, $opts['oldpage'], DOKU_CHANGE_TYPE_DELETE, $summary ); // also writes to global changes
++ if ($opts['purge']) unlink(metaFN($opts['oldpage'],'.changes')); // purge page changes
++ }
+ }
+ // show messages
+ if ($this->errors) {
+ foreach ($this->errors as $error) msg( $error, -1 );
+ }
+ else {
+- $msg = sprintf( $this->getLang('dp_msg_success'), $opts['oldpage'] );
++ $msg = sprintf( $this->getLang('dp_msg_success'), hsc($opts['oldpage']) );
+ msg( $msg, 1 );
+ }
+ // display form and table
+@@ -339,25 +388,26 @@
+ global $ID, $lang;
+ $chk = ' checked="checked"';
+ ?>
+-<h1><?php echo sprintf( $this->getLang('title'), $ID); ?></h1>
++<h1><?php echo sprintf( $this->getLang('title'), hsc($ID)); ?></h1>
+ <div id="config__manager">
+ <?php
+ if ($this->_auth_can_rename($ID)) {
+ ?>
+ <form action="<?php echo wl($ID); ?>" method="post">
++ <?php formSecurityToken(); ?>
+ <fieldset>
+ <legend><?php echo $this->getLang('rp_title'); ?></legend>
+ <input type="hidden" name="do" value="editx" />
+ <input type="hidden" name="work" value="rename" />
+- <input type="hidden" name="oldpage" value="<?php echo $ID; ?>" />
++ <input type="hidden" name="oldpage" value="<?php echo hsc($ID); ?>" />
+ <table class="inline">
+ <tr>
+ <td class="label"><?php echo $this->getLang('rp_newpage'); ?></td>
+- <td class="value"><input class="edit" type="input" name="newpage" value="<?php echo $data['rp_newpage']; ?>" /></td>
++ <td class="value"><input class="edit" type="input" name="newpage" value="<?php echo hsc($data['rp_newpage']); ?>" /></td>
+ </tr>
+ <tr>
+ <td class="label"><?php echo $this->getLang('rp_summary'); ?></td>
+- <td class="value"><input class="edit" type="input" name="summary" value="<?php echo $data['rp_summary']; ?>" /></td>
++ <td class="value"><input class="edit" type="input" name="summary" value="<?php echo hsc($data['rp_summary']); ?>" /></td>
+ </tr>
+ <?php
+ if ($this->_auth_can_rename_nr($ID)) {
+@@ -385,15 +435,16 @@
+ if ($this->_auth_can_delete($ID)) {
+ ?>
+ <form action="<?php echo wl($ID); ?>" method="post">
++ <?php formSecurityToken(); ?>
+ <fieldset>
+ <legend><?php echo $this->getLang('dp_title'); ?></legend>
+ <input type="hidden" name="do" value="editx" />
+ <input type="hidden" name="work" value="delete" />
+- <input type="hidden" name="oldpage" value="<?php echo $ID; ?>" />
++ <input type="hidden" name="oldpage" value="<?php echo hsc($ID); ?>" />
+ <table class="inline">
+ <tr>
+ <td class="label"><?php echo $this->getLang('dp_summary'); ?></td>
+- <td class="value"><input class="edit" type="input" name="summary" value="<?php echo $data['dp_summary']; ?>" /></td>
++ <td class="value"><input class="edit" type="input" name="summary" value="<?php echo hsc($data['dp_summary']); ?>" /></td>
+ </tr>
+ <tr>
+ <td class="label"><?php echo $this->getLang('dp_purge'); ?></td>
+--- a/lang/en/lang.php
++++ b/lang/en/lang.php
+@@ -8,6 +8,8 @@
+ $lang['title'] = '%s Management';
+ $lang['redirecttext'] = 'This page is redirected to [[:@ID@]].';
+ $lang['pagemanagement'] = 'page management';
++
++$lang['msg_io_error'] = 'Could not update file %s. Check file permissions.';
+
+ // rename page = rp
+ $lang['rp_title'] = 'Page Rename';
diff --git a/regression.php b/regression.php
new file mode 100644
index 0000000..a4fccca
--- /dev/null
+++ b/regression.php
@@ -0,0 +1,184 @@
+<?php
+// Integration regressions for PHP 8.5 and DokuWiki 2026-07-14c.
+// Extract an official DokuWiki tarball into a scratch directory, then copy or
+// symlink the plugin extracted from the built RPM into its lib/plugins/editx.
+// Run as a regular user: php regression.php /path/to/scratch/dokuwiki
+// Configuration and all page data are created in a fresh temporary directory.
+// Failed fixtures are retained at the printed path for inspection.
+if ($argc != 2) { fwrite(STDERR, "Usage: php regression.php DOKUWIKI_ROOT\n"); exit(2); }
+define('DOKU_INC', rtrim(realpath($argv[1]), '/') . '/');
+$fixture = sys_get_temp_dir() . '/editx-test-' . bin2hex(random_bytes(8));
+mkdir($fixture); mkdir($fixture . '/conf'); mkdir($fixture . '/data');
+foreach (glob(DOKU_INC . 'conf/*') as $file) {
+ if (is_file($file) && !in_array(basename($file), ['local.php', 'local.protected.php', 'acl.auth.php', 'users.auth.php'])) {
+ copy($file, $fixture . '/conf/' . basename($file));
+ }
+}
+file_put_contents($fixture . '/conf/local.php', '<?php $conf["savedir"] = ' . var_export($fixture . '/data', true) . '; $conf["useacl"] = 1; $conf["superuser"] = ""; $conf["notify"] = ""; $conf["subscribers"] = 0; $conf["send404"] = 0;');
+file_put_contents($fixture . '/conf/users.auth.php', "<?php // test fixture\n");
+file_put_contents($fixture . '/conf/acl.auth.php', "*\t at test\t16\nrestricted:*\t at test\t2\nreadonly:*\t at test\t1\n");
+foreach (['attic', 'cache', 'index', 'log', 'locks', 'media', 'media_attic', 'media_meta', 'meta', 'pages', 'tmp'] as $dir) mkdir($fixture . '/data/' . $dir);
+define('DOKU_CONF', $fixture . '/conf/'); define('NOSESSION', 1); define('DOKU_E_LEVEL', E_ALL);
+$_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $_SERVER['HTTP_HOST'] = 'localhost'; $_SERVER['SERVER_PORT'] = '80';
+$_SERVER['SCRIPT_NAME'] = '/doku.php';
+session_id('editx-regression');
+require DOKU_INC . 'inc/init.php';
+$auth = plugin_load('auth', 'authplain'); $AUTH_ACL = auth_loadACL();
+$_SERVER['REMOTE_USER'] = 'tester'; $USERINFO = ['grps' => ['test']];
+$ID = 'start'; $REV = ''; $INFO = ['exists' => true];
+set_error_handler(function ($level, $message, $file, $line) {
+ if (error_reporting() & $level) throw new ErrorException($message, 0, $level, $file, $line);
+});
+require_once DOKU_PLUGIN . 'editx/action.php';
+function expect($condition, $message) { if (!$condition) throw new RuntimeException($message); }
+function seed($id, $text = "Original content\n") { saveWikiText($id, $text, 'fixture'); clearstatcache(); }
+function request($post = [], $get = []) {
+ global $ID, $INPUT, $MSG;
+ $_POST = $post; $_GET = $get; $_REQUEST = array_merge($get, $post);
+ $INPUT = new dokuwiki\Input\Input(); $MSG = [];
+ $ID = $post['oldpage'] ?? $get['oldpage'] ?? 'start';
+ $action = 'editx'; $event = new Doku_Event('TPL_ACT_UNKNOWN', $action);
+ $plugin = new action_plugin_editx();
+ ob_start();
+ try { $plugin->_handle_tpl_act($event, null); return ob_get_contents(); }
+ finally { ob_end_clean(); }
+}
+function renameRequest($old, $new, $extra = []) {
+ return request($extra + ['work' => 'rename', 'oldpage' => $old, 'newpage' => $new, 'rp_confirm' => '1', 'sectok' => getSecurityToken()]);
+}
+function deleteRequest($old, $extra = []) {
+ return request($extra + ['work' => 'delete', 'oldpage' => $old, 'dp_confirm' => '1', 'sectok' => getSecurityToken()]);
+}
+$tests = [];
+$tests['empty form and hooks'] = function () {
+ $plugin = new action_plugin_editx(); $handler = new Doku_Event_Handler(); $plugin->register($handler);
+ $html = request(); expect(substr_count($html, 'name="sectok"') == 2, 'both forms need CSRF tokens');
+};
+$tests['rename with redirect and history'] = function () {
+ seed('source'); io_saveFile(wikiFN('source', 1500000000), 'old revision');
+ renameRequest('source', 'dest:target');
+ expect(rawWiki('dest:target') === "Original content\n", 'target content');
+ expect(str_contains(rawWiki('source'), 'dest:target'), 'source redirect');
+ expect(io_readFile(wikiFN('dest:target', 1500000000)) === 'old revision', 'history transferred');
+};
+$tests['rename without redirect'] = function () {
+ seed('noredirect'); renameRequest('noredirect', 'renamed', ['rp_nr' => '1']);
+ expect(!page_exists('noredirect') && page_exists('renamed'), 'rename without redirect');
+};
+$tests['delete with and without deletion log'] = function () {
+ foreach (['keepdelete' => '0', 'purgedelete' => '1'] as $id => $purge) {
+ seed($id); io_saveFile(wikiFN($id, 1500000000), 'history');
+ deleteRequest($id, ['dp_purge' => $purge]);
+ expect(!page_exists($id), 'page removed'); expect(!file_exists(wikiFN($id, 1500000000)), 'attic removed');
+ expect(file_exists(metaFN($id, '.changes')) === ($purge === '0'), 'deletion log policy');
+ }
+};
+$tests['confirmation and optional inputs'] = function () {
+ seed('unconfirmed'); renameRequest('unconfirmed', 'unconfirmedtarget', ['rp_confirm' => '']);
+ deleteRequest('unconfirmed', ['dp_confirm' => '']);
+ expect(page_exists('unconfirmed') && !page_exists('unconfirmedtarget'), 'confirmation required');
+ request(['work' => ['rename'], 'summary' => ['invalid']]);
+};
+$tests['CSRF and GET do not mutate'] = function () {
+ seed('csrf');
+ foreach (['', 'invalid'] as $token) {
+ renameRequest('csrf', 'csrftarget', ['sectok' => $token]); deleteRequest('csrf', ['sectok' => $token]);
+ }
+ request([], ['work' => 'delete', 'oldpage' => 'csrf', 'dp_confirm' => '1', 'sectok' => getSecurityToken()]);
+ expect(page_exists('csrf') && !page_exists('csrftarget'), 'CSRF/GET rejected');
+};
+$tests['HTML attributes escaped'] = function () {
+ seed('xss'); $payload = '\"><script>alert(1)</script>';
+ $html = renameRequest('xss', 'target', ['summary' => $payload, 'rp_confirm' => '']);
+ expect(!str_contains($html, '<script>') && str_contains($html, hsc($payload)), 'rename summary escaped');
+ $html = deleteRequest('xss', ['summary' => $payload, 'dp_confirm' => '']);
+ expect(!str_contains($html, '<script>') && str_contains($html, hsc($payload)), 'delete summary escaped');
+};
+$tests['create and delete ACLs'] = function () {
+ seed('acltest'); renameRequest('acltest', 'restricted:target');
+ expect(!page_exists('restricted:target') && rawWiki('acltest') === "Original content\n", 'AUTH_CREATE required');
+ seed('readonly:page'); deleteRequest('readonly:page');
+ expect(page_exists('readonly:page'), 'delete ACL enforced');
+};
+$tests['locked pages'] = function () {
+ seed('locked'); io_saveFile(wikiLockFN('locked'), 'someone-else');
+ deleteRequest('locked'); renameRequest('locked', 'lockedtarget');
+ expect(page_exists('locked') && !page_exists('lockedtarget'), 'locked source protected');
+};
+$tests['history names and regex metacharacters'] = function () {
+ seed('txt'); io_saveFile(wikiFN('txt', 1500000000), 'history'); renameRequest('txt', 'newname');
+ expect(io_readFile(wikiFN('newname', 1500000000)) === 'history', 'extension not renamed');
+ seed('a.b'); seed('axb'); io_saveFile(wikiFN('a.b', 1500000000), 'own'); io_saveFile(wikiFN('axb', 1500000000), 'neighbor');
+ deleteRequest('a.b'); expect(page_exists('axb') && io_readFile(wikiFN('axb', 1500000000)) === 'neighbor', 'literal page filename matching');
+};
+$tests['conflicting history aborts rename'] = function () {
+ seed('conflict'); io_saveFile(wikiFN('conflict', 1500000000), 'source history'); io_saveFile(wikiFN('occupied', 1500000000), 'target history');
+ renameRequest('conflict', 'occupied');
+ expect(rawWiki('conflict') === "Original content\n" && !page_exists('occupied'), 'conflict preserves source');
+ expect(io_readFile(wikiFN('occupied', 1500000000)) === 'target history', 'conflict preserves target history');
+};
+$tests['move failure preserves source and history'] = function () {
+ global $conf, $MSG;
+ seed('movefail'); io_saveFile(wikiFN('movefail', 1500000000), 'history');
+ $dir = $conf['olddir'] . '/blocked'; mkdir($dir); chmod($dir, 0500);
+ try {
+ renameRequest('movefail', 'blocked:target', ['rp_nr' => '1']);
+ expect(rawWiki('movefail') === "Original content\n" && !page_exists('blocked:target'), 'failed move preserves source');
+ expect(io_readFile(wikiFN('movefail', 1500000000)) === 'history', 'failed move preserves history');
+ expect(file_exists(metaFN('movefail', '.changes')), 'earlier metadata move rolled back');
+ expect(!array_filter($MSG, fn($msg) => $msg['lvl'] === 1), 'failure must not report success');
+ } finally { chmod($dir, 0700); }
+};
+$tests['cancelled save preserves source and history'] = function () {
+ global $EVENT_HANDLER;
+ seed('cancelled'); io_saveFile(wikiFN('cancelled', 1500000000), 'history');
+ $hook = new class {
+ function cancel($event, $param) { if ($event->data['id'] === 'cancelledtarget') $event->preventDefault(); }
+ };
+ $EVENT_HANDLER->register_hook('COMMON_WIKIPAGE_SAVE', 'BEFORE', $hook, 'cancel');
+ renameRequest('cancelled', 'cancelledtarget', ['rp_nr' => '1']);
+ expect(rawWiki('cancelled') === "Original content\n" && !page_exists('cancelledtarget'), 'cancelled target save preserves source');
+ expect(io_readFile(wikiFN('cancelled', 1500000000)) === 'history', 'cancelled save restores history');
+};
+$tests['delete failure is reported'] = function () {
+ global $conf, $MSG;
+ seed('deletefail:page'); $dir = dirname(wikiFN('deletefail:page')); chmod($dir, 0500);
+ try {
+ deleteRequest('deletefail:page');
+ expect(page_exists('deletefail:page'), 'unwritable page retained');
+ expect(file_exists(metaFN('deletefail:page', '.changes')), 'history retained on delete failure');
+ expect(!array_filter($MSG, fn($msg) => $msg['lvl'] === 1), 'failed delete must not report success');
+ } finally { chmod($dir, 0700); }
+};
+$tests['real revision remains readable after rename'] = function () {
+ seed('revision', "First version\n"); $rev = filemtime(wikiFN('revision'));
+ while (time() <= $rev) usleep(100000);
+ seed('revision', "Second version\n");
+ renameRequest('revision', 'revisiontarget', ['rp_nr' => '1']);
+ expect(rawWiki('revisiontarget', $rev) === "First version\n", 'DokuWiki reads renamed revision');
+ expect(rawWiki('revisiontarget') === "Second version\n", 'latest revision retained');
+};
+$tests['rename dotted page preserves neighboring history'] = function () {
+ seed('c.d'); seed('cxd'); io_saveFile(wikiFN('cxd', 1500000000), 'neighbor');
+ renameRequest('c.d', 'dotrenamed', ['rp_nr' => '1']);
+ expect(io_readFile(wikiFN('cxd', 1500000000)) === 'neighbor', 'neighbor history must not be moved');
+};
+$tests['missing and existing page validation'] = function () {
+ renameRequest('missing', 'missingtarget'); expect(!page_exists('missingtarget'), 'missing source rejected');
+ seed('existingsource'); seed('existingtarget', 'target content');
+ renameRequest('existingsource', 'existingtarget');
+ expect(rawWiki('existingtarget') === 'target content', 'existing destination preserved');
+};
+$tests['anonymous membership check'] = function () {
+ global $USERINFO;
+ $saved = $USERINFO; unset($_SERVER['REMOTE_USER']); $USERINFO = null;
+ try { expect(!(new action_plugin_editx())->_auth_check_list('@admin'), 'anonymous user not privileged'); }
+ finally { $_SERVER['REMOTE_USER'] = 'tester'; $USERINFO = $saved; }
+};
+$failures = 0;
+foreach ($tests as $name => $test) {
+ try { $test(); fwrite(STDOUT, "PASS $name\n"); }
+ catch (Throwable $error) { $failures++; fwrite(STDERR, "FAIL $name: $error\n"); }
+}
+fwrite(STDOUT, count($tests) . " tests, $failures failures; fixture: $fixture\n");
+exit($failures ? 1 : 0);
================================================================
---- gitweb:
http://git.pld-linux.org/gitweb.cgi/packages/dokuwiki-plugin-editx.git/commitdiff/f4f3a28958d14e10f0e7964790e6e6bc3c6b023e
More information about the pld-cvs-commit
mailing list