1<?php
2/**
3 * DokuWiki Plugin multiorphan (Action Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  i-net software <tools@inetsoftware.de>
7 */
8
9// must be run within Dokuwiki
10if(!defined('DOKU_INC')) {
11    die();
12}
13
14class action_plugin_multiorphan_multiorphan extends DokuWiki_Action_Plugin {
15
16    private $checkInstructions = array('plugin', 'externallink', 'interwikilink', 'locallink', 'windowssharelink');
17    private $pagesInstructions = array('internallink', 'camelcaselink');
18    private $mediaInstructions = array('internalmedia');
19
20    private $renderer = null;
21    private $checkExternal = false;
22    private $includeWindowsShares = false;
23
24    /**
25     * Registers a callback function for a given event
26     *
27     * @param Doku_Event_Handler $controller DokuWiki's event controller object
28     * @return void
29     */
30    public function register(Doku_Event_Handler $controller) {
31
32        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax_call_unknown');
33        $controller->register_hook('MULTIORPHAN_INSTRUCTION_LINKED', 'BEFORE', $this, 'handle_unknown_instructions');
34        $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'extend_JSINFO');
35    }
36
37    /**
38     * [Custom event handler which performs action]
39     *
40     * @param Doku_Event $event  event object by reference
41     * @param mixed      $param  [the parameters passed as fifth argument to register_hook() when this
42     *                           handler was registered]
43     * @return false|null
44     */
45
46    public function handle_ajax_call_unknown(Doku_Event &$event, $param) {
47
48        global $INPUT, $conf, $AUTH;
49
50        if ( $event->data != 'multiorphan' ) {
51            return false;
52        }
53        if ((!$helper = $this->loadHelper('multiorphan'))) {
54            return false;
55        }
56        if ( !checkSecurityToken() ) {
57            return false;
58        }
59        $event->preventDefault();
60
61        $namespace = $INPUT->str('ns');
62        $ns_dir  = utf8_encodeFN(str_replace(':','/',$namespace));
63        $this->checkExternal = $INPUT->bool('checkExternal');
64        $this->includeWindowsShares = $INPUT->bool('includeWindowsShares');
65        $result  = array();
66
67        switch( $INPUT->str('do') ) {
68
69            case 'loadpages': {
70
71                $type = 'both'; //$INPUT->str('type');
72                $includeHidden = $INPUT->bool('includeHidden', false);
73
74                if ( $type == 'both' || $type == 'pages') {
75                    $pages = array();
76                    search($pages,$conf['datadir'],'search_universal',array(
77                        'showhidden' => $includeHidden,
78                        'skipacl' => $includeHidden,
79                        'pagesonly' => true,
80                        'listfiles' => true,
81                        'idmatch' => trim($INPUT->str('filter'))
82                    ),$ns_dir);
83                    array_walk($pages, array($this, '__map_ids'));
84                }
85
86                if ( $type == 'both' || $type == 'media') {
87                    $media = array();
88                    search($media,$conf['mediadir'],'search_media',array(
89                        'showhidden' => $includeHidden,
90                        'skipacl' => $includeHidden,
91                        'pattern' => '/' . str_replace('/', '\/', trim($INPUT->str('filter'))) . '/'
92                    ),$ns_dir);
93                    array_walk($media, array($this, '__map_ids'));
94                }
95
96                $result = array(
97                    'pages' => $pages,
98                    'media' => $media
99                );
100
101                break;
102            }
103
104            case 'checkpage': {
105
106                $id = $INPUT->str('id');
107                $result = $this->__check_pages($id);
108                break;
109            }
110
111            case 'deletePage' : {
112
113                $link = urldecode($INPUT->str('link'));
114                saveWikiText($link, '', "Remove page via multiORPHANS");
115                break;
116            }
117
118            case 'viewMedia' : {
119
120                $link = $INPUT->str('link');
121                ob_start();
122                tpl_mediaFileDetails($link, null);
123                $result = array( 'dialogContent' => ob_get_contents());
124                ob_end_clean();
125
126                // If there is no content, this could be a link only
127                if ( !empty( $result['dialogContent'] ) ) {
128                    break;
129                }
130            }
131
132            case 'deleteMedia' : {
133
134                $link = urldecode($INPUT->str('link'));
135                $status = media_delete($link, $AUTH);
136                break;
137            }
138
139            default: {
140                $result = array(
141                    'error' => 'I do not know what to do.'
142                );
143                break;
144            }
145        }
146
147        print json_encode($result);
148
149    }
150
151    /**
152     * Remove not needed information from search
153     */
154    private function __map_ids(&$element) {
155        $element = $element['id'];
156    }
157
158    /**
159     * Checks a page for the contained links and media.
160     * Returns an array: page|media => array of ids with count
161     */
162    private function __check_pages($id) {
163
164        global $conf;
165
166        $file         = wikiFN($id);
167        $instructions = p_cached_instructions($file, false, $id);
168        $links        = array('pages' => array(), 'media' => array(), 'href' => wl($id) );
169
170        $this->walk_instructions( $links, $id, $instructions );
171        return $links;
172    }
173
174    /**
175     * Walks a list of instructions to find links
176     */
177    public function walk_instructions( &$links, $id, $instructions ) {
178        global $ID;
179
180        if (!is_array($instructions)) {
181            return;
182        }
183
184        $internalID = $ID;
185        $ID = $id;
186        foreach ($instructions as $ins) {
187
188            if ($ins[0] == 'nest' ) {
189                $this->walk_instructions( $links, $id, $ins[1][0] );
190                continue;
191            }
192
193            $data = $this->_getDataContainer( $id, $ins);
194            $evt = new Doku_Event('MULTIORPHAN_INSTRUCTION_LINKED', $data);
195
196            // If prevented, this is definitely an orphan.
197            if (!is_null($data['type']) || (in_array($ins[0], $this->checkInstructions) && $evt->advise_before())) {
198                $this->_addEntryToLinkList( $links, $data);
199            }
200
201            unset($evt);
202        }
203        $ID = $internalID;
204    }
205
206    private function _getDataContainer( $id, $instructions ) {
207
208        return array(
209                    'pageID' => $id,
210                    'instructions' => $instructions[1],
211                    'checkNamespace' => getNS($id),
212                    'entryID' => !empty($instructions[1][0]) ? $instructions[1][0] : null,
213                    'syntax' => $instructions[0],
214                    'type' => $this->getInternalMediaType($instructions[0]),
215                    'exists' => null,
216                    'additionalEntries' => array(),
217                );
218    }
219
220    private function _addEntryToLinkList( &$links, $data ) {
221
222        if ( !$data || is_null($data['type'])) {
223            // still not media type so ignore the entry.
224            return;
225        }
226
227        $mid = $data['entryID'];
228        $hash = null;
229        if (strpos($mid, '#') !== false) {
230            list($mid, $hash) = explode('#', $mid); //record pages without hashs
231        }
232
233		$isLocalLink = $data['syntax'] == 'locallink';
234		if ( $isLocalLink ) {
235	        $mid = $data['pageID'];
236	        $hash = cleanID($data['instructions'][0]);
237	        $data['type'] = 'pages';
238		}
239
240        if (( !is_bool($data['exists']) || !$data['exists']) && $data['type'] == 'media') {
241            resolve_mediaid($data['checkNamespace'], $mid, $data['exists']);
242        } else if (!is_bool($data['exists']) || !$data['exists']) {
243            resolve_pageid($data['checkNamespace'], $mid, $data['exists']);
244            if ( $data['exists'] && !empty( $hash) ) {
245                // check for 'locallink' in a different page than the current one
246                $linkData = array(
247                    'pageID' => $mid,
248                    'entryID' => $hash,
249                    'exists' => null,
250                );
251
252                $this->_check_locallink( $linkData );
253                $data['exists'] = $linkData['exists'];
254            }
255        }
256
257        $itemIndex = $mid . (!empty($hash) ? '#'.$hash : '');
258        if (!isset($links[$data['type']][$itemIndex])) {
259            $links[$data['type']][$itemIndex] = array(
260                'href' => $this->hrefForType( $data['type'], $itemIndex),
261                'amount' => 0
262            );
263        }
264
265        $links[$data['type']][$itemIndex]['amount'] += (is_bool($data['exists']) && $data['exists']) ? 1 : 0;
266
267        if ( !is_array($data['additionalEntries']) ) {
268            return;
269        }
270
271        foreach( $data['additionalEntries'] as $additionalEntry ) {
272            $this->_addEntryToLinkList( $links, array_merge( $this->_getDataContainer( $data['id'], $data['instructions']), $additionalEntry));
273        }
274    }
275
276    private function _plugin_input_to_header( &$input, &$data ) {
277
278        // print_r($input);
279        switch( $input[1][0] ) {
280            case 'box2':
281                if ( $input[1][1][0] == 'title' ) {
282                    $input = array( 'header', array( $input[1][1][1]) );
283                }
284                break;
285            case 'include_include':
286                // Get included instructions
287                $plugin = plugin_load('syntax', 'include_include');
288                if($plugin != null) {
289                    $plugin->render($this->renderer->getFormat(), $this->renderer, $input[1][1]);
290                }
291
292                $instructions = $this->renderer->instructions;
293                $this->renderer->nest_close();
294                $this->_check_locallink( $data, $instructions);
295                break;
296        }
297    }
298
299    private function _check_locallink( &$data, $instructions = null ) {
300        $this->_init_renderer();
301        $renderer = &$this->renderer;
302        $data['type'] = 'pages';
303        $data['exists'] = false;
304        $result = array();
305
306        if ( is_null($instructions) ) {
307            $instructions = p_cached_instructions(wikiFN($data['pageID']), false, $data['pageID']);
308        }
309
310        if ( !is_null($instructions) ) {
311            $result = array_filter($instructions, function( $input ) use ( $data, $renderer ) {
312                // Closure requires PHP >= 5.3
313
314                if ( $input[0] == 'plugin' ) {
315                    $this->_plugin_input_to_header( $input, $data );
316                }
317
318                if ( $input[0] != 'header' ) {
319                    return $data['exists'];
320                }
321
322                $hid = $renderer->_headerToLink( $input[1][0] );
323                $check = $renderer->_headerToLink( $data['entryID'] );
324
325                return ($hid == $check);
326            });
327        }
328
329        $data['exists'] = $data['exists'] || count($result) > 0;
330    }
331
332    /**
333     * Handles unknown instructions using the Event.
334     */
335    public function handle_unknown_instructions(Doku_Event &$event) {
336
337        //print "Beginn:\n";
338        //print_r($event->data);
339        $instructions = $event->data['instructions'];
340        switch( $event->data['syntax'] ) {
341
342            case 'locallink': {
343                $this->_check_locallink( $event->data );
344            }
345            case 'interwikilink': {
346
347                if ( ! $this->checkExternal ) { return false; }
348                $this->_init_renderer();
349                $exists = false;
350                $event->data['entryID'] = $this->renderer->_resolveInterWiki($instructions[2], $instructions[3], $exists);
351            }
352            case 'externallink': {
353
354                if ( ! $this->checkExternal ) { return false; }
355
356                $httpClient = new dokuwiki\HTTP\DokuHTTPClient();
357                $httpClient->keep_alive = false; // just close it already.
358                $httpClient->max_bodysize = 0;
359                $data = $httpClient->sendRequest( $event->data['entryID'], null, 'GET' );
360                $event->data['exists'] = ( $httpClient->status >= 200 && $httpClient->status <= 200 ) || $httpClient->status == 304;
361                $event->data['status'] = $httpClient->status;
362                $event->data['type'] = 'urls';
363                if ( !empty( $httpClient->error ) ) {
364                    $event->data['error'] = $httpClient->error;
365                }
366
367                return true;
368            }
369            case 'windowssharelink': {
370                if ( ! $this->includeWindowsShares ) { return false; }
371
372                if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
373                    return false;
374                }
375                $event->data['exists'] = file_exists($event->data['entryID']);
376                $event->data['type'] = 'media';
377                return true;
378            }
379            case 'plugin': {
380                switch( $instructions[0] ) {
381                    case 'include_include':
382                        $event->data['entryID'] = $instructions[1][1];
383                        $event->data['type'] = 'pages';
384                        return true;
385                    case 'imagemapping':
386                        if ( $instructions[1][1] == 'area' ) {
387                            $event->data['type'] = $this->getInternalMediaType($instructions[1][4]);
388                            $event->data['entryID'] = $instructions[1][6];
389                        } else
390                        {
391                            $event->data['type'] = $this->getInternalMediaType($instructions[1][1]);
392                            $event->data['entryID'] = $instructions[1][2];
393                        }
394                        return true;
395                    case 'mp3play':
396                        $event->data['entryID'] = $instructions[1]['mp3'];
397                        $event->data['type'] = 'media';
398                        return true;
399                    case 'imagebox':
400                        if ( $instructions[1][0] === 1 ) {
401                            $event->data['entryID'] = $instructions[1][1]['src'];
402                            $event->data['exists'] = $instructions[1][1]['exist'];
403                            $event->data['type'] = 'media';
404                            return true;
405                        }
406                    default:
407                        // print_r($instructions);
408                }
409            }
410        }
411
412        return false;
413    }
414
415    public function extend_JSINFO($event, $param) {
416        global $JSINFO;
417        $JSINFO['schemes'] = array_values(getSchemes());
418    }
419
420    private function _init_renderer() {
421        if ( $this->renderer != null ) {
422            return;
423        }
424
425        @include_once( dirname( __FILE__ ) .  "/../inc/MultiOrphanDummyRenderer.php");
426        $this->renderer = new MultiOrphanDummyRenderer();
427        $this->renderer->interwiki = getInterwiki();
428    }
429
430    private function getInternalMediaType($ins) {
431        return in_array($ins, $this->mediaInstructions) ? 'media' : (in_array($ins, $this->pagesInstructions) ? 'pages' : null);
432    }
433
434    private function hrefForType( $type, $id ) {
435        switch( $type ) {
436            case 'pages':
437                list($link, $hash) = explode('#', $id, 2);
438                if ( !empty( $hash) ) {
439                    $this->_init_renderer();
440                    $hash = '#' . $this->renderer->_headerToLink( $hash );
441                }
442
443                return wl($link) . $hash;
444            case 'urls':
445                return $id;
446            case 'media':
447                return  ml($id);
448            default:
449                return null;
450        }
451    }
452}
453
454// vim:ts=4:sw=4:et:
455