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