3 * @file src/Core/Addon.php
6 namespace Friendica\Core;
8 use Friendica\Database\DBA;
10 use Friendica\Util\Strings;
13 * Some functions to handle addons
18 * The addon sub-directory
21 const DIRECTORY = 'addon';
24 * List of the names of enabled addons
28 private static $addons = [];
31 * Returns the list of available addons with their current status and info.
32 * This list is made from scanning the addon/ folder.
33 * Unsupported addons are excluded unless they already are enabled or system.show_unsupported_addon is set.
38 public static function getAvailableList()
41 $files = glob('addon/*/');
42 if (is_array($files)) {
43 foreach ($files as $file) {
45 list($tmp, $addon) = array_map('trim', explode('/', $file));
46 $info = self::getInfo($addon);
48 if (Config::get('system', 'show_unsupported_addons')
49 || strtolower($info['status']) != 'unsupported'
50 || self::isEnabled($addon)
52 $addons[] = [$addon, (self::isEnabled($addon) ? 'on' : 'off'), $info];
62 * Returns a list of addons that can be configured at the node level.
63 * The list is formatted for display in the admin panel aside.
68 public static function getAdminList()
71 $addonsAdminStmt = DBA::select('addon', ['name'], ['plugin_admin' => 1], ['order' => ['name']]);
72 while ($addon = DBA::fetch($addonsAdminStmt)) {
73 $addons_admin[$addon['name']] = [
74 'url' => 'admin/addons/' . $addon['name'],
75 'name' => $addon['name'],
79 DBA::close($addonsAdminStmt);
86 * @brief Synchronize addons:
88 * system.addon contains a comma-separated list of names
89 * of addons which are used on this system.
90 * Go through the database list of already installed addons, and if we have
91 * an entry, but it isn't in the config list, call the uninstall procedure
92 * and mark it uninstalled in the database (for now we'll remove it).
93 * Then go through the config list and if we have a addon that isn't installed,
94 * call the install procedure and add it to the database.
97 public static function loadAddons()
99 $installed_addons = [];
101 $r = DBA::select('addon', [], ['installed' => 1]);
102 if (DBA::isResult($r)) {
103 $installed_addons = DBA::toArray($r);
106 $addons = Config::get('system', 'addon');
110 $addons_arr = explode(',', str_replace(' ', '', $addons));
113 self::$addons = $addons_arr;
117 foreach ($installed_addons as $addon) {
118 if (!self::isEnabled($addon['name'])) {
119 self::uninstall($addon['name']);
121 $installed_arr[] = $addon['name'];
125 foreach (self::$addons as $p) {
126 if (!in_array($p, $installed_arr)) {
133 * @brief uninstalls an addon.
135 * @param string $addon name of the addon
139 public static function uninstall($addon)
141 $addon = Strings::sanitizeFilePathItem($addon);
143 Logger::notice("Addon {addon}: {action}", ['action' => 'uninstall', 'addon' => $addon]);
144 DBA::delete('addon', ['name' => $addon]);
146 @include_once('addon/' . $addon . '/' . $addon . '.php');
147 if (function_exists($addon . '_uninstall')) {
148 $func = $addon . '_uninstall';
152 DBA::delete('hook', ['file' => 'addon/' . $addon . '/' . $addon . '.php']);
154 unset(self::$addons[array_search($addon, self::$addons)]);
156 Addon::saveEnabledList();
160 * @brief installs an addon.
162 * @param string $addon name of the addon
166 public static function install($addon)
168 $addon = Strings::sanitizeFilePathItem($addon);
170 // silently fail if addon was removed of if $addon is funky
171 if (!file_exists('addon/' . $addon . '/' . $addon . '.php')) {
175 Logger::notice("Addon {addon}: {action}", ['action' => 'install', 'addon' => $addon]);
176 $t = @filemtime('addon/' . $addon . '/' . $addon . '.php');
177 @include_once('addon/' . $addon . '/' . $addon . '.php');
178 if (function_exists($addon . '_install')) {
179 $func = $addon . '_install';
182 $addon_admin = (function_exists($addon . "_addon_admin") ? 1 : 0);
184 DBA::insert('addon', ['name' => $addon, 'installed' => true,
185 'timestamp' => $t, 'plugin_admin' => $addon_admin]);
187 // we can add the following with the previous SQL
188 // once most site tables have been updated.
189 // This way the system won't fall over dead during the update.
191 if (file_exists('addon/' . $addon . '/.hidden')) {
192 DBA::update('addon', ['hidden' => true], ['name' => $addon]);
195 if (!self::isEnabled($addon)) {
196 self::$addons[] = $addon;
199 Addon::saveEnabledList();
203 Logger::error("Addon {addon}: {action} failed", ['action' => 'install', 'addon' => $addon]);
209 * reload all updated addons
211 public static function reload()
213 $addons = Config::get('system', 'addon');
214 if (strlen($addons)) {
215 $r = DBA::select('addon', [], ['installed' => 1]);
216 if (DBA::isResult($r)) {
217 $installed = DBA::toArray($r);
222 $addon_list = explode(',', $addons);
224 foreach ($addon_list as $addon) {
225 $addon = Strings::sanitizeFilePathItem(trim($addon));
226 $fname = 'addon/' . $addon . '/' . $addon . '.php';
227 if (file_exists($fname)) {
228 $t = @filemtime($fname);
229 foreach ($installed as $i) {
230 if (($i['name'] == $addon) && ($i['timestamp'] != $t)) {
232 Logger::notice("Addon {addon}: {action}", ['action' => 'reload', 'addon' => $i['name']]);
233 @include_once($fname);
235 if (function_exists($addon . '_uninstall')) {
236 $func = $addon . '_uninstall';
239 if (function_exists($addon . '_install')) {
240 $func = $addon . '_install';
243 DBA::update('addon', ['timestamp' => $t], ['id' => $i['id']]);
252 * @brief Parse addon comment in search of addon infos.
257 * * Description: An addon which plugs in
259 * * Author: John <profile url>
260 * * Author: Jane <email>
261 * * Maintainer: Jess <email>
264 * @param string $addon the name of the addon
265 * @return array with the addon information
268 public static function getInfo($addon)
272 $addon = Strings::sanitizeFilePathItem($addon);
283 if (!is_file("addon/$addon/$addon.php")) {
287 $stamp1 = microtime(true);
288 $f = file_get_contents("addon/$addon/$addon.php");
289 DI::profiler()->saveTimestamp($stamp1, "file", System::callstack());
291 $r = preg_match("|/\*.*\*/|msU", $f, $m);
294 $ll = explode("\n", $m[0]);
295 foreach ($ll as $l) {
296 $l = trim($l, "\t\n\r */");
298 $addon_info = array_map("trim", explode(":", $l, 2));
299 if (count($addon_info) < 2) {
303 list($type, $v) = $addon_info;
304 $type = strtolower($type);
305 if ($type == "author" || $type == "maintainer") {
306 $r = preg_match("|([^<]+)<([^>]+)>|", $v, $m);
308 $info[$type][] = ['name' => $m[1], 'link' => $m[2]];
310 $info[$type][] = ['name' => $v];
313 if (array_key_exists($type, $info)) {
324 * Checks if the provided addon is enabled
326 * @param string $addon
329 public static function isEnabled($addon)
331 return in_array($addon, self::$addons);
335 * Returns a list of the enabled addon names
339 public static function getEnabledList()
341 return self::$addons;
345 * Saves the current enabled addon list in the system.addon config key
349 public static function saveEnabledList()
351 return Config::set('system', 'addon', implode(',', self::$addons));
355 * Returns the list of non-hidden enabled addon names
360 public static function getVisibleList()
362 $visible_addons = [];
363 $stmt = DBA::select('addon', ['name'], ['hidden' => false, 'installed' => true]);
364 if (DBA::isResult($stmt)) {
365 foreach (DBA::toArray($stmt) as $addon) {
366 $visible_addons[] = $addon['name'];
370 return $visible_addons;
374 * Shim of Hook::register left for backward compatibility purpose.
376 * @see Hook::register
377 * @deprecated since version 2018.12
378 * @param string $hook the name of the hook
379 * @param string $file the name of the file that hooks into
380 * @param string $function the name of the function that the hook will call
381 * @param int $priority A priority (defaults to 0)
383 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
385 public static function registerHook($hook, $file, $function, $priority = 0)
387 return Hook::register($hook, $file, $function, $priority);
391 * Shim of Hook::unregister left for backward compatibility purpose.
393 * @see Hook::unregister
394 * @deprecated since version 2018.12
395 * @param string $hook the name of the hook
396 * @param string $file the name of the file that hooks into
397 * @param string $function the name of the function that the hook called
399 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
401 public static function unregisterHook($hook, $file, $function)
403 return Hook::unregister($hook, $file, $function);
407 * Shim of Hook::callAll left for backward-compatibility purpose.
410 * @deprecated since version 2018.12
411 * @param string $name of the hook to call
412 * @param string|array &$data to transmit to the callback handler
413 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
415 public static function callHooks($name, &$data = null)
417 Hook::callAll($name, $data);