1<?php
2/**
3 * DokuWiki Syntax Plugin Related.
4 *
5 */
6if (!defined('DOKU_INC')) {
7    die();
8}
9
10
11require_once(DOKU_INC . 'inc/parserutils.php');
12
13/**
14 * All DokuWiki plugins to extend the parser/rendering mechanism
15 * need to inherit from this class
16 *
17 * The name of the class must follow a pattern (don't change it)
18 */
19class syntax_plugin_webcomponent_related extends DokuWiki_Syntax_Plugin
20{
21
22
23    // Conf property key
24    const MAX_LINKS_CONF = 'maxLinks';
25    // For when you come from another plugin (such as backlinks) and that you don't want to change the pattern on each page
26    const EXTRA_PATTERN_CONF = 'extra_pattern';
27
28    // This is a fake page ID that is added
29    // to the related page array when the number of backlinks is bigger than the max
30    // Poisoning object strategy
31    const MORE_PAGE_ID = 'related_more';
32
33    // The array key of an array of related page
34    const RELATED_PAGE_ID_PROP = 'id';
35    const RELATED_BACKLINKS_COUNT_PROP = 'backlinks';
36
37
38    public static function getElementId()
39    {
40        return webcomponent::PLUGIN_NAME."_".self::getElementName();
41    }
42
43
44    /**
45     * Syntax Type.
46     *
47     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
48     * @see DokuWiki_Syntax_Plugin::getType()
49     */
50    function getType()
51    {
52        return 'substition';
53    }
54
55    /**
56     * @see DokuWiki_Syntax_Plugin::getPType()
57     */
58    function getPType()
59    {
60        return 'block';
61    }
62
63    /**
64     * @see Doku_Parser_Mode::getSort()
65     */
66    function getSort()
67    {
68        return 100;
69    }
70
71    /**
72     * Create a pattern that will called this plugin
73     *
74     * @see Doku_Parser_Mode::connectTo()
75     * @param string $mode
76     */
77    function connectTo($mode)
78    {
79        // The basic
80        $this->Lexer->addSpecialPattern('<' . self::getElementName(). '[^>]*>', $mode, 'plugin_' . webcomponent::PLUGIN_NAME . '_' . $this->getPluginComponent());
81
82        // To replace backlinks, you may add it in the configuration
83        $extraPattern = $this->getConf(self::EXTRA_PATTERN_CONF);
84        if ($extraPattern != ""){
85            $this->Lexer->addSpecialPattern($extraPattern, $mode, 'plugin_' . webcomponent::PLUGIN_NAME . '_' . $this->getPluginComponent());
86        }
87
88    }
89
90    /**
91     *
92     * The handle function goal is to parse the matched syntax through the pattern function
93     * and to return the result for use in the renderer
94     * This result is always cached until the page is modified.
95     * @see DokuWiki_Syntax_Plugin::handle()
96     *
97     * @param string $match
98     * @param int $state
99     * @param int $pos
100     * @param Doku_Handler $handler
101     * @return array|bool
102     */
103    function handle($match, $state, $pos, Doku_Handler $handler)
104    {
105
106        switch ($state) {
107
108            // As there is only one call to connect to in order to a add a pattern,
109            // there is only one state entering the function
110            // but I leave it for better understanding of the process flow
111            case DOKU_LEXER_SPECIAL :
112
113                // Parse the parameters
114                $match = utf8_substr($match, strlen(self::getElementName()), -1);
115
116                // /i not case sensitive
117                $attributePattern = "\\s*(\w+)\\s*=\\s*[\'\"]?([\w\d\s-_\|\*\.\(\)\?\/\\\\]+)[\'\"]?\\s*";
118                $result = preg_match_all('/' . $attributePattern . '/i', $match, $matches);
119                if ($result != 0) {
120                    foreach ($matches[1] as $key => $parameterKey) {
121                        $parameters[strtolower($parameterKey)] = $matches[2][$key];
122                    }
123                }
124
125        }
126
127        // Cache the values
128        return array($state, $parameters);
129    }
130
131    /**
132     * Render the output
133     * @see DokuWiki_Syntax_Plugin::render()
134     *
135     * @param string $mode
136     * @param Doku_Renderer $renderer
137     * @param array $data
138     * @return bool
139     */
140    function render($mode, Doku_Renderer $renderer, $data)
141    {
142        global $lang;
143        global $INFO;
144        global $ID;
145
146        $id = $ID;
147        // If it's a sidebar, get the original id.
148        if (isset($INFO)) {
149            $id = $INFO['id'];
150        }
151
152        if ($mode == 'xhtml') {
153
154            $relatedPages = $this->related($id);
155
156            $renderer->doc .= '<div id="'.self::getElementId().'" class="'.self::getElementName().'-container">' . DOKU_LF;
157
158            if (empty($relatedPages)) {
159
160                // Dokuwiki debug
161                dbglog("No Backlinks", "Related plugins: all backlinks for page: $id");
162                $renderer->doc .= "<strong>Plugin ".webcomponent::PLUGIN_NAME." - Component ".self::getElementName().": " . $lang['nothingfound'] . "</strong>" . DOKU_LF;
163
164            } else {
165
166                // Dokuwiki debug
167                dbglog($relatedPages, self::getElementName()." plugins: all backlinks for page: $id");
168
169                $renderer->doc .= '<ul>' . DOKU_LF;
170
171                foreach ($relatedPages as $backlink) {
172                    $backlinkId = $backlink[self::RELATED_PAGE_ID_PROP];
173                    $name = p_get_metadata($backlinkId, 'title');
174                    if (empty($name)) {
175                        $name = $backlinkId;
176                    }
177                    $renderer->doc .= '<li>';
178                    if ($backlinkId != self::MORE_PAGE_ID) {
179                        $renderer->doc .= html_wikilink(':' . $backlinkId, $name);
180                    } else {
181                        $renderer->doc .=
182                            tpl_link(
183                            wl($id).'?do=backlink',
184                            "More ...",
185                            'class="" rel="nofollow" title="More..."',
186                            $return = true
187                            );
188                    }
189                    $renderer->doc .= '</li>' . DOKU_LF;
190                }
191
192                $renderer->doc .= '</ul>' . DOKU_LF;
193
194            }
195
196            $renderer->doc .= '</div>' . DOKU_LF;
197
198            return true;
199        }
200        return false;
201    }
202
203    /**
204     * @param $id
205     * @param $max
206     * @return array
207     */
208    public function related($id, $max = NULL): array
209    {
210        if ($max == NULL){
211            $max = $this->getConf(self::MAX_LINKS_CONF);
212        }
213        // Call the dokuwiki backlinks function
214        @require_once(DOKU_INC . 'inc/fulltext.php');
215        // Backlinks called the indexer, for more info
216        // See: https://www.dokuwiki.org/devel:metadata#metadata_index
217        $backlinks = ft_backlinks($id, $ignore_perms = false);
218
219        // To minimize the pressure on the index
220        // as we asks then the backlinks of the backlinks on the next step
221        if (sizeof($backlinks) > 50){
222            $backlinks = array_slice($backlinks, 0, 50);
223        }
224
225        $related = array();
226        foreach ($backlinks as $backlink){
227            $page = array();
228            $page[self::RELATED_PAGE_ID_PROP]=$backlink;
229            $page[self::RELATED_BACKLINKS_COUNT_PROP]=sizeof(ft_backlinks($backlink, $ignore_perms = false));
230            $related[]=$page;
231        }
232
233        usort($related, function($a, $b) {
234            return $b[self::RELATED_BACKLINKS_COUNT_PROP] - $a[self::RELATED_BACKLINKS_COUNT_PROP] ;
235        });
236
237        if (sizeof($related)> $max){
238            $related = array_slice($related, 0, $max);
239            $page = array();
240            $page[self::RELATED_PAGE_ID_PROP] = self::MORE_PAGE_ID;
241            $related[] = $page;
242        }
243
244        return $related;
245
246    }
247
248    public static function getElementName()
249    {
250        return webcomponent::getTagName(get_called_class());
251    }
252
253
254}
255