1<?php
2/**
3 * Move Plugin Page Rewriter
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
13// load required handler class
14require_once(dirname(__FILE__) . '/handler.php');
15
16/**
17 * Class helper_plugin_move_rewrite
18 *
19 * This class handles the rewriting of wiki text to update the links
20 */
21class helper_plugin_move_rewrite extends DokuWiki_Plugin {
22
23    /**
24     * Under what key is move data to be saved in metadata
25     */
26    const METAKEY = 'plugin_move';
27
28    /**
29     * What is they filename of the lockfile
30     */
31    const LOCKFILENAME = '_plugin_move.lock';
32
33    /**
34     * @var string symbol to make move operations easily recognizable in change log
35     */
36    public $symbol = '↷';
37
38    /**
39     * This function loads and returns the persistent metadata for the move plugin. If there is metadata for the
40     * pagemove plugin (not the old one but the version that immediately preceeded the move plugin) it will be migrated.
41     *
42     * @param string $id The id of the page the metadata shall be loaded for
43     * @return array|null The metadata of the page
44     */
45    public function getMoveMeta($id) {
46        $all_meta = p_get_metadata($id, '', METADATA_DONT_RENDER);
47
48        /* todo migrate old move data
49        if(isset($all_meta['plugin_pagemove']) && !is_null($all_meta['plugin_pagemove'])) {
50            if(isset($all_meta[self::METAKEY])) {
51                $all_meta[self::METAKEY] = array_merge_recursive($all_meta['plugin_pagemove'], $all_meta[self::METAKEY]);
52            } else {
53                $all_meta[self::METAKEY] = $all_meta['plugin_pagemove'];
54            }
55            p_set_metadata($id, array(self::METAKEY => $all_meta[self::METAKEY], 'plugin_pagemove' => null), false, true);
56        }
57        */
58
59        // discard missing or empty array or string
60        $meta = !empty($all_meta[self::METAKEY]) ? $all_meta[self::METAKEY] : array();
61        if(!isset($meta['origin'])) {
62            $meta['origin'] = '';
63        }
64        if(!isset($meta['pages'])) {
65            $meta['pages'] = array();
66        }
67        if(!isset($meta['media'])) {
68            $meta['media'] = array();
69        }
70
71        return $meta;
72    }
73
74    /**
75     * Remove any existing move meta data for the given page
76     *
77     * @param $id
78     */
79    public function unsetMoveMeta($id) {
80        p_set_metadata($id, array(self::METAKEY => array()), false, true);
81    }
82
83    /**
84     * Add info about a moved document to the metadata of an affected page
85     *
86     * @param string $id   affected page
87     * @param string $src  moved document's original id
88     * @param string $dst  moved document's new id
89     * @param string $type 'media' or 'page'
90     * @throws Exception on wrong argument
91     */
92    public function setMoveMeta($id, $src, $dst, $type) {
93        $this->setMoveMetas($id, array($src => $dst), $type);
94    }
95
96    /**
97     * Add info about several moved documents to the metadata of an affected page
98     *
99     * @param string $id    affected page
100     * @param array  $moves list of moves (src is key, dst is value)
101     * @param string $type  'media' or 'page'
102     * @throws Exception
103     */
104    public function setMoveMetas($id, $moves, $type) {
105        if($type != 'pages' && $type != 'media') {
106            throw new Exception('wrong type specified');
107        }
108        if(!page_exists($id, '', false)) {
109            return;
110        }
111
112        $meta = $this->getMoveMeta($id);
113        foreach($moves as $src => $dst) {
114            $meta[$type][] = array($src, $dst);
115        }
116
117        p_set_metadata($id, array(self::METAKEY => $meta), false, true);
118    }
119
120    /**
121     * Store info about the move of a page in its own meta data
122     *
123     * This has to be called before the move is executed
124     *
125     * @param string $id moved page's original (and still current) id
126     */
127    public function setSelfMoveMeta($id) {
128        $meta = $this->getMoveMeta($id);
129        // was this page moved multiple times? keep the orignal name til rewriting occured
130        if(isset($meta['origin']) && $meta['origin'] !== '') {
131            return;
132        }
133        $meta['origin'] = $id;
134
135        p_set_metadata($id, array(self::METAKEY => $meta), false, true);
136    }
137
138    /**
139     * Check if rewrites may be executed within this process right now
140     *
141     * @return bool
142     */
143    public static function isLocked() {
144        global $PLUGIN_MOVE_WORKING;
145        global $conf;
146        $lockfile = $conf['lockdir'] . self::LOCKFILENAME;
147        return ((isset($PLUGIN_MOVE_WORKING) && $PLUGIN_MOVE_WORKING > 0) || file_exists($lockfile));
148    }
149
150    /**
151     * Do not allow any rewrites in this process right now
152     */
153    public static function addLock() {
154        global $PLUGIN_MOVE_WORKING;
155        global $conf;
156        $PLUGIN_MOVE_WORKING = $PLUGIN_MOVE_WORKING ? $PLUGIN_MOVE_WORKING + 1 : 1;
157        $lockfile = $conf['lockdir'] . self::LOCKFILENAME;
158        if (!file_exists($lockfile)) {
159            io_savefile($lockfile, "1\n");
160        } else {
161            $stack = intval(file_get_contents($lockfile));
162            ++$stack;
163            io_savefile($lockfile, strval($stack));
164        }
165    }
166
167    /**
168     * Allow rerites in this process again, unless some other lock exists
169     */
170    public static function removeLock() {
171        global $PLUGIN_MOVE_WORKING;
172        global $conf;
173        $PLUGIN_MOVE_WORKING = $PLUGIN_MOVE_WORKING ? $PLUGIN_MOVE_WORKING - 1 : 0;
174        $lockfile = $conf['lockdir'] . self::LOCKFILENAME;
175        if (!file_exists($lockfile)) {
176            throw new Exception("removeLock failed: lockfile missing");
177        } else {
178            $stack = intval(file_get_contents($lockfile));
179            if($stack === 1) {
180                unlink($lockfile);
181            } else {
182                --$stack;
183                io_savefile($lockfile, strval($stack));
184            }
185        }
186    }
187
188    /**
189     * Allow rewrites in this process again.
190     *
191     * @author Michael Große <grosse@cosmocode.de>
192     */
193    public static function removeAllLocks() {
194        global $conf;
195        $lockfile = $conf['lockdir'] . self::LOCKFILENAME;
196        if (file_exists($lockfile)) {
197            unlink($lockfile);
198        }
199        unset($GLOBALS['PLUGIN_MOVE_WORKING']);
200    }
201
202
203    /**
204     * Rewrite a text in order to fix the content after the given moves.
205     *
206     * @param string $id   The id of the wiki page, if the page itself was moved the old id
207     * @param string $text The text to be rewritten
208     * @return string        The rewritten wiki text
209     */
210    public function rewrite($id, $text) {
211        $meta = $this->getMoveMeta($id);
212
213        $handlers = array();
214        $pages    = $meta['pages'];
215        $media    = $meta['media'];
216        $origin   = $meta['origin'];
217        if($origin == '') $origin = $id;
218
219        $data = array(
220            'id'          => $id,
221            'origin'      => &$origin,
222            'pages'       => &$pages,
223            'media_moves' => &$media,
224            'handlers'    => &$handlers
225        );
226
227        /*
228         * PLUGIN_MOVE_HANDLERS REGISTER event:
229         *
230         * Plugin handlers can be registered in the $handlers array, the key is the plugin name as it is given to the handler
231         * The handler needs to be a valid callback, it will get the following parameters:
232         * $match, $state, $pos, $pluginname, $handler. The first three parameters are equivalent to the parameters
233         * of the handle()-function of syntax plugins, the $pluginname is just the plugin name again so handler functions
234         * that handle multiple plugins can distinguish for which the match is. The last parameter is the handler object
235         * which is an instance of helper_plugin_move_handle
236         */
237        trigger_event('PLUGIN_MOVE_HANDLERS_REGISTER', $data);
238
239        $modes = p_get_parsermodes();
240
241        // Create the parser
242        $Parser = new Doku_Parser();
243
244        // Add the Handler
245        /** @var $Parser->Handler helper_plugin_move_handler */
246        $Parser->Handler = $this->loadHelper('move_handler');
247        $Parser->Handler->init($id, $origin, $pages, $media, $handlers);
248
249        //add modes to parser
250        foreach($modes as $mode) {
251            $Parser->addMode($mode['mode'], $mode['obj']);
252        }
253
254        return $Parser->parse($text);
255    }
256
257    /**
258     * Rewrite the text of a page according to the recorded moves, the rewritten text is saved
259     *
260     * @param string      $id   The id of the page that shall be rewritten
261     * @param string|null $text Old content of the page. When null is given the content is loaded from disk
262     * @return string|bool The rewritten content, false on error
263     */
264    public function rewritePage($id, $text = null, $save = true) {
265        $meta = $this->getMoveMeta($id);
266        if(is_null($text)) {
267            $text = rawWiki($id);
268        }
269
270        if($meta['pages'] || $meta['media']) {
271            $old_text = $text;
272            $text     = $this->rewrite($id, $text);
273
274            $changed = ($old_text != $text);
275            $file    = wikiFN($id, '', false);
276            if ($save === true) {
277                if(is_writable($file) || !$changed) {
278                    if($changed) {
279                        // Wait a second when the page has just been rewritten
280                        $oldRev = filemtime(wikiFN($id));
281                        if($oldRev == time()) sleep(1);
282
283                        saveWikiText($id, $text, $this->symbol . ' ' . $this->getLang('linkchange'), $this->getConf('minor'));
284                    }
285                    $this->unsetMoveMeta($id);
286                } else {
287                    // FIXME: print error here or fail silently?
288                    msg('Error: Page ' . hsc($id) . ' needs to be rewritten because of page renames but is not writable.', -1);
289                    return false;
290                }
291            }
292        }
293
294        return $text;
295    }
296
297}
298