1<?php
2/**
3 * DokuWiki Plugin linkfix (Admin Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Andreas Gohr <andi@splitbrain.org>
7 */
8
9// must be run within Dokuwiki
10if(!defined('DOKU_INC')) die();
11
12class admin_plugin_linkfix extends DokuWiki_Admin_Plugin {
13
14    protected $searchin = '';
15    protected $changefrom = '';
16    protected $changeto = '';
17    protected $qchangefrom = '';
18    protected $dryrun = false;
19    protected $type = 'links';
20    protected $isextern = false;
21
22    public function getMenuSort() {
23        return 3000;
24    }
25
26    public function handle() {
27        global $INPUT;
28
29        $this->searchin   = cleanID($INPUT->str('searchin'));
30        $this->changefrom = $INPUT->str('changefrom');
31        $this->changeto   = $INPUT->str('changeto');
32        $this->type       = $INPUT->valid('type', array('links', 'media'), 'links');
33        $this->dryrun     = $INPUT->bool('dryrun');
34        $this->isextern   = preg_match('/^(\w+:\/\/|\\\\)/i', $this->changefrom);
35    }
36
37    public function html() {
38        global $ID;
39        global $INPUT;
40
41        if($INPUT->has('go') && checkSecurityToken()) {
42            echo '<h3>' . $this->getLang('processing') . '</h3>';
43            tpl_flush();
44
45            ignore_user_abort(true);
46            set_time_limit(0);
47            if($this->execute()) {
48                echo '<h3>'.$this->getLang('processingdone').'</h3>';
49
50                if($this->dryrun) {
51                    echo '<a href="' . wl(
52                            '', array(
53                                     'do'         => 'admin',
54                                     'page'       => 'linkfix',
55                                     'searchin'   => $this->searchin,
56                                     'changefrom' => $this->changefrom,
57                                     'changeto'   => $this->changeto,
58                                     'dryrun'     => 0,
59                                     'go'         => 'go',
60                                     'sectok'     => getSecurityToken()
61                                )
62                        ) . '">'.$this->getLang('rerunhot').'</a>';
63                }
64                tpl_flush();
65            }
66        } else {
67
68            echo $this->locale_xhtml('intro');
69
70            $form = new Doku_Form(array('action' => wl(), 'class' => 'plugin_linkfix'));
71            $form->addHidden('go', 'go');
72            $form->addHidden('page', 'linkfix');
73            $form->addHidden('id', $ID);
74            $form->startFieldset($this->getLang('menu'));
75            $form->addElement(form_makeTextField('searchin', '', $this->getLang('searchin'), '', 'block'));
76            $form->addElement(form_makeTextField('changefrom', '', $this->getLang('changefrom'), '', 'block'));
77            $form->addElement(form_makeTextField('changeto', '', $this->getLang('changeto'), '', 'block'));
78            $form->addElement(form_makeRadioField('type', 'links', $this->getLang('links'), '', 'block tick', array('checked' => 'checked')));
79            $form->addElement(form_makeRadioField('type', 'media', $this->getLang('media'), '', 'block tick'));
80            $form->addElement(form_makeCheckboxField('dryrun', '1', $this->getLang('dryrun'), '', 'block tick', array('checked' => 'checked')));
81            $form->addElement(form_makeButton('submit', 'admin', $this->getLang('submit')));
82            $form->printForm();
83        }
84    }
85
86    /**
87     * Search for all pages in the set namespace and update them
88     *
89     * @returns bool false if no correct namespace was given
90     */
91    protected function execute() {
92        global $conf;
93
94        // make sure the given search namespace exists
95        $searchin = str_replace(':', '/', $this->searchin);
96        if(!is_dir($conf['datadir'] . '/' . $searchin)) {
97            msg(sprintf($this->getLang('badnamespace'), hsc($this->searchin)), -1);
98            return false;
99        }
100
101        // use indexer to find all possibly affected pages
102        if($this->isextern) {
103            $null = '';
104            $data = ft_pageSearch('"' . $this->changefrom . '"*', $null);
105            $data = array_keys($data);
106        } elseif($this->type == 'media') {
107            $query = $this->changefrom . '*';
108            $data  = idx_get_indexer()->lookupKey('relation_media', $query);
109        } else {
110            $query = $this->changefrom . '*';
111            $data  = idx_get_indexer()->lookupKey('relation_references', $query);
112        }
113        $data = array_unique($data);
114
115        $len = strlen($this->searchin);
116        foreach($data as $id) {
117
118            // skip everything that's not in the wanted namespace
119            if($len && substr($id, 0, $len + 1) != $this->searchin . ':') continue;
120            // skip non existing pages
121            if(!page_exists($id)) continue;
122
123            $this->prnt('<ul>');
124            $this->updatepage($id);
125            $this->prnt('</ul>');
126
127        }
128
129        return true;
130    }
131
132    /**
133     * Rewrite all links and media items in the given page
134     *
135     * @param string $currentpage the page to rewrite
136     */
137    protected function updatepage($currentpage) {
138        $currentns = getNS($currentpage);
139
140        $text         = rawWiki($currentpage);
141        $crc          = md5($text);
142        $instructions = p_get_instructions($text);
143        $instructions = array_reverse($instructions);
144
145        $this->prnt('<li><div class="li">');
146        $this->prnt($this->getLang('checking') . ' <b>' . hsc($currentpage) . "</b><br />");
147
148        tpl_flush();
149
150
151        $this->prnt('<ul>');
152        foreach($instructions as $instruction) {
153            if(
154                ($this->type == 'links' && $this->isextern  && ($instruction[0] == 'externallink' || $instruction[0] == 'windowssharelink')) ||
155                ($this->type == 'links' && !$this->isextern && $instruction[0] == 'internallink') ||
156                ($this->type == 'media' && $this->isextern  && $instruction[0] == 'externalmedia') ||
157                ($this->type == 'media' && !$this->isextern && $instruction[0] == 'internalmedia')
158            ) {
159                $link = $instruction[1][0];
160                $pos  = $instruction[2] - 1;
161
162                while(
163                    $text{$pos} == '[' ||
164                    $text{$pos} == '{' ||
165                    $text{$pos} == ' ' ||
166                    $text{$pos} == "\t"
167                ) {
168                    $pos++;
169                }
170
171                $full   = $link;
172                $exists = false;
173                if(!$this->isextern) {
174                    if($this->type == 'links') {
175                        resolve_pageid($currentns, $full, $exists);
176                    } else {
177                        resolve_mediaid($currentns, $full, $exists);
178                    }
179                }
180
181                // create the new link
182                $newlink = $this->changeLink($link, $full, $currentns);
183
184                // replace the link
185                if(($link != $newlink) && ($full != cleanID($newlink))) {
186                    $this->prnt('<li><div class="li">');
187                    $this->prnt(hsc($full) . ' → ' . hsc($newlink) . '<br />');
188                    $this->prnt('</div></li>');
189
190                    $text = substr($text, 0, $pos) .
191                        $newlink . // new link
192                        substr($text, $pos + strlen($link)); // continue after old link
193                }
194
195            }
196            // everything else is ignored
197        }
198        $this->prnt('</ul>');
199
200        if($crc == md5($text)) {
201            $this->prnt('✗ '.$this->getLang('fail').'<br />');
202        } else {
203            if($this->dryrun) {
204                $this->prnt('✓ '.$this->getLang('successdry').'<br />');
205            } else {
206                saveWikiText($currentpage, $text, $this->getLang('summary'), true);
207                $this->prnt('✓ '.$this->getLang('success').'<br />');
208            }
209        }
210        $this->prnt('</div></li>');
211        tpl_flush();
212    }
213
214    /**
215     * Wrapper around echo, for better testability
216     *
217     * @param $string
218     */
219    protected function prnt($string) {
220        echo $string;
221    }
222
223    /**
224     * Rewrite the given link according to the given settings
225     *
226     * @param string $link      the current link as found in the page source
227     * @param string $full      the full, resolved version of the above link
228     * @param string $currentNS the namespace of the page the link was found in
229     * @return string the corrected link
230     */
231    protected function changeLink($link, $full, $currentNS) {
232        // make sure the prefix matches (caseinsensitive)
233        if($this->changefrom){
234            if(!$this->qchangefrom) {
235                $this->qchangefrom = preg_quote($this->qchangefrom, '/');
236            }
237            if(!preg_match('/^'.$this->qchangefrom.'/ui', $full)) return $link;
238        }
239
240        // strip prefix
241        $new = substr($full, strlen($this->changefrom));
242
243        // add prefix
244        $new = $this->changeto . $new;
245
246        if(!$this->isextern) {
247            // strip left over colons
248            $new = ltrim($new, ':');
249
250            // make absolute if needed
251            if($currentNS && strpos($new, ':') === false) $new = ":$new";
252        }
253
254        return $new;
255    }
256
257}
258
259// vim:ts=4:sw=4:et:
260