1<?php
2/**
3 * Plugin Bookmarkfile: Displays a bookmark file as linklist
4 * Syntax: <BOOKMARKFILE file="..." [separators="hide"] [folder="..."]>
5 * e.g. <BOOKMARKFILE file="bookmarks.json">
6 *
7 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
8 *
9 * @author Ekkart Kleinod
10 * @author LarsDW223
11 */
12
13/**
14 * Plugin-Class for Bookmarkfile-Plugin.
15 *
16 * All DokuWiki plugins to extend the parser/rendering mechanism
17 * need to inherit from DokuWiki_Syntax_Plugin
18 */
19class syntax_plugin_bookmarkfile extends DokuWiki_Syntax_Plugin
20{
21    /** First line of opera/firefox bookmark files in HTML format. */
22    private $netscape_bmf = '<!DOCTYPE NETSCAPE-Bookmark-file-1>';
23
24    /**
25     * What kind of syntax are we?
26     */
27    public function getType()
28    {
29        return 'substition';
30    }
31
32    /**
33     * How to handle paragraphs?
34     */
35    public function getPType()
36    {
37        return 'block';
38    }
39
40    /**
41     * Where to sort in?
42     */
43    public function getSort()
44    {
45        return 100;
46    }
47
48    /**
49     * Connect pattern to lexer.
50     */
51    public function connectTo($mode)
52    {
53        $this->Lexer->addSpecialPattern('\<BOOKMARKFILE .*?\>',$mode,'plugin_bookmarkfile');
54    }
55
56    /**
57     * Handle the match.
58     */
59    public function handle($match, $state, $pos, Doku_Handler $handler)
60    {
61        global $conf;
62
63        preg_match('/ file="(.*?)"/', $match, $matches);
64        $filename = $conf['mediadir'].'/'.str_replace(':', '/', $matches[1]);
65
66        preg_match('/ separators="(.*?)"/', $match, $matches);
67        $separators = $matches[1];
68
69        preg_match('/ folder="(.*?)"/', $match, $matches);
70        $folder = $matches[1];
71
72        // Tries to open the file
73        $result = array();
74        $result['separators'] = $separators;
75        $bookmarkfile = fopen($filename, "r-");
76        if ($bookmarkfile) {
77            // Detect bookmark browser
78            $first_line = trim(fgets($bookmarkfile));
79
80            if (strcasecmp($first_line, $this->netscape_bmf) == 0) {
81                $bookmarks = $this->parseNetscapeFile($bookmarkfile);
82                fclose($bookmarkfile);
83                $result['bookmarks'] = $bookmarks;
84            } else {
85                if (strpos($first_line, 'x-moz') !== false) {
86                    // Close the file
87                    fclose($bookmarkfile);
88
89                    $bookmarks = $this->parseFirefoxFile($filename);
90                    if ($bookmarks !== null) {
91                        $result['bookmarks'] = $bookmarks;
92                    } else {
93                        $result['message'] = $this->getLang('json_failed');
94                    }
95                } else {
96                    $result['message'] = $this->getLang('err_format');
97                }
98            }
99        } else {
100            $result['message'] = $this->getLang('err_nofile');
101        }
102
103        if (!empty($folder) && !empty($result['bookmarks'])) {
104            $result['bookmarks'] = $this->findBookmarksFolder($result['bookmarks'], $folder);
105        }
106
107        return $result;
108    }
109
110    /**
111     * Create output.
112     */
113    public function render($mode, Doku_Renderer $renderer, $data)
114    {
115        // At the moment, only xhtml is supported
116        if ($mode == 'xhtml') {
117            if (is_array($data['bookmarks'])) {
118                $this->renderBookmarks($renderer, $data['bookmarks'], $data['separators']);
119            } else {
120                $renderer->cdata($data['message']);
121            }
122
123            return true;
124        }
125
126        return false;
127    }
128
129    /**
130     * Process one level of a Firefox bookmark array
131     * (decoded from JSON encoded bookmark file).
132     */
133    private function parseFirefoxJSON(array $json)
134    {
135        // Skip root node
136        if ($json['root'] == 'placesRoot') {
137            $pos = $json['children'];
138        } else {
139            $pos = $json;
140        }
141
142        $bookmarks = array();
143        foreach ($pos as $json_item) {
144            if ($json_item['typeCode'] == 2) {
145                // A folder
146                switch ($json_item['guid']) {
147                    case 'toolbar_____':
148                        $title = $this->getLang('firefox_toolbar_folder');
149                    break;
150                    case 'menu________':
151                        $title = $this->getLang('firefox_menu_folder');
152                    break;
153                    case 'mobile______':
154                        $title = $this->getLang('firefox_mobile_folder');
155                    break;
156                    case 'unfiled_____':
157                        $title = $this->getLang('firefox_other_folder');
158                    break;
159                    default:
160                        $title = $json_item['title'];
161                    break;
162                }
163                $item = array();
164                $item ['type']  = 'folder';
165                $item ['title'] = $title;
166                if (is_array($json_item['children'])) {
167                    $item ['children'] = $this->parseFirefoxJSON($json_item['children']);
168                } else {
169                    $item ['children'] = array();
170                }
171            } else if ($json_item['typeCode'] == 1) {
172                // An entry/link
173                $item = array();
174                $item ['type']  = 'link';
175                $item ['title'] = $json_item['title'];
176                $item ['uri'] = $json_item['uri'];
177            } else if ($json_item['typeCode'] == 3) {
178                // A separator
179                $item = array();
180                $item ['type']  = 'separator';
181            }
182            $bookmarks [] = $item;
183        }
184
185        return $bookmarks;
186    }
187
188    /**
189     * Parse a Firefox bookmark file (JSON encoded).
190     */
191    private function parseFirefoxFile($filename)
192    {
193        $json = file_get_contents($filename);
194
195        $json_bookmarks = json_decode($json, true);
196        if ($json_bookmarks === null) {
197            return null;
198        }
199        $bookmarks = $this->parseFirefoxJSON($json_bookmarks);
200        return $bookmarks;
201    }
202
203    /**
204     * Process an HTML (Netscape) bookmark file.
205     */
206    private function parseNetscapeFile($bookmarkfile)
207    {
208        // read file line by line
209        $bookmarks = array();
210        while (!feof($bookmarkfile)) {
211            $sLine = trim(fgets($bookmarkfile));
212
213            // Ordner
214            if (preg_match('/\<H1\>(.*?)\<\/H1\>/', $sLine, $matches) == 1) {
215                // Root folder
216                $item = array();
217                $item ['type']  = 'folder';
218                $item ['title'] = $matches[1];
219            } else if (preg_match('/\<DT\>\<H3.*?\>(.*?)\<\/H3\>/', $sLine, $matches) == 1) {
220                $item = array();
221                $item ['type']  = 'folder';
222                $item ['title'] = $matches[1];
223            } else if (preg_match('/\<DT\>\<A.*?HREF="(.*?)".*?\>(.*?)\<\/A\>/', $sLine, $matches) == 1) {
224                $item = array();
225                $item ['type']  = 'link';
226                $item ['title'] = $matches[2];
227                $item ['uri'] = $matches[1];
228                $bookmarks [] = $item;
229            } else if (preg_match('/\<DL>/', $sLine, $matches) == 1) {
230                $item['children'] = $this->parseNetscapeFile($bookmarkfile);
231                $bookmarks [] = $item;
232            } else if (preg_match('/\<\/DL>/', $sLine, $matches) == 1) {
233                return $bookmarks;
234            }
235        }
236
237        return $bookmarks;
238    }
239
240    /**
241     * Find a folder in the bookmarks array and return it's children.
242     * (or null if the folder doesn't exist).
243     */
244    private function findBookmarksFolder($bookmarks, $folder)
245    {
246        foreach ($bookmarks as $item) {
247            if ($item['type'] == 'folder') {
248                if ($item['title'] == $folder) {
249                    return $item['children'];
250                }
251                $found = $this->findBookmarksFolder($item['children'], $folder);
252                if ($found !== null) {
253                    return $found;
254                }
255            }
256        }
257        return null;
258    }
259
260    /**
261     * Render the bookmarks as a list.
262     */
263    private function renderBookmarks(Doku_Renderer $renderer, array $bookmarks, $separators, $level=1)
264    {
265        $renderer->listu_open('bookmarkfile');
266        foreach ($bookmarks as $item) {
267            switch ($item['type']) {
268                case 'link':
269                    // An entry/link
270                    $renderer->listitem_open($level);
271                    $renderer->listcontent_open();
272                    $renderer->externallink($item['uri'], $item['title']);
273                    $renderer->listcontent_close();
274                    $renderer->listitem_close();
275                break;
276                case 'folder':
277                    // A folder
278                    $renderer->listitem_open($level);
279                    $renderer->listcontent_open();
280                    $renderer->cdata($item['title']);
281                    $renderer->listcontent_close();
282                    $this->renderBookmarks($renderer, $item['children'], $separators, $level+1);
283                    $renderer->listitem_close();
284                break;
285                case 'separator':
286                    // A separator
287                    if ($separators !== 'hide') {
288                        $renderer->doc .= '<li class="level'.$level.' separator">';
289                        $renderer->listcontent_open();
290                        $renderer->hr();
291                        $renderer->listcontent_close();
292                        $renderer->doc .= '</li>'.DOKU_LF;
293                    }
294                break;
295            }
296        }
297        $renderer->listu_close();
298    }
299}
300