]> git.mxchange.org Git - friendica.git/blob - src/Core/Addon.php
Merge pull request #10040 from annando/file-permissions
[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                 Hook::delete(['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
168                 DBA::insert('addon', [
169                         'name' => $addon,
170                         'installed' => true,
171                         'timestamp' => $t,
172                         'plugin_admin' => function_exists($addon . '_addon_admin'),
173                         'hidden' => file_exists('addon/' . $addon . '/.hidden')
174                 ]);
175
176                 if (!self::isEnabled($addon)) {
177                         self::$addons[] = $addon;
178                 }
179
180                 return true;
181         }
182
183         /**
184          * reload all updated addons
185          */
186         public static function reload()
187         {
188                 $addons = DBA::selectToArray('addon', [], ['installed' => true]);
189
190                 foreach ($addons as $addon) {
191                         $addonname = Strings::sanitizeFilePathItem(trim($addon['name']));
192                         $fname = 'addon/' . $addonname . '/' . $addonname . '.php';
193                         $t = @filemtime($fname);
194                         if (!file_exists($fname) || ($addon['timestamp'] == $t)) {
195                                 continue;
196                         }
197
198                         Logger::notice("Addon {addon}: {action}", ['action' => 'reload', 'addon' => $addon['name']]);
199
200                         self::uninstall($fname);
201                         self::install($fname);
202                 }
203         }
204
205         /**
206          * Parse addon comment in search of addon infos.
207          *
208          * like
209          * \code
210          *   * Name: addon
211          *   * Description: An addon which plugs in
212          * . * Version: 1.2.3
213          *   * Author: John <profile url>
214          *   * Author: Jane <email>
215          *   * Maintainer: Jess <email>
216          *   *
217          *   *\endcode
218          * @param string $addon the name of the addon
219          * @return array with the addon information
220          * @throws \Exception
221          */
222         public static function getInfo($addon)
223         {
224                 $addon = Strings::sanitizeFilePathItem($addon);
225
226                 $info = [
227                         'name' => $addon,
228                         'description' => "",
229                         'author' => [],
230                         'maintainer' => [],
231                         'version' => "",
232                         'status' => ""
233                 ];
234
235                 if (!is_file("addon/$addon/$addon.php")) {
236                         return $info;
237                 }
238
239                 $stamp1 = microtime(true);
240                 $f = file_get_contents("addon/$addon/$addon.php");
241                 DI::profiler()->saveTimestamp($stamp1, "file");
242
243                 $r = preg_match("|/\*.*\*/|msU", $f, $m);
244
245                 if ($r) {
246                         $ll = explode("\n", $m[0]);
247                         foreach ($ll as $l) {
248                                 $l = trim($l, "\t\n\r */");
249                                 if ($l != "") {
250                                         $addon_info = array_map("trim", explode(":", $l, 2));
251                                         if (count($addon_info) < 2) {
252                                                 continue;
253                                         }
254
255                                         list($type, $v) = $addon_info;
256                                         $type = strtolower($type);
257                                         if ($type == "author" || $type == "maintainer") {
258                                                 $r = preg_match("|([^<]+)<([^>]+)>|", $v, $m);
259                                                 if ($r) {
260                                                         $info[$type][] = ['name' => $m[1], 'link' => $m[2]];
261                                                 } else {
262                                                         $info[$type][] = ['name' => $v];
263                                                 }
264                                         } else {
265                                                 if (array_key_exists($type, $info)) {
266                                                         $info[$type] = $v;
267                                                 }
268                                         }
269                                 }
270                         }
271                 }
272                 return $info;
273         }
274
275         /**
276          * Checks if the provided addon is enabled
277          *
278          * @param string $addon
279          * @return boolean
280          */
281         public static function isEnabled($addon)
282         {
283                 return in_array($addon, self::$addons);
284         }
285
286         /**
287          * Returns a list of the enabled addon names
288          *
289          * @return array
290          */
291         public static function getEnabledList()
292         {
293                 return self::$addons;
294         }
295
296         /**
297          * Returns the list of non-hidden enabled addon names
298          *
299          * @return array
300          * @throws \Exception
301          */
302         public static function getVisibleList()
303         {
304                 $visible_addons = [];
305                 $stmt = DBA::select('addon', ['name'], ['hidden' => false, 'installed' => true]);
306                 if (DBA::isResult($stmt)) {
307                         foreach (DBA::toArray($stmt) as $addon) {
308                                 $visible_addons[] = $addon['name'];
309                         }
310                 }
311
312                 return $visible_addons;
313         }
314
315         /**
316          * Shim of Hook::register left for backward compatibility purpose.
317          *
318          * @see        Hook::register
319          * @deprecated since version 2018.12
320          * @param string $hook     the name of the hook
321          * @param string $file     the name of the file that hooks into
322          * @param string $function the name of the function that the hook will call
323          * @param int    $priority A priority (defaults to 0)
324          * @return mixed|bool
325          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
326          */
327         public static function registerHook($hook, $file, $function, $priority = 0)
328         {
329                 return Hook::register($hook, $file, $function, $priority);
330         }
331
332         /**
333          * Shim of Hook::unregister left for backward compatibility purpose.
334          *
335          * @see        Hook::unregister
336          * @deprecated since version 2018.12
337          * @param string $hook     the name of the hook
338          * @param string $file     the name of the file that hooks into
339          * @param string $function the name of the function that the hook called
340          * @return boolean
341          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
342          */
343         public static function unregisterHook($hook, $file, $function)
344         {
345                 return Hook::unregister($hook, $file, $function);
346         }
347
348         /**
349          * Shim of Hook::callAll left for backward-compatibility purpose.
350          *
351          * @see        Hook::callAll
352          * @deprecated since version 2018.12
353          * @param string        $name of the hook to call
354          * @param string|array &$data to transmit to the callback handler
355          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
356          */
357         public static function callHooks($name, &$data = null)
358         {
359                 Hook::callAll($name, $data);
360         }
361 }