1<?php 2 3namespace dokuwiki\test\ChangeLog; 4 5use dokuwiki\ChangeLog\MediaChangeLog; 6 7/** 8 * Tests for dokuwiki\ChangeLog\MediaChangeLog. 9 * 10 * Media files differ from pages in one way that matters to external-change detection: media 11 * never archives its current revision (only the previous content is copied to the attic on 12 * replace or delete). So the attic copy of the current revision is always missing, and the 13 * base-class logic that reads it - the unchanged-content guard and the old-size lookup for 14 * the size change - has to be handled specially for media. 15 */ 16class MediaChangeLogTest extends \DokuWikiTest 17{ 18 /** 19 * Write a media file with the given content and mtime and record a matching changelog entry, 20 * leaving file mtime and changelog in agreement (as a real DokuWiki upload does). 21 */ 22 protected function seedMedia($image, $content, $date, $type = DOKU_CHANGE_TYPE_CREATE, $sizechange = null) 23 { 24 io_saveFile(mediaFN($image), $content); 25 touch(mediaFN($image), $date); 26 clearstatcache(); 27 addMediaLogEntry( 28 $date, 29 $image, 30 $type, 31 $type === DOKU_CHANGE_TYPE_CREATE ? 'created' : 'edited', 32 '', 33 null, 34 $sizechange === null ? strlen($content) : $sizechange 35 ); 36 } 37 38 /** 39 * A media file whose mtime is bumped without any content change (a touch, an rsync --times, 40 * an unzip of identical bytes) must not be recorded as an external edit. The current media 41 * revision is never archived, so there is no copy to compare against; the recorded size is 42 * used instead, and an unchanged size means the content did not change: the mtime is reset 43 * to the recorded revision date and no changelog entry is added. 44 */ 45 public function testTouchedMediaWithUnchangedContentIsNotExternalEdit() 46 { 47 $image = 'changelog:touched.png'; 48 $lastRev = time() - 1000; 49 $this->seedMedia($image, str_repeat('x', 100), $lastRev); 50 51 // bump the file mtime forward without changing the content 52 touch(mediaFN($image), $lastRev + 500); 53 clearstatcache(); 54 55 $changelog = new MediaChangeLog($image); 56 $currentRev = $changelog->currentRevision(); 57 $currentInfo = $changelog->getRevisionInfo($currentRev); 58 59 $this->assertEquals($lastRev, $currentRev, 'unchanged content must not create an external revision'); 60 $this->assertArrayNotHasKey('timestamp', $currentInfo, 'should not be a synthesized external edit'); 61 $this->assertCount(1, $changelog->getRevisions(-1, 200), 'no external edit entry should be added'); 62 $this->assertFileDoesNotExist(mediaFN($image, $lastRev + 500), 'no attic snapshot should be written'); 63 64 clearstatcache(); 65 $this->assertEquals($lastRev, filemtime(mediaFN($image)), 'file mtime should be reset to the changelog date'); 66 } 67 68 /** 69 * A media file genuinely replaced outside DokuWiki (different content, newer mtime) is an 70 * external edit. Its size change must be computed against the recorded size of the previous 71 * revision, not against the missing attic copy of the current revision (which would report a 72 * zero old size and therefore the whole new file as the change). The new content is not 73 * snapshotted to the attic: media never archives its current revision. 74 */ 75 public function testExternallyReplacedMediaRecordsCorrectSizeChange() 76 { 77 $image = 'changelog:replaced.png'; 78 $lastRev = time() - 1000; 79 $this->seedMedia($image, str_repeat('a', 100), $lastRev); 80 81 // replace the file externally with a differently-sized one, newer mtime 82 $extRev = $lastRev + 500; 83 io_saveFile(mediaFN($image), str_repeat('b', 250)); 84 touch(mediaFN($image), $extRev); 85 clearstatcache(); 86 87 $changelog = new MediaChangeLog($image); 88 $currentRev = $changelog->currentRevision(); 89 $currentInfo = $changelog->getRevisionInfo($currentRev); 90 91 $this->assertEquals($extRev, $currentRev, 'external edit detected at the file mtime'); 92 $this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $currentInfo['type'], 'should be an external edit'); 93 $this->assertEquals( 94 250 - 100, 95 $currentInfo['sizechange'], 96 'the old size must come from the recorded revision, not zero against the missing attic' 97 ); 98 $this->assertFileDoesNotExist( 99 mediaFN($image, $extRev), 100 'the current revision is not archived, so the external edit is not snapshotted either' 101 ); 102 } 103 104 /** 105 * With more than one recorded revision the old size is reconstructed from the previous 106 * revision's real attic copy plus the size change logged for the current revision, so a 107 * multi-revision media file still yields the correct external-edit size change. 108 */ 109 public function testExternalEditReconstructsOldSizeFromPreviousAttic() 110 { 111 $image = 'changelog:multi.png'; 112 $rev1 = time() - 2000; 113 $this->seedMedia($image, str_repeat('a', 100), $rev1); 114 115 // simulate a DokuWiki replace: archive the current revision, write new content, log the edit 116 $rev2 = $rev1 + 500; 117 io_makeFileDir(mediaFN($image, $rev1)); 118 copy(mediaFN($image), mediaFN($image, $rev1)); // what media_saveOldRevision() does 119 $this->seedMedia($image, str_repeat('b', 300), $rev2, DOKU_CHANGE_TYPE_EDIT, 300 - 100); 120 121 // now replace the current (unarchived) revision externally 122 $rev3 = $rev2 + 500; 123 io_saveFile(mediaFN($image), str_repeat('c', 120)); 124 touch(mediaFN($image), $rev3); 125 clearstatcache(); 126 127 $changelog = new MediaChangeLog($image); 128 $currentRev = $changelog->currentRevision(); 129 $currentInfo = $changelog->getRevisionInfo($currentRev); 130 131 $this->assertEquals($rev3, $currentRev); 132 $this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $currentInfo['type']); 133 $this->assertEquals( 134 120 - 300, 135 $currentInfo['sizechange'], 136 'old size reconstructed from the previous attic (300) plus the logged change' 137 ); 138 } 139 140 /** 141 * An external media edit whose mtime is older than the last revision cannot trust that 142 * mtime as its date, so it is dated just after the last revision. The current file's mtime 143 * is then repaired forward to that date (media does not archive, but the base class still 144 * fixes the mtime) so the same change is not re-detected on the next read. 145 */ 146 public function testExternalEditWithOlderMtimeIsRepairedAndStable() 147 { 148 $image = 'changelog:oldmtime.png'; 149 $lastRev = time() - 1000; 150 $this->seedMedia($image, str_repeat('a', 100), $lastRev); 151 152 // replace externally with different content but an mtime older than the last revision 153 io_saveFile(mediaFN($image), str_repeat('b', 250)); 154 touch(mediaFN($image), $lastRev - 100); 155 clearstatcache(); 156 157 $this->expectLogMessage('current file modification time is older than last revision date'); 158 159 $changelog = new MediaChangeLog($image); 160 $currentRev = $changelog->currentRevision(); 161 $currentInfo = $changelog->getRevisionInfo($currentRev); 162 163 $this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $currentInfo['type']); 164 $this->assertFalse($currentInfo['timestamp'], 'an older-than-last mtime cannot be trusted as the date'); 165 $this->assertEquals($lastRev + 1, $currentRev, 'the edit is dated just after the last revision'); 166 167 // the file mtime is repaired forward, so a fresh read does not re-detect the change 168 clearstatcache(); 169 $this->assertEquals($lastRev + 1, filemtime(mediaFN($image)), 'file mtime repaired to the synthesized date'); 170 $fresh = new MediaChangeLog($image); 171 $this->assertEquals($lastRev + 1, $fresh->currentRevision(), 'no re-detection on the next read'); 172 $this->assertCount(2, $fresh->getRevisions(-1, 200), 'exactly one external edit was recorded'); 173 } 174} 175