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.
 
 
 
 
 

954 lines
30 KiB

  1. <?php
  2. /**
  3. * Move Plugin Operation Planner
  4. *
  5. * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
  6. * @author Michael Hamann <michael@content-space.de>
  7. * @author Andreas Gohr <gohr@cosmocode.de>
  8. */
  9. // must be run within Dokuwiki
  10. if(!defined('DOKU_INC')) die();
  11. /**
  12. * Class helper_plugin_move_plan
  13. *
  14. * This thing prepares and keeps progress info on complex move operations (eg. where more than a single
  15. * object is affected).
  16. *
  17. * Please note: this has not a complex move resolver. Move operations may not depend on each other (eg. you
  18. * can not use a namespace as source that will only be created by a different move operation) instead all given
  19. * operations should be operations on the current state to come to a wanted future state. The tree manager takes
  20. * care of that by abstracting all moves on a DOM representation first, then submitting the needed changes (eg.
  21. * differences between now and wanted).
  22. *
  23. * Glossary:
  24. *
  25. * document - refers to either a page or a media file here
  26. */
  27. class helper_plugin_move_plan extends DokuWiki_Plugin {
  28. /** Number of operations per step */
  29. const OPS_PER_RUN = 10;
  30. const TYPE_PAGES = 1;
  31. const TYPE_MEDIA = 2;
  32. const CLASS_NS = 4;
  33. const CLASS_DOC = 8;
  34. /**
  35. * @var array the options for this move plan
  36. */
  37. protected $options = array(); // defaults are set in loadOptions()
  38. /**
  39. * @var array holds the location of the different list and state files
  40. */
  41. protected $files = array();
  42. /**
  43. * @var array the planned moves
  44. */
  45. protected $plan = array();
  46. /**
  47. * @var array temporary holder of document lists
  48. */
  49. protected $tmpstore = array(
  50. 'pages' => array(),
  51. 'media' => array(),
  52. 'ns' => array(),
  53. 'affpg' => array(),
  54. 'miss' => array(),
  55. 'miss_media' => array(),
  56. );
  57. /** @var helper_plugin_move_op $MoveOperator */
  58. protected $MoveOperator = null;
  59. /**
  60. * Constructor
  61. *
  62. * initializes state (if any) for continuiation of a running move op
  63. */
  64. public function __construct() {
  65. global $conf;
  66. // set the file locations
  67. $this->files = array(
  68. 'opts' => $conf['metadir'] . '/__move_opts',
  69. 'pagelist' => $conf['metadir'] . '/__move_pagelist',
  70. 'medialist' => $conf['metadir'] . '/__move_medialist',
  71. 'affected' => $conf['metadir'] . '/__move_affected',
  72. 'namespaces' => $conf['metadir'] . '/__move_namespaces',
  73. 'missing' => $conf['metadir'] . '/__move_missing',
  74. 'missing_media' => $conf['metadir'] . '/__move_missing_media',
  75. );
  76. $this->MoveOperator = plugin_load('helper', 'move_op');
  77. $this->loadOptions();
  78. }
  79. /**
  80. * Load the current options if any
  81. *
  82. * If no options are found, the default options will be extended by any available
  83. * config options
  84. */
  85. protected function loadOptions() {
  86. // (re)set defaults
  87. $this->options = array(
  88. // status
  89. 'committed' => false,
  90. 'started' => 0,
  91. // counters
  92. 'pages_all' => 0,
  93. 'pages_run' => 0,
  94. 'media_all' => 0,
  95. 'media_run' => 0,
  96. 'affpg_all' => 0,
  97. 'affpg_run' => 0,
  98. // options
  99. 'autoskip' => $this->getConf('autoskip'),
  100. 'autorewrite' => $this->getConf('autorewrite'),
  101. // errors
  102. 'lasterror' => false
  103. );
  104. // merge whatever options are saved currently
  105. $file = $this->files['opts'];
  106. if(file_exists($file)) {
  107. $options = unserialize(io_readFile($file, false));
  108. $this->options = array_merge($this->options, $options);
  109. }
  110. }
  111. /**
  112. * Save the current options
  113. *
  114. * @return bool
  115. */
  116. protected function saveOptions() {
  117. return io_saveFile($this->files['opts'], serialize($this->options));
  118. }
  119. /**
  120. * Return the current state of an option, null for unknown options
  121. *
  122. * @param $name
  123. * @return mixed|null
  124. */
  125. public function getOption($name) {
  126. if(isset($this->options[$name])) {
  127. return $this->options[$name];
  128. }
  129. return null;
  130. }
  131. /**
  132. * Set an option
  133. *
  134. * Note, this otpion will only be set to the current instance of this helper object. It will only
  135. * be written to the option file once the plan gets committed
  136. *
  137. * @param $name
  138. * @param $value
  139. */
  140. public function setOption($name, $value) {
  141. $this->options[$name] = $value;
  142. }
  143. /**
  144. * Returns the progress of this plan in percent
  145. *
  146. * @return float
  147. */
  148. public function getProgress() {
  149. $max =
  150. $this->options['pages_all'] +
  151. $this->options['media_all'];
  152. $remain =
  153. $this->options['pages_run'] +
  154. $this->options['media_run'];
  155. if($this->options['autorewrite']) {
  156. $max += $this->options['affpg_all'];
  157. $remain += $this->options['affpg_run'];
  158. }
  159. if($max == 0) return 0;
  160. return round((($max - $remain) * 100) / $max, 2);
  161. }
  162. /**
  163. * Check if there is a move in progress currently
  164. *
  165. * @return bool
  166. */
  167. public function inProgress() {
  168. return (bool) $this->options['started'];
  169. }
  170. /**
  171. * Check if this plan has been committed, yet
  172. *
  173. * @return bool
  174. */
  175. public function isCommited() {
  176. return $this->options['committed'];
  177. }
  178. /**
  179. * Add a single page to be moved to the plan
  180. *
  181. * @param string $src
  182. * @param string $dst
  183. */
  184. public function addPageMove($src, $dst) {
  185. $this->addMove($src, $dst, self::CLASS_DOC, self::TYPE_PAGES);
  186. }
  187. /**
  188. * Add a single media file to be moved to the plan
  189. *
  190. * @param string $src
  191. * @param string $dst
  192. */
  193. public function addMediaMove($src, $dst) {
  194. $this->addMove($src, $dst, self::CLASS_DOC, self::TYPE_MEDIA);
  195. }
  196. /**
  197. * Add a page namespace to be moved to the plan
  198. *
  199. * @param string $src
  200. * @param string $dst
  201. */
  202. public function addPageNamespaceMove($src, $dst) {
  203. $this->addMove($src, $dst, self::CLASS_NS, self::TYPE_PAGES);
  204. }
  205. /**
  206. * Add a media namespace to be moved to the plan
  207. *
  208. * @param string $src
  209. * @param string $dst
  210. */
  211. public function addMediaNamespaceMove($src, $dst) {
  212. $this->addMove($src, $dst, self::CLASS_NS, self::TYPE_MEDIA);
  213. }
  214. /**
  215. * Plans the move of a namespace or document
  216. *
  217. * @param string $src ID of the item to move
  218. * @param string $dst new ID of item namespace
  219. * @param int $class (self::CLASS_NS|self::CLASS_DOC)
  220. * @param int $type (PLUGIN_MOVE_TYPE_PAGE|self::TYPE_MEDIA)
  221. * @throws Exception
  222. */
  223. protected function addMove($src, $dst, $class = self::CLASS_NS, $type = self::TYPE_PAGES) {
  224. if($this->options['committed']) throw new Exception('plan is committed already, can not be added to');
  225. $src = cleanID($src);
  226. $dst = cleanID($dst);
  227. $this->plan[] = array(
  228. 'src' => $src,
  229. 'dst' => $dst,
  230. 'class' => $class,
  231. 'type' => $type
  232. );
  233. }
  234. /**
  235. * Abort any move or plan in progress and reset the helper
  236. */
  237. public function abort() {
  238. foreach($this->files as $file) {
  239. @unlink($file);
  240. }
  241. $this->plan = array();
  242. $this->loadOptions();
  243. helper_plugin_move_rewrite::removeAllLocks();
  244. }
  245. /**
  246. * This locks up the plan and prepares execution
  247. *
  248. * the plan is reordered an the needed move operations are gathered and stored in the appropriate
  249. * list files
  250. *
  251. * @throws Exception if you try to commit a plan twice
  252. * @return bool true if the plan was committed
  253. */
  254. public function commit() {
  255. global $conf;
  256. if($this->options['committed']) throw new Exception('plan is committed already, can not be committed again');
  257. helper_plugin_move_rewrite::addLock();
  258. usort($this->plan, array($this, 'planSorter'));
  259. // get all the documents to be moved and store them in their lists
  260. foreach($this->plan as $move) {
  261. if($move['class'] == self::CLASS_DOC) {
  262. // these can just be added
  263. $this->addToDocumentList($move['src'], $move['dst'], $move['type']);
  264. } else {
  265. // here we need a list of content first, search for it
  266. $docs = array();
  267. $path = utf8_encodeFN(str_replace(':', '/', $move['src']));
  268. $opts = array('depth' => 0, 'skipacl' => true);
  269. if($move['type'] == self::TYPE_PAGES) {
  270. search($docs, $conf['datadir'], 'search_allpages', $opts, $path);
  271. } else {
  272. search($docs, $conf['mediadir'], 'search_media', $opts, $path);
  273. }
  274. // how much namespace to strip?
  275. if($move['src'] !== '') {
  276. $strip = strlen($move['src']) + 1;
  277. } else {
  278. $strip = 0;
  279. }
  280. if($move['dst']) $move['dst'] .= ':';
  281. // now add all the found documents to our lists
  282. foreach($docs as $doc) {
  283. $from = $doc['id'];
  284. $to = $move['dst'] . substr($doc['id'], $strip);
  285. $this->addToDocumentList($from, $to, $move['type']);
  286. }
  287. // remember the namespace move itself
  288. if($move['type'] == self::TYPE_PAGES) {
  289. // FIXME we use this to move namespace subscriptions later on and for now only do it on
  290. // page namespace moves, but subscriptions work for both, but what when only one of
  291. // them is moved? Should it be copied then? Complicated. This is good enough for now
  292. $this->addToDocumentList($move['src'], $move['dst'], self::CLASS_NS);
  293. }
  294. $this->findMissingDocuments($move['src'] . ':', $move['dst'],$move['type']);
  295. }
  296. // store what pages are affected by this move
  297. $this->findAffectedPages($move['src'], $move['dst'], $move['class'], $move['type']);
  298. }
  299. $this->storeDocumentLists();
  300. if(!$this->options['pages_all'] && !$this->options['media_all']) {
  301. msg($this->getLang('noaction'), -1);
  302. return false;
  303. }
  304. $this->options['committed'] = true;
  305. $this->saveOptions();
  306. return true;
  307. }
  308. /**
  309. * Execute the next steps
  310. *
  311. * @param bool $skip set to true to skip the next first step (skip error)
  312. * @return bool|int false on errors, otherwise the number of remaining steps
  313. * @throws Exception
  314. */
  315. public function nextStep($skip = false) {
  316. if(!$this->options['committed']) throw new Exception('plan is not committed yet!');
  317. // execution has started
  318. if(!$this->options['started']) $this->options['started'] = time();
  319. helper_plugin_move_rewrite::addLock();
  320. if(@filesize($this->files['pagelist']) > 1) {
  321. $todo = $this->stepThroughDocuments(self::TYPE_PAGES, $skip);
  322. if($todo === false) return $this->storeError();
  323. return max($todo, 1); // force one more call
  324. }
  325. if(@filesize($this->files['medialist']) > 1) {
  326. $todo = $this->stepThroughDocuments(self::TYPE_MEDIA, $skip);
  327. if($todo === false) return $this->storeError();
  328. return max($todo, 1); // force one more call
  329. }
  330. if(@filesize($this->files['missing']) > 1 && @filesize($this->files['affected']) > 1) {
  331. $todo = $this->stepThroughMissingDocuments(self::TYPE_PAGES);
  332. if($todo === false) return $this->storeError();
  333. return max($todo, 1); // force one more call
  334. }
  335. if(@filesize($this->files['missing_media']) > 1 && @filesize($this->files['affected']) > 1) {
  336. $todo = $this->stepThroughMissingDocuments(self::TYPE_MEDIA);
  337. if($todo === false)return $this->storeError();
  338. return max($todo, 1); // force one more call
  339. }
  340. if(@filesize($this->files['namespaces']) > 1) {
  341. $todo = $this->stepThroughNamespaces();
  342. if($todo === false) return $this->storeError();
  343. return max($todo, 1); // force one more call
  344. }
  345. helper_plugin_move_rewrite::removeAllLocks();
  346. if($this->options['autorewrite'] && @filesize($this->files['affected']) > 1) {
  347. $todo = $this->stepThroughAffectedPages();
  348. if($todo === false) return $this->storeError();
  349. return max($todo, 1); // force one more call
  350. }
  351. // we're done here, clean up
  352. $this->abort();
  353. return 0;
  354. }
  355. /**
  356. * Returns the list of page and media moves and the affected pages as a HTML list
  357. *
  358. * @return string
  359. */
  360. public function previewHTML() {
  361. $html = '';
  362. $html .= '<ul>';
  363. if(@file_exists($this->files['pagelist'])) {
  364. $pagelist = file($this->files['pagelist']);
  365. foreach($pagelist as $line) {
  366. list($old, $new) = explode("\t", trim($line));
  367. $html .= '<li class="page"><div class="li">';
  368. $html .= hsc($old);
  369. $html .= '→';
  370. $html .= hsc($new);
  371. $html .= '</div></li>';
  372. }
  373. }
  374. if(@file_exists($this->files['medialist'])) {
  375. $medialist = file($this->files['medialist']);
  376. foreach($medialist as $line) {
  377. list($old, $new) = explode("\t", trim($line));
  378. $html .= '<li class="media"><div class="li">';
  379. $html .= hsc($old);
  380. $html .= '→';
  381. $html .= hsc($new);
  382. $html .= '</div></li>';
  383. }
  384. }
  385. if(@file_exists($this->files['affected'])) {
  386. $medialist = file($this->files['affected']);
  387. foreach($medialist as $page) {
  388. $html .= '<li class="affected"><div class="li">';
  389. $html .= '↷';
  390. $html .= hsc($page);
  391. $html .= '</div></li>';
  392. }
  393. }
  394. $html .= '</ul>';
  395. return $html;
  396. }
  397. /**
  398. * Step through the next bunch of pages or media files
  399. *
  400. * @param int $type (self::TYPE_PAGES|self::TYPE_MEDIA)
  401. * @param bool $skip should the first item be skipped?
  402. * @return bool|int false on error, otherwise the number of remaining documents
  403. */
  404. protected function stepThroughDocuments($type = self::TYPE_PAGES, $skip = false) {
  405. if($type == self::TYPE_PAGES) {
  406. $file = $this->files['pagelist'];
  407. $mark = 'P';
  408. $call = 'movePage';
  409. $items_run_counter = 'pages_run';
  410. } else {
  411. $file = $this->files['medialist'];
  412. $mark = 'M';
  413. $call = 'moveMedia';
  414. $items_run_counter = 'media_run';
  415. }
  416. $doclist = fopen($file, 'a+');
  417. for($i = 0; $i < helper_plugin_move_plan::OPS_PER_RUN; $i++) {
  418. $log = "";
  419. $line = $this->getLastLine($doclist);
  420. if($line === false) {
  421. break;
  422. }
  423. list($src, $dst) = explode("\t", trim($line));
  424. // should this item be skipped?
  425. if($skip === true) {
  426. $skip = false;
  427. } else {
  428. // move the page
  429. if(!$this->MoveOperator->$call($src, $dst)) {
  430. $log .= $this->build_log_line($mark, $src, $dst, false); // FAILURE!
  431. // automatically skip this item only if wanted...
  432. if(!$this->options['autoskip']) {
  433. // ...otherwise abort the operation
  434. fclose($doclist);
  435. $return_items_run = false;
  436. break;
  437. }
  438. } else {
  439. $log .= $this->build_log_line($mark, $src, $dst, true); // SUCCESS!
  440. }
  441. }
  442. /*
  443. * This adjusts counters and truncates the document list correctly
  444. * It is used to finalize a successful or skipped move
  445. */
  446. ftruncate($doclist, ftell($doclist));
  447. $this->options[$items_run_counter]--;
  448. $return_items_run = $this->options[$items_run_counter];
  449. $this->write_log($log);
  450. $this->saveOptions();
  451. }
  452. if ($return_items_run !== false) {
  453. fclose($doclist);
  454. }
  455. return $return_items_run;
  456. }
  457. /**
  458. * Step through the next bunch of pages that need link corrections
  459. *
  460. * @return bool|int false on error, otherwise the number of remaining documents
  461. */
  462. protected function stepThroughAffectedPages() {
  463. /** @var helper_plugin_move_rewrite $Rewriter */
  464. $Rewriter = plugin_load('helper', 'move_rewrite');
  465. // handle affected pages
  466. $doclist = fopen($this->files['affected'], 'a+');
  467. for($i = 0; $i < helper_plugin_move_plan::OPS_PER_RUN; $i++) {
  468. $page = $this->getLastLine($doclist);
  469. if($page === false) break;
  470. // rewrite it
  471. $Rewriter->rewritePage($page);
  472. // update the list file
  473. ftruncate($doclist, ftell($doclist));
  474. $this->options['affpg_run']--;
  475. $this->saveOptions();
  476. }
  477. fclose($doclist);
  478. return $this->options['affpg_run'];
  479. }
  480. /**
  481. * Step through all the links to missing pages that should be moved
  482. *
  483. * This simply adds the moved missing pages to all affected pages meta data. This will add
  484. * the meta data to pages not linking to the affected pages but this should still be faster
  485. * than figuring out which pages need this info.
  486. *
  487. * This does not step currently, but handles all pages in one step.
  488. *
  489. * @param int $type
  490. *
  491. * @return int always 0
  492. * @throws Exception
  493. */
  494. protected function stepThroughMissingDocuments($type = self::TYPE_PAGES) {
  495. if($type != self::TYPE_PAGES && $type != self::TYPE_MEDIA) {
  496. throw new Exception('wrong type specified');
  497. }
  498. /** @var helper_plugin_move_rewrite $Rewriter */
  499. $Rewriter = plugin_load('helper', 'move_rewrite');
  500. $miss = array();
  501. if ($type == self::TYPE_PAGES) {
  502. $missing_fn = $this->files['missing'];
  503. } else {
  504. $missing_fn = $this->files['missing_media'];
  505. }
  506. $missing = file($missing_fn);
  507. foreach($missing as $line) {
  508. $line = trim($line);
  509. if($line == '') continue;
  510. list($src, $dst) = explode("\t", $line);
  511. $miss[$src] = $dst;
  512. }
  513. $affected = file($this->files['affected']);
  514. foreach($affected as $page){
  515. $page = trim($page);
  516. if ($type == self::TYPE_PAGES) {
  517. $Rewriter->setMoveMetas($page, $miss, 'pages');
  518. } else {
  519. $Rewriter->setMoveMetas($page, $miss, 'media');
  520. }
  521. }
  522. unlink($missing_fn);
  523. return 0;
  524. }
  525. /**
  526. * Step through all the namespace moves
  527. *
  528. * This does not step currently, but handles all namespaces in one step.
  529. *
  530. * Currently moves namespace subscriptions only.
  531. *
  532. * @return int always 0
  533. * @todo maybe add an event so plugins can move more stuff?
  534. * @todo fixed that $src and $dst are seperated by tab, not newline. This method has no tests?
  535. */
  536. protected function stepThroughNamespaces() {
  537. /** @var helper_plugin_move_file $FileMover */
  538. $FileMover = plugin_load('helper', 'move_file');
  539. $lines = io_readFile($this->files['namespaces']);
  540. $lines = explode("\n", $lines);
  541. foreach($lines as $line) {
  542. // There is an empty line at the end of the list.
  543. if ($line === '') continue;
  544. list($src, $dst) = explode("\t", trim($line));
  545. $FileMover->moveNamespaceSubscription($src, $dst);
  546. }
  547. @unlink($this->files['namespaces']);
  548. return 0;
  549. }
  550. /**
  551. * Retrieve the last error from the MSG array and store it in the options
  552. *
  553. * @todo rebuild error handling based on exceptions
  554. *
  555. * @return bool always false
  556. */
  557. protected function storeError() {
  558. global $MSG;
  559. if(is_array($MSG) && count($MSG)) {
  560. $last = array_shift($MSG);
  561. $this->options['lasterror'] = $last['msg'];
  562. unset($GLOBALS['MSG']);
  563. } else {
  564. $this->options['lasterror'] = 'Unknown error';
  565. }
  566. $this->saveOptions();
  567. return false;
  568. }
  569. /**
  570. * Reset the error state
  571. */
  572. protected function clearError() {
  573. $this->options['lasterror'] = false;
  574. $this->saveOptions();
  575. }
  576. /**
  577. * Get the last error message or false if no error occured
  578. *
  579. * @return bool|string
  580. */
  581. public function getLastError() {
  582. return $this->options['lasterror'];
  583. }
  584. /**
  585. * Appends a page move operation in the list file
  586. *
  587. * If the src has been added before, this is ignored. This makes sure you can move a single page
  588. * out of a namespace first, then move the namespace somewhere else.
  589. *
  590. * @param string $src
  591. * @param string $dst
  592. * @param int $type
  593. * @throws Exception
  594. */
  595. protected function addToDocumentList($src, $dst, $type = self::TYPE_PAGES) {
  596. if($type == self::TYPE_PAGES) {
  597. $store = 'pages';
  598. } else if($type == self::TYPE_MEDIA) {
  599. $store = 'media';
  600. } else if($type == self::CLASS_NS) {
  601. $store = 'ns';
  602. } else {
  603. throw new Exception('Unknown type ' . $type);
  604. }
  605. if(!isset($this->tmpstore[$store][$src])) {
  606. $this->tmpstore[$store][$src] = $dst;
  607. }
  608. }
  609. /**
  610. * Add the list of pages to the list of affected pages whose links need adjustment
  611. *
  612. * @param string|array $pages
  613. */
  614. protected function addToAffectedPagesList($pages) {
  615. if(!is_array($pages)) $pages = array($pages);
  616. foreach($pages as $page) {
  617. if(!isset($this->tmpstore['affpg'][$page])) {
  618. $this->tmpstore['affpg'][$page] = true;
  619. }
  620. }
  621. }
  622. /**
  623. * Looks up pages that will be affected by a move of $src
  624. *
  625. * Calls addToAffectedPagesList() directly to store the result
  626. *
  627. * @param string $src source namespace
  628. * @param string $dst destination namespace
  629. * @param int $class
  630. * @param int $type
  631. */
  632. protected function findAffectedPages($src, $dst, $class, $type) {
  633. $idx = idx_get_indexer();
  634. if($class == self::CLASS_NS) {
  635. $src_ = "$src:*"; // use wildcard lookup for namespaces
  636. } else {
  637. $src_ = $src;
  638. }
  639. $pages = array();
  640. if($type == self::TYPE_PAGES) {
  641. $pages = $idx->lookupKey('relation_references', $src_);
  642. $len = strlen($src);
  643. foreach($pages as &$page) {
  644. if (substr($page, 0, $len + 1) === "$src:") {
  645. $page = $dst . substr($page, $len + 1);
  646. }
  647. }
  648. unset($page);
  649. } else if($type == self::TYPE_MEDIA) {
  650. $pages = $idx->lookupKey('relation_media', $src_);
  651. }
  652. $this->addToAffectedPagesList($pages);
  653. }
  654. /**
  655. * Find missing pages in the $src namespace
  656. *
  657. * @param string $src source namespace
  658. * @param string $dst destination namespace
  659. * @param int $type either self::TYPE_PAGES or self::TYPE_MEDIA
  660. */
  661. protected function findMissingDocuments($src, $dst, $type = self::TYPE_PAGES) {
  662. global $conf;
  663. // FIXME this duplicates Doku_Indexer::getIndex()
  664. if ($type == self::TYPE_PAGES) {
  665. $fn = $conf['indexdir'] . '/relation_references_w.idx';
  666. } else {
  667. $fn = $conf['indexdir'] . '/relation_media_w.idx';
  668. }
  669. if (!@file_exists($fn)){
  670. $referenceidx = array();
  671. } else {
  672. $referenceidx = file($fn, FILE_IGNORE_NEW_LINES);
  673. }
  674. $len = strlen($src);
  675. foreach($referenceidx as $idx => $page) {
  676. if(substr($page, 0, $len) != "$src") continue;
  677. // remember missing pages
  678. if ($type == self::TYPE_PAGES) {
  679. if(!page_exists($page)) {
  680. $newpage = $dst . substr($page, $len);
  681. $this->tmpstore['miss'][$page] = $newpage;
  682. }
  683. } else {
  684. if(!file_exists(mediaFN($page))){
  685. $newpage = $dst . substr($page, $len);
  686. $this->tmpstore['miss_media'][$page] = $newpage;
  687. }
  688. }
  689. }
  690. }
  691. /**
  692. * Store the aggregated document lists in the file system and reset the internal storage
  693. *
  694. * @throws Exception
  695. */
  696. protected function storeDocumentLists() {
  697. $lists = array(
  698. 'pages' => $this->files['pagelist'],
  699. 'media' => $this->files['medialist'],
  700. 'ns' => $this->files['namespaces'],
  701. 'affpg' => $this->files['affected'],
  702. 'miss' => $this->files['missing'],
  703. 'miss_media' => $this->files['missing_media'],
  704. );
  705. foreach($lists as $store => $file) {
  706. // anything to do?
  707. $count = count($this->tmpstore[$store]);
  708. if(!$count) continue;
  709. // prepare and save content
  710. $data = '';
  711. $this->tmpstore[$store] = array_reverse($this->tmpstore[$store]); // store in reverse order
  712. foreach($this->tmpstore[$store] as $src => $dst) {
  713. if($dst === true) {
  714. $data .= "$src\n"; // for affected pages only one ID is saved
  715. } else {
  716. $data .= "$src\t$dst\n";
  717. }
  718. }
  719. io_saveFile($file, $data);
  720. // set counters
  721. if($store != 'ns') {
  722. $this->options[$store . '_all'] = $count;
  723. $this->options[$store . '_run'] = $count;
  724. }
  725. // reset the list
  726. $this->tmpstore[$store] = array();
  727. }
  728. }
  729. /**
  730. * Get the last line from the list that is stored in the file that is referenced by the handle
  731. * The handle is set to the newline before the file id
  732. *
  733. * @param resource $handle The file handle to read from
  734. * @return string|bool the last id from the list or false if there is none
  735. */
  736. protected function getLastLine($handle) {
  737. // begin the seek at the end of the file
  738. fseek($handle, 0, SEEK_END);
  739. $line = '';
  740. // seek one backwards as long as it's possible
  741. while(fseek($handle, -1, SEEK_CUR) >= 0) {
  742. $c = fgetc($handle);
  743. if($c === false) return false; // EOF, i.e. the file is empty
  744. fseek($handle, -1, SEEK_CUR); // reset the position to the character that was read
  745. if($c == "\n") {
  746. if($line === '') {
  747. continue; // this line was empty, continue
  748. } else {
  749. break; // we have a line, finish
  750. }
  751. }
  752. $line = $c . $line; // prepend char to line
  753. }
  754. if($line === '') return false; // beginning of file reached and no content
  755. return $line;
  756. }
  757. /**
  758. * Callback for usort to sort the move plan
  759. *
  760. * @param $a
  761. * @param $b
  762. * @return int
  763. */
  764. public function planSorter($a, $b) {
  765. // do page moves before namespace moves
  766. if($a['class'] == self::CLASS_DOC && $b['class'] == self::CLASS_NS) {
  767. return -1;
  768. }
  769. if($a['class'] == self::CLASS_NS && $b['class'] == self::CLASS_DOC) {
  770. return 1;
  771. }
  772. // do pages before media
  773. if($a['type'] == self::TYPE_PAGES && $b['type'] == self::TYPE_MEDIA) {
  774. return -1;
  775. }
  776. if($a['type'] == self::TYPE_MEDIA && $b['type'] == self::TYPE_PAGES) {
  777. return 1;
  778. }
  779. // from here on we compare only apples to apples
  780. // we sort by depth of namespace, deepest namespaces first
  781. $alen = substr_count($a['src'], ':');
  782. $blen = substr_count($b['src'], ':');
  783. if($alen > $blen) {
  784. return -1;
  785. } elseif($alen < $blen) {
  786. return 1;
  787. }
  788. return 0;
  789. }
  790. /**
  791. * Create line to log result of an operation
  792. *
  793. * @param string $type
  794. * @param string $from
  795. * @param string $to
  796. * @param bool $success
  797. *
  798. * @return string
  799. *
  800. * @author Andreas Gohr <gohr@cosmocode.de>
  801. * @author Michael Große <grosse@cosmocode.de>
  802. */
  803. public function build_log_line ($type, $from, $to, $success) {
  804. global $MSG;
  805. $now = time();
  806. $date = date('Y-m-d H:i:s', $now); // for human readability
  807. if($success) {
  808. $ok = 'success';
  809. $msg = '';
  810. } else {
  811. $ok = 'failed';
  812. $msg = $MSG[count($MSG) - 1]['msg']; // get detail from message array
  813. }
  814. $log = "$now\t$date\t$type\t$from\t$to\t$ok\t$msg\n";
  815. return $log;
  816. }
  817. /**
  818. * write log to file
  819. *
  820. * @param $log
  821. */
  822. protected function write_log ($log) {
  823. global $conf;
  824. $optime = $this->options['started'];
  825. $file = $conf['cachedir'] . '/move/' . strftime('%Y%m%d-%H%M%S', $optime) . '.log';
  826. io_saveFile($file, $log, true);
  827. }
  828. }