]> git.mxchange.org Git - friendica.git/blob - src/Core/Addon.php
ee70a66b6e47fd025b9c6e365f708110ffe29a8b
[friendica.git] / src / Core / Addon.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2020, Friendica
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Core;
23
24 use Friendica\Database\DBA;
25 use Friendica\DI;
26 use Friendica\Util\Strings;
27
28 /**
29  * Some functions to handle addons
30  */
31 class Addon
32 {
33         /**
34          * The addon sub-directory
35          * @var string
36          */
37         const DIRECTORY = 'addon';
38
39         /**
40          * List of the names of enabled addons
41          *
42          * @var array
43          */
44         private static $addons = [];
45
46         /**
47          * Returns the list of available addons with their current status and info.
48          * This list is made from scanning the addon/ folder.
49          * Unsupported addons are excluded unless they already are enabled or system.show_unsupported_addon is set.
50          *
51          * @return array
52          * @throws \Exception
53          */
54         public static function getAvailableList()
55         {
56                 $addons = [];
57                 $files = glob('addon/*/');
58                 if (is_array($files)) {
59                         foreach ($files as $file) {
60                                 if (is_dir($file)) {
61                                         list($tmp, $addon) = array_map('trim', explode('/', $file));
62                                         $info = self::getInfo($addon);
63
64                                         if (DI::config()->get('system', 'show_unsupported_addons')
65                                                 || strtolower($info['status']) != 'unsupported'
66                                                 || self::isEnabled($addon)
67                                         ) {
68                                                 $addons[] = [$addon, (self::isEnabled($addon) ? 'on' : 'off'), $info];
69                                         }
70                                 }
71                         }
72                 }
73
74                 return $addons;
75         }
76
77         /**
78          * Returns a list of addons that can be configured at the node level.
79          * The list is formatted for display in the admin panel aside.
80          *
81          * @return array
82          * @throws \Exception
83          */
84         public static function getAdminList()
85         {
86                 $addons_admin = [];
87                 $addonsAdminStmt = DBA::select('addon', ['name'], ['plugin_admin' => 1], ['order' => ['name']]);
88                 while ($addon = DBA::fetch($addonsAdminStmt)) {
89                         $addons_admin[$addon['name']] = [
90                                 'url' => 'admin/addons/' . $addon['name'],
91                                 'name' => $addon['name'],
92                                 'class' => 'addon'
93                         ];
94                 }
95                 DBA::close($addonsAdminStmt);
96
97                 return $addons_admin;
98         }
99
100
101         /**
102          * Synchronize addons:
103          *
104          * system.addon contains a comma-separated list of names
105          * of addons which are used on this system.
106          * Go through the database list of already installed addons, and if we have
107          * an entry, but it isn't in the config list, call the uninstall procedure
108          * and mark it uninstalled in the database (for now we'll remove it).
109          * Then go through the config list and if we have a addon that isn't installed,
110          * call the install procedure and add it to the database.
111          *
112          */
113         public static function loadAddons()
114         {
115                 $installed_addons = DBA::selectToArray('addon', ['name'], ['installed' => true]);
116                 self::$addons = array_column($installed_addons, 'name');
117         }
118
119         /**
120          * uninstalls an addon.
121          *
122          * @param string $addon name of the addon
123          * @return void
124          * @throws \Exception
125          */
126         public static function uninstall($addon)
127         {
128                 $addon = Strings::sanitizeFilePathItem($addon);
129
130                 Logger::notice("Addon {addon}: {action}", ['action' => 'uninstall', 'addon' => $addon]);
131                 DBA::delete('addon', ['name' => $addon]);
132
133                 @include_once('addon/' . $addon . '/' . $addon . '.php');
134                 if (function_exists($addon . '_uninstall')) {
135                         $func = $addon . '_uninstall';
136                         $func();
137                 }
138
139                 DBA::delete('hook', ['file' => 'addon/' . $addon . '/' . $addon . '.php']);
140
141                 unset(self::$addons[array_search($addon, self::$addons)]);
142         }
143
144         /**
145          * installs an addon.
146          *
147          * @param string $addon name of the addon
148          * @return bool
149          * @throws \Exception
150          */
151         public static function install($addon)
152         {
153                 $addon = Strings::sanitizeFilePathItem($addon);
154
155                 // silently fail if addon was removed of if $addon is funky
156                 if (!file_exists('addon/' . $addon . '/' . $addon . '.php')) {
157                         return false;
158                 }
159
160                 Logger::notice("Addon {addon}: {action}", ['action' => 'install', 'addon' => $addon]);
161                 $t = @filemtime('addon/' . $addon . '/' . $addon . '.php');
162                 @include_once('addon/' . $addon . '/' . $addon . '.php');
163                 if (function_exists($addon . '_install')) {
164                         $func = $addon . '_install';
165                         $func(DI::app());
166
167                         $addon_admin = (function_exists($addon . "_addon_admin") ? 1 : 0);
168
169                         DBA::insert('addon', ['name' => $addon, 'installed' => true,
170                                 'timestamp' => $t, 'plugin_admin' => $addon_admin]);
171
172                         // we can add the following with the previous SQL
173                         // once most site tables have been updated.
174                         // This way the system won't fall over dead during the update.
175
176                         if (file_exists('addon/' . $addon . '/.hidden')) {
177                                 DBA::update('addon', ['hidden' => true], ['name' => $addon]);
178                         }
179
180                         if (!self::isEnabled($addon)) {
181                                 self::$addons[] = $addon;
182                         }
183
184                         return true;
185                 } else {
186                         Logger::error("Addon {addon}: {action} failed", ['action' => 'install', 'addon' => $addon]);
187                         return false;
188                 }
189         }
190
191         /**
192          * reload all updated addons
193          */
194         public static function reload()
195         {
196                 $addons = DBA::selectToArray('addon', [], ['installed' => true]);
197
198                 foreach ($addons as $addon) {
199                         $addonname = Strings::sanitizeFilePathItem(trim($addon['name']));
200                         $fname = 'addon/' . $addonname . '/' . $addonname . '.php';
201                         $t = @filemtime($fname);
202                         if (!file_exists($fname) || ($addon['timestamp'] == $t)) {
203                                 continue;
204                         }
205
206                         Logger::notice("Addon {addon}: {action}", ['action' => 'reload', 'addon' => $addon['name']]);
207                         @include_once($fname);
208
209                         if (function_exists($addonname . '_uninstall')) {
210                                 $func = $addonname . '_uninstall';
211                                 $func(DI::app());
212                         }
213                         if (function_exists($addonname . '_install')) {
214                                 $func = $addonname . '_install';
215                                 $func(DI::app());
216                         }
217                         DBA::update('addon', ['timestamp' => $t], ['id' => $addon['id']]);
218                 }
219         }
220
221         /**
222          * Parse addon comment in search of addon infos.
223          *
224          * like
225          * \code
226          *   * Name: addon
227          *   * Description: An addon which plugs in
228          * . * Version: 1.2.3
229          *   * Author: John <profile url>
230          *   * Author: Jane <email>
231          *   * Maintainer: Jess <email>
232          *   *
233          *   *\endcode
234          * @param string $addon the name of the addon
235          * @return array with the addon information
236          * @throws \Exception
237          */
238         public static function getInfo($addon)
239         {
240                 $a = DI::app();
241
242                 $addon = Strings::sanitizeFilePathItem($addon);
243
244                 $info = [
245                         'name' => $addon,
246                         'description' => "",
247                         'author' => [],
248                         'maintainer' => [],
249                         'version' => "",
250                         'status' => ""
251                 ];
252
253                 if (!is_file("addon/$addon/$addon.php")) {
254                         return $info;
255                 }
256
257                 $stamp1 = microtime(true);
258                 $f = file_get_contents("addon/$addon/$addon.php");
259                 DI::profiler()->saveTimestamp($stamp1, "file");
260
261                 $r = preg_match("|/\*.*\*/|msU", $f, $m);
262
263                 if ($r) {
264                         $ll = explode("\n", $m[0]);
265                         foreach ($ll as $l) {
266                                 $l = trim($l, "\t\n\r */");
267                                 if ($l != "") {
268                                         $addon_info = array_map("trim", explode(":", $l, 2));
269                                         if (count($addon_info) < 2) {
270                                                 continue;
271                                         }
272
273                                         list($type, $v) = $addon_info;
274                                         $type = strtolower($type);
275                                         if ($type == "author" || $type == "maintainer") {
276                                                 $r = preg_match("|([^<]+)<([^>]+)>|", $v, $m);
277                                                 if ($r) {
278                                                         $info[$type][] = ['name' => $m[1], 'link' => $m[2]];
279                                                 } else {
280                                                         $info[$type][] = ['name' => $v];
281                                                 }
282                                         } else {
283                                                 if (array_key_exists($type, $info)) {
284                                                         $info[$type] = $v;
285                                                 }
286                                         }
287                                 }
288                         }
289                 }
290                 return $info;
291         }
292
293         /**
294          * Checks if the provided addon is enabled
295          *
296          * @param string $addon
297          * @return boolean
298          */
299         public static function isEnabled($addon)
300         {
301                 return in_array($addon, self::$addons);
302         }
303
304         /**
305          * Returns a list of the enabled addon names
306          *
307          * @return array
308          */
309         public static function getEnabledList()
310         {
311                 return self::$addons;
312         }
313
314         /**
315          * Returns the list of non-hidden enabled addon names
316          *
317          * @return array
318          * @throws \Exception
319          */
320         public static function getVisibleList()
321         {
322                 $visible_addons = [];
323                 $stmt = DBA::select('addon', ['name'], ['hidden' => false, 'installed' => true]);
324                 if (DBA::isResult($stmt)) {
325                         foreach (DBA::toArray($stmt) as $addon) {
326                                 $visible_addons[] = $addon['name'];
327                         }
328                 }
329
330                 return $visible_addons;
331         }
332
333         /**
334          * Shim of Hook::register left for backward compatibility purpose.
335          *
336          * @see        Hook::register
337          * @deprecated since version 2018.12
338          * @param string $hook     the name of the hook
339          * @param string $file     the name of the file that hooks into
340          * @param string $function the name of the function that the hook will call
341          * @param int    $priority A priority (defaults to 0)
342          * @return mixed|bool
343          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
344          */
345         public static function registerHook($hook, $file, $function, $priority = 0)
346         {
347                 return Hook::register($hook, $file, $function, $priority);
348         }
349
350         /**
351          * Shim of Hook::unregister left for backward compatibility purpose.
352          *
353          * @see        Hook::unregister
354          * @deprecated since version 2018.12
355          * @param string $hook     the name of the hook
356          * @param string $file     the name of the file that hooks into
357          * @param string $function the name of the function that the hook called
358          * @return boolean
359          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
360          */
361         public static function unregisterHook($hook, $file, $function)
362         {
363                 return Hook::unregister($hook, $file, $function);
364         }
365
366         /**
367          * Shim of Hook::callAll left for backward-compatibility purpose.
368          *
369          * @see        Hook::callAll
370          * @deprecated since version 2018.12
371          * @param string        $name of the hook to call
372          * @param string|array &$data to transmit to the callback handler
373          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
374          */
375         public static function callHooks($name, &$data = null)
376         {
377                 Hook::callAll($name, $data);
378         }
379 }