setupLocale(); } /** * return some info */ function getInfo(){ return array( 'author' => 'Gary Owen, Arno Puschmann, Christoph Jähnigen', 'email' => 'pagemove@gmail.com', 'date' => '2011-08-11', 'name' => 'Pagemove', 'desc' => $this->lang['desc'], 'url' => 'http://www.dokuwiki.org/plugin:pagemove', ); } /** * Only show the menu text for pages we can move or rename. */ function getMenuText() { global $INFO; global $ID; global $conf; if( !$INFO['exists'] ) return $this->lang['menu'].' ('.$this->lang['pm_notexist'].')'; elseif( $ID == $conf['start'] ) return $this->lang['menu'].' ('.$this->lang['pm_notstart'].')'; elseif( !$INFO['writable'] ) return $this->lang['menu'].' ('.$this->lang['pm_notwrite'].')'; else return $this->lang['menu']; } /** * output appropriate html * * @author Gary Owen */ function html() { global $lang; ptln(''); if( $this->show_form ) { ptln( $this->locale_xhtml('pagemove') ); //We didn't get here from submit. if( $this->have_rights && count($this->locked_files) == 0 ) { $this->_pm_form(); } else { ptln( '

' ); if ( !$this->have_rights ) { ptln( $this->errors[0].'
' ); } $c = count($this->locked_files); if ( $c == 1 ) { ptln( $this->lang['pm_filelocked'].$this->locked_files[0].'
'.$this->lang['pm_tryagain'] ); } elseif ( $c > 1 ) { ptln( $this->lang['pm_fileslocked'] ); for ( $i = 0 ; $i < $c ; $i++ ) { ptln ( ($i > 0 ? ', ' : '').$this->locked_files[$i] ); } ptln( '
'.$this->lang['pm_tryagain'] ); } ptln ( '

' ); } } else { // display the moved/renamed page ptln( $this->render($this->text) ); } ptln(''); } /** * show the move and/or rename a page form * * @author Gary Owen */ function _pm_form() { global $ID; global $lang; global $conf; $ns = getNS($ID); $name = noNS($ID); ptln('
'); ptln(' '); ptln('
'); // output hidden values to ensure dokuwiki will return back to this plugin ptln(' '); ptln(' '); ptln(' '); ptln('
'); ptln(' '. $this->lang['pm_movepage'] .''); ptln(' '); //Show any errors if (count($this->errors) > 0) { ptln (''); } //create a list of namespaces ptln( ' '); ptln( ' '); ptln( ' \n "); ptln( ' '); ptln( ' '); ptln( ' '); ptln( ' '); ptln( ' '); ptln( ' '); ptln(' '); // ptln( ' '); ptln( ' '); ptln( ' '); ptln( ' '); ptln( '
'); foreach($this->errors as $error) { ptln ($error.'
'); } ptln ('
' ? 'CHECKED' : '').'>
'); ptln( '
'); ptln('
'); ptln('
'); ptln(' '. $this->lang['pm_movens'] .''); ptln(' '); ptln( ' '); ptln( ' \n "); ptln( ' '); ptln( ' '); ptln( ' '); ptln( ' '); ptln( '
'); ptln('
'); ptln( '
'); ptln( '
'); ptln(''); ptln(' "); ptln( '
'); } /** * create a list of namespaces for the html form * * @author Gary Owen * @author Arno Puschmann (bin out of _pm_form) */ function _pm_form_create_list_ns($ns) { global $conf; $namesp = array( 0 => '' ); //Include root search($namesp, $conf['datadir'], 'search_namespaces', array()); sort($namesp); foreach($namesp as $row) { if ( auth_quickaclcheck($row['id'].':*') >= AUTH_CREATE || $row['id'] == $ns ) { ptln ( ' " ); } } } /** * handle user request * * @author Gary Owen */ function handle() { global $conf; global $lang; global $ID; global $INFO; global $ACT; // check we have rights to move this document if( !$INFO['exists'] ) { $this->have_rights = false; $this->errors[] = $this->lang['pm_notexist']; return; } // do not move start page if( $ID == $conf['start'] ) { $this->have_rights = false; $this->errors[] = $this->lang['pm_notstart']; return; } // was a form send? if (! array_key_exists('page_ns', $_REQUEST)) { // @fixme do something more intelligent like showing in message return; } // extract namespace and document name from ID $this->opts['ns'] = getNS($ID); $this->opts['name'] = noNS($ID); $this->opts['page_ns'] = $_REQUEST['page_ns']; // check the input for completeness if( $this->opts['page_ns'] == 'ns' ) { // @todo Target namespace needn't be new (check pages for overwrite!) if( $_REQUEST['namespacename'] == '' ) { $this->errors[] = $this->lang['pm_emptynamespace']; return; } $this->opts['newnsname'] = $_REQUEST['namespacename']; if ( cleanID($this->opts['newnsname']) == '' ) { $this->errors[] = $this->lang['pm_badns']; return; } if ($_REQUEST['ns'] == ':') { $this->opts['newns'] = $this->opts['newnsname']; } else { $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'] == '' ) { $this->errors[] = $this->lang['pm_emptypagename']; return; } $this->opts['newname'] = $_REQUEST['pagename']; // check that the pagename is valid if ( cleanID($this->opts['newname']) == '' ) { $this->errors[] = $this->lang['pm_badname']; return; } if ($_REQUEST['nsr'] == '') { $this->opts['newns'] = ($_REQUEST['ns_for_page'] == ':' ? '' : $_REQUEST['ns_for_page']); } elseif ($_REQUEST['nsr'] =='') { // if a new namespace was requested, check and use it if ($_REQUEST['newns'] != '') { $this->opts['newns'] = $_REQUEST['newns']; // check that the new namespace is valid if ( cleanID($this->opts['newns']) == '' ) { $this->errors[] = $this->lang['pm_badns']; return; } } else { $this->errors[] = $this->lang['pm_badns']; return; } } else { $this->errors[] = $this->lang['pm_fatal']; return; } $this->_pm_move_page($this->opts); // @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']; $ACT = 'show'; $INFO = pageinfo(); $this->show_form = false; } else { $this->errors[] = $this->lang['pm_fatal']; return; } // only go on if no errors occured and inputs are not empty if (count($this->errors) != 0 ) { return; } // delete empty namespaces if possible // @fixme does not work like that foreach ($this->idsToDelete as $idToDelete) { io_sweepNS($idToDelete); } } /** * 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 * @param $pathToSearch * @param $opts * @return unknown_type */ function _pm_move_recursive($pathToSearch, $opts) { global $ID; global $conf; $pagelist = array(); search($pagelist, $conf['datadir'], 'search_index', '', $pathToSearch); foreach ($pagelist as $page) { if ($page['type'] == 'd') { $pathToSearch = utf8_encodeFN(str_replace(':', '/', $page['id'])); // @fixme shouldn't be necessary as ID already exists io_createNamespace($page['id']); // NS to move is this one $nsOpts = $opts; $nsOpts['ns'] = $page['id']; // target NS is this folder under the current target NS $thisFolder = end(explode(':', $page['id'])); $nsOpts['newns'] .= ':'.$thisFolder; array_push($this->idsToDelete, $page['id']); // Recursion $this->_pm_move_recursive($pathToSearch, $nsOpts); } elseif ($page['type'] == 'f') { $ID = $page['id']; $pageOpts = $opts; $pageOpts['ns'] = getNS($ID); $pageOpts['name'] = noNS($ID); $pageOpts['newname'] = noNS($ID); $this->_pm_move_page($pageOpts); } else { $this->errors[] = $this->lang['pm_unknown_file_type']; return; } } } /** * move page * * @author Gary Owen , modified by Kay Roesler * * @param array $opts */ function _pm_move_page($opts) { global $conf; global $lang; global $ID; global $INFO; global $ACT; // Check we have rights to move this document if ( !$INFO['exists']) { $this->have_rights = false; $this->errors[] = $this->lang['pm_notexist']; return; } if ( $ID == $conf['start']) { $this->have_rights = false; $this->errors[] = $this->lang['pm_notstart']; return; } if ( auth_quickaclcheck($ID) < AUTH_EDIT ) { $this->have_rights = false; $this->errors[] = $this->lang['pm_norights']; return; } // Check file is not locked if (checklock($ID)) { $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']); // Has the document name and/or namespace changed? if ( $opts['newns'] == $opts['ns'] && $opts['newname'] == $opts['name'] ) { $this->errors[] = $this->lang['pm_nochange']; return; } // Check the page does not already exist if ( @file_exists($opts['new_path']) ) { $this->errors[] = sprintf($this->lang['pm_existing'], $opts['newname'], ($opts['newns'] == '' ? $this->lang['pm_root'] : $opts['newns'])); return; } if ( count($this->errors) != 0 ) { return; } /** * End of init (checks) */ // Open the old document and change forward links lock($ID); $this->text = io_readFile(wikiFN($ID), True); // Get an array of forward links from the document $forward = $this->_pm_getforwardlinks($ID); // 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 $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)); } elseif ( $tns == "" ) { // Target is in root namespace $this->_pm_updatelinks($this->text, array($lnk => $lid )); } else { $this->_pm_updatelinks($this->text, array($lnk => $lid )); } } 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 ); } } } // 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); } /** * 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']); // 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 .= ':..'; } } 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']); } } $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; } } 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; } /** * move meta files (Old Revs, Subscriptions, Meta, etc) * * This function meta files between directories * * @author Gary Owen */ function _pm_movemeta($dir, $regex, $opts) { global $conf; $old_path = $conf[$dir].'/'.str_replace(':','/',$opts['ns']).'/'; $new_path = $conf[$dir].'/'.str_replace(':','/',$opts['newns']).'/'; $dh = @opendir($old_path); if($dh) { while(($file = readdir($dh)) !== false) { // skip hidden files and upper dirs if(preg_match('/^\./',$file)) continue; if(is_file($old_path.$file) and preg_match($regex,$file)) { io_mkdir_p($new_path); io_rename($old_path.$file,$new_path.str_replace($opts['name'], $opts['newname'], $file)); continue; } } 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; } } } }