diff --git a/_test/pagemove.group.php b/_test/pagemove.group.php deleted file mode 100644 index 8c3a2e2..0000000 --- a/_test/pagemove.group.php +++ /dev/null @@ -1,10 +0,0 @@ -GroupTest('pagemove_group_test'); - $this->addTestFile($dir . 'pagemove.test.php'); - } -} diff --git a/_test/pagemove.test.php b/_test/pagemove.test.php index be19f58..8787d29 100644 --- a/_test/pagemove.test.php +++ b/_test/pagemove.test.php @@ -1,23 +1,24 @@ pluginsEnabled[] = 'pagemove'; global $ID; global $INFO; @@ -41,6 +42,9 @@ function setUp() { saveWikiText($this->movedId, $text, $summary); $INFO = pageinfo(); + $references = array_keys(p_get_metadata($this->movedId, 'relation references', METADATA_RENDER_UNLIMITED)); + idx_get_indexer()->addMetaKeys($this->movedId, 'relation_references', $references); + $text = <<movedId|$this->movedId]] [[:$this->movedId|:$this->movedId]] @@ -66,6 +70,8 @@ function setUp() { [[/start|/start]] EOT; saveWikiText($this->parentBacklinkingId, $text, $summary); + $references = array_keys(p_get_metadata($this->parentBacklinkingId, 'relation references', METADATA_RENDER_UNLIMITED)); + idx_get_indexer()->addMetaKeys($this->parentBacklinkingId, 'relation_references', $references); $text = <<movedId|$this->movedId]] @@ -96,6 +102,8 @@ function setUp() { [[/start|/start]] EOT; saveWikiText($this->currentNsBacklinkingId, $text, $summary); + $references = array_keys(p_get_metadata($this->currentNsBacklinkingId, 'relation references', METADATA_RENDER_UNLIMITED)); + idx_get_indexer()->addMetaKeys($this->currentNsBacklinkingId, 'relation_references', $references); $text = <<movedId|$this->movedId]] @@ -121,6 +129,8 @@ function setUp() { [[/start|/start]] EOT; saveWikiText($this->otherBacklinkingId, $text, $summary); + $references = array_keys(p_get_metadata($this->otherBacklinkingId, 'relation references', METADATA_RENDER_UNLIMITED)); + idx_get_indexer()->addMetaKeys($this->otherBacklinkingId, 'relation_references', $references); $text = <<movedId|$this->movedId]] @@ -150,8 +160,11 @@ function setUp() { [[/start|/start]] EOT; saveWikiText($this->subNsPage, $text, $summary); + $references = array_keys(p_get_metadata($this->subNsPage, 'relation references', METADATA_RENDER_UNLIMITED)); + idx_get_indexer()->addMetaKeys($this->subNsPage, 'relation_references', $references); $this->pagemove = new admin_plugin_pagemove(); + parent::setUp(); } # function testPagemove() { @@ -181,18 +194,18 @@ function test_move_page_in_same_ns() { $expectedContent = <<assertEqual($expectedContent, $newContent); + $this->assertEquals($expectedContent, $newContent); $newContent = rawWiki($this->parentBacklinkingId); $expectedContent = <<assertEqual($expectedContent, $newContent); + $this->assertEquals($expectedContent, $newContent); $newContent = rawWiki($this->currentNsBacklinkingId); $expectedContent = <<assertEqual($expectedContent, $newContent); + $this->assertEquals($expectedContent, $newContent); $newContent = rawWiki($this->otherBacklinkingId); $expectedContent = <<assertEqual($expectedContent, $newContent); + $this->assertEquals($expectedContent, $newContent); } @@ -299,18 +312,18 @@ function test_move_page_to_parallel_ns() { $expectedContent = <<assertEqual($expectedContent, $newContent); + $this->assertEquals($expectedContent, $newContent); $newContent = rawWiki($this->parentBacklinkingId); $expectedContent = <<assertEqual($expectedContent, $newContent); + $this->assertEquals($expectedContent, $newContent); $newContent = rawWiki($this->currentNsBacklinkingId); $expectedContent = <<assertEqual($expectedContent, $newContent); + $this->assertEquals($expectedContent, $newContent); $newContent = rawWiki($this->otherBacklinkingId); $expectedContent = <<assertEqual($expectedContent, $newContent); + $this->assertEquals($expectedContent, $newContent); } @@ -418,18 +431,18 @@ function test_move_page_to_parent_ns() { $expectedContent = <<assertEqual($expectedContent, $newContent); + $this->assertEquals($expectedContent, $newContent); // page is moved to same NS as backlinking page (parent_ns) $newContent = rawWiki($this->parentBacklinkingId); @@ -457,7 +470,7 @@ function test_move_page_to_parent_ns() { [[parent_ns/new_page|parent_ns/new_page]] [[/start|/start]] EOT; - $this->assertEqual($expectedContent, $newContent); + $this->assertEquals($expectedContent, $newContent); $newContent = rawWiki($this->currentNsBacklinkingId); $expectedContent = <<assertEqual($expectedContent, $newContent); + $this->assertEquals($expectedContent, $newContent); $newContent = rawWiki($this->otherBacklinkingId); $expectedContent = <<assertEqual($expectedContent, $newContent); + $this->assertEquals($expectedContent, $newContent); } function test_move_ns_in_same_ns() { - global $ID; $newNamespace = 'new_ns'; + $newPagename = ''; $opts = array(); $opts['page_ns'] = 'ns'; @@ -533,15 +546,5 @@ function test_move_ns_in_same_ns() { } - function tearDown() { - saveWikiText($this->movedId, '', 'removed'); - saveWikiText($this->movedToId, '', 'removed'); - saveWikiText($this->parentBacklinkingId, '', 'removed'); - saveWikiText($this->currentNsBacklinkingId, '', 'removed'); - saveWikiText($this->otherBacklinkingId, '', 'removed'); - saveWikiText($this->subNsPage, '', 'removed'); - } - } -?> \ No newline at end of file diff --git a/action.php b/action.php new file mode 100644 index 0000000..31169ed --- /dev/null +++ b/action.php @@ -0,0 +1,72 @@ + + */ +class action_plugin_pagemove extends DokuWiki_Action_Plugin { + /** + * Register event handlers. + * + * @param Doku_Event_Handler $controller The plugin controller + */ + public function register($controller) { + $controller->register_hook('IO_WIKIPAGE_READ', 'AFTER', $this, 'handle_read', array()); + $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'handle_cache', array()); + } + + /** + * Rewrite pages when they are read and they need to be updated. + * + * @param Doku_Event $event The event object + * @param mixed $param Optional parameters (not used) + */ + function handle_read(Doku_Event $event, $param) { + static $stack = array(); + // handle only reads of the current revision + if ($event->data[3]) return; + + $id = $event->data[2]; + if ($event->data[1]) $id = $event->data[1].':'.$id; + if (isset($stack[$id])) return; + $meta = p_get_metadata($id, 'plugin_pagemove', METADATA_DONT_RENDER); + if ($meta && isset($meta['moves'])) { + $stack[$id] = true; + $helper = $this->loadHelper('pagemove', true); + if (!is_null($helper)) { + $event->result = $helper->rewrite_content($event->result, $id, $meta['moves']); + } + $file = wikiFN($id, '', false); + if (is_writable($file)) { + saveWikiText($id,$event->result,$this->getLang('pm_linkchange')); + unset($meta['moves']); + p_set_metadata($id, array('plugin_pagemove' => $meta), false, true); + } 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); + } + unset($stack[$id]); + } + } + + /** + * Handle the cache events, it looks if a page needs to be rewritten so it can expire the cache of the page + * + * @param Doku_Event $event The even object + * @param mixed $param Optional parameters (not used) + */ + function handle_cache(Doku_Event $event, $param) { + /** @var $cache cache_parser */ + $cache = $event->data; + $id = $cache->page; + if ($id) { + $meta = p_get_metadata($id, 'plugin_pagemove', METADATA_DONT_RENDER); + if ($meta && isset($meta['moves'])) { + $file = wikiFN($id, '', false); + if (is_writable($file)) + $cache->depends['purge'] = true; + 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); + } + } + } +} \ No newline at end of file diff --git a/admin.php b/admin.php index e6d7c94..0930bb0 100644 --- a/admin.php +++ b/admin.php @@ -16,9 +16,6 @@ require_once DOKU_PLUGIN.'admin.php'; -require_once(DOKU_INC.'inc/search.php'); - - class admin_plugin_pagemove extends DokuWiki_Admin_Plugin { var $show_form = true; @@ -26,11 +23,10 @@ class admin_plugin_pagemove extends DokuWiki_Admin_Plugin { var $locked_files = array(); var $errors = array(); var $opts = array(); - var $text = ''; var $idsToDelete = array(); - function getMenuSort() { return FIXME; } + function getMenuSort() { return 1000; } function forAdminOnly() { return false; } /** @@ -81,6 +77,7 @@ function getMenuText() { * @author Gary Owen */ function html() { + global $ID; global $lang; ptln(''); @@ -111,7 +108,7 @@ function html() { } else { // display the moved/renamed page - ptln( $this->render($this->text) ); + p_wiki_xhtml($ID); } ptln(''); } @@ -236,7 +233,7 @@ function _pm_form() { function _pm_form_create_list_ns($ns) { global $conf; - $namesp = array( 0 => '' ); //Include root + $namesp = array( 0 => array('id' => '') ); //Include root search($namesp, $conf['datadir'], 'search_namespaces', array()); sort($namesp); foreach($namesp as $row) { @@ -310,24 +307,8 @@ function handle() { $this->opts['newns'] = $_REQUEST['ns'].':'.$this->opts['newnsname']; } - // check the NS if a recursion is needed - // @fixme Is this still needed? - $pagelist = array(); - $needrecursion = false; - $nsRelPath = utf8_encodeFN(str_replace(':', '/', $this->opts['ns'])); - search($items, $conf['datadir'], 'search_index', '', $nsRelPath); - foreach ($items as $item) { - if ($item['type'] == 'd') { - $needrecursion = true; - break; - } - } - $nsRelPath = utf8_encodeFN(str_replace(':', '/', $this->opts['ns'])); $this->_pm_move_recursive($nsRelPath, $this->opts); - - $newNsAbsPath = $conf['datadir'].'/'.str_replace(':', '/', $this->opts['newns']); - $this->_pm_disable_cache($newNsAbsPath); } elseif( $this->opts['page_ns'] == 'page' ) { if( $_REQUEST['pagename'] == '' ) { @@ -369,8 +350,7 @@ function handle() { // @todo if the namespace is now empty, delete it // Set things up to display the new page. - io_saveFile($conf['cachedir'].'/purgefile', time()); - $ID = $opts['new_id']; + $ID = $this->opts['new_id']; $ACT = 'show'; $INFO = pageinfo(); $this->show_form = false; @@ -394,31 +374,6 @@ function handle() { } - /** - * touch every file which was moved, because of cached backlinks inside of moved namespace - * - * @author Arno Puschmann 2010-01-29 - * @param $pathToSearch - * @return unknown_type - */ - function _pm_disable_cache($pathToSearch) { - $files = scandir($pathToSearch); - if( !empty($files) ) { - foreach($files as $file) { - if( $file == '.' || $file == '..' ) continue; - if( is_dir($pathToSearch.'/'.$file) ) { - $this->_pm_disable_cache($pathToSearch.'/'.$file); - } - else { - if( preg_match('#\.txt$#', $file) ) { - touch($pathToSearch.'/'.$file, time()+1); - } - } - } - } - } - - /** * * @author Bastian Wolf @@ -431,7 +386,7 @@ function _pm_move_recursive($pathToSearch, $opts) { global $conf; $pagelist = array(); - search($pagelist, $conf['datadir'], 'search_index', '', $pathToSearch); + search($pagelist, $conf['datadir'], 'search_index', array(), $pathToSearch); foreach ($pagelist as $page) { if ($page['type'] == 'd') { @@ -501,20 +456,6 @@ function _pm_move_page($opts) { $this->locked_files[] = $ID; } - // get all backlink information - $backlinksById = array(); - $this->_pm_search($backlinksById, $conf['datadir'], '_pm_search_backlinks', $opts); - - // Check we have edit rights on the backlinks and they are not locked - foreach($backlinksById as $backlinkingId=>$backlinks) { - if (auth_quickaclcheck($backlinkingId) < AUTH_EDIT) { - $this->have_rights = false; - } - if (checklock($backlinkingId)) { - $this->locked_files[] = $backlinkingId; - } - } - // Assemble fill document name and path $opts['new_id'] = cleanID($opts['newns'].':'.$opts['newname']); $opts['new_path'] = wikiFN($opts['new_id']); @@ -539,296 +480,89 @@ function _pm_move_page($opts) { * End of init (checks) */ - // Open the old document and change forward links - lock($ID); - $this->text = io_readFile(wikiFN($ID), True); + $page_meta = p_get_metadata($ID, 'plugin_pagemove', METADATA_DONT_RENDER); + if (!$page_meta) $page_meta = array(); + if (!isset($page_meta['old_ids'])) $page_meta['old_ids'] = array(); + $page_meta['old_ids'][$ID] = time(); - // Get an array of forward links from the document - $forward = $this->_pm_getforwardlinks($ID); + // 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', array_keys($page_meta['old_ids'])); - // Change the forward links - foreach($forward as $lnk => $lid) { - // Get namespace of target document - $tns = getNS($lid); - $tname = noNS($lid); - // Form new document ID for the target + $data = array('opts' => &$opts, 'old_ids' => $page_meta['old_ids'], 'affected_pages' => &$affected_pages); + // 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 + // note that old_ids is in the form 'id' => timestamp of move and affected_pages is indexed by these ids + $event = new Doku_Event('PAGEMOVE_PAGE_RENAME', $data); + if ($event->advise_before()) { + // Open the old document and change forward links + lock($ID); + $text = rawWiki($ID); - $matches = array(); - if ( $tns == $opts['newns'] ) { - // Document is in same namespace as target - $this->_pm_updatelinks($this->text, array($lnk => $tname)); - } - elseif ( preg_match('#^'.$opts['newns'].':(.*:)$#', $tns, $matches) ) { - // Target is in a sub-namespace - $this->_pm_updatelinks($this->text, array($lnk => '.:'.$matches[1].':'.$tname)); + /** @var helper_plugin_pagemove $helper */ + $helper = $this->loadHelper('pagemove', true); + if (is_null($helper)) return; + + $text = $helper->rewrite_content($text, $ID, array($ID => $opts['new_id'])); + + // Move the Subscriptions & Indexes + $this->_pm_movemeta('metadir', '/^'.$opts['name'].'\.\w*?$/', $opts); + + // Save the updated document in its new location + if ($opts['ns'] == $opts['newns']) { + $lang_key = 'pm_renamed'; } - elseif ( $tns == "" ) { - // Target is in root namespace - $this->_pm_updatelinks($this->text, array($lnk => $lid )); + elseif ( $opts['name'] == $opts['newname'] ) { + $lang_key = 'pm_moved'; } else { - $this->_pm_updatelinks($this->text, array($lnk => $lid )); + $lang_key = 'pm_move_rename'; } - } + $summary = sprintf($this->lang[$lang_key], $ID, $opts['new_id']); + saveWikiText($opts['new_id'], $text, $summary); - if ( $opts['ns'] != $opts['newns'] ) { - // Change media links when moving between namespaces - $media = $this->_pm_getmedialinks($ID); - foreach($media as $lnk => $lid) { - $tns = getNS($lid); - $tname = noNS($lid); - // Form new document id for the target - $matches = array(); - if ( $tns == $opts['newns'] ) { - // Document is in same namespace as target - $this->_pm_updatemedialinks($this->text, $lnk, $tname ); - } - elseif ( preg_match('#^'.$opts['newns'].':(.*:)$#', $tns, $matches) ) { - // Target is in a sub-namespace - $this->_pm_updatemedialinks($this->text, $lnk, '.:'.$matches[1].':'.$tname ); - } - elseif ( $tns == "" ) { - // Target is in root namespace - $this->_pm_updatemedialinks($this->text, $lnk, ':'.$lid ); - } - else { - $this->_pm_updatemedialinks($this->text, $lnk, $lid ); - } + // Delete the orginal file + if (@file_exists(wikiFN($opts['new_id']))) { + saveWikiText($ID, '', $this->lang['pm_delete'] ); } - } - - // Move the Subscriptions & Indexes - $this->_pm_movemeta('metadir', '/^'.$opts['name'].'\.\w*?$/', $opts); - - // Save the updated document in its new location - if ($opts['ns'] == $opts['newns']) { - $lang_key = 'pm_renamed'; - } - elseif ( $opts['name'] == $opts['newname'] ) { - $lang_key = 'pm_moved'; - } - else { - $lang_key = 'pm_move_rename'; - } - $summary = sprintf($this->lang[$lang_key], $ID, $opts['new_id']); - saveWikiText($opts['new_id'], $this->text, $summary); - - // Delete the orginal file - if (@file_exists(wikiFN($opts['new_id']))) { - saveWikiText($ID, '', $this->lang['pm_delete'] ); - } - - // Loop through backlinks - foreach($backlinksById as $backlinkingId => $backlinks) { - $this->_pm_updatebacklinks($backlinkingId, $backlinks, $opts, $brackets); - } - - // Move the old revisions - $this->_pm_movemeta('olddir', '/^'.$opts['name'].'\.[0-9]{10}\.txt(\.gz)?$/', $opts); - - } + // Move the old revisions + $this->_pm_movemeta('olddir', '/^'.$ID.'\.[0-9]{10}\.txt(\.gz)?$/', $opts); - /** - * Modify the links in a backlink. - * - * @param id Page ID of the backlinking page - * @param links Array of page names on this page. - * - * @author Gary Owen - */ - function _pm_updatebacklinks($backlinkingId, $links, $opts, &$brackets) { - global $ID; - - // Get namespace of document we are editing - $bns = getNS($backlinkingId); - - // Create a clean version of the new name - $cleanname = cleanID($opts['newname']); + asort($page_meta['old_ids']); - // Open backlink - lock($backlinkingId); - $text = io_readFile(wikiFN($backlinkingId),True); - - // Form new document ID for this backlink - $matches = array(); - // new page is in same namespace as backlink - if ( $bns == $opts['newns'] ) { - $replacementNamespace = ''; - } - // new page is in sub-namespace of backlink - elseif ( preg_match('#^'.$bns.':(.*)$#', $opts['newns'], $matches) ) { - $replacementNamespace = '.:'.$matches[1].':'; - } - // not same or sub namespace: use absolute reference - else { - $replacementNamespace = $opts['newns'].':'; - } - - // @fixme stupid: for each page get original backlink and its replacement - $matches = array(); - // get an array of: backlinks => replacement - $oid = array(); - if ( $bns == $opts['ns'] ) { - // old page was in same namespace as backlink - foreach ( $links as $link ) { - $oid[$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); - $oid['.:'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); - $oid['.'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); - } - } - if ( preg_match('#^'.$bns.':(.*)$#', $opts['ns'], $matches) ) { - // old page was in sub namespace of backlink namespace - foreach ( $links as $link ) { - $oid['.:'.$matches[1].':'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); - $oid['.'.$matches[1].':'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); - } - } - if ( preg_match('#^'.$opts['ns'].':(.*)$#', $bns , $matches) && $opts['page_ns'] == 'page' ) { - // old page was in upper namespace of backlink - foreach ( $links as $link ) { - $oid['..:'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); - $oid['..'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); - $oid['.:..:'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); - } - } - // replace all other links - foreach ( $links as $link ) { - // absolute links - $oid[$opts['ns'].':'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); - //$oid['.:'.$opts['ns'].':'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); - - // check backwards relative links - $relLink = $link; - $relDots = '..'; - $backlinkingNamespaceCount = count(explode(':', $bns)); - $oldNamespaces = explode(':', $opts['ns'], $backlinkingNamespaceCount); - $oldNamespaceCount = count($oldNamespaces); - if ($backlinkingNamespaceCount > $oldNamespaceCount) { - $levelDiff = $backlinkingNamespaceCount - $oldNamespaceCount; - for ($i = 0; $i < $levelDiff; $i++) { - $relDots .= ':..'; + // additional pages that should be considered because they were affected by moves from previous names + // if the page has been rendered in the meantime and but the new links aren't in the index yet the + // page might need information about a more recent rename even though it is not listed for this more recent link + $additional_pages = array(); + foreach ($page_meta['old_ids'] as $page_id => $time) { + if (!isset($affected_pages[$page_id])) { + $affected_pages[$page_id] = $additional_pages; + } else { + $affected_pages[$page_id] = array_unique(array_merge($affected_pages[$page_id], $additional_pages)); } - } - - foreach (array_reverse($oldNamespaces) as $nextUpperNs) { - $relLink = $nextUpperNs.':'.$relLink; - foreach (array($relDots.$relLink, $relDots.':'.$relLink) as $dottedRelLink) { - $absLink=$dottedRelLink; - resolve_pageid($bns, $absLink, $exists); - if ($absLink == $ID) { - $oid[$dottedRelLink] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); - } + foreach ($affected_pages[$page_id] as $id) { + if (!page_exists($id, '', false) || $id == $page_id || $id == $opts['new_id']) continue; + // if the page has been modified since the rename of the old page, the link in the new page is most + // probably intentionally to the old page and shouldn't be changed + if (filemtime(wikiFN($id, '', false)) > $time) continue; + $additional_pages[] = $id; + // we are only interested in persistent metadata, so no need to render anything. + $meta = p_get_metadata($id, 'plugin_pagemove', METADATA_DONT_RENDER); + if (!$meta) $meta = array('moves' => array()); + if (!isset($meta['moves'])) $meta['moves'] = array(); + $meta['moves'][$page_id] = $opts['new_id']; + // remove redundant moves (can happen when a page is moved back to its old id) + if ($page_id == $opts['new_id']) unset($meta['moves'][$page_id]); + if (empty($meta['moves'])) unset($meta['moves']); + p_set_metadata($id, array('plugin_pagemove' => $meta), false, true); } - $relDots = '..:'.$relDots; } - //$oid['..:'.$opts['ns'].':'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); - //$oid['..'.$opts['ns'].':'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); - } - - // Make the changes - $this->_pm_updatelinks($text, $oid); - - // Save backlink and release lock - saveWikiText($backlinkingId, $text, sprintf($this->lang['pm_linkchange'], $ID, $opts['new_id'])); - unlock($backlinkingId); - } - - /** - * modify the links using the pairs in $links - * - * @author Gary Owen - */ - function _pm_updatelinks(&$text, $links) { - foreach( $links as $old => $new ) { - $text = preg_replace( '#\[\[:?' . $old . '((\]\])|[\|\#])#i', '[[' . $new . '\1', $text); - } - } - - /** - * modify the medialinks from namepspace $old to namespace $new - * - * @author Gary Owen - */ - function _pm_updatemedialinks(&$text, $old, $new) { - // Question marks in media links need some extra handling - $text = preg_replace('#\{\{' . $old . '([\?\|]|(\}\}))#i', '{{' . $new . '\1', $text); - } - - /** - * Get forward links in a given page which need to be changed. - * - * Not changed: local sections, absolute links - * Changed need to be - * - * @author Gary Owen - */ - function _pm_getforwardlinks($id) { - $data = array(); - $text = io_readfile(wikiFN($id)); - - // match all links - // FIXME may be incorrect because of code blocks - // TODO CamelCase isn't supported, too - preg_match_all('#\[\[(.+?)\]\]#si', $text, $matches, PREG_SET_ORDER); - foreach($matches as $match) { - // ignore local headings [[#some_heading]] - if ( preg_match('/^#/', $match[1])) continue; - - // get ID from link and discard most non wikilinks - list($mid) = split('[\|#]', $match[1], 2); - // ignore links with URL schema prefix ([[prefix://]]) - if(preg_match('#^\w+://#', $mid)) continue; - // if(preg_match('#^(https?|telnet|gopher|file|wais|ftp|ed2k|irc)://#',$mid)) continue; - // inter-wiki link - if(preg_match('#\w+>#', $mid)) continue; - // baselink ([[/some_link]]) - if(preg_match('#^/#', $mid)) continue; - // email addresses - if(strpos($mid, '@') !== FALSE) continue; - // ignore absolute links - if( strpos($mid, ':') === 0 ) continue; - - $absoluteMatchId = $mid; - $exists = FALSE; - resolve_pageid(getNS($id), $absoluteMatchId, $exists); - if($absoluteMatchId != FALSE) { - $data[$mid] = $absoluteMatchId; - } + p_set_metadata($opts['new_id'], array('plugin_pagemove' => $page_meta), false, true); } - return $data; - } - /** - * Get media links in a given page - * - * @author Gary Owen - */ - function _pm_getmedialinks($id) { - $data = array(); - $text = io_readfile(wikiFN($id)); - // match all links - // FIXME may be incorrect because of code blocks - // TODO CamelCase isn't supported, too - preg_match_all('#{{(.[^>]+?)}}#si', $text, $matches, PREG_SET_ORDER); - foreach($matches as $match) { - // get ID from link and discard most non wikilinks - list($mid) = split('(\?|\|)', $match[1], 2); - $mns = getNS($mid); - $lnk = $mid; - - // namespace starting with "." - prepend current namespace - if(strpos($mns, '.')===0) { - $mid = getNS($id).':'.substr($mid, 1); - } - elseif($mns === FALSE){ - // no namespace in link? add current - $mid = getNS($id) . ':' . $mid; - } - $data[$lnk] = preg_replace('#:+#', ':', $mid); - } - return $data; + $event->advise_after(); } /** @@ -857,109 +591,4 @@ function _pm_movemeta($dir, $regex, $opts) { closedir($dh); } } - - - /** - * recurse directory - * - * This function recurses into a given base directory - * and calls the supplied function for each file and directory - * - * @author Andreas Gohr - * @param array $data Found data is collected - * @param string $base Directory to be searched in - * @param string $func Name of real search function - * @param array $opts Options to the search functions - * @param string $dir Current relative directory - * @param integer $lvl Level of recursion - */ - function _pm_search(&$data, $base, $func, $opts, $dir='' ,$lvl=1) { - $dirs = array(); - $files = array(); - - // read in directories and files - $dh = @opendir($base.'/'.$dir); - if(!$dh) return; - while(($file = readdir($dh)) !== false) { - // skip hidden files and upper dirs - if(preg_match('/^\./',$file)) continue; - if(is_dir($base.'/'.$dir.'/'.$file)) { - $dirs[] = $dir.'/'.$file; - continue; - } - $files[] = $dir.'/'.$file; - } - closedir($dh); - sort($files); - sort($dirs); - - // give directories to userfunction then recurse - foreach($dirs as $dir) { - if ($this->$func($data, $base, $dir, 'd', $lvl, $opts)) { - $this->_pm_search($data, $base, $func, $opts, $dir, $lvl+1); - } - } - // now handle the files - foreach($files as $file) { - $this->$func($data, $base, $file, 'f', $lvl, $opts); - } - } - - /** - * Search for backlinks to a given page - * - * $opts['ns'] namespace of the page - * $opts['name'] name of the page without namespace - * - * @author Andreas Gohr - * @author Gary Owen - */ - function _pm_search_backlinks(&$data, $base, $file, $type, $lvl, $opts) { - // we do nothing with directories - if($type == 'd') return true; - // only search txt files - if(!preg_match('#\.txt$#', $file)) return true; - - $text = io_readfile($base.'/'.$file); - // absolute search ID -// $absSearchedId = cleanID($opts['ns'].':'.$opts['name']); - $absSearchedId = $opts['name']; - resolve_pageid($opts['ns'], $absSearchedId, $exists); - - // construct current namespace - $cid = pathID($file); - $cns = getNS($cid); - - // match all links - // FIXME may be incorrect because of code blocks - // FIXME CamelCase isn't supported, too - preg_match_all('#\[\[(.+?)\]\]#si', $text, $matches, PREG_SET_ORDER); - foreach($matches as $match) { - // get ID from link and discard most non wikilinks - list($matchLink) = split('[\|#]', $match[1], 2); - // all URLs with a scheme - if(preg_match('#^\w+://#', $matchLink)) continue; -// if(preg_match('#^(https?|telnet|gopher|file|wais|ftp|ed2k|irc)://#',$matchLink)) continue; - // baselinks - if(preg_match('#^/#', $matchLink)) continue; - // inter-wiki links - if(preg_match('#\w+>#', $matchLink)) continue; - // email addresses - if(strpos($matchLink, '@') !== FALSE) continue; - - // get the ID the link refers to by cleaning and resolving it - $matchId = cleanID($matchLink); - resolve_pageid($cns, $matchId, $exists); - $matchPagename = ltrim(noNS($matchId), '.:'); - - // only collect IDs not in collected $data already - if ($matchId == $absSearchedId // matching link refers to the searched ID - && (! array_key_exists($cid, $data) // not in $data already - || empty($data[$cid]) - || ! in_array($matchPagename, $data[$cid]))) { - // @fixme return original link and its replacement - $data[$cid][] = $matchPagename; - } - } - } } \ No newline at end of file diff --git a/helper.php b/helper.php new file mode 100644 index 0000000..dcada54 --- /dev/null +++ b/helper.php @@ -0,0 +1,380 @@ + + */ +class helper_plugin_pagemove extends DokuWiki_Plugin { + /** + * Rewrite a text in order to fix the content after the given moves. + * + * @param string $text The wiki text that shall be rewritten + * @param string $id The id of the wiki page, if the page itself was moved the old id + * @param array $moves Array of all moves, the keys are the old ids, the values the new ids + * @return string The rewritten wiki text + */ + function rewrite_content($text, $id, $moves) { + // resolve moves of pages that were moved more than once + $tmp_moves = array(); + foreach($moves as $old => $new) { + if($old != $id && isset($moves[$new]) && $moves[$new] != $new) { + // write to temp array in order to correctly handle rename circles + $tmp_moves[$old] = $moves[$new]; + } + } + + $changed = !empty($tmp_moves); + + // this correctly resolves rename circles by moving forward one step a time + while($changed) { + $changed = false; + foreach($tmp_moves as $old => $new) { + if(isset($moves[$new]) && $moves[$new] != $new) { + $tmp_moves[$old] = $moves[$new]; + $changed = true; + } + } + } + + // manual merge, we can't use array_merge here as ids can be numeric + foreach($tmp_moves as $old => $new) { + $moves[$old] = $new; + } + + $handlers = array(); + $data = array('id' => $id, 'moves' => &$moves, 'handlers' => &$handlers); + + /* + * PAGEMOVE_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. + * It has the following properties and functions that can be used: + * - id, ns: id and namespace of the old page + * - new_id, new_ns: new id and namespace (can be identical to id and ns) + * - moves: array of moves, the same as $moves in the event + * - adaptRelativeId($id): adapts the relative $id according to the moves + */ + trigger_event('PAGEMOVE_HANDLERS_REGISTER', $data); + + $modes = p_get_parsermodes(); + + // Create the parser + $Parser = new Doku_Parser(); + + // Add the Handler + $Parser->Handler = new helper_plugin_pagemove_handler($id, $moves, $handlers); + + //add modes to parser + foreach($modes as $mode) { + $Parser->addMode($mode['mode'], $mode['obj']); + } + + return $Parser->parse($text); + } +} + +/** + * Handler class for pagemove. It does the actual rewriting of the content. + */ +class helper_plugin_pagemove_handler { + public $calls = ''; + public $id; + public $ns; + public $new_id; + public $new_ns; + public $moves; + private $handlers; + + /** + * Construct the pagemove handler. + * + * @param string $id The id of the text that is passed to the handler + * @param array $moves Moves that shall be considered in the form $old => $new ($old can be $id) + * @param array $handlers Handlers for plugin content in the form $plugin_anme => $callback + */ + public function __construct($id, $moves, $handlers) { + $this->id = $id; + $this->ns = getNS($id); + $this->moves = $moves; + $this->handlers = $handlers; + if (isset($moves[$id])) { + $this->new_id = $moves[$id]; + $this->new_ns = getNS($moves[$id]); + } else { + $this->new_id = $id; + $this->new_ns = $this->ns; + } + } + + /** + * 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) { + if ($this->ns) + $old = cleanID($this->ns.':'.$match); + else + $old = cleanID($match); + if (isset($this->moves[$old]) || $this->id != $this->new_id) { + if (isset($this->moves[$old])) { + $new = $this->moves[$old]; + } else { + $new = $old; + } + $new_ns = getNS($new); + // preserve capitalization either in the link or in the title + if (noNS($new) == noNS($old)) { + // camelcase link still seems to work + if ($new_ns == $this->new_ns) { + $this->calls .= $match; + } else { // just the namespace was changed, the camelcase word is a valid id + $this->calls .= "[[$new_ns:$match]]"; + } + } else { + $this->calls .= "[[$new|$match]]"; + } + } else { + $this->calls .= $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; + } + $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 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->adaptRelativeId($id); + + 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) { + $p = Doku_Handler_Parse_Media($match); + if ($p['type'] == 'internalmedia') { + $new_src = $this->adaptRelativeId($p['src']); + if ($new_src == $p['src']) { + $this->calls .= $match; + } else { + // 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']); + $this->calls .= substr_replace($match, $new_src, $srcpos, $srclen); + } + } else { // external media + $this->calls .= $match; + } + return true; + } + + /** + * 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); + } + + /** + * Adapts a link respecting all moves and making it a relative link according to the new id + * + * @param string $id A relative id + * @return string The relative id, adapted according to the new/old id and the moves + */ + public function adaptRelativeId($id) { + global $conf; + + if ($id === '') { + return $id; + } + + $abs_id = str_replace('/', ':', $id); + $abs_id = resolve_id($this->ns, $abs_id, false); + if (substr($abs_id, -1) === ':') + $abs_id .= $conf['start']; + $clean_id = cleanID($abs_id); + // 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($clean_id, -1) === ':') + $clean_id .= $conf['start']; + + if (isset($this->moves[$clean_id]) || $this->ns !== $this->new_ns) { + if (isset($this->moves[$clean_id])) { + $new = $this->moves[$clean_id]; + } else { + $new = $clean_id; + + // only the namespace was changed so if the link still resolves to the same absolute id, we can skip the rest + $new_abs_id = str_replace('/', ':', $id); + $new_abs_id = resolve_id($this->new_ns, $new_abs_id, false); + if (substr($new_abs_id, -1) === ':') + $new_abs_id .= $conf['start']; + if ($new_abs_id == $abs_id) return $id; + } + $new_link = $new; + $new_ns = getNS($new); + // try to keep original pagename + if ($this->noNS($new) == $this->noNS($clean_id)) { + if ($new_ns == $this->new_ns) { + $new_link = $this->noNS($id); + if ($new_link === false) $new_link = $this->noNS($new); + if ($id == ':') + $new_link = ':'; + else if ($id == '/') + $new_link = '/'; + } else if ($new_ns != false) { + $new_link = $new_ns.':'.$this->noNS($id); + } else { + $new_link = $this->noNS($id); + if ($new_link === false) $new_link = $new; + } + } else if ($new_ns == $this->new_ns) { + $new_link = $this->noNS($new_link); + } else if (strpos($new_ns, $this->ns.':') === 0) { + $new_link = '.:'.substr($new_link, strlen($this->ns)+1); + } + + if ($this->new_ns != '' && $new_ns == false) { + $new_link = ':'.$new_link; + } + + return $new_link; + } else { + return $id; + } + } + + private function noNS($id) { + $pos = strrpos($id, ':'); + $spos = strrpos($id, '/'); + if ($pos === false) $pos = $spos; + if ($spos === false) $spos = $pos; + $pos = max($pos, $spos); + if ($pos!==false) { + return substr($id, $pos+1); + } else { + return $id; + } + } +} +