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