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