はじまりの大地
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
/**
|
||||
* Move Plugin File Mover
|
||||
*
|
||||
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
|
||||
* @author Michael Hamann <michael@content-space.de>
|
||||
* @author Andreas Gohr <gohr@cosmocode.de>
|
||||
*/
|
||||
// must be run within Dokuwiki
|
||||
if(!defined('DOKU_INC')) die();
|
||||
|
||||
/**
|
||||
* Class helper_plugin_move_file
|
||||
*
|
||||
* This helps with moving files from one folder to another. It simply matches files and moves them
|
||||
* arround. No fancy rewriting happens here.
|
||||
*/
|
||||
class helper_plugin_move_file extends DokuWiki_Plugin {
|
||||
|
||||
/**
|
||||
* Move the meta files of a page
|
||||
*
|
||||
* @param string $src_ns The original namespace
|
||||
* @param string $src_name The original basename of the moved doc (empty for namespace moves)
|
||||
* @param string $dst_ns The namespace after the move
|
||||
* @param string $dst_name The basename after the move (empty for namespace moves)
|
||||
* @return bool If the meta files were moved successfully
|
||||
*/
|
||||
public function movePageMeta($src_ns, $src_name, $dst_ns, $dst_name) {
|
||||
global $conf;
|
||||
|
||||
$regex = '\.[^.]+';
|
||||
return $this->execute($conf['metadir'], $src_ns, $src_name, $dst_ns, $dst_name, $regex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the old revisions of a page
|
||||
*
|
||||
* @param string $src_ns The original namespace
|
||||
* @param string $src_name The original basename of the moved doc (empty for namespace moves)
|
||||
* @param string $dst_ns The namespace after the move
|
||||
* @param string $dst_name The basename after the move (empty for namespace moves)
|
||||
* @return bool If the attic files were moved successfully
|
||||
*/
|
||||
public function movePageAttic($src_ns, $src_name, $dst_ns, $dst_name) {
|
||||
global $conf;
|
||||
|
||||
$regex = '\.\d+\.txt(?:\.gz|\.bz2)?';
|
||||
return $this->execute($conf['olddir'], $src_ns, $src_name, $dst_ns, $dst_name, $regex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the meta files of the page that is specified in the options.
|
||||
*
|
||||
* @param string $src_ns The original namespace
|
||||
* @param string $src_name The original basename of the moved doc (empty for namespace moves)
|
||||
* @param string $dst_ns The namespace after the move
|
||||
* @param string $dst_name The basename after the move (empty for namespace moves)
|
||||
* @return bool If the meta files were moved successfully
|
||||
*/
|
||||
public function moveMediaMeta($src_ns, $src_name, $dst_ns, $dst_name) {
|
||||
global $conf;
|
||||
|
||||
$regex = '\.[^.]+';
|
||||
return $this->execute($conf['mediametadir'], $src_ns, $src_name, $dst_ns, $dst_name, $regex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the old revisions of the media file that is specified in the options
|
||||
*
|
||||
* @param string $src_ns The original namespace
|
||||
* @param string $src_name The original basename of the moved doc (empty for namespace moves)
|
||||
* @param string $dst_ns The namespace after the move
|
||||
* @param string $dst_name The basename after the move (empty for namespace moves)
|
||||
* @return bool If the attic files were moved successfully
|
||||
*/
|
||||
public function moveMediaAttic($src_ns, $src_name, $dst_ns, $dst_name) {
|
||||
global $conf;
|
||||
|
||||
$ext = mimetype($src_name);
|
||||
if($ext[0] !== false) {
|
||||
$name = substr($src_name, 0, -1 * strlen($ext[0]) - 1);
|
||||
} else {
|
||||
$name = $src_name;
|
||||
}
|
||||
$newext = mimetype($dst_name);
|
||||
if($newext[0] !== false) {
|
||||
$newname = substr($dst_name, 0, -1 * strlen($newext[0]) - 1);
|
||||
} else {
|
||||
$newname = $dst_name;
|
||||
}
|
||||
$regex = '\.\d+\.' . preg_quote((string) $ext[0], '/');
|
||||
|
||||
return $this->execute($conf['mediaolddir'], $src_ns, $name, $dst_ns, $newname, $regex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the subscription file for a namespace
|
||||
*
|
||||
* @param string $src_ns
|
||||
* @param string $dst_ns
|
||||
* @return bool
|
||||
*/
|
||||
public function moveNamespaceSubscription($src_ns, $dst_ns){
|
||||
global $conf;
|
||||
|
||||
$regex = '\.mlist';
|
||||
return $this->execute($conf['metadir'], $src_ns, '', $dst_ns, '', $regex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the move op
|
||||
*
|
||||
* @param string $dir The root path of the files (e.g. $conf['metadir'] or $conf['olddir']
|
||||
* @param string $src_ns The original namespace
|
||||
* @param string $src_name The original basename of the moved doc (empty for namespace moves)
|
||||
* @param string $dst_ns The namespace after the move
|
||||
* @param string $dst_name The basename after the move (empty for namespace moves)
|
||||
* @param string $extregex Regular expression for matching the extension of the file that shall be moved
|
||||
* @return bool If the files were moved successfully
|
||||
*/
|
||||
protected function execute($dir, $src_ns, $src_name, $dst_ns, $dst_name, $extregex) {
|
||||
$old_path = $dir;
|
||||
if($src_ns != '') $old_path .= '/' . utf8_encodeFN(str_replace(':', '/', $src_ns));
|
||||
$new_path = $dir;
|
||||
if($dst_ns != '') $new_path .= '/' . utf8_encodeFN(str_replace(':', '/', $dst_ns));
|
||||
$regex = '/^' . preg_quote(utf8_encodeFN($src_name)) . '(' . $extregex . ')$/u';
|
||||
|
||||
if(!is_dir($old_path)) return true; // no media files found
|
||||
|
||||
$dh = @opendir($old_path);
|
||||
if($dh) {
|
||||
while(($file = readdir($dh)) !== false) {
|
||||
if($file == '.' || $file == '..') continue;
|
||||
$match = array();
|
||||
if(is_file($old_path . '/' . $file) && preg_match($regex, $file, $match)) {
|
||||
if(!is_dir($new_path)) {
|
||||
if(!io_mkdir_p($new_path)) {
|
||||
msg('Creating directory ' . hsc($new_path) . ' failed.', -1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(!io_rename($old_path . '/' . $file, $new_path . '/' . utf8_encodeFN($dst_name . $match[1]))) {
|
||||
msg('Moving ' . hsc($old_path . '/' . $file) . ' to ' . hsc($new_path . '/' . utf8_encodeFN($dst_name . $match[1])) . ' failed.', -1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($dh);
|
||||
} else {
|
||||
msg('Directory ' . hsc($old_path) . ' couldn\'t be opened.', -1);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
<?php
|
||||
/**
|
||||
* Move Plugin Rewriting Handler
|
||||
*
|
||||
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
|
||||
* @author Michael Hamann <michael@content-space.de>
|
||||
*/
|
||||
|
||||
// must be run within Dokuwiki
|
||||
if(!defined('DOKU_INC')) die();
|
||||
|
||||
/**
|
||||
* Handler class for move. It does the actual rewriting of the content.
|
||||
*
|
||||
* Note: This is not actually a valid DokuWiki Helper plugin and can not be loaded via plugin_load()
|
||||
*/
|
||||
class helper_plugin_move_handler extends DokuWiki_Plugin {
|
||||
public $calls = '';
|
||||
|
||||
protected $id;
|
||||
protected $ns;
|
||||
protected $origID;
|
||||
protected $origNS;
|
||||
protected $page_moves;
|
||||
protected $media_moves;
|
||||
protected $handlers;
|
||||
|
||||
/**
|
||||
* Do not allow re-using instances.
|
||||
*
|
||||
* @return bool false - the handler must not be re-used.
|
||||
*/
|
||||
public function isSingleton() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the move handler.
|
||||
*
|
||||
* @param string $id The id of the text that is passed to the handler
|
||||
* @param string $original The name of the original ID of this page. Same as $id if this page wasn't moved
|
||||
* @param array $page_moves Moves that shall be considered in the form [[$old,$new],...] ($old can be $original)
|
||||
* @param array $media_moves Moves of media files that shall be considered in the form $old => $new
|
||||
* @param array $handlers Handlers for plugin content in the form $plugin_name => $callback
|
||||
*/
|
||||
public function init($id, $original, $page_moves, $media_moves, $handlers) {
|
||||
$this->id = $id;
|
||||
$this->ns = getNS($id);
|
||||
$this->origID = $original;
|
||||
$this->origNS = getNS($original);
|
||||
$this->page_moves = $page_moves;
|
||||
$this->media_moves = $media_moves;
|
||||
$this->handlers = $handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through the list of moves and find the new value for the given old ID
|
||||
*
|
||||
* @param string $old the old, full qualified ID
|
||||
* @param string $type 'media' or 'page'
|
||||
* @throws Exception on bad argument
|
||||
* @return string the new full qualified ID
|
||||
*/
|
||||
public function resolveMoves($old, $type) {
|
||||
global $conf;
|
||||
|
||||
if($type != 'media' && $type != 'page') throw new Exception('Not a valid type');
|
||||
|
||||
$old = resolve_id($this->origNS, $old, false);
|
||||
|
||||
if($type == 'page') {
|
||||
// FIXME this simply assumes that the link pointed to :$conf['start'], but it could also point to another page
|
||||
// resolve_pageid does a lot more here, but we can't really assume this as the original pages might have been
|
||||
// deleted already
|
||||
if(substr($old, -1) === ':' || $old === '') $old .= $conf['start'];
|
||||
|
||||
$moves = $this->page_moves;
|
||||
} else {
|
||||
$moves = $this->media_moves;
|
||||
}
|
||||
|
||||
$old = cleanID($old);
|
||||
|
||||
foreach($moves as $move) {
|
||||
if($move[0] == $old) {
|
||||
$old = $move[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $old; // this is now new
|
||||
}
|
||||
|
||||
/**
|
||||
* if the old link ended with a colon and the new one is a start page, adjust
|
||||
*
|
||||
* @param $relold string the old, possibly relative ID
|
||||
* @param $new string the new, full qualified ID
|
||||
* @param $type 'media' or 'page'
|
||||
* @return string
|
||||
*/
|
||||
protected function _nsStartCheck($relold, $new, $type) {
|
||||
global $conf;
|
||||
if($type == 'page' && substr($relold, -1) == ':') {
|
||||
$len = strlen($conf['start']);
|
||||
if($new == $conf['start']) {
|
||||
$new = '.:';
|
||||
} else if(substr($new, -1 * ($len + 1)) == ':' . $conf['start']) {
|
||||
$new = substr($new, 0, -1 * $len);
|
||||
}
|
||||
}
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new ID relative to the current page's location
|
||||
*
|
||||
* Uses a relative link only if the original was relative, too. This function is for
|
||||
* pages and media files.
|
||||
*
|
||||
* @param string $relold the old, possibly relative ID
|
||||
* @param string $new the new, full qualified ID
|
||||
* @param string $type 'media' or 'page'
|
||||
* @throws Exception on bad argument
|
||||
* @return string
|
||||
*/
|
||||
public function relativeLink($relold, $new, $type) {
|
||||
global $conf;
|
||||
if($type != 'media' && $type != 'page') throw new Exception('Not a valid type');
|
||||
|
||||
// first check if the old link still resolves
|
||||
$exists = false;
|
||||
$old = $relold;
|
||||
if($type == 'page') {
|
||||
resolve_pageid($this->ns, $old, $exists);
|
||||
// Work around bug in DokuWiki 2020-07-29 where resolve_pageid doesn't append the start page to a link to
|
||||
// the root.
|
||||
if ($old === '') {
|
||||
$old = $conf['start'];
|
||||
}
|
||||
} else {
|
||||
resolve_mediaid($this->ns, $old, $exists);
|
||||
}
|
||||
if($old == $new) {
|
||||
return $relold; // old link still resolves, keep as is
|
||||
}
|
||||
|
||||
if($conf['useslash']) $relold = str_replace('/', ':', $relold);
|
||||
|
||||
// check if the link was relative
|
||||
if(strpos($relold, ':') === false ||$relold[0] == '.') {
|
||||
$wasrel = true;
|
||||
} else {
|
||||
$wasrel = false;
|
||||
}
|
||||
|
||||
// if it wasn't relative then, leave it absolute now, too
|
||||
if(!$wasrel) {
|
||||
if($this->ns && !getNS($new)) $new = ':' . $new;
|
||||
$new = $this->_nsStartCheck($relold, $new, $type);
|
||||
return $new;
|
||||
}
|
||||
|
||||
// split the paths and see how much common parts there are
|
||||
$selfpath = explode(':', $this->ns);
|
||||
$goalpath = explode(':', getNS($new));
|
||||
$min = min(count($selfpath), count($goalpath));
|
||||
for($common = 0; $common < $min; $common++) {
|
||||
if($selfpath[$common] != $goalpath[$common]) break;
|
||||
}
|
||||
|
||||
// we now have the non-common part and a number of uppers
|
||||
$ups = max(count($selfpath) - $common, 0);
|
||||
$remainder = array_slice($goalpath, $common);
|
||||
$upper = $ups ? array_fill(0, $ups, '..:') : array();
|
||||
|
||||
// build the new relative path
|
||||
$newrel = join(':', $upper);
|
||||
if($remainder) $newrel .= join(':', $remainder) . ':';
|
||||
$newrel .= noNS($new);
|
||||
$newrel = str_replace('::', ':', trim($newrel, ':'));
|
||||
if($newrel[0] != '.' && $this->ns && getNS($newrel)) $newrel = '.' . $newrel;
|
||||
|
||||
// if the old link ended with a colon and the new one is a start page, adjust
|
||||
$newrel = $this->_nsStartCheck($relold,$newrel,$type);
|
||||
|
||||
// don't use relative paths if it is ridicoulus:
|
||||
if(strlen($newrel) > strlen($new)) {
|
||||
$newrel = $new;
|
||||
if($this->ns && !getNS($new)) $newrel = ':' . $newrel;
|
||||
$newrel = $this->_nsStartCheck($relold,$newrel,$type);
|
||||
}
|
||||
|
||||
return $newrel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle camelcase links
|
||||
*
|
||||
* @param string $match The text match
|
||||
* @param string $state The starte of the parser
|
||||
* @param int $pos The position in the input
|
||||
* @return bool If parsing should be continued
|
||||
*/
|
||||
public function camelcaselink($match, $state, $pos) {
|
||||
$oldID = cleanID($this->origNS . ':' . $match);
|
||||
$newID = $this->resolveMoves($oldID, 'page');
|
||||
$newNS = getNS($newID);
|
||||
|
||||
if($oldID == $newID || $this->origNS == $newNS) {
|
||||
// link is still valid as is
|
||||
$this->calls .= $match;
|
||||
} else {
|
||||
if(noNS($oldID) == noNS($newID)) {
|
||||
// only namespace changed, keep CamelCase in link
|
||||
$this->calls .= "[[$newNS:$match]]";
|
||||
} else {
|
||||
// all new, keep CamelCase in title
|
||||
$this->calls .= "[[$newID|$match]]";
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rewriting of internal links
|
||||
*
|
||||
* @param string $match The text match
|
||||
* @param string $state The starte of the parser
|
||||
* @param int $pos The position in the input
|
||||
* @return bool If parsing should be continued
|
||||
*/
|
||||
public function internallink($match, $state, $pos) {
|
||||
// Strip the opening and closing markup
|
||||
$link = preg_replace(array('/^\[\[/', '/\]\]$/u'), '', $match);
|
||||
|
||||
// Split title from URL
|
||||
$link = explode('|', $link, 2);
|
||||
if(!isset($link[1])) {
|
||||
$link[1] = null;
|
||||
} else if(preg_match('/^\{\{[^\}]+\}\}$/', $link[1])) {
|
||||
// If the title is an image, rewrite it
|
||||
$old_title = $link[1];
|
||||
$link[1] = $this->rewrite_media($link[1]);
|
||||
// do a simple replace of the first match so really only the id is changed and not e.g. the alignment
|
||||
$oldpos = strpos($match, $old_title);
|
||||
$oldlen = strlen($old_title);
|
||||
$match = substr_replace($match, $link[1], $oldpos, $oldlen);
|
||||
}
|
||||
$link[0] = trim($link[0]);
|
||||
|
||||
//decide which kind of link it is
|
||||
|
||||
if(preg_match('/^[a-zA-Z0-9\.]+>{1}.*$/u', $link[0])) {
|
||||
// Interwiki
|
||||
$this->calls .= $match;
|
||||
} elseif(preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u', $link[0])) {
|
||||
// Windows Share
|
||||
$this->calls .= $match;
|
||||
} elseif(preg_match('#^([a-z0-9\-\.+]+?)://#i', $link[0])) {
|
||||
// external link (accepts all protocols)
|
||||
$this->calls .= $match;
|
||||
} elseif(preg_match('<' . PREG_PATTERN_VALID_EMAIL . '>', $link[0])) {
|
||||
// E-Mail (pattern above is defined in inc/mail.php)
|
||||
$this->calls .= $match;
|
||||
} elseif(preg_match('!^#.+!', $link[0])) {
|
||||
// local hash link
|
||||
$this->calls .= $match;
|
||||
} else {
|
||||
$id = $link[0];
|
||||
|
||||
$hash = '';
|
||||
$parts = explode('#', $id, 2);
|
||||
if(count($parts) === 2) {
|
||||
$id = $parts[0];
|
||||
$hash = $parts[1];
|
||||
}
|
||||
|
||||
$params = '';
|
||||
$parts = explode('?', $id, 2);
|
||||
if(count($parts) === 2) {
|
||||
$id = $parts[0];
|
||||
$params = $parts[1];
|
||||
}
|
||||
|
||||
$new_id = $this->resolveMoves($id, 'page');
|
||||
$new_id = $this->relativeLink($id, $new_id, 'page');
|
||||
|
||||
if($id == $new_id) {
|
||||
$this->calls .= $match;
|
||||
} else {
|
||||
if($params !== '') {
|
||||
$new_id .= '?' . $params;
|
||||
}
|
||||
|
||||
if($hash !== '') {
|
||||
$new_id .= '#' . $hash;
|
||||
}
|
||||
|
||||
if($link[1] != null) {
|
||||
$new_id .= '|' . $link[1];
|
||||
}
|
||||
|
||||
$this->calls .= '[[' . $new_id . ']]';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rewriting of media links
|
||||
*
|
||||
* @param string $match The text match
|
||||
* @param string $state The starte of the parser
|
||||
* @param int $pos The position in the input
|
||||
* @return bool If parsing should be continued
|
||||
*/
|
||||
public function media($match, $state, $pos) {
|
||||
$this->calls .= $this->rewrite_media($match);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite a media syntax
|
||||
*
|
||||
* @param string $match The text match of the media syntax
|
||||
* @return string The rewritten syntax
|
||||
*/
|
||||
protected function rewrite_media($match) {
|
||||
$p = Doku_Handler_Parse_Media($match);
|
||||
if($p['type'] == 'internalmedia') { // else: external media
|
||||
|
||||
$new_src = $this->resolveMoves($p['src'], 'media');
|
||||
$new_src = $this->relativeLink($p['src'], $new_src, 'media');
|
||||
|
||||
if($new_src !== $p['src']) {
|
||||
// do a simple replace of the first match so really only the id is changed and not e.g. the alignment
|
||||
$srcpos = strpos($match, $p['src']);
|
||||
$srclen = strlen($p['src']);
|
||||
return substr_replace($match, $new_src, $srcpos, $srclen);
|
||||
}
|
||||
}
|
||||
return $match;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rewriting of plugin syntax, calls the registered handlers
|
||||
*
|
||||
* @param string $match The text match
|
||||
* @param string $state The starte of the parser
|
||||
* @param int $pos The position in the input
|
||||
* @param string $pluginname The name of the plugin
|
||||
* @return bool If parsing should be continued
|
||||
*/
|
||||
public function plugin($match, $state, $pos, $pluginname) {
|
||||
if(isset($this->handlers[$pluginname])) {
|
||||
$this->calls .= call_user_func($this->handlers[$pluginname], $match, $state, $pos, $pluginname, $this);
|
||||
} else {
|
||||
$this->calls .= $match;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Catchall handler for the remaining syntax
|
||||
*
|
||||
* @param string $name Function name that was called
|
||||
* @param array $params Original parameters
|
||||
* @return bool If parsing should be continue
|
||||
*/
|
||||
public function __call($name, $params) {
|
||||
if(count($params) == 3) {
|
||||
$this->calls .= $params[0];
|
||||
return true;
|
||||
} else {
|
||||
trigger_error('Error, handler function ' . hsc($name) . ' with ' . count($params) . ' parameters called which isn\'t implemented', E_USER_ERROR);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function _finalize() {
|
||||
// remove padding that is added by the parser in parse()
|
||||
$this->calls = substr($this->calls, 1, -1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
<?php
|
||||
/**
|
||||
* Move Plugin Operation Execution
|
||||
*
|
||||
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
|
||||
* @author Michael Hamann <michael@content-space.de>
|
||||
* @author Gary Owen <gary@isection.co.uk>
|
||||
* @author Andreas Gohr <gohr@cosmocode.de>
|
||||
*/
|
||||
// must be run within Dokuwiki
|
||||
if(!defined('DOKU_INC')) die();
|
||||
|
||||
class helper_plugin_move_op extends DokuWiki_Plugin {
|
||||
|
||||
/**
|
||||
* @var string symbol to make move operations easily recognizable in change log
|
||||
*/
|
||||
public $symbol = '↷';
|
||||
|
||||
/**
|
||||
* @var array stores the affected pages of the last operation
|
||||
*/
|
||||
protected $affectedPages = array();
|
||||
|
||||
/**
|
||||
* Check if the given page can be moved to the given destination
|
||||
*
|
||||
* @param $src
|
||||
* @param $dst
|
||||
* @return bool
|
||||
*/
|
||||
public function checkPage($src, $dst) {
|
||||
// Check we have rights to move this document
|
||||
if(!page_exists($src)) {
|
||||
msg(sprintf($this->getLang('notexist'), $src), -1);
|
||||
return false;
|
||||
}
|
||||
if(auth_quickaclcheck($src) < AUTH_EDIT) {
|
||||
msg(sprintf($this->getLang('norights'), $src), -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check file is not locked
|
||||
// checklock checks if the page lock hasn't expired and the page hasn't been locked by another user
|
||||
// the file exists check checks if the page is reported unlocked if a lock exists which means that
|
||||
// the page is locked by the current user
|
||||
if(checklock($src) !== false || @file_exists(wikiLockFN($src))) {
|
||||
msg(sprintf($this->getLang('filelocked'), $src), -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Has the document name and/or namespace changed?
|
||||
if($src == $dst) {
|
||||
msg(sprintf($this->getLang('notchanged'), $src), -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the page does not already exist
|
||||
if(page_exists($dst)) {
|
||||
msg(sprintf($this->getLang('exists'), $src, $dst), -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the current user can create the new page
|
||||
if(auth_quickaclcheck($dst) < AUTH_CREATE) {
|
||||
msg(sprintf($this->getLang('notargetperms'), $dst), -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given media file can be moved to the given destination
|
||||
*
|
||||
* @param $src
|
||||
* @param $dst
|
||||
* @return bool
|
||||
*/
|
||||
public function checkMedia($src, $dst) {
|
||||
// Check we have rights to move this document
|
||||
if(!file_exists(mediaFN($src))) {
|
||||
msg(sprintf($this->getLang('medianotexist'), $src), -1);
|
||||
return false;
|
||||
}
|
||||
if(auth_quickaclcheck($src) < AUTH_DELETE) {
|
||||
msg(sprintf($this->getLang('nomediarights'), $src), -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Has the document name and/or namespace changed?
|
||||
if($src == $dst) {
|
||||
msg(sprintf($this->getLang('medianotchanged'), $src), -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the page does not already exist
|
||||
if(@file_exists(mediaFN($dst))) {
|
||||
msg(sprintf($this->getLang('mediaexists'), $src, $dst), -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the current user can create the new page
|
||||
if(auth_quickaclcheck($dst) < AUTH_UPLOAD) {
|
||||
msg(sprintf($this->getLang('nomediatargetperms'), $dst), -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if the file extension is unchanged
|
||||
if (pathinfo(mediaFN($src), PATHINFO_EXTENSION) !== pathinfo(mediaFN($dst), PATHINFO_EXTENSION)) {
|
||||
msg($this->getLang('extensionchange'), -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a page move/rename
|
||||
*
|
||||
* @param string $src original ID
|
||||
* @param string $dst new ID
|
||||
* @return bool
|
||||
*/
|
||||
public function movePage($src, $dst) {
|
||||
if(!$this->checkPage($src, $dst)) return false;
|
||||
|
||||
// lock rewrites
|
||||
helper_plugin_move_rewrite::addLock();
|
||||
|
||||
/** @var helper_plugin_move_rewrite $Rewriter */
|
||||
$Rewriter = plugin_load('helper', 'move_rewrite');
|
||||
|
||||
// remember what this page was called before the move in meta data
|
||||
$Rewriter->setSelfMoveMeta($src);
|
||||
|
||||
// ft_backlinks() is not used here, as it does a hidden page and acl check but we really need all pages
|
||||
$affected_pages = idx_get_indexer()->lookupKey('relation_references', $src);
|
||||
$affected_pages[] = $dst; // the current page is always affected, because all relative links may have changed
|
||||
$affected_pages = array_unique($affected_pages);
|
||||
|
||||
$src_ns = getNS($src);
|
||||
$src_name = noNS($src);
|
||||
$dst_ns = getNS($dst);
|
||||
$dst_name = noNS($dst);
|
||||
|
||||
// pass this info on to other plugins
|
||||
$eventdata = array(
|
||||
// this is for compatibility to old plugin
|
||||
'opts' => array(
|
||||
'ns' => $src_ns,
|
||||
'name' => $src_name,
|
||||
'newns' => $dst_ns,
|
||||
'newname' => $dst_name,
|
||||
),
|
||||
'affected_pages' => &$affected_pages,
|
||||
'src_id' => $src,
|
||||
'dst_id' => $dst,
|
||||
);
|
||||
|
||||
// give plugins the option to add their own meta files to the list of files that need to be moved
|
||||
// to the oldfiles/newfiles array or to adjust their own metadata, database, ...
|
||||
// and to add other pages to the affected pages
|
||||
$event = new Doku_Event('PLUGIN_MOVE_PAGE_RENAME', $eventdata);
|
||||
if($event->advise_before()) {
|
||||
lock($src);
|
||||
|
||||
/** @var helper_plugin_move_file $FileMover */
|
||||
$FileMover = plugin_load('helper', 'move_file');
|
||||
|
||||
// Move the Subscriptions & Indexes (new feature since Spring 2013 release)
|
||||
$Indexer = idx_get_indexer();
|
||||
if(($idx_msg = $Indexer->renamePage($src, $dst)) !== true
|
||||
|| ($idx_msg = $Indexer->renameMetaValue('relation_references', $src, $dst)) !== true
|
||||
) {
|
||||
msg(sprintf($this->getLang('indexerror'), $idx_msg), -1);
|
||||
return false;
|
||||
}
|
||||
if(!$FileMover->movePageMeta($src_ns, $src_name, $dst_ns, $dst_name)) {
|
||||
msg(sprintf($this->getLang('metamoveerror'), $src), -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// prepare the summary for the changelog entry
|
||||
if($src_ns == $dst_ns) {
|
||||
$lang_key = 'renamed';
|
||||
} elseif($src_name == $dst_name) {
|
||||
$lang_key = 'moved';
|
||||
} else {
|
||||
$lang_key = 'move_rename';
|
||||
}
|
||||
$summary = $this->symbol . ' ' . sprintf($this->getLang($lang_key), $src, $dst);
|
||||
|
||||
// Wait a second when the page has just been rewritten
|
||||
$oldRev = filemtime(wikiFN($src));
|
||||
if($oldRev == time()) sleep(1);
|
||||
|
||||
// Save the updated document in its new location
|
||||
$text = rawWiki($src);
|
||||
saveWikiText($dst, $text, $summary);
|
||||
|
||||
// Delete the orginal file
|
||||
if(@file_exists(wikiFN($dst))) {
|
||||
saveWikiText($src, '', $summary);
|
||||
}
|
||||
|
||||
// Move the old revisions
|
||||
if(!$FileMover->movePageAttic($src_ns, $src_name, $dst_ns, $dst_name)) {
|
||||
// it's too late to stop the move, so just display a message.
|
||||
msg(sprintf($this->getLang('atticmoveerror'), $src ), -1);
|
||||
}
|
||||
|
||||
// Add meta data to all affected pages, so links get updated later
|
||||
foreach($affected_pages as $id) {
|
||||
$Rewriter->setMoveMeta($id, $src, $dst, 'pages');
|
||||
}
|
||||
|
||||
unlock($src);
|
||||
}
|
||||
$event->advise_after();
|
||||
|
||||
// store this for later use
|
||||
$this->affectedPages = $affected_pages;
|
||||
|
||||
// unlock rewrites
|
||||
helper_plugin_move_rewrite::removeLock();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a media file move/rename
|
||||
*
|
||||
* @param string $src original ID
|
||||
* @param string $dst new ID
|
||||
* @return bool true if the move was successfully executed
|
||||
*/
|
||||
public function moveMedia($src, $dst) {
|
||||
if(!$this->checkMedia($src, $dst)) return false;
|
||||
|
||||
// get all pages using this media
|
||||
$affected_pages = idx_get_indexer()->lookupKey('relation_media', $src);
|
||||
|
||||
$src_ns = getNS($src);
|
||||
$src_name = noNS($src);
|
||||
$dst_ns = getNS($dst);
|
||||
$dst_name = noNS($dst);
|
||||
|
||||
// pass this info on to other plugins
|
||||
$eventdata = array(
|
||||
// this is for compatibility to old plugin
|
||||
'opts' => array(
|
||||
'ns' => $src_ns,
|
||||
'name' => $src_name,
|
||||
'newns' => $dst_ns,
|
||||
'newname' => $dst_name,
|
||||
),
|
||||
'affected_pages' => &$affected_pages,
|
||||
'src_id' => $src,
|
||||
'dst_id' => $dst,
|
||||
);
|
||||
|
||||
// give plugins the option to add their own meta files to the list of files that need to be moved
|
||||
// to the oldfiles/newfiles array or to adjust their own metadata, database, ...
|
||||
// and to add other pages to the affected pages
|
||||
$event = new Doku_Event('PLUGIN_MOVE_MEDIA_RENAME', $eventdata);
|
||||
if($event->advise_before()) {
|
||||
/** @var helper_plugin_move_file $FileMover */
|
||||
$FileMover = plugin_load('helper', 'move_file');
|
||||
/** @var helper_plugin_move_rewrite $Rewriter */
|
||||
$Rewriter = plugin_load('helper', 'move_rewrite');
|
||||
|
||||
// Move the Subscriptions & Indexes (new feature since Spring 2013 release)
|
||||
$Indexer = idx_get_indexer();
|
||||
if(($idx_msg = $Indexer->renameMetaValue('relation_media', $src, $dst)) !== true) {
|
||||
msg(sprintf($this->getLang('indexerror'), $idx_msg), -1);
|
||||
return false;
|
||||
}
|
||||
if(!$FileMover->moveMediaMeta($src_ns, $src_name, $dst_ns, $dst_name)) {
|
||||
msg(sprintf($this->getLang('mediametamoveerror'), $src), -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// prepare directory
|
||||
io_createNamespace($dst, 'media');
|
||||
|
||||
// move it FIXME this does not create a changelog entry!
|
||||
if(!io_rename(mediaFN($src), mediaFN($dst))) {
|
||||
msg(sprintf($this->getLang('mediamoveerror'), $src), -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// clean up old ns
|
||||
io_sweepNS($src, 'mediadir');
|
||||
|
||||
// Move the old revisions
|
||||
if(!$FileMover->moveMediaAttic($src_ns, $src_name, $dst_ns, $dst_name)) {
|
||||
// it's too late to stop the move, so just display a message.
|
||||
msg(sprintf($this->getLang('mediaatticmoveerror'), $src), -1);
|
||||
}
|
||||
|
||||
// Add meta data to all affected pages, so links get updated later
|
||||
foreach($affected_pages as $id) {
|
||||
$Rewriter->setMoveMeta($id, $src, $dst, 'media');
|
||||
}
|
||||
}
|
||||
$event->advise_after();
|
||||
|
||||
// store this for later use
|
||||
$this->affectedPages = $affected_pages;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of pages that where affected by the last successful move operation
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAffectedPages() {
|
||||
return $this->affectedPages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,953 @@
|
||||
<?php
|
||||
/**
|
||||
* Move Plugin Operation Planner
|
||||
*
|
||||
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
|
||||
* @author Michael Hamann <michael@content-space.de>
|
||||
* @author Andreas Gohr <gohr@cosmocode.de>
|
||||
*/
|
||||
// must be run within Dokuwiki
|
||||
if(!defined('DOKU_INC')) die();
|
||||
|
||||
/**
|
||||
* Class helper_plugin_move_plan
|
||||
*
|
||||
* This thing prepares and keeps progress info on complex move operations (eg. where more than a single
|
||||
* object is affected).
|
||||
*
|
||||
* Please note: this has not a complex move resolver. Move operations may not depend on each other (eg. you
|
||||
* can not use a namespace as source that will only be created by a different move operation) instead all given
|
||||
* operations should be operations on the current state to come to a wanted future state. The tree manager takes
|
||||
* care of that by abstracting all moves on a DOM representation first, then submitting the needed changes (eg.
|
||||
* differences between now and wanted).
|
||||
*
|
||||
* Glossary:
|
||||
*
|
||||
* document - refers to either a page or a media file here
|
||||
*/
|
||||
class helper_plugin_move_plan extends DokuWiki_Plugin {
|
||||
/** Number of operations per step */
|
||||
const OPS_PER_RUN = 10;
|
||||
|
||||
const TYPE_PAGES = 1;
|
||||
const TYPE_MEDIA = 2;
|
||||
const CLASS_NS = 4;
|
||||
const CLASS_DOC = 8;
|
||||
|
||||
/**
|
||||
* @var array the options for this move plan
|
||||
*/
|
||||
protected $options = array(); // defaults are set in loadOptions()
|
||||
|
||||
/**
|
||||
* @var array holds the location of the different list and state files
|
||||
*/
|
||||
protected $files = array();
|
||||
|
||||
/**
|
||||
* @var array the planned moves
|
||||
*/
|
||||
protected $plan = array();
|
||||
|
||||
/**
|
||||
* @var array temporary holder of document lists
|
||||
*/
|
||||
protected $tmpstore = array(
|
||||
'pages' => array(),
|
||||
'media' => array(),
|
||||
'ns' => array(),
|
||||
'affpg' => array(),
|
||||
'miss' => array(),
|
||||
'miss_media' => array(),
|
||||
);
|
||||
|
||||
/** @var helper_plugin_move_op $MoveOperator */
|
||||
protected $MoveOperator = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* initializes state (if any) for continuiation of a running move op
|
||||
*/
|
||||
public function __construct() {
|
||||
global $conf;
|
||||
|
||||
// set the file locations
|
||||
$this->files = array(
|
||||
'opts' => $conf['metadir'] . '/__move_opts',
|
||||
'pagelist' => $conf['metadir'] . '/__move_pagelist',
|
||||
'medialist' => $conf['metadir'] . '/__move_medialist',
|
||||
'affected' => $conf['metadir'] . '/__move_affected',
|
||||
'namespaces' => $conf['metadir'] . '/__move_namespaces',
|
||||
'missing' => $conf['metadir'] . '/__move_missing',
|
||||
'missing_media' => $conf['metadir'] . '/__move_missing_media',
|
||||
);
|
||||
|
||||
$this->MoveOperator = plugin_load('helper', 'move_op');
|
||||
|
||||
$this->loadOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the current options if any
|
||||
*
|
||||
* If no options are found, the default options will be extended by any available
|
||||
* config options
|
||||
*/
|
||||
protected function loadOptions() {
|
||||
// (re)set defaults
|
||||
$this->options = array(
|
||||
// status
|
||||
'committed' => false,
|
||||
'started' => 0,
|
||||
|
||||
// counters
|
||||
'pages_all' => 0,
|
||||
'pages_run' => 0,
|
||||
'media_all' => 0,
|
||||
'media_run' => 0,
|
||||
'affpg_all' => 0,
|
||||
'affpg_run' => 0,
|
||||
|
||||
// options
|
||||
'autoskip' => $this->getConf('autoskip'),
|
||||
'autorewrite' => $this->getConf('autorewrite'),
|
||||
|
||||
// errors
|
||||
'lasterror' => false
|
||||
);
|
||||
|
||||
// merge whatever options are saved currently
|
||||
$file = $this->files['opts'];
|
||||
if(file_exists($file)) {
|
||||
$options = unserialize(io_readFile($file, false));
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current options
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function saveOptions() {
|
||||
return io_saveFile($this->files['opts'], serialize($this->options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current state of an option, null for unknown options
|
||||
*
|
||||
* @param $name
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getOption($name) {
|
||||
if(isset($this->options[$name])) {
|
||||
return $this->options[$name];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an option
|
||||
*
|
||||
* Note, this otpion will only be set to the current instance of this helper object. It will only
|
||||
* be written to the option file once the plan gets committed
|
||||
*
|
||||
* @param $name
|
||||
* @param $value
|
||||
*/
|
||||
public function setOption($name, $value) {
|
||||
$this->options[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the progress of this plan in percent
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getProgress() {
|
||||
$max =
|
||||
$this->options['pages_all'] +
|
||||
$this->options['media_all'];
|
||||
|
||||
$remain =
|
||||
$this->options['pages_run'] +
|
||||
$this->options['media_run'];
|
||||
|
||||
if($this->options['autorewrite']) {
|
||||
$max += $this->options['affpg_all'];
|
||||
$remain += $this->options['affpg_run'];
|
||||
}
|
||||
|
||||
if($max == 0) return 0;
|
||||
return round((($max - $remain) * 100) / $max, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is a move in progress currently
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function inProgress() {
|
||||
return (bool) $this->options['started'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this plan has been committed, yet
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isCommited() {
|
||||
return $this->options['committed'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single page to be moved to the plan
|
||||
*
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
*/
|
||||
public function addPageMove($src, $dst) {
|
||||
$this->addMove($src, $dst, self::CLASS_DOC, self::TYPE_PAGES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single media file to be moved to the plan
|
||||
*
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
*/
|
||||
public function addMediaMove($src, $dst) {
|
||||
$this->addMove($src, $dst, self::CLASS_DOC, self::TYPE_MEDIA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a page namespace to be moved to the plan
|
||||
*
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
*/
|
||||
public function addPageNamespaceMove($src, $dst) {
|
||||
$this->addMove($src, $dst, self::CLASS_NS, self::TYPE_PAGES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a media namespace to be moved to the plan
|
||||
*
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
*/
|
||||
public function addMediaNamespaceMove($src, $dst) {
|
||||
$this->addMove($src, $dst, self::CLASS_NS, self::TYPE_MEDIA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plans the move of a namespace or document
|
||||
*
|
||||
* @param string $src ID of the item to move
|
||||
* @param string $dst new ID of item namespace
|
||||
* @param int $class (self::CLASS_NS|self::CLASS_DOC)
|
||||
* @param int $type (PLUGIN_MOVE_TYPE_PAGE|self::TYPE_MEDIA)
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function addMove($src, $dst, $class = self::CLASS_NS, $type = self::TYPE_PAGES) {
|
||||
if($this->options['committed']) throw new Exception('plan is committed already, can not be added to');
|
||||
|
||||
$src = cleanID($src);
|
||||
$dst = cleanID($dst);
|
||||
|
||||
$this->plan[] = array(
|
||||
'src' => $src,
|
||||
'dst' => $dst,
|
||||
'class' => $class,
|
||||
'type' => $type
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort any move or plan in progress and reset the helper
|
||||
*/
|
||||
public function abort() {
|
||||
foreach($this->files as $file) {
|
||||
@unlink($file);
|
||||
}
|
||||
$this->plan = array();
|
||||
$this->loadOptions();
|
||||
helper_plugin_move_rewrite::removeAllLocks();
|
||||
}
|
||||
|
||||
/**
|
||||
* This locks up the plan and prepares execution
|
||||
*
|
||||
* the plan is reordered an the needed move operations are gathered and stored in the appropriate
|
||||
* list files
|
||||
*
|
||||
* @throws Exception if you try to commit a plan twice
|
||||
* @return bool true if the plan was committed
|
||||
*/
|
||||
public function commit() {
|
||||
global $conf;
|
||||
|
||||
if($this->options['committed']) throw new Exception('plan is committed already, can not be committed again');
|
||||
|
||||
helper_plugin_move_rewrite::addLock();
|
||||
|
||||
|
||||
usort($this->plan, array($this, 'planSorter'));
|
||||
|
||||
// get all the documents to be moved and store them in their lists
|
||||
foreach($this->plan as $move) {
|
||||
if($move['class'] == self::CLASS_DOC) {
|
||||
// these can just be added
|
||||
$this->addToDocumentList($move['src'], $move['dst'], $move['type']);
|
||||
} else {
|
||||
// here we need a list of content first, search for it
|
||||
$docs = array();
|
||||
$path = utf8_encodeFN(str_replace(':', '/', $move['src']));
|
||||
$opts = array('depth' => 0, 'skipacl' => true);
|
||||
if($move['type'] == self::TYPE_PAGES) {
|
||||
search($docs, $conf['datadir'], 'search_allpages', $opts, $path);
|
||||
} else {
|
||||
search($docs, $conf['mediadir'], 'search_media', $opts, $path);
|
||||
}
|
||||
|
||||
// how much namespace to strip?
|
||||
if($move['src'] !== '') {
|
||||
$strip = strlen($move['src']) + 1;
|
||||
} else {
|
||||
$strip = 0;
|
||||
}
|
||||
if($move['dst']) $move['dst'] .= ':';
|
||||
|
||||
// now add all the found documents to our lists
|
||||
foreach($docs as $doc) {
|
||||
$from = $doc['id'];
|
||||
$to = $move['dst'] . substr($doc['id'], $strip);
|
||||
$this->addToDocumentList($from, $to, $move['type']);
|
||||
}
|
||||
|
||||
// remember the namespace move itself
|
||||
if($move['type'] == self::TYPE_PAGES) {
|
||||
// FIXME we use this to move namespace subscriptions later on and for now only do it on
|
||||
// page namespace moves, but subscriptions work for both, but what when only one of
|
||||
// them is moved? Should it be copied then? Complicated. This is good enough for now
|
||||
$this->addToDocumentList($move['src'], $move['dst'], self::CLASS_NS);
|
||||
}
|
||||
$this->findMissingDocuments($move['src'] . ':', $move['dst'],$move['type']);
|
||||
}
|
||||
// store what pages are affected by this move
|
||||
$this->findAffectedPages($move['src'], $move['dst'], $move['class'], $move['type']);
|
||||
}
|
||||
|
||||
$this->storeDocumentLists();
|
||||
|
||||
if(!$this->options['pages_all'] && !$this->options['media_all']) {
|
||||
msg($this->getLang('noaction'), -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->options['committed'] = true;
|
||||
$this->saveOptions();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the next steps
|
||||
*
|
||||
* @param bool $skip set to true to skip the next first step (skip error)
|
||||
* @return bool|int false on errors, otherwise the number of remaining steps
|
||||
* @throws Exception
|
||||
*/
|
||||
public function nextStep($skip = false) {
|
||||
if(!$this->options['committed']) throw new Exception('plan is not committed yet!');
|
||||
|
||||
// execution has started
|
||||
if(!$this->options['started']) $this->options['started'] = time();
|
||||
|
||||
helper_plugin_move_rewrite::addLock();
|
||||
|
||||
if(@filesize($this->files['pagelist']) > 1) {
|
||||
$todo = $this->stepThroughDocuments(self::TYPE_PAGES, $skip);
|
||||
if($todo === false) return $this->storeError();
|
||||
return max($todo, 1); // force one more call
|
||||
}
|
||||
|
||||
if(@filesize($this->files['medialist']) > 1) {
|
||||
$todo = $this->stepThroughDocuments(self::TYPE_MEDIA, $skip);
|
||||
if($todo === false) return $this->storeError();
|
||||
return max($todo, 1); // force one more call
|
||||
}
|
||||
|
||||
if(@filesize($this->files['missing']) > 1 && @filesize($this->files['affected']) > 1) {
|
||||
$todo = $this->stepThroughMissingDocuments(self::TYPE_PAGES);
|
||||
if($todo === false) return $this->storeError();
|
||||
return max($todo, 1); // force one more call
|
||||
}
|
||||
|
||||
if(@filesize($this->files['missing_media']) > 1 && @filesize($this->files['affected']) > 1) {
|
||||
$todo = $this->stepThroughMissingDocuments(self::TYPE_MEDIA);
|
||||
if($todo === false)return $this->storeError();
|
||||
return max($todo, 1); // force one more call
|
||||
}
|
||||
|
||||
if(@filesize($this->files['namespaces']) > 1) {
|
||||
$todo = $this->stepThroughNamespaces();
|
||||
if($todo === false) return $this->storeError();
|
||||
return max($todo, 1); // force one more call
|
||||
}
|
||||
|
||||
helper_plugin_move_rewrite::removeAllLocks();
|
||||
|
||||
if($this->options['autorewrite'] && @filesize($this->files['affected']) > 1) {
|
||||
$todo = $this->stepThroughAffectedPages();
|
||||
if($todo === false) return $this->storeError();
|
||||
return max($todo, 1); // force one more call
|
||||
}
|
||||
|
||||
// we're done here, clean up
|
||||
$this->abort();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of page and media moves and the affected pages as a HTML list
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function previewHTML() {
|
||||
$html = '';
|
||||
|
||||
$html .= '<ul>';
|
||||
if(@file_exists($this->files['pagelist'])) {
|
||||
$pagelist = file($this->files['pagelist']);
|
||||
foreach($pagelist as $line) {
|
||||
list($old, $new) = explode("\t", trim($line));
|
||||
|
||||
$html .= '<li class="page"><div class="li">';
|
||||
$html .= hsc($old);
|
||||
$html .= '→';
|
||||
$html .= hsc($new);
|
||||
$html .= '</div></li>';
|
||||
}
|
||||
}
|
||||
if(@file_exists($this->files['medialist'])) {
|
||||
$medialist = file($this->files['medialist']);
|
||||
foreach($medialist as $line) {
|
||||
list($old, $new) = explode("\t", trim($line));
|
||||
|
||||
$html .= '<li class="media"><div class="li">';
|
||||
$html .= hsc($old);
|
||||
$html .= '→';
|
||||
$html .= hsc($new);
|
||||
$html .= '</div></li>';
|
||||
}
|
||||
}
|
||||
if(@file_exists($this->files['affected'])) {
|
||||
$medialist = file($this->files['affected']);
|
||||
foreach($medialist as $page) {
|
||||
$html .= '<li class="affected"><div class="li">';
|
||||
$html .= '↷';
|
||||
$html .= hsc($page);
|
||||
$html .= '</div></li>';
|
||||
}
|
||||
}
|
||||
$html .= '</ul>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step through the next bunch of pages or media files
|
||||
*
|
||||
* @param int $type (self::TYPE_PAGES|self::TYPE_MEDIA)
|
||||
* @param bool $skip should the first item be skipped?
|
||||
* @return bool|int false on error, otherwise the number of remaining documents
|
||||
*/
|
||||
protected function stepThroughDocuments($type = self::TYPE_PAGES, $skip = false) {
|
||||
|
||||
if($type == self::TYPE_PAGES) {
|
||||
$file = $this->files['pagelist'];
|
||||
$mark = 'P';
|
||||
$call = 'movePage';
|
||||
$items_run_counter = 'pages_run';
|
||||
} else {
|
||||
$file = $this->files['medialist'];
|
||||
$mark = 'M';
|
||||
$call = 'moveMedia';
|
||||
$items_run_counter = 'media_run';
|
||||
}
|
||||
|
||||
$doclist = fopen($file, 'a+');
|
||||
|
||||
for($i = 0; $i < helper_plugin_move_plan::OPS_PER_RUN; $i++) {
|
||||
$log = "";
|
||||
$line = $this->getLastLine($doclist);
|
||||
if($line === false) {
|
||||
break;
|
||||
}
|
||||
list($src, $dst) = explode("\t", trim($line));
|
||||
|
||||
// should this item be skipped?
|
||||
if($skip === true) {
|
||||
$skip = false;
|
||||
} else {
|
||||
// move the page
|
||||
if(!$this->MoveOperator->$call($src, $dst)) {
|
||||
$log .= $this->build_log_line($mark, $src, $dst, false); // FAILURE!
|
||||
|
||||
// automatically skip this item only if wanted...
|
||||
if(!$this->options['autoskip']) {
|
||||
// ...otherwise abort the operation
|
||||
fclose($doclist);
|
||||
$return_items_run = false;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$log .= $this->build_log_line($mark, $src, $dst, true); // SUCCESS!
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This adjusts counters and truncates the document list correctly
|
||||
* It is used to finalize a successful or skipped move
|
||||
*/
|
||||
|
||||
ftruncate($doclist, ftell($doclist));
|
||||
$this->options[$items_run_counter]--;
|
||||
$return_items_run = $this->options[$items_run_counter];
|
||||
$this->write_log($log);
|
||||
$this->saveOptions();
|
||||
}
|
||||
|
||||
if ($return_items_run !== false) {
|
||||
fclose($doclist);
|
||||
}
|
||||
return $return_items_run;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step through the next bunch of pages that need link corrections
|
||||
*
|
||||
* @return bool|int false on error, otherwise the number of remaining documents
|
||||
*/
|
||||
protected function stepThroughAffectedPages() {
|
||||
/** @var helper_plugin_move_rewrite $Rewriter */
|
||||
$Rewriter = plugin_load('helper', 'move_rewrite');
|
||||
|
||||
// handle affected pages
|
||||
$doclist = fopen($this->files['affected'], 'a+');
|
||||
for($i = 0; $i < helper_plugin_move_plan::OPS_PER_RUN; $i++) {
|
||||
$page = $this->getLastLine($doclist);
|
||||
if($page === false) break;
|
||||
|
||||
// rewrite it
|
||||
$Rewriter->rewritePage($page);
|
||||
|
||||
// update the list file
|
||||
ftruncate($doclist, ftell($doclist));
|
||||
$this->options['affpg_run']--;
|
||||
$this->saveOptions();
|
||||
}
|
||||
|
||||
fclose($doclist);
|
||||
return $this->options['affpg_run'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Step through all the links to missing pages that should be moved
|
||||
*
|
||||
* This simply adds the moved missing pages to all affected pages meta data. This will add
|
||||
* the meta data to pages not linking to the affected pages but this should still be faster
|
||||
* than figuring out which pages need this info.
|
||||
*
|
||||
* This does not step currently, but handles all pages in one step.
|
||||
*
|
||||
* @param int $type
|
||||
*
|
||||
* @return int always 0
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function stepThroughMissingDocuments($type = self::TYPE_PAGES) {
|
||||
if($type != self::TYPE_PAGES && $type != self::TYPE_MEDIA) {
|
||||
throw new Exception('wrong type specified');
|
||||
}
|
||||
/** @var helper_plugin_move_rewrite $Rewriter */
|
||||
$Rewriter = plugin_load('helper', 'move_rewrite');
|
||||
|
||||
$miss = array();
|
||||
if ($type == self::TYPE_PAGES) {
|
||||
$missing_fn = $this->files['missing'];
|
||||
} else {
|
||||
$missing_fn = $this->files['missing_media'];
|
||||
}
|
||||
$missing = file($missing_fn);
|
||||
foreach($missing as $line) {
|
||||
$line = trim($line);
|
||||
if($line == '') continue;
|
||||
list($src, $dst) = explode("\t", $line);
|
||||
$miss[$src] = $dst;
|
||||
}
|
||||
|
||||
$affected = file($this->files['affected']);
|
||||
foreach($affected as $page){
|
||||
$page = trim($page);
|
||||
|
||||
if ($type == self::TYPE_PAGES) {
|
||||
$Rewriter->setMoveMetas($page, $miss, 'pages');
|
||||
} else {
|
||||
$Rewriter->setMoveMetas($page, $miss, 'media');
|
||||
}
|
||||
}
|
||||
|
||||
unlink($missing_fn);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step through all the namespace moves
|
||||
*
|
||||
* This does not step currently, but handles all namespaces in one step.
|
||||
*
|
||||
* Currently moves namespace subscriptions only.
|
||||
*
|
||||
* @return int always 0
|
||||
* @todo maybe add an event so plugins can move more stuff?
|
||||
* @todo fixed that $src and $dst are seperated by tab, not newline. This method has no tests?
|
||||
*/
|
||||
protected function stepThroughNamespaces() {
|
||||
/** @var helper_plugin_move_file $FileMover */
|
||||
$FileMover = plugin_load('helper', 'move_file');
|
||||
|
||||
$lines = io_readFile($this->files['namespaces']);
|
||||
$lines = explode("\n", $lines);
|
||||
|
||||
foreach($lines as $line) {
|
||||
// There is an empty line at the end of the list.
|
||||
if ($line === '') continue;
|
||||
|
||||
list($src, $dst) = explode("\t", trim($line));
|
||||
$FileMover->moveNamespaceSubscription($src, $dst);
|
||||
}
|
||||
|
||||
@unlink($this->files['namespaces']);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the last error from the MSG array and store it in the options
|
||||
*
|
||||
* @todo rebuild error handling based on exceptions
|
||||
*
|
||||
* @return bool always false
|
||||
*/
|
||||
protected function storeError() {
|
||||
global $MSG;
|
||||
|
||||
if(is_array($MSG) && count($MSG)) {
|
||||
$last = array_shift($MSG);
|
||||
$this->options['lasterror'] = $last['msg'];
|
||||
unset($GLOBALS['MSG']);
|
||||
} else {
|
||||
$this->options['lasterror'] = 'Unknown error';
|
||||
}
|
||||
$this->saveOptions();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the error state
|
||||
*/
|
||||
protected function clearError() {
|
||||
$this->options['lasterror'] = false;
|
||||
$this->saveOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last error message or false if no error occured
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function getLastError() {
|
||||
return $this->options['lasterror'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a page move operation in the list file
|
||||
*
|
||||
* If the src has been added before, this is ignored. This makes sure you can move a single page
|
||||
* out of a namespace first, then move the namespace somewhere else.
|
||||
*
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
* @param int $type
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function addToDocumentList($src, $dst, $type = self::TYPE_PAGES) {
|
||||
if($type == self::TYPE_PAGES) {
|
||||
$store = 'pages';
|
||||
} else if($type == self::TYPE_MEDIA) {
|
||||
$store = 'media';
|
||||
} else if($type == self::CLASS_NS) {
|
||||
$store = 'ns';
|
||||
} else {
|
||||
throw new Exception('Unknown type ' . $type);
|
||||
}
|
||||
|
||||
if(!isset($this->tmpstore[$store][$src])) {
|
||||
$this->tmpstore[$store][$src] = $dst;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the list of pages to the list of affected pages whose links need adjustment
|
||||
*
|
||||
* @param string|array $pages
|
||||
*/
|
||||
protected function addToAffectedPagesList($pages) {
|
||||
if(!is_array($pages)) $pages = array($pages);
|
||||
|
||||
foreach($pages as $page) {
|
||||
if(!isset($this->tmpstore['affpg'][$page])) {
|
||||
$this->tmpstore['affpg'][$page] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up pages that will be affected by a move of $src
|
||||
*
|
||||
* Calls addToAffectedPagesList() directly to store the result
|
||||
*
|
||||
* @param string $src source namespace
|
||||
* @param string $dst destination namespace
|
||||
* @param int $class
|
||||
* @param int $type
|
||||
*/
|
||||
protected function findAffectedPages($src, $dst, $class, $type) {
|
||||
$idx = idx_get_indexer();
|
||||
|
||||
if($class == self::CLASS_NS) {
|
||||
$src_ = "$src:*"; // use wildcard lookup for namespaces
|
||||
} else {
|
||||
$src_ = $src;
|
||||
}
|
||||
|
||||
$pages = array();
|
||||
if($type == self::TYPE_PAGES) {
|
||||
$pages = $idx->lookupKey('relation_references', $src_);
|
||||
$len = strlen($src);
|
||||
foreach($pages as &$page) {
|
||||
if (substr($page, 0, $len + 1) === "$src:") {
|
||||
$page = $dst . substr($page, $len + 1);
|
||||
}
|
||||
}
|
||||
unset($page);
|
||||
} else if($type == self::TYPE_MEDIA) {
|
||||
$pages = $idx->lookupKey('relation_media', $src_);
|
||||
}
|
||||
|
||||
$this->addToAffectedPagesList($pages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find missing pages in the $src namespace
|
||||
*
|
||||
* @param string $src source namespace
|
||||
* @param string $dst destination namespace
|
||||
* @param int $type either self::TYPE_PAGES or self::TYPE_MEDIA
|
||||
*/
|
||||
protected function findMissingDocuments($src, $dst, $type = self::TYPE_PAGES) {
|
||||
global $conf;
|
||||
|
||||
// FIXME this duplicates Doku_Indexer::getIndex()
|
||||
if ($type == self::TYPE_PAGES) {
|
||||
$fn = $conf['indexdir'] . '/relation_references_w.idx';
|
||||
} else {
|
||||
$fn = $conf['indexdir'] . '/relation_media_w.idx';
|
||||
}
|
||||
if (!@file_exists($fn)){
|
||||
$referenceidx = array();
|
||||
} else {
|
||||
$referenceidx = file($fn, FILE_IGNORE_NEW_LINES);
|
||||
}
|
||||
|
||||
$len = strlen($src);
|
||||
foreach($referenceidx as $idx => $page) {
|
||||
if(substr($page, 0, $len) != "$src") continue;
|
||||
|
||||
// remember missing pages
|
||||
if ($type == self::TYPE_PAGES) {
|
||||
if(!page_exists($page)) {
|
||||
$newpage = $dst . substr($page, $len);
|
||||
$this->tmpstore['miss'][$page] = $newpage;
|
||||
}
|
||||
} else {
|
||||
if(!file_exists(mediaFN($page))){
|
||||
$newpage = $dst . substr($page, $len);
|
||||
$this->tmpstore['miss_media'][$page] = $newpage;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the aggregated document lists in the file system and reset the internal storage
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function storeDocumentLists() {
|
||||
$lists = array(
|
||||
'pages' => $this->files['pagelist'],
|
||||
'media' => $this->files['medialist'],
|
||||
'ns' => $this->files['namespaces'],
|
||||
'affpg' => $this->files['affected'],
|
||||
'miss' => $this->files['missing'],
|
||||
'miss_media' => $this->files['missing_media'],
|
||||
);
|
||||
|
||||
foreach($lists as $store => $file) {
|
||||
// anything to do?
|
||||
$count = count($this->tmpstore[$store]);
|
||||
if(!$count) continue;
|
||||
|
||||
// prepare and save content
|
||||
$data = '';
|
||||
$this->tmpstore[$store] = array_reverse($this->tmpstore[$store]); // store in reverse order
|
||||
foreach($this->tmpstore[$store] as $src => $dst) {
|
||||
if($dst === true) {
|
||||
$data .= "$src\n"; // for affected pages only one ID is saved
|
||||
} else {
|
||||
$data .= "$src\t$dst\n";
|
||||
}
|
||||
|
||||
}
|
||||
io_saveFile($file, $data);
|
||||
|
||||
// set counters
|
||||
if($store != 'ns') {
|
||||
$this->options[$store . '_all'] = $count;
|
||||
$this->options[$store . '_run'] = $count;
|
||||
}
|
||||
|
||||
// reset the list
|
||||
$this->tmpstore[$store] = array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last line from the list that is stored in the file that is referenced by the handle
|
||||
* The handle is set to the newline before the file id
|
||||
*
|
||||
* @param resource $handle The file handle to read from
|
||||
* @return string|bool the last id from the list or false if there is none
|
||||
*/
|
||||
protected function getLastLine($handle) {
|
||||
// begin the seek at the end of the file
|
||||
fseek($handle, 0, SEEK_END);
|
||||
$line = '';
|
||||
|
||||
// seek one backwards as long as it's possible
|
||||
while(fseek($handle, -1, SEEK_CUR) >= 0) {
|
||||
$c = fgetc($handle);
|
||||
if($c === false) return false; // EOF, i.e. the file is empty
|
||||
fseek($handle, -1, SEEK_CUR); // reset the position to the character that was read
|
||||
|
||||
if($c == "\n") {
|
||||
if($line === '') {
|
||||
continue; // this line was empty, continue
|
||||
} else {
|
||||
break; // we have a line, finish
|
||||
}
|
||||
}
|
||||
|
||||
$line = $c . $line; // prepend char to line
|
||||
}
|
||||
|
||||
if($line === '') return false; // beginning of file reached and no content
|
||||
|
||||
return $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for usort to sort the move plan
|
||||
*
|
||||
* @param $a
|
||||
* @param $b
|
||||
* @return int
|
||||
*/
|
||||
public function planSorter($a, $b) {
|
||||
// do page moves before namespace moves
|
||||
if($a['class'] == self::CLASS_DOC && $b['class'] == self::CLASS_NS) {
|
||||
return -1;
|
||||
}
|
||||
if($a['class'] == self::CLASS_NS && $b['class'] == self::CLASS_DOC) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// do pages before media
|
||||
if($a['type'] == self::TYPE_PAGES && $b['type'] == self::TYPE_MEDIA) {
|
||||
return -1;
|
||||
}
|
||||
if($a['type'] == self::TYPE_MEDIA && $b['type'] == self::TYPE_PAGES) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// from here on we compare only apples to apples
|
||||
// we sort by depth of namespace, deepest namespaces first
|
||||
|
||||
$alen = substr_count($a['src'], ':');
|
||||
$blen = substr_count($b['src'], ':');
|
||||
|
||||
if($alen > $blen) {
|
||||
return -1;
|
||||
} elseif($alen < $blen) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create line to log result of an operation
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @param bool $success
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @author Andreas Gohr <gohr@cosmocode.de>
|
||||
* @author Michael Große <grosse@cosmocode.de>
|
||||
*/
|
||||
public function build_log_line ($type, $from, $to, $success) {
|
||||
global $MSG;
|
||||
|
||||
$now = time();
|
||||
$date = date('Y-m-d H:i:s', $now); // for human readability
|
||||
if($success) {
|
||||
$ok = 'success';
|
||||
$msg = '';
|
||||
} else {
|
||||
$ok = 'failed';
|
||||
$msg = $MSG[count($MSG) - 1]['msg']; // get detail from message array
|
||||
}
|
||||
|
||||
$log = "$now\t$date\t$type\t$from\t$to\t$ok\t$msg\n";
|
||||
return $log;
|
||||
}
|
||||
|
||||
/**
|
||||
* write log to file
|
||||
*
|
||||
* @param $log
|
||||
*/
|
||||
protected function write_log ($log) {
|
||||
global $conf;
|
||||
$optime = $this->options['started'];
|
||||
$file = $conf['cachedir'] . '/move/' . strftime('%Y%m%d-%H%M%S', $optime) . '.log';
|
||||
io_saveFile($file, $log, true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
/**
|
||||
* Move Plugin Page Rewriter
|
||||
*
|
||||
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
|
||||
* @author Michael Hamann <michael@content-space.de>
|
||||
* @author Gary Owen <gary@isection.co.uk>
|
||||
* @author Andreas Gohr <gohr@cosmocode.de>
|
||||
*/
|
||||
// must be run within Dokuwiki
|
||||
if(!defined('DOKU_INC')) die();
|
||||
|
||||
// load required handler class
|
||||
require_once(dirname(__FILE__) . '/handler.php');
|
||||
|
||||
/**
|
||||
* Class helper_plugin_move_rewrite
|
||||
*
|
||||
* This class handles the rewriting of wiki text to update the links
|
||||
*/
|
||||
class helper_plugin_move_rewrite extends DokuWiki_Plugin {
|
||||
|
||||
/**
|
||||
* Under what key is move data to be saved in metadata
|
||||
*/
|
||||
const METAKEY = 'plugin_move';
|
||||
|
||||
/**
|
||||
* What is they filename of the lockfile
|
||||
*/
|
||||
const LOCKFILENAME = '_plugin_move.lock';
|
||||
|
||||
/**
|
||||
* @var string symbol to make move operations easily recognizable in change log
|
||||
*/
|
||||
public $symbol = '↷';
|
||||
|
||||
/**
|
||||
* This function loads and returns the persistent metadata for the move plugin. If there is metadata for the
|
||||
* pagemove plugin (not the old one but the version that immediately preceeded the move plugin) it will be migrated.
|
||||
*
|
||||
* @param string $id The id of the page the metadata shall be loaded for
|
||||
* @return array|null The metadata of the page
|
||||
*/
|
||||
public function getMoveMeta($id) {
|
||||
$all_meta = p_get_metadata($id, '', METADATA_DONT_RENDER);
|
||||
|
||||
/* todo migrate old move data
|
||||
if(isset($all_meta['plugin_pagemove']) && !is_null($all_meta['plugin_pagemove'])) {
|
||||
if(isset($all_meta[self::METAKEY])) {
|
||||
$all_meta[self::METAKEY] = array_merge_recursive($all_meta['plugin_pagemove'], $all_meta[self::METAKEY]);
|
||||
} else {
|
||||
$all_meta[self::METAKEY] = $all_meta['plugin_pagemove'];
|
||||
}
|
||||
p_set_metadata($id, array(self::METAKEY => $all_meta[self::METAKEY], 'plugin_pagemove' => null), false, true);
|
||||
}
|
||||
*/
|
||||
|
||||
// discard missing or empty array or string
|
||||
$meta = !empty($all_meta[self::METAKEY]) ? $all_meta[self::METAKEY] : array();
|
||||
if(!isset($meta['origin'])) {
|
||||
$meta['origin'] = '';
|
||||
}
|
||||
if(!isset($meta['pages'])) {
|
||||
$meta['pages'] = array();
|
||||
}
|
||||
if(!isset($meta['media'])) {
|
||||
$meta['media'] = array();
|
||||
}
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any existing move meta data for the given page
|
||||
*
|
||||
* @param $id
|
||||
*/
|
||||
public function unsetMoveMeta($id) {
|
||||
p_set_metadata($id, array(self::METAKEY => array()), false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add info about a moved document to the metadata of an affected page
|
||||
*
|
||||
* @param string $id affected page
|
||||
* @param string $src moved document's original id
|
||||
* @param string $dst moved document's new id
|
||||
* @param string $type 'media' or 'page'
|
||||
* @throws Exception on wrong argument
|
||||
*/
|
||||
public function setMoveMeta($id, $src, $dst, $type) {
|
||||
$this->setMoveMetas($id, array($src => $dst), $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add info about several moved documents to the metadata of an affected page
|
||||
*
|
||||
* @param string $id affected page
|
||||
* @param array $moves list of moves (src is key, dst is value)
|
||||
* @param string $type 'media' or 'page'
|
||||
* @throws Exception
|
||||
*/
|
||||
public function setMoveMetas($id, $moves, $type) {
|
||||
if($type != 'pages' && $type != 'media') {
|
||||
throw new Exception('wrong type specified');
|
||||
}
|
||||
if(!page_exists($id, '', false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$meta = $this->getMoveMeta($id);
|
||||
foreach($moves as $src => $dst) {
|
||||
$meta[$type][] = array($src, $dst);
|
||||
}
|
||||
|
||||
p_set_metadata($id, array(self::METAKEY => $meta), false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store info about the move of a page in its own meta data
|
||||
*
|
||||
* This has to be called before the move is executed
|
||||
*
|
||||
* @param string $id moved page's original (and still current) id
|
||||
*/
|
||||
public function setSelfMoveMeta($id) {
|
||||
$meta = $this->getMoveMeta($id);
|
||||
// was this page moved multiple times? keep the orignal name til rewriting occured
|
||||
if(isset($meta['origin']) && $meta['origin'] !== '') {
|
||||
return;
|
||||
}
|
||||
$meta['origin'] = $id;
|
||||
|
||||
p_set_metadata($id, array(self::METAKEY => $meta), false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if rewrites may be executed within this process right now
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isLocked() {
|
||||
global $PLUGIN_MOVE_WORKING;
|
||||
global $conf;
|
||||
$lockfile = $conf['lockdir'] . self::LOCKFILENAME;
|
||||
return ((isset($PLUGIN_MOVE_WORKING) && $PLUGIN_MOVE_WORKING > 0) || file_exists($lockfile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not allow any rewrites in this process right now
|
||||
*/
|
||||
public static function addLock() {
|
||||
global $PLUGIN_MOVE_WORKING;
|
||||
global $conf;
|
||||
$PLUGIN_MOVE_WORKING = $PLUGIN_MOVE_WORKING ? $PLUGIN_MOVE_WORKING + 1 : 1;
|
||||
$lockfile = $conf['lockdir'] . self::LOCKFILENAME;
|
||||
if (!file_exists($lockfile)) {
|
||||
io_savefile($lockfile, "1\n");
|
||||
} else {
|
||||
$stack = intval(file_get_contents($lockfile));
|
||||
++$stack;
|
||||
io_savefile($lockfile, strval($stack));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow rerites in this process again, unless some other lock exists
|
||||
*/
|
||||
public static function removeLock() {
|
||||
global $PLUGIN_MOVE_WORKING;
|
||||
global $conf;
|
||||
$PLUGIN_MOVE_WORKING = $PLUGIN_MOVE_WORKING ? $PLUGIN_MOVE_WORKING - 1 : 0;
|
||||
$lockfile = $conf['lockdir'] . self::LOCKFILENAME;
|
||||
if (!file_exists($lockfile)) {
|
||||
throw new Exception("removeLock failed: lockfile missing");
|
||||
} else {
|
||||
$stack = intval(file_get_contents($lockfile));
|
||||
if($stack === 1) {
|
||||
unlink($lockfile);
|
||||
} else {
|
||||
--$stack;
|
||||
io_savefile($lockfile, strval($stack));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow rewrites in this process again.
|
||||
*
|
||||
* @author Michael Große <grosse@cosmocode.de>
|
||||
*/
|
||||
public static function removeAllLocks() {
|
||||
global $conf;
|
||||
$lockfile = $conf['lockdir'] . self::LOCKFILENAME;
|
||||
if (file_exists($lockfile)) {
|
||||
unlink($lockfile);
|
||||
}
|
||||
unset($GLOBALS['PLUGIN_MOVE_WORKING']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Rewrite a text in order to fix the content after the given moves.
|
||||
*
|
||||
* @param string $id The id of the wiki page, if the page itself was moved the old id
|
||||
* @param string $text The text to be rewritten
|
||||
* @return string The rewritten wiki text
|
||||
*/
|
||||
public function rewrite($id, $text) {
|
||||
$meta = $this->getMoveMeta($id);
|
||||
|
||||
$handlers = array();
|
||||
$pages = $meta['pages'];
|
||||
$media = $meta['media'];
|
||||
$origin = $meta['origin'];
|
||||
if($origin == '') $origin = $id;
|
||||
|
||||
$data = array(
|
||||
'id' => $id,
|
||||
'origin' => &$origin,
|
||||
'pages' => &$pages,
|
||||
'media_moves' => &$media,
|
||||
'handlers' => &$handlers
|
||||
);
|
||||
|
||||
/*
|
||||
* PLUGIN_MOVE_HANDLERS REGISTER event:
|
||||
*
|
||||
* Plugin handlers can be registered in the $handlers array, the key is the plugin name as it is given to the handler
|
||||
* The handler needs to be a valid callback, it will get the following parameters:
|
||||
* $match, $state, $pos, $pluginname, $handler. The first three parameters are equivalent to the parameters
|
||||
* of the handle()-function of syntax plugins, the $pluginname is just the plugin name again so handler functions
|
||||
* that handle multiple plugins can distinguish for which the match is. The last parameter is the handler object
|
||||
* which is an instance of helper_plugin_move_handle
|
||||
*/
|
||||
trigger_event('PLUGIN_MOVE_HANDLERS_REGISTER', $data);
|
||||
|
||||
$modes = p_get_parsermodes();
|
||||
|
||||
// Create the parser
|
||||
$Parser = new Doku_Parser();
|
||||
|
||||
// Add the Handler
|
||||
/** @var $Parser->Handler helper_plugin_move_handler */
|
||||
$Parser->Handler = $this->loadHelper('move_handler');
|
||||
$Parser->Handler->init($id, $origin, $pages, $media, $handlers);
|
||||
|
||||
//add modes to parser
|
||||
foreach($modes as $mode) {
|
||||
$Parser->addMode($mode['mode'], $mode['obj']);
|
||||
}
|
||||
|
||||
return $Parser->parse($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite the text of a page according to the recorded moves, the rewritten text is saved
|
||||
*
|
||||
* @param string $id The id of the page that shall be rewritten
|
||||
* @param string|null $text Old content of the page. When null is given the content is loaded from disk
|
||||
* @return string|bool The rewritten content, false on error
|
||||
*/
|
||||
public function rewritePage($id, $text = null, $save = true) {
|
||||
$meta = $this->getMoveMeta($id);
|
||||
if(is_null($text)) {
|
||||
$text = rawWiki($id);
|
||||
}
|
||||
|
||||
if($meta['pages'] || $meta['media']) {
|
||||
$old_text = $text;
|
||||
$text = $this->rewrite($id, $text);
|
||||
|
||||
$changed = ($old_text != $text);
|
||||
$file = wikiFN($id, '', false);
|
||||
if ($save === true) {
|
||||
if(is_writable($file) || !$changed) {
|
||||
if($changed) {
|
||||
// Wait a second when the page has just been rewritten
|
||||
$oldRev = filemtime(wikiFN($id));
|
||||
if($oldRev == time()) sleep(1);
|
||||
|
||||
saveWikiText($id, $text, $this->symbol . ' ' . $this->getLang('linkchange'), $this->getConf('minor'));
|
||||
}
|
||||
$this->unsetMoveMeta($id);
|
||||
} else {
|
||||
// FIXME: print error here or fail silently?
|
||||
msg('Error: Page ' . hsc($id) . ' needs to be rewritten because of page renames but is not writable.', -1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user