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