3 * @copyright Copyright (C) 2010-2021, the Friendica project
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\Model\Contact;
27 use Friendica\Util\Strings;
30 * Some functions to handle addons
35 * The addon sub-directory
38 const DIRECTORY = 'addon';
41 * List of the names of enabled addons
45 private static $addons = [];
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.
55 public static function getAvailableList()
58 $files = glob('addon/*/');
59 if (is_array($files)) {
60 foreach ($files as $file) {
62 list($tmp, $addon) = array_map('trim', explode('/', $file));
63 $info = self::getInfo($addon);
65 if (DI::config()->get('system', 'show_unsupported_addons')
66 || strtolower($info['status']) != 'unsupported'
67 || self::isEnabled($addon)
69 $addons[] = [$addon, (self::isEnabled($addon) ? 'on' : 'off'), $info];
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.
85 public static function getAdminList()
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'],
96 DBA::close($addonsAdminStmt);
103 * Synchronize addons:
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.
114 public static function loadAddons()
116 $installed_addons = DBA::selectToArray('addon', ['name'], ['installed' => true]);
117 self::$addons = array_column($installed_addons, 'name');
121 * uninstalls an addon.
123 * @param string $addon name of the addon
127 public static function uninstall($addon)
129 $addon = Strings::sanitizeFilePathItem($addon);
131 Logger::notice("Addon {addon}: {action}", ['action' => 'uninstall', 'addon' => $addon]);
132 DBA::delete('addon', ['name' => $addon]);
134 @include_once('addon/' . $addon . '/' . $addon . '.php');
135 if (function_exists($addon . '_uninstall')) {
136 $func = $addon . '_uninstall';
140 Hook::delete(['file' => 'addon/' . $addon . '/' . $addon . '.php']);
142 unset(self::$addons[array_search($addon, self::$addons)]);
148 * @param string $addon name of the addon
152 public static function install($addon)
154 $addon = Strings::sanitizeFilePathItem($addon);
156 // silently fail if addon was removed of if $addon is funky
157 if (!file_exists('addon/' . $addon . '/' . $addon . '.php')) {
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';
169 DBA::insert('addon', [
173 'plugin_admin' => function_exists($addon . '_addon_admin'),
174 'hidden' => file_exists('addon/' . $addon . '/.hidden')
177 if (!self::isEnabled($addon)) {
178 self::$addons[] = $addon;
185 * reload all updated addons
187 public static function reload()
189 $addons = DBA::selectToArray('addon', [], ['installed' => true]);
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)) {
199 Logger::notice("Addon {addon}: {action}", ['action' => 'reload', 'addon' => $addon['name']]);
201 self::uninstall($fname);
202 self::install($fname);
207 * Parse addon comment in search of addon infos.
212 * * Description: An addon which plugs in
214 * * Author: John <profile url>
215 * * Author: Jane <email>
216 * * Maintainer: Jess <email>
219 * @param string $addon the name of the addon
220 * @return array with the addon information
223 public static function getInfo($addon)
225 $addon = Strings::sanitizeFilePathItem($addon);
236 if (!is_file("addon/$addon/$addon.php")) {
240 DI::profiler()->startRecording('file');
241 $f = file_get_contents("addon/$addon/$addon.php");
242 DI::profiler()->stopRecording();
244 $r = preg_match("|/\*.*\*/|msU", $f, $m);
247 $ll = explode("\n", $m[0]);
248 foreach ($ll as $l) {
249 $l = trim($l, "\t\n\r */");
251 $addon_info = array_map("trim", explode(":", $l, 2));
252 if (count($addon_info) < 2) {
256 list($type, $v) = $addon_info;
257 $type = strtolower($type);
258 if ($type == "author" || $type == "maintainer") {
259 $r = preg_match("|([^<]+)<([^>]+)>|", $v, $m);
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'];
267 $info[$type][] = ['name' => $m[1], 'link' => $m[2]];
269 $info[$type][] = ['name' => $v];
272 if (array_key_exists($type, $info)) {
283 * Checks if the provided addon is enabled
285 * @param string $addon
288 public static function isEnabled($addon)
290 return in_array($addon, self::$addons);
294 * Returns a list of the enabled addon names
298 public static function getEnabledList()
300 return self::$addons;
304 * Returns the list of non-hidden enabled addon names
309 public static function getVisibleList()
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'];
319 return $visible_addons;
323 * Shim of Hook::register left for backward compatibility purpose.
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)
332 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
334 public static function registerHook($hook, $file, $function, $priority = 0)
336 return Hook::register($hook, $file, $function, $priority);
340 * Shim of Hook::unregister left for backward compatibility purpose.
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
348 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
350 public static function unregisterHook($hook, $file, $function)
352 return Hook::unregister($hook, $file, $function);
356 * Shim of Hook::callAll left for backward-compatibility purpose.
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
364 public static function callHooks($name, &$data = null)
366 Hook::callAll($name, $data);