Elgg  Version master
Plugins.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Database;
4 
6 use Elgg\Config;
7 use Elgg\Context;
8 use Elgg\Database;
13 use Elgg\Invoker;
21 
28 class Plugins {
29 
30  use Profilable;
31  use Loggable;
32 
33  const BUNDLED_PLUGINS = [
34  'activity',
35  'blog',
36  'bookmarks',
37  'ckeditor',
38  'custom_index',
39  'dashboard',
40  'developers',
41  'discussions',
42  'externalpages',
43  'file',
44  'friends',
45  'friends_collections',
46  'garbagecollector',
47  'groups',
48  'invitefriends',
49  'likes',
50  'members',
51  'messageboard',
52  'messages',
53  'notifications',
54  'pages',
55  'profile',
56  'reportedcontent',
57  'search',
58  'site_notifications',
59  'system_log',
60  'tagcloud',
61  'theme_sandbox',
62  'thewire',
63  'uservalidationbyemail',
64  'web_services',
65  ];
66 
70  protected ?array $boot_plugins;
71 
72  protected Context $context;
73 
88  public function __construct(
89  protected PluginsCache $cache,
90  protected Database $db,
91  protected SessionManagerService $session_manager,
92  protected EventsService $events,
93  protected Translator $translator,
94  protected ViewsService $views,
95  protected Config $config,
96  protected SystemMessagesService $system_messages,
97  protected Invoker $invoker,
99  ) {
100  $this->context = $request->getContextStack();
101  }
102 
108  public function getPath(): string {
109  return $this->config->plugins_path;
110  }
111 
120  public function setBootPlugins(array $plugins = null, bool $order_plugins = true): void {
121  $this->cache->clear();
122  if (!is_array($plugins)) {
123  unset($this->boot_plugins);
124  return;
125  }
126 
127  // Always (re)set the boot_plugins. This makes sure that even if you have no plugins active this is known to the system.
128  $this->boot_plugins = [];
129 
130  if ($order_plugins) {
131  $plugins = $this->orderPluginsByPriority($plugins);
132  }
133 
134  foreach ($plugins as $plugin) {
135  if (!$plugin instanceof \ElggPlugin) {
136  continue;
137  }
138 
139  $plugin_id = $plugin->getID();
140  if (!$plugin_id) {
141  continue;
142  }
143 
144  $plugin->registerLanguages();
145 
146  $this->boot_plugins[$plugin_id] = $plugin;
147 
148  // make sure the plugin is in the entity and plugin cache
149  $plugin->cache();
150 
151  // can't use ElggEntity::cache() as it conflict with metadata preloading
152  $this->cache->save($plugin_id, $plugin);
153  }
154  }
155 
164  public function getDirsInDir(string $dir = null): array {
165  if (!$dir) {
166  $dir = $this->getPath();
167  }
168 
169  if (!is_dir($dir)) {
170  return [];
171  }
172 
173  $handle = opendir($dir);
174  if ($handle === false) {
175  return [];
176  }
177 
178  $plugin_dirs = [];
179  while (($plugin_dir = readdir($handle)) !== false) {
180  // must be directory and not begin with a .
181  if (!str_starts_with($plugin_dir, '.') && is_dir($dir . $plugin_dir)) {
182  $plugin_dirs[] = $plugin_dir;
183  }
184  }
185 
186  sort($plugin_dirs);
187 
188  return $plugin_dirs;
189  }
190 
199  public function generateEntities(): bool {
200  // ignore access in case this is called with no admin logged in - needed for creating plugins perhaps?
201  // show hidden entities so that we can enable them if appropriate
202  return $this->invoker->call(ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES, function() {
203  $mod_dir = $this->getPath();
204 
205  $known_plugins = $this->find('all');
206 
207  // keeps track if reindexing is needed
208  $reindex = false;
209 
210  // map paths to indexes
211  $id_map = [];
212  $latest_priority = 0;
213  foreach ($known_plugins as $i => $plugin) {
214  // if the ID is wrong, delete the plugin because we can never load it.
215  $id = $plugin->getID() . $plugin->guid;
216  if (!$id) {
217  $plugin->delete();
218  unset($known_plugins[$i]);
219  continue;
220  }
221 
222  $id_map[$plugin->getID()] = $i;
223 
224  // disabled plugins should have no priority, so no need to check if the priority is incorrect
225  if (!$plugin->isEnabled()) {
226  continue;
227  }
228 
229  $current_priority = $plugin->getPriority();
230  if (($current_priority - $latest_priority) > 1) {
231  $reindex = true;
232  }
233 
234  $latest_priority = $current_priority;
235  }
236 
237  $physical_plugins = $this->getDirsInDir($mod_dir);
238  if (empty($physical_plugins)) {
239  return false;
240  }
241 
242  // check real plugins against known ones
243  foreach ($physical_plugins as $plugin_id) {
244  // is this already in the db?
245  if (array_key_exists($plugin_id, $id_map)) {
246  $index = $id_map[$plugin_id];
247  $plugin = $known_plugins[$index];
248  // was this plugin deleted and its entity disabled?
249  if (!$plugin->isEnabled()) {
250  $plugin->enable();
251  try {
252  $plugin->deactivate();
253  } catch (PluginException $e) {
254  // do nothing
255  }
256 
257  $plugin->setPriority('new');
258  }
259 
260  // remove from the list of plugins to disable
261  unset($known_plugins[$index]);
262  } else {
263  // create new plugin
264  // priority is forced to last in save() if not set.
265  \ElggPlugin::fromId($plugin_id);
266  }
267  }
268 
269  // everything remaining in $known_plugins needs to be disabled
270  // because they are entities, but their dirs were removed.
271  // don't delete the entities because they hold settings.
272  foreach ($known_plugins as $plugin) {
273  if (!$plugin->isEnabled()) {
274  continue;
275  }
276 
277  $reindex = true;
278 
279  if ($plugin->isActive()) {
280  try {
281  $plugin->deactivate();
282  } catch (PluginException $e) {
283  // do nothing
284  }
285  }
286 
287  // remove the priority.
288  $plugin->deleteMetadata(\ElggPlugin::PRIORITY_SETTING_NAME);
289 
290  $plugin->disable();
291  }
292 
293  if ($reindex) {
294  $this->reindexPriorities();
295  }
296 
297  return true;
298  });
299  }
300 
308  public function get(string $plugin_id): ?\ElggPlugin {
309  if (empty($plugin_id)) {
310  return null;
311  }
312 
313  $plugin = $this->cache->load($plugin_id);
314  if ($plugin instanceof \ElggPlugin) {
315  return $plugin;
316  }
317 
318  $plugins = elgg_get_entities([
319  'type' => 'object',
320  'subtype' => 'plugin',
321  'metadata_name_value_pairs' => [
322  'name' => 'title',
323  'value' => $plugin_id,
324  ],
325  'limit' => 1,
326  'distinct' => false,
327  ]);
328 
329  if (empty($plugins)) {
330  return null;
331  }
332 
333  $plugin = $plugins[0];
334 
335  $this->cache->save($plugin_id, $plugin);
336 
337  return $plugin;
338  }
339 
345  public function getMaxPriority(): int {
347  $qb->select('MAX(CAST(md.value AS unsigned)) as max')
348  ->join($qb->getTableAlias(), MetadataTable::TABLE_NAME, 'md', "{$qb->getTableAlias()}.guid = md.entity_guid")
349  ->where($qb->compare('md.name', '=', \ElggPlugin::PRIORITY_SETTING_NAME, ELGG_VALUE_STRING))
350  ->andWhere($qb->compare("{$qb->getTableAlias()}.type", '=', 'object', ELGG_VALUE_STRING))
351  ->andWhere($qb->compare("{$qb->getTableAlias()}.subtype", '=', 'plugin', ELGG_VALUE_STRING));
352 
353  $data = $this->db->getDataRow($qb);
354  if (empty($data)) {
355  return 1;
356  }
357 
358  return max(1, (int) $data->max);
359  }
360 
368  public function isActive(string $plugin_id): bool {
369  if (isset($this->boot_plugins) && is_array($this->boot_plugins)) {
370  return array_key_exists($plugin_id, $this->boot_plugins);
371  }
372 
373  $plugin = $this->get($plugin_id);
374  if (!$plugin instanceof \ElggPlugin) {
375  return false;
376  }
377 
378  return $plugin->hasRelationship(1, 'active_plugin');
379  }
380 
390  public function build(): bool {
391  $plugins_path = $this->getPath();
392 
393  // temporary disable all plugins if there is a file called 'disabled' in the plugin dir
394  if (file_exists("{$plugins_path}/disabled")) {
395  if ($this->session_manager->isAdminLoggedIn() && $this->context->contains('admin')) {
396  $this->system_messages->addSuccessMessage($this->translator->translate('plugins:disabled'));
397  }
398 
399  return false;
400  }
401 
402  $this->events->registerHandler('plugins_load', 'system', [$this, 'register']);
403  $this->events->registerHandler('plugins_boot:before', 'system', [$this, 'boot']);
404  $this->events->registerHandler('init', 'system', [$this, 'init']);
405  $this->events->registerHandler('ready', 'system', [$this, 'ready']);
406  $this->events->registerHandler('upgrade', 'system', [$this, 'upgrade']);
407  $this->events->registerHandler('shutdown', 'system', [$this, 'shutdown']);
408 
409  return true;
410  }
411 
418  public function register(): void {
419  $plugins = $this->find('active');
420  if (empty($plugins)) {
421  return;
422  }
423 
424  $this->beginTimer([__METHOD__]);
425 
426  foreach ($plugins as $plugin) {
427  try {
428  $plugin->register();
429  } catch (\Exception $ex) {
430  $this->disable($plugin, $ex);
431  }
432  }
433 
434  $this->endTimer([__METHOD__]);
435  }
436 
442  public function boot(): void {
443  $plugins = $this->find('active');
444  if (empty($plugins)) {
445  return;
446  }
447 
448  $this->beginTimer([__METHOD__]);
449 
450  foreach ($plugins as $plugin) {
451  try {
452  $plugin->boot();
453  } catch (\Exception $ex) {
454  $this->disable($plugin, $ex);
455  }
456  }
457 
458  $this->endTimer([__METHOD__]);
459  }
460 
466  public function init(): void {
467  $plugins = $this->find('active');
468  if (empty($plugins)) {
469  return;
470  }
471 
472  $this->beginTimer([__METHOD__]);
473 
474  foreach ($plugins as $plugin) {
475  try {
476  $plugin->init();
477  } catch (\Exception $ex) {
478  $this->disable($plugin, $ex);
479  }
480  }
481 
482  $this->endTimer([__METHOD__]);
483  }
484 
490  public function ready(): void {
491  $plugins = $this->find('active');
492  if (empty($plugins)) {
493  return;
494  }
495 
496  $this->beginTimer([__METHOD__]);
497 
498  foreach ($plugins as $plugin) {
499  try {
500  $plugin->getBootstrap()->ready();
501  } catch (\Exception $ex) {
502  $this->disable($plugin, $ex);
503  }
504  }
505 
506  $this->endTimer([__METHOD__]);
507  }
508 
514  public function upgrade(): void {
515  $plugins = $this->find('active');
516  if (empty($plugins)) {
517  return;
518  }
519 
520  $this->beginTimer([__METHOD__]);
521 
522  foreach ($plugins as $plugin) {
523  try {
524  $plugin->getBootstrap()->upgrade();
525  } catch (\Exception $ex) {
526  $this->disable($plugin, $ex);
527  }
528  }
529 
530  $this->endTimer([__METHOD__]);
531  }
532 
538  public function shutdown(): void {
539  $plugins = $this->find('active');
540  if (empty($plugins)) {
541  return;
542  }
543 
544  $this->beginTimer([__METHOD__]);
545 
546  foreach ($plugins as $plugin) {
547  try {
548  $plugin->getBootstrap()->shutdown();
549  } catch (\Exception $ex) {
550  $this->disable($plugin, $ex);
551  }
552  }
553 
554  $this->endTimer([__METHOD__]);
555  }
556 
565  protected function disable(\ElggPlugin $plugin, \Exception $previous): void {
566  $this->getLogger()->log(LogLevel::ERROR, $previous, [
567  'context' => [
568  'plugin' => $plugin,
569  ],
570  ]);
571 
572  if (!$this->config->auto_disable_plugins) {
573  return;
574  }
575 
576  try {
577  $id = $plugin->getID();
578  $plugin->deactivate();
579 
580  $msg = $this->translator->translate(
581  'PluginException:CannotStart',
582  [$id, $plugin->guid, $previous->getMessage()]
583  );
584 
585  elgg_add_admin_notice("cannot_start {$id}", $msg);
586  } catch (PluginException $ex) {
587  $this->getLogger()->log(LogLevel::ERROR, $ex, [
588  'context' => [
589  'plugin' => $plugin,
590  ],
591  ]);
592  }
593  }
594 
602  public function find(string $status = 'active'): array {
603  if (!$this->db || !$this->config->installed) {
604  return [];
605  }
606 
607  if ($status === 'active' && isset($this->boot_plugins)) {
608  // boot_plugins is an already ordered list of plugins
609  return array_values($this->boot_plugins);
610  }
611 
612  $volatile_data_name = null;
613  $site_guid = 1;
614 
615  // grab plugins
616  $options = [
617  'type' => 'object',
618  'subtype' => 'plugin',
619  'limit' => false,
620  'order_by' => false,
621  ];
622 
623  switch ($status) {
624  case 'active':
625  $options['relationship'] = 'active_plugin';
626  $options['relationship_guid'] = $site_guid;
627  $options['inverse_relationship'] = true;
628 
629  // shorten callstack
630  $volatile_data_name = 'select:value';
631  $options['select'] = [MetadataTable::DEFAULT_JOIN_ALIAS . '.value'];
632  $options['metadata_names'] = [
634  ];
635  break;
636 
637  case 'inactive':
638  $options['wheres'][] = function (QueryBuilder $qb, $main_alias) use ($site_guid) {
639  $subquery = $qb->subquery('entity_relationships', 'active_er');
640  $subquery->select('active_er.guid_one')
641  ->where($qb->compare('active_er.relationship', '=', 'active_plugin', ELGG_VALUE_STRING))
642  ->andWhere($qb->compare('active_er.guid_two', '=', $site_guid, ELGG_VALUE_GUID));
643 
644  return $qb->compare("{$main_alias}.guid", 'NOT IN', $subquery->getSQL());
645  };
646  break;
647 
648  case 'all':
649  default:
650  break;
651  }
652 
653  $plugins = $this->invoker->call(ELGG_IGNORE_ACCESS, function () use ($options) {
654  return elgg_get_entities($options) ?: [];
655  });
656 
657  $result = $this->orderPluginsByPriority($plugins, $volatile_data_name);
658 
659  if ($status === 'active' && !isset($this->boot_plugins)) {
660  // populate local cache if for some reason this is not set yet
661  $this->setBootPlugins($result, false);
662  }
663 
664  foreach ($plugins as $plugin) {
665  // can't use ElggEntity::cache() as it conflict with metadata preloading
666  $this->cache->save($plugin->getID(), $plugin);
667  }
668 
669  return $result;
670  }
671 
680  protected function orderPluginsByPriority(array $plugins = [], string $volatile_data_name = null): array {
681  $priorities = [];
682  $sorted_plugins = [];
683 
684  foreach ($plugins as $plugin) {
685  $priority = null;
686  if (!empty($volatile_data_name)) {
687  $priority = $plugin->getVolatileData($volatile_data_name);
688  }
689 
690  if (!isset($priority)) {
691  $priority = $plugin->getPriority();
692  }
693 
694  $priorities[$plugin->guid] = (int) $priority;
695  $sorted_plugins[$plugin->guid] = $plugin;
696  }
697 
698  asort($priorities);
699 
700  return array_values(array_replace($priorities, $sorted_plugins));
701  }
702 
715  public function setPriorities(array $order): bool {
717 
718  $plugins = $this->find('all');
719  if (empty($plugins)) {
720  return false;
721  }
722 
723  // reindex to get standard counting. no need to increment by 10.
724  // though we do start with 1
725  $order = array_values($order);
726 
727  /* @var \ElggPlugin[] $missing_plugins */
728  $missing_plugins = [];
729 
730  $priority = 0;
731  foreach ($plugins as $plugin) {
732  if (!$plugin->isEnabled()) {
733  // disabled plugins should not have a priority
734  if ($plugin->getPriority() !== null) {
735  // remove the priority
736  unset($plugin->$name);
737  }
738 
739  continue;
740  }
741 
742  $plugin_id = $plugin->getID();
743 
744  if (!in_array($plugin_id, $order)) {
745  $missing_plugins[] = $plugin;
746  continue;
747  }
748 
749  $priority = array_search($plugin_id, $order) + 1;
750 
751  if (!$plugin->setMetadata($name, $priority)) {
752  return false;
753  }
754  }
755 
756  // set the missing plugins' priorities
757  if (empty($missing_plugins)) {
758  return true;
759  }
760 
761  foreach ($missing_plugins as $plugin) {
762  $priority++;
763  if (!$plugin->setMetadata($name, $priority)) {
764  return false;
765  }
766  }
767 
768  return true;
769  }
770 
776  public function reindexPriorities(): bool {
777  return $this->setPriorities([]);
778  }
779 
788  public function setPriority(\ElggPlugin $plugin, int $priority): int|false {
789  $old_priority = $plugin->getPriority() ?: 1;
790 
792 
793  if (!$plugin->setMetadata($name, $priority)) {
794  return false;
795  }
796 
797  if (!$plugin->guid) {
798  return false;
799  }
800 
802  $qb->where($qb->compare('name', '=', $name, ELGG_VALUE_STRING))
803  ->andWhere($qb->compare('entity_guid', '!=', $plugin->guid, ELGG_VALUE_INTEGER));
804 
805  if ($priority > $old_priority) {
806  $qb->set('value', 'CAST(value AS UNSIGNED) - 1');
807  $qb->andWhere($qb->between('CAST(value AS UNSIGNED)', $old_priority, $priority, ELGG_VALUE_INTEGER));
808  } else {
809  $qb->set('value', 'CAST(value AS UNSIGNED) + 1');
810  $qb->andWhere($qb->between('CAST(value AS UNSIGNED)', $priority, $old_priority, ELGG_VALUE_INTEGER));
811  }
812 
813  if (!$this->db->updateData($qb)) {
814  return false;
815  }
816 
817  return $priority;
818  }
819 }
isActive(string $plugin_id)
Returns if a plugin is active for a current site.
Definition: Plugins.php:368
trait Profilable
Make an object accept a timer.
Definition: Profilable.php:12
static table(string $table)
Returns a QueryBuilder for updating data in a given table.
Definition: Update.php:17
getID()
Returns the ID (dir name) of this plugin.
Definition: ElggPlugin.php:139
$plugin
Elgg HTTP request.
Definition: Request.php:17
reindexPriorities()
Reindexes all plugin priorities starting at 1.
Definition: Plugins.php:776
getMaxPriority()
Returns the highest priority of the plugins.
Definition: Plugins.php:345
Plugin class containing helper functions for plugin activation/deactivation, dependency checking capa...
Definition: ElggPlugin.php:17
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
subquery(string $table, string $alias=null)
Creates a new SelectQueryBuilder for join/where sub queries using the DB connection of the primary Qu...
build()
Registers lifecycle events for all active plugins sorted by their priority.
Definition: Plugins.php:390
The Elgg database.
Definition: Database.php:26
$request
Definition: livesearch.php:12
const ELGG_VALUE_INTEGER
Value types.
Definition: constants.php:111
c Accompany it with the information you received as to the offer to distribute corresponding source complete source code means all the source code for all modules it plus any associated interface definition plus the scripts used to control compilation and installation of the executable as a special the source code distributed need not include anything that is normally and so on of the operating system on which the executable unless that component itself accompanies the executable If distribution of executable or object code is made by offering access to copy from a designated then offering equivalent access to copy the source code from the same place counts as distribution of the source even though third parties are not compelled to copy the source along with the object code You may not or distribute the Program except as expressly provided under this License Any attempt otherwise to sublicense or distribute the Program is void
Definition: LICENSE.txt:215
const ELGG_VALUE_GUID
Definition: constants.php:113
Events service.
Database abstraction query builder.
getDirsInDir(string $dir=null)
Returns a list of plugin directory names from a base directory.
Definition: Plugins.php:164
boot()
Boot the plugins.
Definition: Plugins.php:442
disable(\ElggPlugin $plugin,\Exception $previous)
Disable a plugin upon exception.
Definition: Plugins.php:565
$plugin_id
Remove all user and plugin settings from the give plugin ID.
Definition: remove.php:8
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
if($who_can_change_language=== 'nobody') elseif($who_can_change_language=== 'admin_only'&&!elgg_is_admin_logged_in()) $options
Definition: language.php:20
if(!$entity instanceof\ElggUser) $data
Definition: attributes.php:13
trait Loggable
Enables adding a logger.
Definition: Loggable.php:14
const ELGG_IGNORE_ACCESS
elgg_call() flags
Definition: constants.php:121
setBootPlugins(array $plugins=null, bool $order_plugins=true)
Set the list of active plugins according to the boot data cache.
Definition: Plugins.php:120
orderPluginsByPriority(array $plugins=[], string $volatile_data_name=null)
Sorts plugins by priority.
Definition: Plugins.php:680
const ELGG_SHOW_DISABLED_ENTITIES
Definition: constants.php:123
elgg_get_entities(array $options=[])
Fetches/counts entities or performs a calculation on their properties.
Definition: entities.php:507
ready()
Run plugin ready handlers.
Definition: Plugins.php:490
init()
Initialize plugins.
Definition: Plugins.php:466
foreach($recommendedExtensions as $extension) if(empty(ini_get('session.gc_probability'))||empty(ini_get('session.gc_divisor'))) $db
generateEntities()
Discovers plugins in the plugins_path setting and creates entities for them if they don&#39;t exist...
Definition: Plugins.php:199
compare(string $x, string $comparison, $y=null, string $type=null, bool $case_sensitive=null)
Build value comparison clause.
Views service.
upgrade()
Run plugin upgrade handlers.
Definition: Plugins.php:514
const PRIORITY_SETTING_NAME
Definition: ElggPlugin.php:19
__construct(protected PluginsCache $cache, protected Database $db, protected SessionManagerService $session_manager, protected EventsService $events, protected Translator $translator, protected ViewsService $views, protected Config $config, protected SystemMessagesService $system_messages, protected Invoker $invoker, Request $request)
Constructor.
Definition: Plugins.php:88
getLogger()
Returns logger.
Definition: Loggable.php:37
deactivate()
Deactivates the plugin.
Definition: ElggPlugin.php:653
getContextStack()
Returns context stack.
Definition: Request.php:110
setPriority(\ElggPlugin $plugin, int $priority)
Set plugin priority and adjust the priorities of other plugins.
Definition: Plugins.php:788
const ELGG_VALUE_STRING
Definition: constants.php:112
beginTimer(array $keys)
Start the timer (when enabled)
Definition: Profilable.php:43
System messages service.
getPath()
Get the plugin path for this installation, ending with slash.
Definition: Plugins.php:108
getPriority()
Gets the plugin&#39;s load priority.
Definition: ElggPlugin.php:226
setPriorities(array $order)
Reorder plugins to an order specified by the array.
Definition: Plugins.php:715
static fromTable(string $table, string $alias=null)
Returns a QueryBuilder for selecting data from a given table.
Definition: Select.php:18
elgg_add_admin_notice(string $id, string $message)
Write a persistent message to the admin view.
Definition: admin.php:51
shutdown()
Run plugin shutdown handlers.
Definition: Plugins.php:538
$index
Definition: gallery.php:40
Persistent, installation-wide key-value storage.
Definition: Plugins.php:28
static fromId(string $plugin_id, string $path=null)
Load a plugin object from its ID Create a new plugin entity if doesn&#39;t exist.
Definition: ElggPlugin.php:82
find(string $status= 'active')
Returns an ordered list of plugins.
Definition: Plugins.php:602
$id
Generic annotation delete action.
Definition: delete.php:6
$qb
Definition: queue.php:12
$priority
Manages a global stack of strings for sharing information about the current execution context...
Definition: Context.php:27
$views
Definition: item.php:17
endTimer(array $keys)
Ends the timer (when enabled)
Definition: Profilable.php:59
Invocation service.
Definition: Invoker.php:10