Plugins::generateEntities()   F
last analyzed

Complexity

Conditions 16
Paths 510

Size

Total Lines 113
Code Lines 57

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 48
CRAP Score 16.528

Importance

Changes 0
Metric Value
cc 16
eloc 57
nc 510
nop 0
dl 0
loc 113
ccs 48
cts 55
cp 0.8727
crap 16.528
rs 2.0804
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Elgg\Database;
4
5
use Elgg\Cache\BaseCache;
6
use Elgg\Config;
7
use Elgg\Context;
8
use Elgg\Database;
9
use Elgg\EventsService;
10
use Elgg\Exceptions\InvalidArgumentException;
11
use Elgg\Exceptions\PluginException;
12
use Elgg\Http\Request;
13
use Elgg\I18n\Translator;
14
use Elgg\Project\Paths;
15
use Elgg\SessionManagerService;
16
use Elgg\SystemMessagesService;
17
use Elgg\Traits\Cacheable;
18
use Elgg\Traits\Debug\Profilable;
19
use Elgg\Traits\Loggable;
20
use Elgg\ViewsService;
21
use Psr\Log\LogLevel;
22
23
/**
24
 * Persistent, installation-wide key-value storage.
25
 *
26
 * @internal
27
 * @since 1.10.0
28
 */
29
class Plugins {
30
31
	use Profilable;
32
	use Cacheable;
33
	use Loggable;
34
	
35
	const BUNDLED_PLUGINS = [
36
		'activity',
37
		'blog',
38
		'bookmarks',
39
		'ckeditor',
40
		'custom_index',
41
		'dashboard',
42
		'developers',
43
		'discussions',
44
		'externalpages',
45
		'file',
46
		'friends',
47
		'friends_collections',
48
		'garbagecollector',
49
		'groups',
50
		'invitefriends',
51
		'likes',
52
		'members',
53
		'messageboard',
54
		'messages',
55
		'notifications',
56
		'pages',
57
		'profile',
58
		'reportedcontent',
59
		'search',
60
		'site_notifications',
61
		'system_log',
62
		'tagcloud',
63
		'theme_sandbox',
64
		'thewire',
65
		'uservalidationbyemail',
66
		'web_services',
67
	];
68
69
	/**
70
	 * @var \ElggPlugin[]
71
	 */
72
	protected ?array $boot_plugins;
73
74
	protected Database $db;
75
76
	protected SessionManagerService $session_manager;
77
78
	protected EventsService $events;
79
80
	protected Translator $translator;
81
82
	protected ViewsService $views;
83
84
	protected Config $config;
85
86
	protected SystemMessagesService $system_messages;
87
88
	protected Context $context;
89
90
	/**
91
	 * Constructor
92
	 *
93
	 * @param BaseCache             $cache           Cache for referencing plugins by ID
94
	 * @param Database              $db              Database
95
	 * @param SessionManagerService $session_manager Session
96
	 * @param EventsService         $events          Events
97
	 * @param Translator            $translator      Translator
98
	 * @param ViewsService          $views           Views service
99
	 * @param Config                $config          Config
100
	 * @param SystemMessagesService $system_messages System messages
101
	 * @param Request               $request         Context
102
	 */
103 6920
	public function __construct(
104
		BaseCache $cache,
105
		Database $db,
106
		SessionManagerService $session_manager,
107
		EventsService $events,
108
		Translator $translator,
109
		ViewsService $views,
110
		Config $config,
111
		SystemMessagesService $system_messages,
112
		Request $request
113
	) {
114 6920
		$this->cache = $cache;
115 6920
		$this->db = $db;
116 6920
		$this->session_manager = $session_manager;
117 6920
		$this->events = $events;
118 6920
		$this->translator = $translator;
119 6920
		$this->views = $views;
120 6920
		$this->config = $config;
121 6920
		$this->system_messages = $system_messages;
122
		
123 6920
		$this->context = $request->getContextStack();
124
	}
125
126
	/**
127
	 * Get the plugin path for this installation, ending with slash.
128
	 *
129
	 * @return string
130
	 */
131 2916
	public function getPath(): string {
132 2916
		$path = $this->config->plugins_path;
133 2916
		if (!$path) {
134 5
			$path = Paths::project() . 'mod/';
135
		}
136
		
137 2916
		return $path;
138
	}
139
140
	/**
141
	 * Set the list of active plugins according to the boot data cache
142
	 *
143
	 * @param \ElggPlugin[]|null $plugins       Set of active plugins
144
	 * @param bool               $order_plugins Make sure plugins are saved in the correct order (set to false if provided plugins are already sorted)
145
	 *
146
	 * @return void
147
	 */
148 6951
	public function setBootPlugins(array $plugins = null, bool $order_plugins = true): void {
149 6951
		if (!is_array($plugins)) {
150 146
			unset($this->boot_plugins);
151 146
			return;
152
		}
153
		
154
		// Always (re)set the boot_plugins. This makes sure that even if you have no plugins active this is known to the system.
155 6925
		$this->boot_plugins = [];
156
		
157 6925
		if ($order_plugins) {
158 1
			$plugins = $this->orderPluginsByPriority($plugins);
159
		}
160
		
161 6925
		foreach ($plugins as $plugin) {
162 2344
			if (!$plugin instanceof \ElggPlugin) {
163
				continue;
164
			}
165
			
166 2344
			$plugin_id = $plugin->getID();
167 2344
			if (!$plugin_id) {
168
				continue;
169
			}
170
171 2344
			$plugin->registerLanguages();
172
			
173 2344
			$this->boot_plugins[$plugin_id] = $plugin;
174 2344
			$this->cache->save($plugin_id, $plugin);
175
		}
176
	}
177
178
	/**
179
	 * Clear plugin caches
180
	 *
181
	 * @return void
182
	 */
183 8
	public function clear(): void {
184 8
		$this->cache->clear();
185
	}
186
	
187
	/**
188
	 * Invalidate plugin cache
189
	 *
190
	 * @return void
191
	 */
192 24
	public function invalidate(): void {
193 24
		$this->cache->invalidate();
194
	}
195
	
196
	/**
197
	 * Returns a list of plugin directory names from a base directory.
198
	 *
199
	 * @param string $dir A dir to scan for plugins. Defaults to config's plugins_path.
200
	 *                    Must have a trailing slash.
201
	 *
202
	 * @return array Array of directory names (not full paths)
203
	 */
204 8
	public function getDirsInDir(string $dir = null): array {
205 8
		if (!$dir) {
206
			$dir = $this->getPath();
207
		}
208
209 8
		if (!is_dir($dir)) {
210
			return [];
211
		}
212
		
213 8
		$handle = opendir($dir);
214 8
		if ($handle === false) {
215
			return [];
216
		}
217
		
218 8
		$plugin_dirs = [];
219 8
		while (($plugin_dir = readdir($handle)) !== false) {
220
			// must be directory and not begin with a .
221 8
			if (!str_starts_with($plugin_dir, '.') && is_dir($dir . $plugin_dir)) {
222 8
				$plugin_dirs[] = $plugin_dir;
223
			}
224
		}
225
226 8
		sort($plugin_dirs);
227
228 8
		return $plugin_dirs;
229
	}
230
231
	/**
232
	 * Discovers plugins in the plugins_path setting and creates \ElggPlugin
233
	 * entities for them if they don't exist.  If there are plugins with entities
234
	 * but not actual files, will disable the \ElggPlugin entities and mark as inactive.
235
	 * The \ElggPlugin object holds config data, so don't delete.
236
	 *
237
	 * @return bool
238
	 */
239 8
	public function generateEntities(): bool {
240 8
		$mod_dir = $this->getPath();
241
242
		// ignore access in case this is called with no admin logged in - needed for creating plugins perhaps?
243 8
		$old_ia = $this->session_manager->setIgnoreAccess(true);
244
245
		// show hidden entities so that we can enable them if appropriate
246 8
		$old_access = $this->session_manager->setDisabledEntityVisibility(true);
247
248 8
		$known_plugins = $this->find('all');
249 8
		if (empty($known_plugins)) {
250 3
			$known_plugins = [];
251
		}
252
253
		// keeps track if reindexing is needed
254 8
		$reindex = false;
255
		
256
		// map paths to indexes
257 8
		$id_map = [];
258 8
		$latest_priority = -1;
259 8
		foreach ($known_plugins as $i => $plugin) {
260
			// if the ID is wrong, delete the plugin because we can never load it.
261 5
			$id = $plugin->getID();
262 5
			if (!$id) {
263
				$plugin->delete();
264
				unset($known_plugins[$i]);
265
				continue;
266
			}
267
			
268 5
			$id_map[$plugin->getID()] = $i;
269 5
			$plugin->cache();
270
			
271
			// disabled plugins should have no priority, so no need to check if the priority is incorrect
272 5
			if (!$plugin->isEnabled()) {
273 2
				continue;
274
			}
275
			
276 5
			$current_priority = $plugin->getPriority();
277 5
			if (($current_priority - $latest_priority) > 1) {
278 5
				$reindex = true;
279
			}
280
			
281 5
			$latest_priority = $current_priority;
282
		}
283
284 8
		$physical_plugins = $this->getDirsInDir($mod_dir);
285 8
		if (empty($physical_plugins)) {
286
			$this->session_manager->setIgnoreAccess($old_ia);
287
			$this->session_manager->setDisabledEntityVisibility($old_access);
288
289
			return false;
290
		}
291
292
		// check real plugins against known ones
293 8
		foreach ($physical_plugins as $plugin_id) {
294
			// is this already in the db?
295 8
			if (array_key_exists($plugin_id, $id_map)) {
296 5
				$index = $id_map[$plugin_id];
297 5
				$plugin = $known_plugins[$index];
298
				// was this plugin deleted and its entity disabled?
299 5
				if (!$plugin->isEnabled()) {
300 1
					$plugin->enable();
301
					try {
302 1
						$plugin->deactivate();
303
					} catch (PluginException $e) {
304
						// do nothing
305
					}
306
					
307 1
					$plugin->setPriority('new');
308
				}
309
310
				// remove from the list of plugins to disable
311 5
				unset($known_plugins[$index]);
312
			} else {
313
				// create new plugin
314
				// priority is forced to last in save() if not set.
315 5
				$plugin = \ElggPlugin::fromId($plugin_id);
316 5
				$plugin->cache();
317
			}
318
		}
319
320
		// everything remaining in $known_plugins needs to be disabled
321
		// because they are entities, but their dirs were removed.
322
		// don't delete the entities because they hold settings.
323 8
		foreach ($known_plugins as $plugin) {
324 2
			if (!$plugin->isEnabled()) {
325 2
				continue;
326
			}
327
			
328 2
			$reindex = true;
329
			
330 2
			if ($plugin->isActive()) {
331
				try {
332 2
					$plugin->deactivate();
333 2
				} catch (PluginException $e) {
334
					// do nothing
335
				}
336
			}
337
			
338
			// remove the priority.
339 2
			$plugin->deleteMetadata(\ElggPlugin::PRIORITY_SETTING_NAME);
340
			
341 2
			$plugin->disable();
342
		}
343
		
344 8
		if ($reindex) {
345 5
			$this->reindexPriorities();
346
		}
347
348 8
		$this->session_manager->setIgnoreAccess($old_ia);
349 8
		$this->session_manager->setDisabledEntityVisibility($old_access);
350
351 8
		return true;
352
	}
353
354
	/**
355
	 * Cache a reference to this plugin by its ID
356
	 *
357
	 * @param \ElggPlugin $plugin the plugin to cache
358
	 *
359
	 * @return void
360
	 */
361 143
	public function cache(\ElggPlugin $plugin): void {
362 143
		if (!$plugin->getID()) {
363 72
			return;
364
		}
365
		
366 117
		$this->cache->save($plugin->getID(), $plugin);
367
	}
368
369
	/**
370
	 * Remove plugin from cache
371
	 *
372
	 * @param string $plugin_id Plugin ID
373
	 *
374
	 * @return void
375
	 */
376 99
	public function invalidateCache($plugin_id): void {
377
		try {
378 99
			$this->cache->delete($plugin_id);
379
		} catch (InvalidArgumentException $ex) {
380
			// A plugin must have been deactivated due to missing folder
381
			// without proper cleanup
382
			elgg_invalidate_caches();
383
		}
384
	}
385
386
	/**
387
	 * Returns an \ElggPlugin object with the path $path.
388
	 *
389
	 * @param string $plugin_id The id (dir name) of the plugin. NOT the guid.
390
	 *
391
	 * @return \ElggPlugin|null
392
	 */
393 1141
	public function get(string $plugin_id): ?\ElggPlugin {
394 1141
		if (empty($plugin_id)) {
395 1
			return null;
396
		}
397
398 1141
		$plugin = $this->cache->load($plugin_id);
399 1141
		if ($plugin instanceof \ElggPlugin) {
400 1092
			return $plugin;
401
		}
402
403 94
		$plugins = elgg_get_entities([
404 94
			'type' => 'object',
405 94
			'subtype' => 'plugin',
406 94
			'metadata_name_value_pairs' => [
407 94
				'name' => 'title',
408 94
				'value' => $plugin_id,
409 94
			],
410 94
			'limit' => 1,
411 94
			'distinct' => false,
412 94
		]);
413
414 94
		if (empty($plugins)) {
415 84
			return null;
416
		}
417
418 22
		$plugins[0]->cache();
419
420 22
		return $plugins[0];
421
	}
422
423
	/**
424
	 * Returns if a plugin exists in the system.
425
	 *
426
	 * @warning This checks only plugins that are registered in the system!
427
	 * If the plugin cache is outdated, be sure to regenerate it with
428
	 * {@link _elgg_generate_plugin_objects()} first.
429
	 *
430
	 * @param string $id The plugin ID.
431
	 *
432
	 * @return bool
433
	 */
434
	public function exists(string $id): bool {
435
		return $this->get($id) instanceof \ElggPlugin;
436
	}
437
438
	/**
439
	 * Returns the highest priority of the plugins
440
	 *
441
	 * @return int
442
	 */
443 36
	public function getMaxPriority(): int {
444 36
		$qb = Select::fromTable('entities', 'e');
445 36
		$qb->select('MAX(CAST(md.value AS unsigned)) as max')
446 36
			->join('e', 'metadata', 'md', 'e.guid = md.entity_guid')
447 36
			->where($qb->compare('md.name', '=', \ElggPlugin::PRIORITY_SETTING_NAME, ELGG_VALUE_STRING))
448 36
			->andWhere($qb->compare('e.type', '=', 'object', ELGG_VALUE_STRING))
449 36
			->andWhere($qb->compare('e.subtype', '=', 'plugin', ELGG_VALUE_STRING));
450
451 36
		$data = $this->db->getDataRow($qb);
452 36
		if (empty($data)) {
453
			return 1;
454
		}
455
456 36
		return max(1, (int) $data->max);
457
	}
458
459
	/**
460
	 * Returns if a plugin is active for a current site.
461
	 *
462
	 * @param string $plugin_id The plugin ID
463
	 *
464
	 * @return bool
465
	 */
466 3140
	public function isActive(string $plugin_id): bool {
467 3140
		if (isset($this->boot_plugins) && is_array($this->boot_plugins)) {
468 2556
			return array_key_exists($plugin_id, $this->boot_plugins);
469
		}
470
		
471 627
		$plugin = $this->get($plugin_id);
472 627
		if (!$plugin instanceof \ElggPlugin) {
473 2
			return false;
474
		}
475
		
476 626
		return $plugin->hasRelationship(1, 'active_plugin');
477
	}
478
479
	/**
480
	 * Registers lifecycle events for all active plugins sorted by their priority
481
	 *
482
	 * @note   This is called on every page load. If a plugin is active and problematic, it
483
	 * will be disabled and a visible error emitted. This does not check the deps system because
484
	 * that was too slow.
485
	 *
486
	 * @return bool
487
	 */
488 2321
	public function build(): bool {
489 2321
		$plugins_path = $this->getPath();
490
491
		// temporary disable all plugins if there is a file called 'disabled' in the plugin dir
492 2321
		if (file_exists("{$plugins_path}/disabled")) {
493
			if ($this->session_manager->isAdminLoggedIn() && $this->context->contains('admin')) {
494
				$this->system_messages->addSuccessMessage($this->translator->translate('plugins:disabled'));
495
			}
496
497
			return false;
498
		}
499
500 2321
		$this->events->registerHandler('plugins_load', 'system', [$this, 'register']);
501 2321
		$this->events->registerHandler('plugins_boot:before', 'system', [$this, 'boot']);
502 2321
		$this->events->registerHandler('init', 'system', [$this, 'init']);
503 2321
		$this->events->registerHandler('ready', 'system', [$this, 'ready']);
504 2321
		$this->events->registerHandler('upgrade', 'system', [$this, 'upgrade']);
505 2321
		$this->events->registerHandler('shutdown', 'system', [$this, 'shutdown']);
506
507 2321
		return true;
508
	}
509
510
	/**
511
	 * Autoload plugin classes and files
512
	 * Register views, translations and custom entity types
513
	 *
514
	 * @return void
515
	 */
516 2321
	public function register(): void {
517 2321
		$plugins = $this->find('active');
518 2321
		if (empty($plugins)) {
519 27
			return;
520
		}
521
522 2294
		$this->beginTimer([__METHOD__]);
523
524 2294
		foreach ($plugins as $plugin) {
525
			try {
526 2294
				$plugin->register();
527
			} catch (\Exception $ex) {
528
				$this->disable($plugin, $ex);
529
			}
530
		}
531
532 2294
		$this->endTimer([__METHOD__]);
533
	}
534
535
	/**
536
	 * Boot the plugins
537
	 *
538
	 * @return void
539
	 */
540 2321
	public function boot(): void {
541 2321
		$plugins = $this->find('active');
542 2321
		if (empty($plugins)) {
543 27
			return;
544
		}
545
546 2294
		$this->beginTimer([__METHOD__]);
547
548 2294
		foreach ($plugins as $plugin) {
549
			try {
550 2294
				$plugin->boot();
551
			} catch (\Exception $ex) {
552
				$this->disable($plugin, $ex);
553
			}
554
		}
555
556 2294
		$this->endTimer([__METHOD__]);
557
	}
558
559
	/**
560
	 * Initialize plugins
561
	 *
562
	 * @return void
563
	 */
564 2320
	public function init(): void {
565 2320
		$plugins = $this->find('active');
566 2320
		if (empty($plugins)) {
567 26
			return;
568
		}
569
570 2294
		$this->beginTimer([__METHOD__]);
571
572 2294
		foreach ($plugins as $plugin) {
573
			try {
574 2294
				$plugin->init();
575
			} catch (\Exception $ex) {
576
				$this->disable($plugin, $ex);
577
			}
578
		}
579
580 2294
		$this->endTimer([__METHOD__]);
581
	}
582
583
	/**
584
	 * Run plugin ready handlers
585
	 *
586
	 * @return void
587
	 */
588 2320
	public function ready(): void {
589 2320
		$plugins = $this->find('active');
590 2320
		if (empty($plugins)) {
591 26
			return;
592
		}
593
594 2294
		$this->beginTimer([__METHOD__]);
595
596 2294
		foreach ($plugins as $plugin) {
597
			try {
598 2294
				$plugin->getBootstrap()->ready();
599
			} catch (\Exception $ex) {
600
				$this->disable($plugin, $ex);
601
			}
602
		}
603
604 2294
		$this->endTimer([__METHOD__]);
605
	}
606
607
	/**
608
	 * Run plugin upgrade handlers
609
	 *
610
	 * @return void
611
	 */
612 4
	public function upgrade(): void {
613 4
		$plugins = $this->find('active');
614 4
		if (empty($plugins)) {
615
			return;
616
		}
617
618 4
		$this->beginTimer([__METHOD__]);
619
620 4
		foreach ($plugins as $plugin) {
621
			try {
622 4
				$plugin->getBootstrap()->upgrade();
623
			} catch (\Exception $ex) {
624
				$this->disable($plugin, $ex);
625
			}
626
		}
627
628 4
		$this->endTimer([__METHOD__]);
629
	}
630
631
	/**
632
	 * Run plugin shutdown handlers
633
	 *
634
	 * @return void
635
	 */
636
	public function shutdown(): void {
637
		$plugins = $this->find('active');
638
		if (empty($plugins)) {
639
			return;
640
		}
641
642
		$this->beginTimer([__METHOD__]);
643
644
		foreach ($plugins as $plugin) {
645
			try {
646
				$plugin->getBootstrap()->shutdown();
647
			} catch (\Exception $ex) {
648
				$this->disable($plugin, $ex);
649
			}
650
		}
651
652
		$this->endTimer([__METHOD__]);
653
	}
654
655
	/**
656
	 * Disable a plugin upon exception
657
	 *
658
	 * @param \ElggPlugin $plugin   Plugin entity to disable
659
	 * @param \Exception  $previous Exception thrown
660
	 *
661
	 * @return void
662
	 */
663
	protected function disable(\ElggPlugin $plugin, \Exception $previous): void {
664
		$this->getLogger()->log(LogLevel::ERROR, $previous, [
665
			'context' => [
666
				'plugin' => $plugin,
667
			],
668
		]);
669
670
		if (!$this->config->auto_disable_plugins) {
671
			return;
672
		}
673
674
		try {
675
			$id = $plugin->getID();
676
			$plugin->deactivate();
677
678
			$msg = $this->translator->translate(
679
				'PluginException:CannotStart',
680
				[$id, $plugin->guid, $previous->getMessage()]
681
			);
682
683
			elgg_add_admin_notice("cannot_start {$id}", $msg);
684
		} catch (PluginException $ex) {
685
			$this->getLogger()->log(LogLevel::ERROR, $ex, [
686
				'context' => [
687
					'plugin' => $plugin,
688
				],
689
			]);
690
		}
691
	}
692
693
	/**
694
	 * Returns an ordered list of plugins
695
	 *
696
	 * @param string $status The status of the plugins. active, inactive, or all.
697
	 *
698
	 * @return \ElggPlugin[]
699
	 */
700 2317
	public function find(string $status = 'active'): array {
701 2317
		if (!$this->db || !$this->config->installed) {
702
			return [];
703
		}
704
705 2317
		if ($status === 'active' && isset($this->boot_plugins)) {
706
			// boot_plugins is an already ordered list of plugins
707 2297
			return array_values($this->boot_plugins);
708
		}
709
		
710 93
		$volatile_data_name = null;
711 93
		$site_guid = 1;
712
713
		// grab plugins
714 93
		$options = [
715 93
			'type' => 'object',
716 93
			'subtype' => 'plugin',
717 93
			'limit' => false,
718 93
			'order_by' => false,
719 93
		];
720
721
		switch ($status) {
722 93
			case 'active':
723 93
				$options['relationship'] = 'active_plugin';
724 93
				$options['relationship_guid'] = $site_guid;
725 93
				$options['inverse_relationship'] = true;
726
				
727
				// shorten callstack
728 93
				$volatile_data_name = 'select:value';
729 93
				$options['select'] = ['n_table.value'];
730 93
				$options['metadata_names'] = [
731 93
					\ElggPlugin::PRIORITY_SETTING_NAME,
732 93
				];
733 93
				break;
734
735 5
			case 'inactive':
736
				$options['wheres'][] = function (QueryBuilder $qb, $main_alias) use ($site_guid) {
737
					$subquery = $qb->subquery('entity_relationships', 'active_er');
738
					$subquery->select('active_er.guid_one')
739
						->where($qb->compare('active_er.relationship', '=', 'active_plugin', ELGG_VALUE_STRING))
740
						->andWhere($qb->compare('active_er.guid_two', '=', $site_guid, ELGG_VALUE_GUID));
741
742
					return $qb->compare("{$main_alias}.guid", 'NOT IN', $subquery->getSQL());
743
				};
744
				break;
745
746 5
			case 'all':
747
			default:
748 5
				break;
749
		}
750
751 93
		$old_ia = $this->session_manager->setIgnoreAccess(true);
752 93
		$plugins = elgg_get_entities($options) ?: [];
753 93
		$this->session_manager->setIgnoreAccess($old_ia);
754
755 93
		$result = $this->orderPluginsByPriority($plugins, $volatile_data_name);
756
		
757 93
		if ($status === 'active' && !isset($this->boot_plugins)) {
758
			// populate local cache if for some reason this is not set yet
759 93
			$this->setBootPlugins($result, false);
760
		}
761
		
762 93
		return $result;
763
	}
764
	
765
	/**
766
	 * Sorts plugins by priority
767
	 *
768
	 * @param \ElggPlugin[] $plugins            Array of plugins
769
	 * @param string        $volatile_data_name Use an optional volatile data name to retrieve priority
770
	 *
771
	 * @return \ElggPlugin[]
772
	 */
773 94
	protected function orderPluginsByPriority(array $plugins = [], string $volatile_data_name = null): array {
774 94
		$priorities = [];
775 94
		$sorted_plugins = [];
776
				
777 94
		foreach ($plugins as $plugin) {
778 94
			$priority = null;
779 94
			if (!empty($volatile_data_name)) {
780 93
				$priority = $plugin->getVolatileData($volatile_data_name);
781
			}
782
			
783 94
			if (!isset($priority)) {
784 6
				$priority = $plugin->getPriority();
785
			}
786
			
787 94
			$priorities[$plugin->guid] = (int) $priority;
788 94
			$sorted_plugins[$plugin->guid] = $plugin;
789
		}
790
		
791 94
		asort($priorities);
792
		
793 94
		return array_values(array_replace($priorities, $sorted_plugins));
794
	}
795
796
	/**
797
	 * Reorder plugins to an order specified by the array.
798
	 * Plugins not included in this array will be appended to the end.
799
	 *
800
	 * @note This doesn't use the \ElggPlugin->setPriority() method because
801
	 *       all plugins are being changed and we don't want it to automatically
802
	 *       reorder plugins.
803
	 *
804
	 * @param array $order An array of plugin ids in the order to set them
805
	 *
806
	 * @return bool
807
	 */
808 5
	public function setPriorities(array $order): bool {
809 5
		$name = \ElggPlugin::PRIORITY_SETTING_NAME;
810
811 5
		$plugins = $this->find('any');
812 5
		if (empty($plugins)) {
813
			return false;
814
		}
815
816
		// reindex to get standard counting. no need to increment by 10.
817
		// though we do start with 1
818 5
		$order = array_values($order);
819
820
		/* @var \ElggPlugin[] $missing_plugins */
821 5
		$missing_plugins = [];
822
823 5
		$priority = 0;
824 5
		foreach ($plugins as $plugin) {
825 5
			if (!$plugin->isEnabled()) {
826
				// disabled plugins should not have a priority
827 2
				if ($plugin->getPriority() !== null) {
828
					// remove the priority
829
					unset($plugin->$name);
830
				}
831
				
832 2
				continue;
833
			}
834
			
835 5
			$plugin_id = $plugin->getID();
836
837 5
			if (!in_array($plugin_id, $order)) {
838 5
				$missing_plugins[] = $plugin;
839 5
				continue;
840
			}
841
842 2
			$priority = array_search($plugin_id, $order) + 1;
843
844 2
			if (!$plugin->setMetadata($name, $priority)) {
845
				return false;
846
			}
847
		}
848
849
		// set the missing plugins' priorities
850 5
		if (empty($missing_plugins)) {
851 2
			return true;
852
		}
853
854 5
		foreach ($missing_plugins as $plugin) {
855 5
			$priority++;
856 5
			if (!$plugin->setMetadata($name, $priority)) {
857
				return false;
858
			}
859
		}
860
861 5
		return true;
862
	}
863
864
	/**
865
	 * Reindexes all plugin priorities starting at 1.
866
	 *
867
	 * @return bool
868
	 */
869 5
	public function reindexPriorities(): bool {
870 5
		return $this->setPriorities([]);
871
	}
872
873
	/**
874
	 * Set plugin priority and adjust the priorities of other plugins
875
	 *
876
	 * @param \ElggPlugin $plugin   Plugin
877
	 * @param int         $priority New priority
878
	 *
879
	 * @return int|false
880
	 */
881 36
	public function setPriority(\ElggPlugin $plugin, int $priority): int|false {
882 36
		$old_priority = $plugin->getPriority() ?: 1;
883
884 36
		$name = \ElggPlugin::PRIORITY_SETTING_NAME;
885
886 36
		if (!$plugin->setMetadata($name, $priority)) {
887
			return false;
888
		}
889
890 36
		if (!$plugin->guid) {
891 36
			return false;
892
		}
893
894 4
		$qb = Update::table('metadata');
895 4
		$qb->where($qb->compare('name', '=', $name, ELGG_VALUE_STRING))
896 4
			->andWhere($qb->compare('entity_guid', '!=', $plugin->guid, ELGG_VALUE_INTEGER));
897
898 4
		if ($priority > $old_priority) {
899 3
			$qb->set('value', 'CAST(value AS UNSIGNED) - 1');
900 3
			$qb->andWhere($qb->between('CAST(value AS UNSIGNED)', $old_priority, $priority, ELGG_VALUE_INTEGER));
901
		} else {
902 3
			$qb->set('value', 'CAST(value AS UNSIGNED) + 1');
903 3
			$qb->andWhere($qb->between('CAST(value AS UNSIGNED)', $priority, $old_priority, ELGG_VALUE_INTEGER));
904
		}
905
906 4
		if (!$this->db->updateData($qb)) {
907
			return false;
908
		}
909
910 4
		return $priority;
911
	}
912
}
913