1<?php 2/** 3 * FarmSync DokuWiki Plugin 4 * 5 * @author Michael Große <grosse@cosmocode.de> 6 * @license GPL 2 7 */ 8 9namespace dokuwiki\plugin\farmsync\meta; 10 11/** 12 * Utility methods for accessing animal and farmer data 13 */ 14class FarmSyncUtil { 15 16 /** @var \helper_plugin_farmer */ 17 protected $farmer; 18 19 private $originalConfDirs; 20 21 /** 22 * FarmSyncUtil constructor. 23 */ 24 function __construct() { 25 $this->farmer = plugin_load('helper', 'farmer'); 26 } 27 28 function __destruct() { 29 if (!empty($this->originalConfDirs)) { 30 throw new \Exception('Animal context has not been reset!'); 31 } 32 } 33 34 /** 35 * A list of available animals as provided by the Farmer plugin 36 * 37 * @return array 38 */ 39 public function getAllAnimals() { 40 return $this->farmer->getAllAnimals(); 41 } 42 43 /** 44 * Constructs the path to the data directory of a given animal 45 * 46 * @param string $animal 47 * @return string 48 */ 49 public function getAnimalDataDir($animal) { 50 return $this->getAnimalDir($animal) . 'data/'; 51 } 52 53 public function getAnimalDir($animal) { 54 return DOKU_FARMDIR . $animal . '/'; 55 } 56 57 public function getAnimalLink($animal) { 58 return $this->farmer->getAnimalURL($animal); 59 } 60 61 public function clearAnimalCache($animal) { 62 $animalDir = $this->getAnimalDir($animal); 63 touch($animalDir.'conf/local.php'); 64 } 65 66 private function setAnimalContext($animal) { 67 if (!empty($this->originalConfDirs)) { 68 throw new \Exception('Animal context has not been reset!'); 69 } 70 global $conf; 71 $this->originalConfDirs = array(); 72 $animaldir = $this->getAnimalDataDir($animal); 73 $this->originalConfDirs['mediaolddir'] = $conf['mediaolddir']; 74 $this->originalConfDirs['mediadir'] = $conf['mediadir']; 75 $this->originalConfDirs['datadir'] = $conf['datadir']; 76 $this->originalConfDirs['olddir'] = $conf['olddir']; 77 $this->originalConfDirs['metadir'] = $conf['metadir']; 78 $conf['mediaolddir'] = $animaldir . 'media_attic'; 79 $conf['mediadir'] = $animaldir . 'media'; 80 $conf['datadir'] = $animaldir . 'pages'; 81 $conf['olddir'] = $animaldir . 'attic'; 82 $conf['metadir'] = $animaldir . 'meta'; 83 } 84 85 private function resetContext() { 86 global $conf; 87 if (empty($this->originalConfDirs)) { 88 throw new \Exception('No animal context set!'); 89 } 90 $conf['mediaolddir'] = $this->originalConfDirs['mediaolddir']; 91 $conf['mediadir'] = $this->originalConfDirs['mediadir']; 92 $conf['datadir'] = $this->originalConfDirs['datadir']; 93 $conf['olddir'] = $this->originalConfDirs['olddir']; 94 $conf['metadir'] = $this->originalConfDirs['metadir']; 95 unset($this->originalConfDirs); 96 } 97 98 /** 99 * Saves a file with the given content and set its latmodified date if given 100 * 101 * @param string $remoteFile 102 * @param string $content 103 * @param int $timestamp 104 */ 105 public function replaceRemoteFile($remoteFile, $content, $timestamp = 0) { 106 io_saveFile($remoteFile, $content); 107 if ($timestamp) touch($remoteFile, $timestamp); 108 } 109 110 /** 111 * Saves a page in the given animal and updates the timestamp if given 112 * 113 * @param string $animal 114 * @param string $page 115 * @param string $content 116 * @param int $timestamp 117 */ 118 public function saveRemotePage($animal, $page, $content, $timestamp = 0) { 119 global $INPUT, $conf; 120 $this->checkForExternalEdit($animal, $page); 121 if (!$timestamp) $timestamp = time(); 122 $changelogLine = join("\t", array($timestamp, clientIP(true), DOKU_CHANGE_TYPE_EDIT, $page, $INPUT->server->str('REMOTE_USER'), "Page updated from $conf[title] (" . DOKU_URL . ")")); 123 $this->addRemotePageChangelogRevision($animal, $page, $changelogLine); 124 $this->replaceRemoteFile($this->getRemoteFilename($animal, $page), $content, $timestamp); 125 $this->replaceRemoteFile($this->getRemoteFilename($animal, $page, $timestamp), $content); 126 // FIXME: update .meta ? 127 } 128 129 /** 130 * Saves the given local media file to the specified animal 131 * 132 * @param string $source 133 * @param string $target 134 * @param string $media a valid local MediaID 135 */ 136 public function saveRemoteMedia($source, $target, $media) { 137 global $INPUT, $conf; 138 $timestamp = $this->getRemoteFilemtime($source, $media, true); 139 $changelogLine = join("\t", array($timestamp, clientIP(true), DOKU_CHANGE_TYPE_EDIT, $media, $INPUT->server->str('REMOTE_USER'), "Media updated from $conf[title] (" . DOKU_URL . ")")); 140 $this->addRemoteMediaChangelogRevision($target, $media, $changelogLine); 141 $sourceContent = $this->readRemoteMedia($source, $media); 142 $this->replaceRemoteFile($this->getRemoteMediaFilename($target, $media), $sourceContent, $timestamp); 143 $this->replaceRemoteFile($this->getRemoteMediaFilename($target, $media, $timestamp), $sourceContent, $timestamp); 144 } 145 146 /** 147 * Read the contents of a page in an animal 148 * 149 * @param string $animal 150 * @param string $page a page ID 151 * @param bool $clean does the pageID need cleaning? 152 * @return string 153 */ 154 public function readRemotePage($animal, $page, $clean = true, $timestamp = null) { 155 return io_readFile($this->getRemoteFilename($animal, $page, $timestamp, $clean)); 156 } 157 158 /** 159 * Read the contents of a media item in an animal 160 * 161 * @param string $animal 162 * @param string $media mediaID 163 * @param int $timestamp revision 164 * @return string 165 */ 166 public function readRemoteMedia($animal, $media, $timestamp = 0) { 167 return io_readFile($this->getRemoteMediaFilename($animal, $media, $timestamp), false); 168 } 169 170 /** 171 * Get the path to a media item in an animal 172 * 173 * @param string $animal 174 * @param string $media 175 * @param int $timestamp 176 * @return string 177 */ 178 public function getRemoteMediaFilename($animal, $media, $timestamp = 0, $clean = true) { 179 global $conf; 180 $animaldir = $this->getAnimalDataDir($animal); 181 $source_mediaolddir = $conf['mediaolddir']; 182 $conf['mediaolddir'] = $animaldir . 'media_attic'; 183 $source_mediadir = $conf['mediadir']; 184 $conf['mediadir'] = $animaldir . 'media'; 185 186 $mediaFN = mediaFN($media, $timestamp, $clean); 187 188 $conf['mediaolddir'] = $source_mediaolddir; 189 $conf['mediadir'] = $source_mediadir; 190 191 return $mediaFN; 192 } 193 194 /** 195 * Get the filename of a page at an animal 196 * 197 * @param string $animal the animal 198 * @param string $document the full pageid 199 * @param string|null $timestamp set to get a version in the attic 200 * @param bool $clean Should the pageid be cleaned? 201 * @return string The path to the page at the animal 202 */ 203 public function getRemoteFilename($animal, $document, $timestamp = null, $clean = true) { 204 global $cache_wikifn; 205 206 $this->setAnimalContext($animal); 207 208 unset($cache_wikifn[str_replace(':', '/', $clean ? cleanID($document) : $document)]); 209 $FN = wikiFN($document, $timestamp, $clean); 210 unset($cache_wikifn[str_replace(':', '/', $clean ? cleanID($document) : $document)]); 211 212 $this->resetContext(); 213 214 return $FN; 215 } 216 217 /** 218 * Get the last modified time of an animal's page or media file 219 * 220 * @param string $animal 221 * @param string $document Either the page-id or the media-id, colon-separated 222 * @param bool $ismedia 223 * @param bool $clean For pages only: define if the pageid should be cleaned 224 * @return int The modified time of the given document 225 */ 226 public function getRemoteFilemtime($animal, $document, $ismedia = false, $clean = true) { 227 if ($ismedia) { 228 return filemtime($this->getRemoteMediaFilename($animal, $document)); 229 } 230 return filemtime($this->getRemoteFilename($animal, $document, null, $clean)); 231 } 232 233 /** 234 * Check if a page in a given animal exists 235 * 236 * @param string $animal 237 * @param string $page 238 * @param bool $clean 239 * @return bool 240 */ 241 public function remotePageExists($animal, $page, $clean = true) { 242 return file_exists($this->getRemoteFilename($animal, $page, null, $clean)); 243 } 244 245 public function remoteMediaExists($animal, $medium, $timestamp = null) { 246 return file_exists($this->getRemoteMediaFilename($animal, $medium, $timestamp)); 247 } 248 249 /** 250 * Finds the common ancestor revision of two revisions of a page. 251 * 252 * The goal is to find the revision that exists at both target and animal with the same timestamp and content. 253 * 254 * @param string $page 255 * @param string $source 256 * @param string $target 257 * @return string 258 */ 259 public function findCommonAncestor($page, $source, $target) { 260 $targetDataDir = $this->getAnimalDataDir($target); 261 $parts = explode(':', $page); 262 $pageid = array_pop($parts); 263 $atticdir = $targetDataDir . 'attic/' . join('/', $parts); 264 $atticdir = rtrim($atticdir, '/') . '/'; 265 if (!file_exists($atticdir)) return ""; 266 /** @var \Directory $dir */ 267 $dir = dir($atticdir); 268 $oldrevisions = array(); 269 while (false !== ($entry = $dir->read())) { 270 if ($entry == '.' || $entry == '..' || is_dir($atticdir . $entry)) { 271 continue; 272 } 273 list($atticpageid, $timestamp,) = explode('.', $entry); 274 if ($atticpageid == $pageid) $oldrevisions[] = $timestamp; 275 } 276 rsort($oldrevisions); 277 $sourceMtime = $this->getRemoteFilemtime($source, $page); 278 foreach ($oldrevisions as $rev) { 279 if (!file_exists($this->getRemoteFilename($source, $page, $rev)) && $rev != $sourceMtime) continue; 280 $sourceArchiveText = $rev == $sourceMtime ? $this->readRemotePage($source, $page) : $this->readRemotePage($source, $page, null, $rev); 281 $targetArchiveText = $this->readRemotePage($target, $page, null, $rev); 282 if ($sourceArchiveText == $targetArchiveText) { 283 return $sourceArchiveText; 284 } 285 } 286 return ""; 287 } 288 289 /** 290 * @param string $animal 291 * @param string $page 292 * @param string $changelogLine 293 * @param bool $truncate 294 * @throws \Exception 295 */ 296 public function addRemotePageChangelogRevision($animal, $page, $changelogLine, $truncate = true) { 297 $remoteChangelog = $this->getAnimalDataDir($animal) . 'meta/' . join('/', explode(':', $page)) . '.changes'; 298 $revisionsToAdjust = $this->addRemoteChangelogRevision($remoteChangelog, $changelogLine, $truncate); 299 foreach ($revisionsToAdjust as $revision) { 300 $this->replaceRemoteFile($this->getRemoteFilename($animal, $page, intval($revision) - 1), io_readFile($this->getRemoteFilename($animal, $page, intval($revision)))); 301 } 302 } 303 304 /** 305 * @param string $animal 306 * @param string $medium 307 * @param string $changelogLine 308 * @param bool $truncate 309 * @throws \Exception 310 */ 311 public function addRemoteMediaChangelogRevision($animal, $medium, $changelogLine, $truncate = true) { 312 $remoteChangelog = $this->getAnimalDataDir($animal) . 'media_meta/' . join('/', explode(':', $medium)) . '.changes'; 313 $revisionsToAdjust = $this->addRemoteChangelogRevision($remoteChangelog, $changelogLine, $truncate); 314 foreach ($revisionsToAdjust as $revision) { 315 $this->replaceRemoteFile($this->getRemoteMediaFilename($animal, $medium, intval($revision) - 1), io_readFile($this->getRemoteMediaFilename($animal, $medium, intval($revision)))); 316 } 317 } 318 319 public function addRemoteChangelogRevision($remoteChangelog, $changelogLine, $truncate = true) { 320 $rev = substr($changelogLine, 0, 10); 321 if (!$this->isValidTimeStamp($rev)) { 322 throw new \Exception('2nd Argument must start with timestamp!'); 323 }; 324 $lines = explode("\n", io_readFile($remoteChangelog)); 325 $lineindex = count($lines); 326 $revisionsToAdjust = array(); 327 foreach ($lines as $index => $line) { 328 if (substr($line, 0, 10) == $rev) { 329 $revisionsToAdjust = $this->freeChangelogRevision($lines, $rev); 330 $lineindex = $index + 1; 331 break; 332 } 333 if (substr($line, 0, 10) > $rev) { 334 $lineindex = $index; 335 break; 336 } 337 } 338 array_splice($lines, $lineindex, $truncate ? count($lines) : 0, $changelogLine); 339 340 $this->replaceRemoteFile($remoteChangelog, join("\n", $lines) . "\n"); 341 return $revisionsToAdjust; 342 } 343 344 /** 345 * Modify the changelog so that the revision $rev does not have a changelog entry. However modifying the timestamps 346 * in the changelog only works if we move the attic revisions as well. 347 * 348 * @param string[] $lines the changelog lines. This array will be adjusted by this function 349 * @param string $rev The timestamp which should not have an entry 350 * @return string[] List of attic revisions that need to be moved 1s back in time 351 */ 352 public function freeChangelogRevision(&$lines, $rev) { 353 $lineToMakeFree = -1; 354 foreach ($lines as $index => $line) { 355 if (substr($line, 0, 10) == $rev) { 356 $lineToMakeFree = $index; 357 break; 358 } 359 } 360 if ($lineToMakeFree == -1) return array(); 361 362 $i = 0; 363 $revisionsToAdjust = array($rev); 364 while ($lineToMakeFree > 0 && substr($lines[$lineToMakeFree - ($i + 1)], 0, 10) == $rev - ($i + 1)) { 365 $revisionsToAdjust[] = $rev - ($i + 1); 366 $i += 1; 367 } 368 369 for (; $i >= 0; $i -= 1) { 370 $parts = explode("\t", $lines[$lineToMakeFree - $i]); 371 array_shift($parts); 372 array_unshift($parts, intval($rev) - $i - 1); 373 374 $lines[$lineToMakeFree - $i] = join("\t", $parts); 375 } 376 sort($revisionsToAdjust); 377 return $revisionsToAdjust; 378 } 379 380 /** 381 * taken from http://stackoverflow.com/questions/2524680/check-whether-the-string-is-a-unix-timestamp#2524761 382 * 383 * @param $timestamp 384 * @return bool 385 */ 386 private function isValidTimeStamp($timestamp) { 387 return ((string)(int)$timestamp === (string)$timestamp); 388 } 389 390 public function getAllStructSchemasList($animal) { 391 /** @var \helper_plugin_struct_imexport $struct */ 392 $struct = plugin_load('helper', 'struct_imexport'); 393 if (empty($struct)) { 394 return array(); 395 } 396 global $conf; 397 398 $remoteDataDir = $this->getAnimalDataDir($animal); 399 $farmer_metadir = $conf['metadir']; 400 $conf['metadir'] = $remoteDataDir . 'meta'; 401 $schemas = $struct->getAllSchemasList(); 402 $conf['metadir'] = $farmer_metadir; 403 return $schemas; 404 } 405 406 public function getAnimalStructAssignments($sourceAnimal, $schemas) { 407 /** @var \helper_plugin_struct_imexport $struct */ 408 $struct = plugin_load('helper', 'struct_imexport'); 409 410 $this->setAnimalContext($sourceAnimal); 411 412 foreach ($schemas as $key => $assignment) { 413 $schemas[$assignment] = $struct->getSchemaAssignmentPatterns($assignment); 414 unset($schemas[$key]); 415 } 416 417 $this->resetContext(); 418 419 return $schemas; 420 421 } 422 423 /** 424 * @param string $targetAnimal target-animal 425 * @param array $assignments 426 */ 427 public function replaceAnimalStructAssignments($targetAnimal, $assignments) { 428 /** @var \helper_plugin_struct_imexport $struct */ 429 $struct = plugin_load('helper', 'struct_imexport'); 430 431 $this->setAnimalContext($targetAnimal); 432 433 foreach ($assignments as $schema => $patterns) { 434 $struct->replaceSchemaAssignmentPatterns($schema, $patterns); 435 } 436 437 $this->resetContext(); 438 } 439 440 public function getAnimalStructSchemasJSON($sourceAnimal, $schemas) { 441 /** @var \helper_plugin_struct_imexport $struct */ 442 $struct = plugin_load('helper', 'struct_imexport'); 443 444 $this->setAnimalContext($sourceAnimal); 445 foreach ($schemas as $key => $schema) { 446 $schemas[$schema] = json_decode($struct->getCurrentSchemaJSON($schema)); 447 $schemas[$schema]->user = 'FARMSYNC'; 448 $schemas[$schema]->id = 0; 449 $schemas[$schema] = json_encode($schemas[$schema]); 450 unset($schemas[$key]); 451 } 452 453 $this->resetContext(); 454 return $schemas; 455 } 456 457 public function importAnimalStructSchema($targetAnimal, $schemaName, $json) { 458 /** @var \helper_plugin_struct_imexport $struct */ 459 $struct = plugin_load('helper', 'struct_imexport'); 460 461 $this->setAnimalContext($targetAnimal); 462 463 $struct->importSchema($schemaName, $json, 'FARMSYNC'); 464 465 $this->resetContext(); 466 } 467 468 public function updateAnimalStructSchema($targetAnimal, $schemaName, $json) { 469 $this->setAnimalContext($targetAnimal); 470 $result = $this->_updateAnimalStructSchema($targetAnimal, $schemaName, $json); 471 $this->resetContext(); 472 return $result; 473 } 474 475 private function _updateAnimalStructSchema($target, $schemaName, $json) { 476 /** @var \helper_plugin_struct_imexport $struct */ 477 $struct = plugin_load('helper', 'struct_imexport'); 478 $result = new UpdateResults($schemaName, $target); 479 $targetJSON = $struct->getCurrentSchemaJSON($schemaName); 480 481 if ($targetJSON == false) { 482 $struct->importSchema($schemaName, $json, 'FARMSYNC'); 483 $result->setMergeResult('new file'); 484 return $result; 485 } 486 $targetSchema = json_decode($targetJSON); 487 if ($targetSchema->user == 'FARMSYNC') { 488 $targetSchema->id = 0; 489 if (json_encode($targetSchema) == $json) { 490 $result->setMergeResult('unchanged'); 491 return $result; 492 } 493 $struct->importSchema($schemaName, $json, 'FARMSYNC'); 494 $result->setMergeResult('file overwritten'); 495 return $result; 496 } 497 $result = new StructConflict($schemaName, $target); 498 $result->setMergeResult('merged with conflicts'); 499 return $result; 500 } 501 502 private function checkForExternalEdit($animal, $page) { 503 $this->setAnimalContext($animal); 504 detectExternalEdit($page); 505 $this->resetContext(); 506 } 507 508} 509