1<?php
2/**
3 * Move Plugin Operation Execution
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Michael Hamann <michael@content-space.de>
7 * @author     Gary Owen <gary@isection.co.uk>
8 * @author     Andreas Gohr <gohr@cosmocode.de>
9 */
10// must be run within Dokuwiki
11if(!defined('DOKU_INC')) die();
12
13class helper_plugin_move_op extends DokuWiki_Plugin {
14
15    /**
16     * @var string symbol to make move operations easily recognizable in change log
17     */
18    public $symbol = '↷';
19
20    /**
21     * @var array stores the affected pages of the last operation
22     */
23    protected $affectedPages = array();
24
25    /**
26     * Check if the given page can be moved to the given destination
27     *
28     * @param $src
29     * @param $dst
30     * @return bool
31     */
32    public function checkPage($src, $dst) {
33        // Check we have rights to move this document
34        if(!page_exists($src)) {
35            msg(sprintf($this->getLang('notexist'), $src), -1);
36            return false;
37        }
38        if(auth_quickaclcheck($src) < AUTH_EDIT) {
39            msg(sprintf($this->getLang('norights'), $src), -1);
40            return false;
41        }
42
43        // Check file is not locked
44        // checklock checks if the page lock hasn't expired and the page hasn't been locked by another user
45        // the file exists check checks if the page is reported unlocked if a lock exists which means that
46        // the page is locked by the current user
47        if(checklock($src) !== false || @file_exists(wikiLockFN($src))) {
48            msg(sprintf($this->getLang('filelocked'), $src), -1);
49            return false;
50        }
51
52        // Has the document name and/or namespace changed?
53        if($src == $dst) {
54            msg(sprintf($this->getLang('notchanged'), $src), -1);
55            return false;
56        }
57
58        // Check the page does not already exist
59        if(page_exists($dst)) {
60            msg(sprintf($this->getLang('exists'), $src, $dst), -1);
61            return false;
62        }
63
64        // Check if the current user can create the new page
65        if(auth_quickaclcheck($dst) < AUTH_CREATE) {
66            msg(sprintf($this->getLang('notargetperms'), $dst), -1);
67            return false;
68        }
69
70        return true;
71    }
72
73    /**
74     * Check if the given media file can be moved to the given destination
75     *
76     * @param $src
77     * @param $dst
78     * @return bool
79     */
80    public function checkMedia($src, $dst) {
81        // Check we have rights to move this document
82        if(!file_exists(mediaFN($src))) {
83            msg(sprintf($this->getLang('medianotexist'), $src), -1);
84            return false;
85        }
86        if(auth_quickaclcheck($src) < AUTH_DELETE) {
87            msg(sprintf($this->getLang('nomediarights'), $src), -1);
88            return false;
89        }
90
91        // Has the document name and/or namespace changed?
92        if($src == $dst) {
93            msg(sprintf($this->getLang('medianotchanged'), $src), -1);
94            return false;
95        }
96
97        // Check the page does not already exist
98        if(@file_exists(mediaFN($dst))) {
99            msg(sprintf($this->getLang('mediaexists'), $src, $dst), -1);
100            return false;
101        }
102
103        // Check if the current user can create the new page
104        if(auth_quickaclcheck($dst) < AUTH_UPLOAD) {
105            msg(sprintf($this->getLang('nomediatargetperms'), $dst), -1);
106            return false;
107        }
108
109        // check if the file extension is unchanged
110        if (pathinfo(mediaFN($src), PATHINFO_EXTENSION) !== pathinfo(mediaFN($dst), PATHINFO_EXTENSION)) {
111            msg($this->getLang('extensionchange'), -1);
112            return false;
113        }
114
115        return true;
116    }
117
118    /**
119     * Execute a page move/rename
120     *
121     * @param string $src original ID
122     * @param string $dst new ID
123     * @return bool
124     */
125    public function movePage($src, $dst) {
126        if(!$this->checkPage($src, $dst)) return false;
127
128        // lock rewrites
129        helper_plugin_move_rewrite::addLock();
130
131        /** @var helper_plugin_move_rewrite $Rewriter */
132        $Rewriter = plugin_load('helper', 'move_rewrite');
133
134        // remember what this page was called before the move in meta data
135        $Rewriter->setSelfMoveMeta($src);
136
137        // ft_backlinks() is not used here, as it does a hidden page and acl check but we really need all pages
138        $affected_pages = idx_get_indexer()->lookupKey('relation_references', $src);
139        $affected_pages[] = $dst; // the current page is always affected, because all relative links may have changed
140        $affected_pages = array_unique($affected_pages);
141
142        $src_ns   = getNS($src);
143        $src_name = noNS($src);
144        $dst_ns   = getNS($dst);
145        $dst_name = noNS($dst);
146
147        // pass this info on to other plugins
148        $eventdata = array(
149            // this is for compatibility to old plugin
150            'opts'           => array(
151                'ns'      => $src_ns,
152                'name'    => $src_name,
153                'newns'   => $dst_ns,
154                'newname' => $dst_name,
155            ),
156            'affected_pages' => &$affected_pages,
157            'src_id'         => $src,
158            'dst_id'         => $dst,
159        );
160
161        // give plugins the option to add their own meta files to the list of files that need to be moved
162        // to the oldfiles/newfiles array or to adjust their own metadata, database, ...
163        // and to add other pages to the affected pages
164        $event = new Doku_Event('PLUGIN_MOVE_PAGE_RENAME', $eventdata);
165        if($event->advise_before()) {
166            lock($src);
167
168            /** @var helper_plugin_move_file $FileMover */
169            $FileMover = plugin_load('helper', 'move_file');
170
171            // Move the Subscriptions & Indexes (new feature since Spring 2013 release)
172            $Indexer = idx_get_indexer();
173            if(($idx_msg = $Indexer->renamePage($src, $dst)) !== true
174                || ($idx_msg = $Indexer->renameMetaValue('relation_references', $src, $dst)) !== true
175            ) {
176                msg(sprintf($this->getLang('indexerror'), $idx_msg), -1);
177                return false;
178            }
179            if(!$FileMover->movePageMeta($src_ns, $src_name, $dst_ns, $dst_name)) {
180                msg(sprintf($this->getLang('metamoveerror'), $src), -1);
181                return false;
182            }
183
184            // prepare the summary for the changelog entry
185            if($src_ns == $dst_ns) {
186                $lang_key = 'renamed';
187            } elseif($src_name == $dst_name) {
188                $lang_key = 'moved';
189            } else {
190                $lang_key = 'move_rename';
191            }
192            $summary = $this->symbol . ' ' . sprintf($this->getLang($lang_key), $src, $dst);
193
194            // Wait a second when the page has just been rewritten
195            $oldRev = filemtime(wikiFN($src));
196            if($oldRev == time()) sleep(1);
197
198            // Save the updated document in its new location
199            $text = rawWiki($src);
200            saveWikiText($dst, $text, $summary);
201
202            // Delete the orginal file
203            if(@file_exists(wikiFN($dst))) {
204                saveWikiText($src, '', $summary);
205            }
206
207            // Move the old revisions
208            if(!$FileMover->movePageAttic($src_ns, $src_name, $dst_ns, $dst_name)) {
209                // it's too late to stop the move, so just display a message.
210                msg(sprintf($this->getLang('atticmoveerror'), $src ), -1);
211            }
212
213            // Add meta data to all affected pages, so links get updated later
214            foreach($affected_pages as $id) {
215                $Rewriter->setMoveMeta($id, $src, $dst, 'pages');
216            }
217
218            unlock($src);
219        }
220        $event->advise_after();
221
222        // store this for later use
223        $this->affectedPages = $affected_pages;
224
225        // unlock rewrites
226        helper_plugin_move_rewrite::removeLock();
227
228        return true;
229    }
230
231    /**
232     * Execute a media file move/rename
233     *
234     * @param string $src original ID
235     * @param string $dst new ID
236     * @return bool true if the move was successfully executed
237     */
238    public function moveMedia($src, $dst) {
239        if(!$this->checkMedia($src, $dst)) return false;
240
241        // get all pages using this media
242        $affected_pages = idx_get_indexer()->lookupKey('relation_media', $src);
243
244        $src_ns   = getNS($src);
245        $src_name = noNS($src);
246        $dst_ns   = getNS($dst);
247        $dst_name = noNS($dst);
248
249        // pass this info on to other plugins
250        $eventdata = array(
251            // this is for compatibility to old plugin
252            'opts'           => array(
253                'ns'      => $src_ns,
254                'name'    => $src_name,
255                'newns'   => $dst_ns,
256                'newname' => $dst_name,
257            ),
258            'affected_pages' => &$affected_pages,
259            'src_id'         => $src,
260            'dst_id'         => $dst,
261        );
262
263        // give plugins the option to add their own meta files to the list of files that need to be moved
264        // to the oldfiles/newfiles array or to adjust their own metadata, database, ...
265        // and to add other pages to the affected pages
266        $event = new Doku_Event('PLUGIN_MOVE_MEDIA_RENAME', $eventdata);
267        if($event->advise_before()) {
268            /** @var helper_plugin_move_file $FileMover */
269            $FileMover = plugin_load('helper', 'move_file');
270            /** @var helper_plugin_move_rewrite $Rewriter */
271            $Rewriter = plugin_load('helper', 'move_rewrite');
272
273            // Move the Subscriptions & Indexes (new feature since Spring 2013 release)
274            $Indexer = idx_get_indexer();
275            if(($idx_msg = $Indexer->renameMetaValue('relation_media', $src, $dst)) !== true) {
276                msg(sprintf($this->getLang('indexerror'), $idx_msg), -1);
277                return false;
278            }
279            if(!$FileMover->moveMediaMeta($src_ns, $src_name, $dst_ns, $dst_name)) {
280                msg(sprintf($this->getLang('mediametamoveerror'), $src), -1);
281                return false;
282            }
283
284            // prepare directory
285            io_createNamespace($dst, 'media');
286
287            // move it FIXME this does not create a changelog entry!
288            if(!io_rename(mediaFN($src), mediaFN($dst))) {
289                msg(sprintf($this->getLang('mediamoveerror'), $src), -1);
290                return false;
291            }
292
293            // clean up old ns
294            io_sweepNS($src, 'mediadir');
295
296            // Move the old revisions
297            if(!$FileMover->moveMediaAttic($src_ns, $src_name, $dst_ns, $dst_name)) {
298                // it's too late to stop the move, so just display a message.
299                msg(sprintf($this->getLang('mediaatticmoveerror'), $src), -1);
300            }
301
302            // Add meta data to all affected pages, so links get updated later
303            foreach($affected_pages as $id) {
304                $Rewriter->setMoveMeta($id, $src, $dst, 'media');
305            }
306        }
307        $event->advise_after();
308
309        // store this for later use
310        $this->affectedPages = $affected_pages;
311
312        return true;
313    }
314
315    /**
316     * Get a list of pages that where affected by the last successful move operation
317     *
318     * @return array
319     */
320    public function getAffectedPages() {
321        return $this->affectedPages;
322    }
323}
324