3 * @copyright Copyright (C) 2020, Friendica
5 * @license GNU AGPL version 3 or any later version
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.
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.
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/>.
22 namespace Friendica\Core;
24 use Friendica\Database\DBA;
26 use Friendica\Util\Strings;
29 * Some functions to handle addons
34 * The addon sub-directory
37 const DIRECTORY = 'addon';
40 * List of the names of enabled addons
44 private static $addons = [];
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.
54 public static function getAvailableList()
57 $files = glob('addon/*/');
58 if (is_array($files)) {
59 foreach ($files as $file) {
61 list($tmp, $addon) = array_map('trim', explode('/', $file));
62 $info = self::getInfo($addon);
64 if (DI::config()->get('system', 'show_unsupported_addons')
65 || strtolower($info['status']) != 'unsupported'
66 || self::isEnabled($addon)
68 $addons[] = [$addon, (self::isEnabled($addon) ? 'on' : 'off'), $info];
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.
84 public static function getAdminList()
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'],
95 DBA::close($addonsAdminStmt);
102 * Synchronize addons:
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.
113 public static function loadAddons()
115 $installed_addons = [];
117 $r = DBA::select('addon', [], ['installed' => 1]);
118 if (DBA::isResult($r)) {
119 $installed_addons = DBA::toArray($r);
122 $addons = DI::config()->get('system', 'addon');
126 $addons_arr = explode(',', str_replace(' ', '', $addons));
129 self::$addons = $addons_arr;
133 foreach ($installed_addons as $addon) {
134 if (!self::isEnabled($addon['name'])) {
135 self::uninstall($addon['name']);
137 $installed_arr[] = $addon['name'];
141 foreach (self::$addons as $p) {
142 if (!in_array($p, $installed_arr)) {
149 * uninstalls an addon.
151 * @param string $addon name of the addon
155 public static function uninstall($addon)
157 $addon = Strings::sanitizeFilePathItem($addon);
159 Logger::notice("Addon {addon}: {action}", ['action' => 'uninstall', 'addon' => $addon]);
160 DBA::delete('addon', ['name' => $addon]);
162 @include_once('addon/' . $addon . '/' . $addon . '.php');
163 if (function_exists($addon . '_uninstall')) {
164 $func = $addon . '_uninstall';
168 DBA::delete('hook', ['file' => 'addon/' . $addon . '/' . $addon . '.php']);
170 unset(self::$addons[array_search($addon, self::$addons)]);
172 Addon::saveEnabledList();
178 * @param string $addon name of the addon
182 public static function install($addon)
184 $addon = Strings::sanitizeFilePathItem($addon);
186 // silently fail if addon was removed of if $addon is funky
187 if (!file_exists('addon/' . $addon . '/' . $addon . '.php')) {
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';
198 $addon_admin = (function_exists($addon . "_addon_admin") ? 1 : 0);
200 DBA::insert('addon', ['name' => $addon, 'installed' => true,
201 'timestamp' => $t, 'plugin_admin' => $addon_admin]);
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.
207 if (file_exists('addon/' . $addon . '/.hidden')) {
208 DBA::update('addon', ['hidden' => true], ['name' => $addon]);
211 if (!self::isEnabled($addon)) {
212 self::$addons[] = $addon;
215 Addon::saveEnabledList();
219 Logger::error("Addon {addon}: {action} failed", ['action' => 'install', 'addon' => $addon]);
225 * reload all updated addons
227 public static function reload()
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);
238 $addon_list = explode(',', $addons);
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)) {
248 Logger::notice("Addon {addon}: {action}", ['action' => 'reload', 'addon' => $i['name']]);
249 @include_once($fname);
251 if (function_exists($addon . '_uninstall')) {
252 $func = $addon . '_uninstall';
255 if (function_exists($addon . '_install')) {
256 $func = $addon . '_install';
259 DBA::update('addon', ['timestamp' => $t], ['id' => $i['id']]);
268 * Parse addon comment in search of addon infos.
273 * * Description: An addon which plugs in
275 * * Author: John <profile url>
276 * * Author: Jane <email>
277 * * Maintainer: Jess <email>
280 * @param string $addon the name of the addon
281 * @return array with the addon information
284 public static function getInfo($addon)
288 $addon = Strings::sanitizeFilePathItem($addon);
299 if (!is_file("addon/$addon/$addon.php")) {
303 $stamp1 = microtime(true);
304 $f = file_get_contents("addon/$addon/$addon.php");
305 DI::profiler()->saveTimestamp($stamp1, "file", System::callstack());
307 $r = preg_match("|/\*.*\*/|msU", $f, $m);
310 $ll = explode("\n", $m[0]);
311 foreach ($ll as $l) {
312 $l = trim($l, "\t\n\r */");
314 $addon_info = array_map("trim", explode(":", $l, 2));
315 if (count($addon_info) < 2) {
319 list($type, $v) = $addon_info;
320 $type = strtolower($type);
321 if ($type == "author" || $type == "maintainer") {
322 $r = preg_match("|([^<]+)<([^>]+)>|", $v, $m);
324 $info[$type][] = ['name' => $m[1], 'link' => $m[2]];
326 $info[$type][] = ['name' => $v];
329 if (array_key_exists($type, $info)) {
340 * Checks if the provided addon is enabled
342 * @param string $addon
345 public static function isEnabled($addon)
347 return in_array($addon, self::$addons);
351 * Returns a list of the enabled addon names
355 public static function getEnabledList()
357 return self::$addons;
361 * Saves the current enabled addon list in the system.addon config key
365 public static function saveEnabledList()
367 return DI::config()->set('system', 'addon', implode(',', self::$addons));
371 * Returns the list of non-hidden enabled addon names
376 public static function getVisibleList()
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'];
386 return $visible_addons;
390 * Shim of Hook::register left for backward compatibility purpose.
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)
399 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
401 public static function registerHook($hook, $file, $function, $priority = 0)
403 return Hook::register($hook, $file, $function, $priority);
407 * Shim of Hook::unregister left for backward compatibility purpose.
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
415 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
417 public static function unregisterHook($hook, $file, $function)
419 return Hook::unregister($hook, $file, $function);
423 * Shim of Hook::callAll left for backward-compatibility purpose.
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
431 public static function callHooks($name, &$data = null)
433 Hook::callAll($name, $data);