You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

388 lines
13 KiB

  1. <?php
  2. /**
  3. * Move Plugin Rewriting Handler
  4. *
  5. * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
  6. * @author Michael Hamann <michael@content-space.de>
  7. */
  8. // must be run within Dokuwiki
  9. if(!defined('DOKU_INC')) die();
  10. /**
  11. * Handler class for move. It does the actual rewriting of the content.
  12. *
  13. * Note: This is not actually a valid DokuWiki Helper plugin and can not be loaded via plugin_load()
  14. */
  15. class helper_plugin_move_handler extends DokuWiki_Plugin {
  16. public $calls = '';
  17. protected $id;
  18. protected $ns;
  19. protected $origID;
  20. protected $origNS;
  21. protected $page_moves;
  22. protected $media_moves;
  23. protected $handlers;
  24. /**
  25. * Do not allow re-using instances.
  26. *
  27. * @return bool false - the handler must not be re-used.
  28. */
  29. public function isSingleton() {
  30. return false;
  31. }
  32. /**
  33. * Initialize the move handler.
  34. *
  35. * @param string $id The id of the text that is passed to the handler
  36. * @param string $original The name of the original ID of this page. Same as $id if this page wasn't moved
  37. * @param array $page_moves Moves that shall be considered in the form [[$old,$new],...] ($old can be $original)
  38. * @param array $media_moves Moves of media files that shall be considered in the form $old => $new
  39. * @param array $handlers Handlers for plugin content in the form $plugin_name => $callback
  40. */
  41. public function init($id, $original, $page_moves, $media_moves, $handlers) {
  42. $this->id = $id;
  43. $this->ns = getNS($id);
  44. $this->origID = $original;
  45. $this->origNS = getNS($original);
  46. $this->page_moves = $page_moves;
  47. $this->media_moves = $media_moves;
  48. $this->handlers = $handlers;
  49. }
  50. /**
  51. * Go through the list of moves and find the new value for the given old ID
  52. *
  53. * @param string $old the old, full qualified ID
  54. * @param string $type 'media' or 'page'
  55. * @throws Exception on bad argument
  56. * @return string the new full qualified ID
  57. */
  58. public function resolveMoves($old, $type) {
  59. global $conf;
  60. if($type != 'media' && $type != 'page') throw new Exception('Not a valid type');
  61. $old = resolve_id($this->origNS, $old, false);
  62. if($type == 'page') {
  63. // FIXME this simply assumes that the link pointed to :$conf['start'], but it could also point to another page
  64. // resolve_pageid does a lot more here, but we can't really assume this as the original pages might have been
  65. // deleted already
  66. if(substr($old, -1) === ':' || $old === '') $old .= $conf['start'];
  67. $moves = $this->page_moves;
  68. } else {
  69. $moves = $this->media_moves;
  70. }
  71. $old = cleanID($old);
  72. foreach($moves as $move) {
  73. if($move[0] == $old) {
  74. $old = $move[1];
  75. }
  76. }
  77. return $old; // this is now new
  78. }
  79. /**
  80. * if the old link ended with a colon and the new one is a start page, adjust
  81. *
  82. * @param $relold string the old, possibly relative ID
  83. * @param $new string the new, full qualified ID
  84. * @param $type 'media' or 'page'
  85. * @return string
  86. */
  87. protected function _nsStartCheck($relold, $new, $type) {
  88. global $conf;
  89. if($type == 'page' && substr($relold, -1) == ':') {
  90. $len = strlen($conf['start']);
  91. if($new == $conf['start']) {
  92. $new = '.:';
  93. } else if(substr($new, -1 * ($len + 1)) == ':' . $conf['start']) {
  94. $new = substr($new, 0, -1 * $len);
  95. }
  96. }
  97. return $new;
  98. }
  99. /**
  100. * Construct a new ID relative to the current page's location
  101. *
  102. * Uses a relative link only if the original was relative, too. This function is for
  103. * pages and media files.
  104. *
  105. * @param string $relold the old, possibly relative ID
  106. * @param string $new the new, full qualified ID
  107. * @param string $type 'media' or 'page'
  108. * @throws Exception on bad argument
  109. * @return string
  110. */
  111. public function relativeLink($relold, $new, $type) {
  112. global $conf;
  113. if($type != 'media' && $type != 'page') throw new Exception('Not a valid type');
  114. // first check if the old link still resolves
  115. $exists = false;
  116. $old = $relold;
  117. if($type == 'page') {
  118. resolve_pageid($this->ns, $old, $exists);
  119. // Work around bug in DokuWiki 2020-07-29 where resolve_pageid doesn't append the start page to a link to
  120. // the root.
  121. if ($old === '') {
  122. $old = $conf['start'];
  123. }
  124. } else {
  125. resolve_mediaid($this->ns, $old, $exists);
  126. }
  127. if($old == $new) {
  128. return $relold; // old link still resolves, keep as is
  129. }
  130. if($conf['useslash']) $relold = str_replace('/', ':', $relold);
  131. // check if the link was relative
  132. if(strpos($relold, ':') === false ||$relold[0] == '.') {
  133. $wasrel = true;
  134. } else {
  135. $wasrel = false;
  136. }
  137. // if it wasn't relative then, leave it absolute now, too
  138. if(!$wasrel) {
  139. if($this->ns && !getNS($new)) $new = ':' . $new;
  140. $new = $this->_nsStartCheck($relold, $new, $type);
  141. return $new;
  142. }
  143. // split the paths and see how much common parts there are
  144. $selfpath = explode(':', $this->ns);
  145. $goalpath = explode(':', getNS($new));
  146. $min = min(count($selfpath), count($goalpath));
  147. for($common = 0; $common < $min; $common++) {
  148. if($selfpath[$common] != $goalpath[$common]) break;
  149. }
  150. // we now have the non-common part and a number of uppers
  151. $ups = max(count($selfpath) - $common, 0);
  152. $remainder = array_slice($goalpath, $common);
  153. $upper = $ups ? array_fill(0, $ups, '..:') : array();
  154. // build the new relative path
  155. $newrel = join(':', $upper);
  156. if($remainder) $newrel .= join(':', $remainder) . ':';
  157. $newrel .= noNS($new);
  158. $newrel = str_replace('::', ':', trim($newrel, ':'));
  159. if($newrel[0] != '.' && $this->ns && getNS($newrel)) $newrel = '.' . $newrel;
  160. // if the old link ended with a colon and the new one is a start page, adjust
  161. $newrel = $this->_nsStartCheck($relold,$newrel,$type);
  162. // don't use relative paths if it is ridicoulus:
  163. if(strlen($newrel) > strlen($new)) {
  164. $newrel = $new;
  165. if($this->ns && !getNS($new)) $newrel = ':' . $newrel;
  166. $newrel = $this->_nsStartCheck($relold,$newrel,$type);
  167. }
  168. return $newrel;
  169. }
  170. /**
  171. * Handle camelcase links
  172. *
  173. * @param string $match The text match
  174. * @param string $state The starte of the parser
  175. * @param int $pos The position in the input
  176. * @return bool If parsing should be continued
  177. */
  178. public function camelcaselink($match, $state, $pos) {
  179. $oldID = cleanID($this->origNS . ':' . $match);
  180. $newID = $this->resolveMoves($oldID, 'page');
  181. $newNS = getNS($newID);
  182. if($oldID == $newID || $this->origNS == $newNS) {
  183. // link is still valid as is
  184. $this->calls .= $match;
  185. } else {
  186. if(noNS($oldID) == noNS($newID)) {
  187. // only namespace changed, keep CamelCase in link
  188. $this->calls .= "[[$newNS:$match]]";
  189. } else {
  190. // all new, keep CamelCase in title
  191. $this->calls .= "[[$newID|$match]]";
  192. }
  193. }
  194. return true;
  195. }
  196. /**
  197. * Handle rewriting of internal links
  198. *
  199. * @param string $match The text match
  200. * @param string $state The starte of the parser
  201. * @param int $pos The position in the input
  202. * @return bool If parsing should be continued
  203. */
  204. public function internallink($match, $state, $pos) {
  205. // Strip the opening and closing markup
  206. $link = preg_replace(array('/^\[\[/', '/\]\]$/u'), '', $match);
  207. // Split title from URL
  208. $link = explode('|', $link, 2);
  209. if(!isset($link[1])) {
  210. $link[1] = null;
  211. } else if(preg_match('/^\{\{[^\}]+\}\}$/', $link[1])) {
  212. // If the title is an image, rewrite it
  213. $old_title = $link[1];
  214. $link[1] = $this->rewrite_media($link[1]);
  215. // do a simple replace of the first match so really only the id is changed and not e.g. the alignment
  216. $oldpos = strpos($match, $old_title);
  217. $oldlen = strlen($old_title);
  218. $match = substr_replace($match, $link[1], $oldpos, $oldlen);
  219. }
  220. $link[0] = trim($link[0]);
  221. //decide which kind of link it is
  222. if(preg_match('/^[a-zA-Z0-9\.]+>{1}.*$/u', $link[0])) {
  223. // Interwiki
  224. $this->calls .= $match;
  225. } elseif(preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u', $link[0])) {
  226. // Windows Share
  227. $this->calls .= $match;
  228. } elseif(preg_match('#^([a-z0-9\-\.+]+?)://#i', $link[0])) {
  229. // external link (accepts all protocols)
  230. $this->calls .= $match;
  231. } elseif(preg_match('<' . PREG_PATTERN_VALID_EMAIL . '>', $link[0])) {
  232. // E-Mail (pattern above is defined in inc/mail.php)
  233. $this->calls .= $match;
  234. } elseif(preg_match('!^#.+!', $link[0])) {
  235. // local hash link
  236. $this->calls .= $match;
  237. } else {
  238. $id = $link[0];
  239. $hash = '';
  240. $parts = explode('#', $id, 2);
  241. if(count($parts) === 2) {
  242. $id = $parts[0];
  243. $hash = $parts[1];
  244. }
  245. $params = '';
  246. $parts = explode('?', $id, 2);
  247. if(count($parts) === 2) {
  248. $id = $parts[0];
  249. $params = $parts[1];
  250. }
  251. $new_id = $this->resolveMoves($id, 'page');
  252. $new_id = $this->relativeLink($id, $new_id, 'page');
  253. if($id == $new_id) {
  254. $this->calls .= $match;
  255. } else {
  256. if($params !== '') {
  257. $new_id .= '?' . $params;
  258. }
  259. if($hash !== '') {
  260. $new_id .= '#' . $hash;
  261. }
  262. if($link[1] != null) {
  263. $new_id .= '|' . $link[1];
  264. }
  265. $this->calls .= '[[' . $new_id . ']]';
  266. }
  267. }
  268. return true;
  269. }
  270. /**
  271. * Handle rewriting of media links
  272. *
  273. * @param string $match The text match
  274. * @param string $state The starte of the parser
  275. * @param int $pos The position in the input
  276. * @return bool If parsing should be continued
  277. */
  278. public function media($match, $state, $pos) {
  279. $this->calls .= $this->rewrite_media($match);
  280. return true;
  281. }
  282. /**
  283. * Rewrite a media syntax
  284. *
  285. * @param string $match The text match of the media syntax
  286. * @return string The rewritten syntax
  287. */
  288. protected function rewrite_media($match) {
  289. $p = Doku_Handler_Parse_Media($match);
  290. if($p['type'] == 'internalmedia') { // else: external media
  291. $new_src = $this->resolveMoves($p['src'], 'media');
  292. $new_src = $this->relativeLink($p['src'], $new_src, 'media');
  293. if($new_src !== $p['src']) {
  294. // do a simple replace of the first match so really only the id is changed and not e.g. the alignment
  295. $srcpos = strpos($match, $p['src']);
  296. $srclen = strlen($p['src']);
  297. return substr_replace($match, $new_src, $srcpos, $srclen);
  298. }
  299. }
  300. return $match;
  301. }
  302. /**
  303. * Handle rewriting of plugin syntax, calls the registered handlers
  304. *
  305. * @param string $match The text match
  306. * @param string $state The starte of the parser
  307. * @param int $pos The position in the input
  308. * @param string $pluginname The name of the plugin
  309. * @return bool If parsing should be continued
  310. */
  311. public function plugin($match, $state, $pos, $pluginname) {
  312. if(isset($this->handlers[$pluginname])) {
  313. $this->calls .= call_user_func($this->handlers[$pluginname], $match, $state, $pos, $pluginname, $this);
  314. } else {
  315. $this->calls .= $match;
  316. }
  317. return true;
  318. }
  319. /**
  320. * Catchall handler for the remaining syntax
  321. *
  322. * @param string $name Function name that was called
  323. * @param array $params Original parameters
  324. * @return bool If parsing should be continue
  325. */
  326. public function __call($name, $params) {
  327. if(count($params) == 3) {
  328. $this->calls .= $params[0];
  329. return true;
  330. } else {
  331. trigger_error('Error, handler function ' . hsc($name) . ' with ' . count($params) . ' parameters called which isn\'t implemented', E_USER_ERROR);
  332. return false;
  333. }
  334. }
  335. public function _finalize() {
  336. // remove padding that is added by the parser in parse()
  337. $this->calls = substr($this->calls, 1, -1);
  338. }
  339. }