3 * @file src/Core/Addon.php
6 namespace Friendica\Core;
8 use Friendica\BaseObject;
9 use Friendica\Database\DBA;
11 use Friendica\Util\Strings;
14 * Some functions to handle addons
16 class Addon extends BaseObject
19 * The addon sub-directory
22 const DIRECTORY = 'addon';
25 * List of the names of enabled addons
29 private static $addons = [];
32 * Returns the list of available addons with their current status and info.
33 * This list is made from scanning the addon/ folder.
34 * Unsupported addons are excluded unless they already are enabled or system.show_unsupported_addon is set.
39 public static function getAvailableList()
42 $files = glob('addon/*/');
43 if (is_array($files)) {
44 foreach ($files as $file) {
46 list($tmp, $addon) = array_map('trim', explode('/', $file));
47 $info = self::getInfo($addon);
49 if (Config::get('system', 'show_unsupported_addons')
50 || strtolower($info['status']) != 'unsupported'
51 || self::isEnabled($addon)
53 $addons[] = [$addon, (self::isEnabled($addon) ? 'on' : 'off'), $info];
63 * Returns a list of addons that can be configured at the node level.
64 * The list is formatted for display in the admin panel aside.
69 public static function getAdminList()
72 $addonsAdminStmt = DBA::select('addon', ['name'], ['plugin_admin' => 1], ['order' => ['name']]);
73 while ($addon = DBA::fetch($addonsAdminStmt)) {
74 $addons_admin[$addon['name']] = [
75 'url' => 'admin/addons/' . $addon['name'],
76 'name' => $addon['name'],
80 DBA::close($addonsAdminStmt);
87 * @brief Synchronize addons:
89 * system.addon contains a comma-separated list of names
90 * of addons which are used on this system.
91 * Go through the database list of already installed addons, and if we have
92 * an entry, but it isn't in the config list, call the uninstall procedure
93 * and mark it uninstalled in the database (for now we'll remove it).
94 * Then go through the config list and if we have a addon that isn't installed,
95 * call the install procedure and add it to the database.
98 public static function loadAddons()
100 $installed_addons = [];
102 $r = DBA::select('addon', [], ['installed' => 1]);
103 if (DBA::isResult($r)) {
104 $installed_addons = DBA::toArray($r);
107 $addons = Config::get('system', 'addon');
111 $addons_arr = explode(',', str_replace(' ', '', $addons));
114 self::$addons = $addons_arr;
118 foreach ($installed_addons as $addon) {
119 if (!self::isEnabled($addon['name'])) {
120 self::uninstall($addon['name']);
122 $installed_arr[] = $addon['name'];
126 foreach (self::$addons as $p) {
127 if (!in_array($p, $installed_arr)) {
134 * @brief uninstalls an addon.
136 * @param string $addon name of the addon
140 public static function uninstall($addon)
142 $addon = Strings::sanitizeFilePathItem($addon);
144 Logger::notice("Addon {addon}: {action}", ['action' => 'uninstall', 'addon' => $addon]);
145 DBA::delete('addon', ['name' => $addon]);
147 @include_once('addon/' . $addon . '/' . $addon . '.php');
148 if (function_exists($addon . '_uninstall')) {
149 $func = $addon . '_uninstall';
153 DBA::delete('hook', ['file' => 'addon/' . $addon . '/' . $addon . '.php']);
155 unset(self::$addons[array_search($addon, self::$addons)]);
157 Addon::saveEnabledList();
161 * @brief installs an addon.
163 * @param string $addon name of the addon
167 public static function install($addon)
169 $addon = Strings::sanitizeFilePathItem($addon);
171 // silently fail if addon was removed of if $addon is funky
172 if (!file_exists('addon/' . $addon . '/' . $addon . '.php')) {
176 Logger::notice("Addon {addon}: {action}", ['action' => 'install', 'addon' => $addon]);
177 $t = @filemtime('addon/' . $addon . '/' . $addon . '.php');
178 @include_once('addon/' . $addon . '/' . $addon . '.php');
179 if (function_exists($addon . '_install')) {
180 $func = $addon . '_install';
183 $addon_admin = (function_exists($addon . "_addon_admin") ? 1 : 0);
185 DBA::insert('addon', ['name' => $addon, 'installed' => true,
186 'timestamp' => $t, 'plugin_admin' => $addon_admin]);
188 // we can add the following with the previous SQL
189 // once most site tables have been updated.
190 // This way the system won't fall over dead during the update.
192 if (file_exists('addon/' . $addon . '/.hidden')) {
193 DBA::update('addon', ['hidden' => true], ['name' => $addon]);
196 if (!self::isEnabled($addon)) {
197 self::$addons[] = $addon;
200 Addon::saveEnabledList();
204 Logger::error("Addon {addon}: {action} failed", ['action' => 'install', 'addon' => $addon]);
210 * reload all updated addons
212 public static function reload()
214 $addons = Config::get('system', 'addon');
215 if (strlen($addons)) {
216 $r = DBA::select('addon', [], ['installed' => 1]);
217 if (DBA::isResult($r)) {
218 $installed = DBA::toArray($r);
223 $addon_list = explode(',', $addons);
225 foreach ($addon_list as $addon) {
226 $addon = Strings::sanitizeFilePathItem(trim($addon));
227 $fname = 'addon/' . $addon . '/' . $addon . '.php';
228 if (file_exists($fname)) {
229 $t = @filemtime($fname);
230 foreach ($installed as $i) {
231 if (($i['name'] == $addon) && ($i['timestamp'] != $t)) {
233 Logger::notice("Addon {addon}: {action}", ['action' => 'reload', 'addon' => $i['name']]);
234 @include_once($fname);
236 if (function_exists($addon . '_uninstall')) {
237 $func = $addon . '_uninstall';
240 if (function_exists($addon . '_install')) {
241 $func = $addon . '_install';
244 DBA::update('addon', ['timestamp' => $t], ['id' => $i['id']]);
253 * @brief Parse addon comment in search of addon infos.
258 * * Description: An addon which plugs in
260 * * Author: John <profile url>
261 * * Author: Jane <email>
262 * * Maintainer: Jess <email>
265 * @param string $addon the name of the addon
266 * @return array with the addon information
269 public static function getInfo($addon)
273 $addon = Strings::sanitizeFilePathItem($addon);
284 if (!is_file("addon/$addon/$addon.php")) {
288 $stamp1 = microtime(true);
289 $f = file_get_contents("addon/$addon/$addon.php");
290 $a->getProfiler()->saveTimestamp($stamp1, "file", System::callstack());
292 $r = preg_match("|/\*.*\*/|msU", $f, $m);
295 $ll = explode("\n", $m[0]);
296 foreach ($ll as $l) {
297 $l = trim($l, "\t\n\r */");
299 $addon_info = array_map("trim", explode(":", $l, 2));
300 if (count($addon_info) < 2) {
304 list($type, $v) = $addon_info;
305 $type = strtolower($type);
306 if ($type == "author" || $type == "maintainer") {
307 $r = preg_match("|([^<]+)<([^>]+)>|", $v, $m);
309 $info[$type][] = ['name' => $m[1], 'link' => $m[2]];
311 $info[$type][] = ['name' => $v];
314 if (array_key_exists($type, $info)) {
325 * Checks if the provided addon is enabled
327 * @param string $addon
330 public static function isEnabled($addon)
332 return in_array($addon, self::$addons);
336 * Returns a list of the enabled addon names
340 public static function getEnabledList()
342 return self::$addons;
346 * Saves the current enabled addon list in the system.addon config key
350 public static function saveEnabledList()
352 return Config::set('system', 'addon', implode(',', self::$addons));
356 * Returns the list of non-hidden enabled addon names
361 public static function getVisibleList()
363 $visible_addons = [];
364 $stmt = DBA::select('addon', ['name'], ['hidden' => false, 'installed' => true]);
365 if (DBA::isResult($stmt)) {
366 foreach (DBA::toArray($stmt) as $addon) {
367 $visible_addons[] = $addon['name'];
371 return $visible_addons;
375 * Shim of Hook::register left for backward compatibility purpose.
377 * @see Hook::register
378 * @deprecated since version 2018.12
379 * @param string $hook the name of the hook
380 * @param string $file the name of the file that hooks into
381 * @param string $function the name of the function that the hook will call
382 * @param int $priority A priority (defaults to 0)
384 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
386 public static function registerHook($hook, $file, $function, $priority = 0)
388 return Hook::register($hook, $file, $function, $priority);
392 * Shim of Hook::unregister left for backward compatibility purpose.
394 * @see Hook::unregister
395 * @deprecated since version 2018.12
396 * @param string $hook the name of the hook
397 * @param string $file the name of the file that hooks into
398 * @param string $function the name of the function that the hook called
400 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
402 public static function unregisterHook($hook, $file, $function)
404 return Hook::unregister($hook, $file, $function);
408 * Shim of Hook::callAll left for backward-compatibility purpose.
411 * @deprecated since version 2018.12
412 * @param string $name of the hook to call
413 * @param string|array &$data to transmit to the callback handler
414 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
416 public static function callHooks($name, &$data = null)
418 Hook::callAll($name, $data);