xref: /dokuwiki/_test/tests/ChangeLog/PageChangeLogTest.php (revision 884caed926ca0aa0af6ce3f34ae3aa7317a3361a)
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