1<?php 2 3namespace dokuwiki\test\ChangeLog; 4 5use dokuwiki\ChangeLog\PageChangeLog; 6 7/** 8 * Tests for dokuwiki\ChangeLog\PageChangeLog. 9 */ 10class PageChangeLogTest extends \DokuWikiTest 11{ 12 /** 13 * A page deleted through DokuWiki is recorded as its own revision, newer than the 14 * last revision that still had content. getRelativeRevision() must walk back from 15 * that deletion entry to the last content revision (issue #4635). 16 */ 17 public function testRevisionBeforeNormalDeletion() 18 { 19 $page = 'changelog_deleted'; 20 saveWikiText($page, 'first content', 'create', false); 21 $this->waitForTick(true); 22 saveWikiText($page, 'second content longer', 'edit', false); 23 $this->waitForTick(true); 24 25 $editRev = (new PageChangeLog($page))->currentRevision(); 26 27 saveWikiText($page, '', 'delete', false); 28 clearstatcache(); 29 30 $changelog = new PageChangeLog($page); 31 $delRev = $changelog->currentRevision(); 32 33 $this->assertNotEquals($editRev, $delRev, 'deletion should get its own revision'); 34 $this->assertEquals( 35 DOKU_CHANGE_TYPE_DELETE, 36 $changelog->getRevisionInfo($delRev)['type'], 37 'current revision should be the deletion' 38 ); 39 $this->assertEquals( 40 $editRev, 41 $changelog->getRelativeRevision($delRev, -1), 42 'the revision before the deletion should be the last edit' 43 ); 44 } 45 46 /** 47 * An external deletion is detected and persisted on first read as its own revision 48 * with an unknown exact date, newer than the last content revision. 49 * getRelativeRevision() must walk back from it to that last content revision 50 * (issue #4635). 51 */ 52 public function testRevisionBeforeExternalDeletion() 53 { 54 $page = 'changelog_extdeleted'; 55 saveWikiText($page, 'first content', 'create', false); 56 $this->waitForTick(true); 57 saveWikiText($page, 'second content longer', 'edit', false); 58 $this->waitForTick(true); 59 60 $editRev = (new PageChangeLog($page))->currentRevision(); 61 62 // delete the page file externally, bypassing DokuWiki 63 unlink(wikiFN($page)); 64 clearstatcache(); 65 66 // first read detects and persists the external deletion 67 $changelog = new PageChangeLog($page); 68 $delRev = $changelog->currentRevision(); 69 $delInfo = $changelog->getRevisionInfo($delRev); 70 71 $this->assertNotEquals($editRev, $delRev, 'external deletion should get its own revision'); 72 $this->assertEquals(DOKU_CHANGE_TYPE_DELETE, $delInfo['type'], 'current revision should be the deletion'); 73 $this->assertFalse($delInfo['timestamp'], 'external deletion has an unknown exact date'); 74 $this->assertEquals( 75 $editRev, 76 $changelog->getRelativeRevision($delRev, -1), 77 'the revision before the external deletion should be the last edit' 78 ); 79 } 80 81 /** 82 * A current revision's file can have its modification time bumped without any content 83 * change (a backup restore, a git checkout, ...). That must not be recorded as an 84 * external edit: the content is compared against the last revision and, when identical, 85 * the file mtime is reset to the recorded revision date instead (issue #4634). 86 */ 87 public function testTouchedFileWithUnchangedContentIsNotExternalEdit() 88 { 89 $page = 'changelog_touched'; 90 saveWikiText($page, 'first content', 'create', false); 91 92 $changelog = new PageChangeLog($page); 93 $lastRev = $changelog->currentRevision(); 94 95 // bump the file mtime forward without changing the content 96 touch(wikiFN($page), $lastRev + 1000); 97 clearstatcache(); 98 99 $changelog = new PageChangeLog($page); 100 $currentRev = $changelog->currentRevision(); 101 $currentInfo = $changelog->getRevisionInfo($currentRev); 102 103 $this->assertEquals($lastRev, $currentRev, 'unchanged content must not create an external revision'); 104 $this->assertArrayNotHasKey('timestamp', $currentInfo, 'should not be a synthesized external edit'); 105 $this->assertCount(1, $changelog->getRevisions(-1, 200), 'no external edit entry should be added'); 106 107 clearstatcache(); 108 $this->assertEquals($lastRev, filemtime(wikiFN($page)), 'file mtime should be reset to the changelog date'); 109 } 110 111 /** 112 * A detected external edit whose date predates the most recent change already recorded 113 * in the global changelog must stay out of the recent-changes feed (or it would appear 114 * above newer entries with an old date), but is still recorded in the page's own 115 * changelog (issue #4634). 116 */ 117 public function testOutOfOrderExternalEditKeptOutOfGlobalChangelog() 118 { 119 global $conf; 120 $page = 'changelog_outoforder'; 121 saveWikiText($page, 'first content', 'create', false); 122 123 $changelog = new PageChangeLog($page); 124 $createRev = $changelog->currentRevision(); 125 126 // external edit with different content, dated after the create but well before the 127 // global changelog's last-modified time 128 $globalFile = $conf['changelog']; 129 $extRev = $createRev + 10; 130 file_put_contents(wikiFN($page), 'externally edited content'); 131 touch(wikiFN($page), $extRev); 132 touch($globalFile, $createRev + 100000); 133 clearstatcache(); 134 135 $changelog = new PageChangeLog($page); 136 $detectedRev = $changelog->currentRevision(); 137 $detectedInfo = $changelog->getRevisionInfo($detectedRev); 138 139 // detected and recorded in the page's own changelog: the create plus the external edit 140 $this->assertEquals($extRev, $detectedRev, 'external edit should be detected at the file mtime'); 141 $this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $detectedInfo['type'], 'should be an external edit'); 142 $this->assertEquals( 143 [$extRev, $createRev], 144 $changelog->getRevisions(-1, 200), 145 'page changelog should hold the create and the external edit' 146 ); 147 148 // ...but kept out of the global recent-changes feed 149 $this->assertStringNotContainsString( 150 "$extRev\t", 151 file_get_contents($globalFile), 152 'out-of-order external edit must not be appended to the global changelog' 153 ); 154 } 155 156 /** 157 * A genuinely current external edit (dated at or after the global changelog's last 158 * recorded change) must still reach the recent-changes feed (issue #4634). 159 */ 160 public function testCurrentExternalEditReachesGlobalChangelog() 161 { 162 global $conf; 163 $page = 'changelog_freshext'; 164 saveWikiText($page, 'first content', 'create', false); 165 166 $changelog = new PageChangeLog($page); 167 $createRev = $changelog->currentRevision(); 168 169 // external edit dated after the create, so it is newer than the feed's most recent 170 // change (the create just written there) and should be appended 171 $extRev = $createRev + 100; 172 file_put_contents(wikiFN($page), 'externally edited content'); 173 touch(wikiFN($page), $extRev); 174 clearstatcache(); 175 176 $changelog = new PageChangeLog($page); 177 $changelog->currentRevision(); 178 179 $this->assertStringContainsString( 180 "$extRev\t", 181 file_get_contents($conf['changelog']), 182 'a current external edit should be appended to the global changelog' 183 ); 184 } 185} 186