]> git.mxchange.org Git - friendica.git/blob - src/Core/Addon.php
be4e94152f44f8e9dd4c99bcea5bc59f736e246e
[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 = [];
116
117                 $r = DBA::select('addon', [], ['installed' => 1]);
118                 if (DBA::isResult($r)) {
119                         $installed_addons = DBA::toArray($r);
120                 }
121
122                 $addons = DI::config()->get('system', 'addon');
123                 $addons_arr = [];
124
125                 if ($addons) {
126                         $addons_arr = explode(',', str_replace(' ', '', $addons));
127                 }
128
129                 self::$addons = $addons_arr;
130
131                 $installed_arr = [];
132
133                 foreach ($installed_addons as $addon) {
134                         if (!self::isEnabled($addon['name'])) {
135                                 self::uninstall($addon['name']);
136                         } else {
137                                 $installed_arr[] = $addon['name'];
138                         }
139                 }
140
141                 foreach (self::$addons as $p) {
142                         if (!in_array($p, $installed_arr)) {
143                                 self::install($p);
144                         }
145                 }
146         }
147
148         /**
149          * uninstalls an addon.
150          *
151          * @param string $addon name of the addon
152          * @return void
153          * @throws \Exception
154          */
155         public static function uninstall($addon)
156         {
157                 $addon = Strings::sanitizeFilePathItem($addon);
158
159                 Logger::notice("Addon {addon}: {action}", ['action' => 'uninstall', 'addon' => $addon]);
160                 DBA::delete('addon', ['name' => $addon]);
161
162                 @include_once('addon/' . $addon . '/' . $addon . '.php');
163                 if (function_exists($addon . '_uninstall')) {
164                         $func = $addon . '_uninstall';
165                         $func();
166                 }
167
168                 DBA::delete('hook', ['file' => 'addon/' . $addon . '/' . $addon . '.php']);
169
170                 unset(self::$addons[array_search($addon, self::$addons)]);
171
172                 Addon::saveEnabledList();
173         }
174
175         /**
176          * installs an addon.
177          *
178          * @param string $addon name of the addon
179          * @return bool
180          * @throws \Exception
181          */
182         public static function install($addon)
183         {
184                 $addon = Strings::sanitizeFilePathItem($addon);
185
186                 // silently fail if addon was removed of if $addon is funky
187                 if (!file_exists('addon/' . $addon . '/' . $addon . '.php')) {
188                         return false;
189                 }
190
191                 Logger::notice("Addon {addon}: {action}", ['action' => 'install', 'addon' => $addon]);
192                 $t = @filemtime('addon/' . $addon . '/' . $addon . '.php');
193                 @include_once('addon/' . $addon . '/' . $addon . '.php');
194                 if (function_exists($addon . '_install')) {
195                         $func = $addon . '_install';
196                         $func(DI::app());
197
198                         $addon_admin = (function_exists($addon . "_addon_admin") ? 1 : 0);
199
200                         DBA::insert('addon', ['name' => $addon, 'installed' => true,
201                                 'timestamp' => $t, 'plugin_admin' => $addon_admin]);
202
203                         // we can add the following with the previous SQL
204                         // once most site tables have been updated.
205                         // This way the system won't fall over dead during the update.
206
207                         if (file_exists('addon/' . $addon . '/.hidden')) {
208                                 DBA::update('addon', ['hidden' => true], ['name' => $addon]);
209                         }
210
211                         if (!self::isEnabled($addon)) {
212                                 self::$addons[] = $addon;
213                         }
214
215                         Addon::saveEnabledList();
216
217                         return true;
218                 } else {
219                         Logger::error("Addon {addon}: {action} failed", ['action' => 'install', 'addon' => $addon]);
220                         return false;
221                 }
222         }
223
224         /**
225          * reload all updated addons
226          */
227         public static function reload()
228         {
229                 $addons = DI::config()->get('system', 'addon');
230                 if (strlen($addons)) {
231                         $r = DBA::select('addon', [], ['installed' => 1]);
232                         if (DBA::isResult($r)) {
233                                 $installed = DBA::toArray($r);
234                         } else {
235                                 $installed = [];
236                         }
237
238                         $addon_list = explode(',', $addons);
239
240                         foreach ($addon_list as $addon) {
241                                 $addon = Strings::sanitizeFilePathItem(trim($addon));
242                                 $fname = 'addon/' . $addon . '/' . $addon . '.php';
243                                 if (file_exists($fname)) {
244                                         $t = @filemtime($fname);
245                                         foreach ($installed as $i) {
246                                                 if (($i['name'] == $addon) && ($i['timestamp'] != $t)) {
247
248                                                         Logger::notice("Addon {addon}: {action}", ['action' => 'reload', 'addon' => $i['name']]);
249                                                         @include_once($fname);
250
251                                                         if (function_exists($addon . '_uninstall')) {
252                                                                 $func = $addon . '_uninstall';
253                                                                 $func(DI::app());
254                                                         }
255                                                         if (function_exists($addon . '_install')) {
256                                                                 $func = $addon . '_install';
257                                                                 $func(DI::app());
258                                                         }
259                                                         DBA::update('addon', ['timestamp' => $t], ['id' => $i['id']]);
260                                                 }
261                                         }
262                                 }
263                         }
264                 }
265         }
266
267         /**
268          * Parse addon comment in search of addon infos.
269          *
270          * like
271          * \code
272          *   * Name: addon
273          *   * Description: An addon which plugs in
274          * . * Version: 1.2.3
275          *   * Author: John <profile url>
276          *   * Author: Jane <email>
277          *   * Maintainer: Jess <email>
278          *   *
279          *   *\endcode
280          * @param string $addon the name of the addon
281          * @return array with the addon information
282          * @throws \Exception
283          */
284         public static function getInfo($addon)
285         {
286                 $a = DI::app();
287
288                 $addon = Strings::sanitizeFilePathItem($addon);
289
290                 $info = [
291                         'name' => $addon,
292                         'description' => "",
293                         'author' => [],
294                         'maintainer' => [],
295                         'version' => "",
296                         'status' => ""
297                 ];
298
299                 if (!is_file("addon/$addon/$addon.php")) {
300                         return $info;
301                 }
302
303                 $stamp1 = microtime(true);
304                 $f = file_get_contents("addon/$addon/$addon.php");
305                 DI::profiler()->saveTimestamp($stamp1, "file", System::callstack());
306
307                 $r = preg_match("|/\*.*\*/|msU", $f, $m);
308
309                 if ($r) {
310                         $ll = explode("\n", $m[0]);
311                         foreach ($ll as $l) {
312                                 $l = trim($l, "\t\n\r */");
313                                 if ($l != "") {
314                                         $addon_info = array_map("trim", explode(":", $l, 2));
315                                         if (count($addon_info) < 2) {
316                                                 continue;
317                                         }
318
319                                         list($type, $v) = $addon_info;
320                                         $type = strtolower($type);
321                                         if ($type == "author" || $type == "maintainer") {
322                                                 $r = preg_match("|([^<]+)<([^>]+)>|", $v, $m);
323                                                 if ($r) {
324                                                         $info[$type][] = ['name' => $m[1], 'link' => $m[2]];
325                                                 } else {
326                                                         $info[$type][] = ['name' => $v];
327                                                 }
328                                         } else {
329                                                 if (array_key_exists($type, $info)) {
330                                                         $info[$type] = $v;
331                                                 }
332                                         }
333                                 }
334                         }
335                 }
336                 return $info;
337         }
338
339         /**
340          * Checks if the provided addon is enabled
341          *
342          * @param string $addon
343          * @return boolean
344          */
345         public static function isEnabled($addon)
346         {
347                 return in_array($addon, self::$addons);
348         }
349
350         /**
351          * Returns a list of the enabled addon names
352          *
353          * @return array
354          */
355         public static function getEnabledList()
356         {
357                 return self::$addons;
358         }
359
360         /**
361          * Saves the current enabled addon list in the system.addon config key
362          *
363          * @return boolean
364          */
365         public static function saveEnabledList()
366         {
367                 return DI::config()->set('system', 'addon', implode(',', self::$addons));
368         }
369
370         /**
371          * Returns the list of non-hidden enabled addon names
372          *
373          * @return array
374          * @throws \Exception
375          */
376         public static function getVisibleList()
377         {
378                 $visible_addons = [];
379                 $stmt = DBA::select('addon', ['name'], ['hidden' => false, 'installed' => true]);
380                 if (DBA::isResult($stmt)) {
381                         foreach (DBA::toArray($stmt) as $addon) {
382                                 $visible_addons[] = $addon['name'];
383                         }
384                 }
385
386                 return $visible_addons;
387         }
388
389         /**
390          * Shim of Hook::register left for backward compatibility purpose.
391          *
392          * @see        Hook::register
393          * @deprecated since version 2018.12
394          * @param string $hook     the name of the hook
395          * @param string $file     the name of the file that hooks into
396          * @param string $function the name of the function that the hook will call
397          * @param int    $priority A priority (defaults to 0)
398          * @return mixed|bool
399          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
400          */
401         public static function registerHook($hook, $file, $function, $priority = 0)
402         {
403                 return Hook::register($hook, $file, $function, $priority);
404         }
405
406         /**
407          * Shim of Hook::unregister left for backward compatibility purpose.
408          *
409          * @see        Hook::unregister
410          * @deprecated since version 2018.12
411          * @param string $hook     the name of the hook
412          * @param string $file     the name of the file that hooks into
413          * @param string $function the name of the function that the hook called
414          * @return boolean
415          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
416          */
417         public static function unregisterHook($hook, $file, $function)
418         {
419                 return Hook::unregister($hook, $file, $function);
420         }
421
422         /**
423          * Shim of Hook::callAll left for backward-compatibility purpose.
424          *
425          * @see        Hook::callAll
426          * @deprecated since version 2018.12
427          * @param string        $name of the hook to call
428          * @param string|array &$data to transmit to the callback handler
429          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
430          */
431         public static function callHooks($name, &$data = null)
432         {
433                 Hook::callAll($name, $data);
434         }
435 }