1<?php
2
3/**
4 * DokuWiki Plugin extension (Helper Component)
5 *
6 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
7 * @author  Michael Hamann <michael@content-space.de>
8 */
9
10use dokuwiki\Cache\Cache;
11use dokuwiki\Extension\Plugin;
12use dokuwiki\Extension\PluginController;
13use dokuwiki\HTTP\DokuHTTPClient;
14
15/**
16 * Class helper_plugin_extension_repository provides access to the extension repository on dokuwiki.org
17 */
18class helper_plugin_extension_repository extends Plugin
19{
20    public const EXTENSION_REPOSITORY_API = 'https://www.dokuwiki.org/lib/plugins/pluginrepo/api.php';
21
22    private $loaded_extensions = [];
23    private $has_access;
24
25    /**
26     * Initialize the repository (cache), fetches data for all installed plugins
27     */
28    public function init()
29    {
30        /* @var PluginController $plugin_controller */
31        global $plugin_controller;
32        if ($this->hasAccess()) {
33            $list = $plugin_controller->getList('', true);
34            $request_data = ['fmt' => 'json'];
35            $request_needed = false;
36            foreach ($list as $name) {
37                $cache = new Cache('##extension_manager##' . $name, '.repo');
38
39                if (
40                    !isset($this->loaded_extensions[$name]) &&
41                    $this->hasAccess() &&
42                    !$cache->useCache(['age' => 3600 * 24])
43                ) {
44                    $this->loaded_extensions[$name] = true;
45                    $request_data['ext'][] = $name;
46                    $request_needed = true;
47                }
48            }
49
50            if ($request_needed) {
51                $httpclient = new DokuHTTPClient();
52                $data = $httpclient->post(self::EXTENSION_REPOSITORY_API, $request_data);
53                if ($data !== false) {
54                    try {
55                        $extensions = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
56                        foreach ($extensions as $extension) {
57                            $cache = new Cache('##extension_manager##' . $extension['plugin'], '.repo');
58                            $cache->storeCache(serialize($extension));
59                        }
60                    } catch (JsonException $e) {
61                        msg($this->getLang('repo_badresponse'), -1);
62                        $this->has_access = false;
63                    }
64                } else {
65                    $this->has_access = false;
66                }
67            }
68        }
69    }
70
71    /**
72     * If repository access is available
73     *
74     * @param bool $usecache use cached result if still valid
75     * @return bool If repository access is available
76     */
77    public function hasAccess($usecache = true)
78    {
79        if ($this->has_access === null) {
80            $cache = new Cache('##extension_manager###hasAccess', '.repo');
81
82            if (!$cache->useCache(['age' => 60 * 10, 'purge' => !$usecache])) {
83                $httpclient = new DokuHTTPClient();
84                $httpclient->timeout = 5;
85                $data = $httpclient->get(self::EXTENSION_REPOSITORY_API . '?cmd=ping');
86                if ($data === false) {
87                    $this->has_access = false;
88                    $cache->storeCache(0);
89                } elseif ($data !== '1') {
90                    msg($this->getLang('repo_badresponse'), -1);
91                    $this->has_access = false;
92                    $cache->storeCache(0);
93                } else {
94                    $this->has_access = true;
95                    $cache->storeCache(1);
96                }
97            } else {
98                $this->has_access = ($cache->retrieveCache(false) == 1);
99            }
100        }
101        return $this->has_access;
102    }
103
104    /**
105     * Get the remote data of an individual plugin or template
106     *
107     * @param string $name The plugin name to get the data for, template names need to be prefix by 'template:'
108     * @return array The data or null if nothing was found (possibly no repository access)
109     */
110    public function getData($name)
111    {
112        $cache = new Cache('##extension_manager##' . $name, '.repo');
113
114        if (
115            !isset($this->loaded_extensions[$name]) &&
116            $this->hasAccess() &&
117            !$cache->useCache(['age' => 3600 * 24])
118        ) {
119            $this->loaded_extensions[$name] = true;
120            $httpclient = new DokuHTTPClient();
121            $data = $httpclient->get(self::EXTENSION_REPOSITORY_API . '?fmt=json&ext[]=' . urlencode($name));
122            if ($data !== false) {
123                try {
124                    $result = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
125                    if (count($result)) {
126                        $cache->storeCache(serialize($result[0]));
127                        return $result[0];
128                    }
129                } catch (JsonException $e) {
130                    msg($this->getLang('repo_badresponse'), -1);
131                    $this->has_access = false;
132                }
133            } else {
134                $this->has_access = false;
135            }
136        }
137        if (file_exists($cache->cache)) {
138            return unserialize($cache->retrieveCache(false));
139        }
140        return [];
141    }
142
143    /**
144     * Search for plugins or templates using the given query string
145     *
146     * @param string $q the query string
147     * @return array a list of matching extensions
148     */
149    public function search($q)
150    {
151        $query = $this->parseQuery($q);
152        $query['fmt'] = 'json';
153
154        $httpclient = new DokuHTTPClient();
155        $data = $httpclient->post(self::EXTENSION_REPOSITORY_API, $query);
156        if ($data === false) return [];
157        try {
158            $result = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
159        } catch (JsonException $e) {
160            msg($this->getLang('repo_badresponse'), -1);
161            return [];
162        }
163
164        $ids = [];
165
166        // store cache info for each extension
167        foreach ($result as $ext) {
168            $name = $ext['plugin'];
169            $cache = new Cache('##extension_manager##' . $name, '.repo');
170            $cache->storeCache(serialize($ext));
171            $ids[] = $name;
172        }
173
174        return $ids;
175    }
176
177    /**
178     * Parses special queries from the query string
179     *
180     * @param string $q
181     * @return array
182     */
183    protected function parseQuery($q)
184    {
185        $parameters = ['tag' => [], 'mail' => [], 'type' => [], 'ext' => []];
186
187        // extract tags
188        if (preg_match_all('/(^|\s)(tag:([\S]+))/', $q, $matches, PREG_SET_ORDER)) {
189            foreach ($matches as $m) {
190                $q = str_replace($m[2], '', $q);
191                $parameters['tag'][] = $m[3];
192            }
193        }
194        // extract author ids
195        if (preg_match_all('/(^|\s)(authorid:([\S]+))/', $q, $matches, PREG_SET_ORDER)) {
196            foreach ($matches as $m) {
197                $q = str_replace($m[2], '', $q);
198                $parameters['mail'][] = $m[3];
199            }
200        }
201        // extract extensions
202        if (preg_match_all('/(^|\s)(ext:([\S]+))/', $q, $matches, PREG_SET_ORDER)) {
203            foreach ($matches as $m) {
204                $q = str_replace($m[2], '', $q);
205                $parameters['ext'][] = $m[3];
206            }
207        }
208        // extract types
209        if (preg_match_all('/(^|\s)(type:([\S]+))/', $q, $matches, PREG_SET_ORDER)) {
210            foreach ($matches as $m) {
211                $q = str_replace($m[2], '', $q);
212                $parameters['type'][] = $m[3];
213            }
214        }
215
216        // FIXME make integer from type value
217
218        $parameters['q'] = trim($q);
219        return $parameters;
220    }
221}
222
223// vim:ts=4:sw=4:et:
224