Completed
Push — 3.3 ( 5fced4...d53524 )
by Jerome
22:15 queued 11s
created

engine/classes/Elgg/Database/Plugins.php (1 issue)

1
<?php
2
3
namespace Elgg\Database;
4
5
use Closure;
6
use DatabaseException;
7
use Elgg\Application;
8
use Elgg\Cacheable;
9
use Elgg\Config;
10
use Elgg\Context;
11
use Elgg\Database;
12
use Elgg\I18n\Translator;
13
use Elgg\Includer;
14
use Elgg\EventsService;
15
use Elgg\Loggable;
16
use Elgg\Profilable;
17
use Elgg\Project\Paths;
18
use Elgg\SystemMessagesService;
19
use Elgg\ViewsService;
20
use ElggCache;
21
use ElggPlugin;
22
use ElggSession;
23
use ElggUser;
24
use Exception;
25
use Psr\Log\LogLevel;
26
use Elgg\Cache\PrivateSettingsCache;
27
28
/**
29
 * Persistent, installation-wide key-value storage.
30
 *
31
 * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
32
 *
33
 * @internal
34
 * @since  1.10.0
35
 */
36
class Plugins {
37
38
	use Profilable;
39
	use Cacheable;
40
	use Loggable;
41
	
42
	const BUNDLED_PLUGINS = [
43
		'activity',
44
		'blog',
45
		'bookmarks',
46
		'ckeditor',
47
		'custom_index',
48
		'dashboard',
49
		'developers',
50
		'diagnostics',
51
		'discussions',
52
		'embed',
53
		'externalpages',
54
		'file',
55
		'friends',
56
		'friends_collections',
57
		'garbagecollector',
58
		'groups',
59
		'invitefriends',
60
		'likes',
61
		'login_as',
62
		'members',
63
		'messageboard',
64
		'messages',
65
		'notifications',
66
		'pages',
67
		'profile',
68
		'reportedcontent',
69
		'search',
70
		'site_notifications',
71
		'system_log',
72
		'tagcloud',
73
		'thewire',
74
		'uservalidationbyemail',
75
		'web_services',
76
	];
77
78
	/**
79
	 * @var ElggPlugin[]
80
	 */
81
	protected $boot_plugins;
82
83
	/**
84
	 * @var array|null
85
	 */
86
	protected $provides_cache;
87
88
	/**
89
	 * @var Database
90
	 */
91
	protected $db;
92
93
	/**
94
	 * @var ElggSession
95
	 */
96
	protected $session;
97
98
	/**
99
	 * @var EventsService
100
	 */
101
	protected $events;
102
103
	/**
104
	 * @var Translator
105
	 */
106
	protected $translator;
107
108
	/**
109
	 * @var ViewsService
110
	 */
111
	protected $views;
112
113
	/**
114
	 * @var PrivateSettingsCache
115
	 */
116
	protected $private_settings_cache;
117
118
	/**
119
	 * @var Config
120
	 */
121
	protected $config;
122
123
	/**
124
	 * @var SystemMessagesService
125
	 */
126
	protected $system_messages;
127
128
	/**
129
	 * @var Context
130
	 */
131
	protected $context;
132
133
134
	/**
135
	 * Constructor
136
	 *
137
	 * @param ElggCache             $cache                  Cache for referencing plugins by ID
138
	 * @param Database              $db                     Database
139
	 * @param ElggSession           $session                Session
140
	 * @param EventsService         $events                 Events
141
	 * @param Translator            $translator             Translator
142
	 * @param ViewsService          $views                  Views service
143
	 * @param PrivateSettingsCache  $private_settings_cache Settings cache
144
	 * @param Config                $config                 Config
145
	 * @param SystemMessagesService $system_messages        System messages
146
	 * @param Context               $context                Context
147
	 */
148
	public function __construct(
149
		ElggCache $cache,
150
		Database $db,
151
		ElggSession $session,
152
		EventsService $events,
153
		Translator $translator,
154
		ViewsService $views,
155
		PrivateSettingsCache $private_settings_cache,
156
		Config $config,
157
		SystemMessagesService $system_messages,
158
		Context $context
159
	) {
160
		$this->cache = $cache;
161
		$this->db = $db;
162
		$this->session = $session;
163
		$this->events = $events;
164
		$this->translator = $translator;
165
		$this->views = $views;
166
		$this->private_settings_cache = $private_settings_cache;
167
		$this->config = $config;
168
		$this->system_messages = $system_messages;
169
		$this->context = $context;
170
	}
171
172
	/**
173
	 * Get the plugin path for this installation, ending with slash.
174
	 *
175
	 * @return string
176
	 */
177
	public function getPath() {
178
		$path = $this->config->plugins_path;
179
		if (!$path) {
180
			$path = Paths::project() . 'mod/';
181
		}
182
		return $path;
183
	}
184
185
	/**
186
	 * Set the list of active plugins according to the boot data cache
187
	 *
188
	 * @param ElggPlugin[]|null $plugins       Set of active plugins
189
	 * @param bool              $order_plugins Make sure plugins are saved in the correct order (set to false if provided plugins are already sorted)
190
	 *
191
	 * @return void
192
	 */
193
	public function setBootPlugins($plugins, $order_plugins = true) {
194
		if (!is_array($plugins)) {
195
			unset($this->boot_plugins);
196
			return;
197
		}
198
		
199
		// Always (re)set the boot_plugins. This makes sure that even if you have no plugins active this is known to the system.
200
		$this->boot_plugins = [];
201
		
202
		if ($order_plugins) {
203
			$plugins = $this->orderPluginsByPriority($plugins);
204
		}
205
		
206
		foreach ($plugins as $plugin) {
207
			if (!$plugin instanceof ElggPlugin) {
208
				continue;
209
			}
210
			
211
			$plugin_id = $plugin->getID();
212
			if (!$plugin_id) {
213
				continue;
214
			}
215
216
			$plugin->registerLanguages();
217
			
218
			$this->boot_plugins[$plugin_id] = $plugin;
219
			$this->cache->save($plugin_id, $plugin);
220
		}
221
	}
222
223
	/**
224
	 * Clear plugin caches
225
	 * @return void
226
	 */
227
	public function clear() {
228
		$this->cache->clear();
229
		$this->invalidateProvidesCache();
230
	}
231
	
232
	/**
233
	 * Invalidate plugin cache
234
	 *
235
	 * @return void
236
	 */
237
	public function invalidate() {
238
		$this->cache->invalidate();
239
		$this->invalidateProvidesCache();
240
	}
241
	
242
	/**
243
	 * Returns a list of plugin directory names from a base directory.
244
	 *
245
	 * @param string $dir A dir to scan for plugins. Defaults to config's plugins_path.
246
	 *                    Must have a trailing slash.
247
	 *
248
	 * @return array Array of directory names (not full paths)
249
	 */
250
	public function getDirsInDir($dir = null) {
251
		if (!$dir) {
252
			$dir = $this->getPath();
253
		}
254
255
		if (!is_dir($dir)) {
256
			return [];
257
		}
258
		
259
		$handle = opendir($dir);
260
		if ($handle === false) {
261
			return [];
262
		}
263
		
264
		$plugin_dirs = [];
265
		while (($plugin_dir = readdir($handle)) !== false) {
266
			// must be directory and not begin with a .
267
			if (substr($plugin_dir, 0, 1) !== '.' && is_dir($dir . $plugin_dir)) {
268
				$plugin_dirs[] = $plugin_dir;
269
			}
270
		}
271
272
		sort($plugin_dirs);
273
274
		return $plugin_dirs;
275
	}
276
277
	/**
278
	 * Discovers plugins in the plugins_path setting and creates \ElggPlugin
279
	 * entities for them if they don't exist.  If there are plugins with entities
280
	 * but not actual files, will disable the \ElggPlugin entities and mark as inactive.
281
	 * The \ElggPlugin object holds config data, so don't delete.
282
	 *
283
	 * @return bool
284
	 * @throws DatabaseException
285
	 * @throws \PluginException
286
	 */
287
	public function generateEntities() {
288
289
		$mod_dir = $this->getPath();
290
291
		// ignore access in case this is called with no admin logged in - needed for creating plugins perhaps?
292
		$old_ia = $this->session->setIgnoreAccess(true);
293
294
		// show hidden entities so that we can enable them if appropriate
295
		$old_access = $this->session->setDisabledEntityVisibility(true);
296
297
		$known_plugins = $this->find('all');
298
		if (empty($known_plugins)) {
299
			$known_plugins = [];
300
		}
301
302
		// map paths to indexes
303
		$id_map = [];
304
		foreach ($known_plugins as $i => $plugin) {
305
			// if the ID is wrong, delete the plugin because we can never load it.
306
			$id = $plugin->getID();
307
			if (!$id) {
308
				$plugin->delete();
309
				unset($known_plugins[$i]);
310
				continue;
311
			}
312
			$id_map[$plugin->getID()] = $i;
313
			$plugin->cache();
314
		}
315
316
		$physical_plugins = $this->getDirsInDir($mod_dir);
317
		if (empty($physical_plugins)) {
318
			$this->session->setIgnoreAccess($old_ia);
319
			$this->session->setDisabledEntityVisibility($old_access);
320
321
			return false;
322
		}
323
324
		// check real plugins against known ones
325
		foreach ($physical_plugins as $plugin_id) {
326
			// is this already in the db?
327
			if (array_key_exists($plugin_id, $id_map)) {
328
				$index = $id_map[$plugin_id];
329
				$plugin = $known_plugins[$index];
330
				// was this plugin deleted and its entity disabled?
331
				if (!$plugin->isEnabled()) {
332
					$plugin->enable();
333
					$plugin->deactivate();
334
					$plugin->setPriority('new');
335
				}
336
337
				// remove from the list of plugins to disable
338
				unset($known_plugins[$index]);
339
			} else {
340
				// create new plugin
341
				// priority is forced to last in save() if not set.
342
				$plugin = ElggPlugin::fromId($plugin_id);
343
				$plugin->cache();
344
			}
345
		}
346
347
		// everything remaining in $known_plugins needs to be disabled
348
		// because they are entities, but their dirs were removed.
349
		// don't delete the entities because they hold settings.
350
		$reindex = false;
351
		foreach ($known_plugins as $plugin) {
352
			if (!$plugin->isEnabled()) {
353
				continue;
354
			}
355
			
356
			$reindex = true;
357
			
358
			if ($plugin->isActive()) {
359
				$plugin->deactivate();
360
			}
361
			// remove the priority.
362
			$name = $this->namespacePrivateSetting('internal', 'priority');
363
			$plugin->removePrivateSetting($name);
364
			
365
			$plugin->disable();
366
		}
367
		
368
		if ($reindex) {
369
			$this->reindexPriorities();
370
		}
371
372
		$this->session->setIgnoreAccess($old_ia);
373
		$this->session->setDisabledEntityVisibility($old_access);
374
375
		return true;
376
	}
377
378
	/**
379
	 * Cache a reference to this plugin by its ID
380
	 *
381
	 * @param ElggPlugin $plugin the plugin to cache
382
	 *
383
	 * @return void
384
	 */
385
	public function cache(ElggPlugin $plugin) {
386
		if (!$plugin->getID()) {
387
			return;
388
		}
389
		$this->cache->save($plugin->getID(), $plugin);
390
	}
391
392
	/**
393
	 * Remove plugin from cache
394
	 *
395
	 * @param string $plugin_id Plugin ID
396
	 *
397
	 * @return void
398
	 */
399
	public function invalidateCache($plugin_id) {
400
		try {
401
			$this->cache->delete($plugin_id);
402
			$this->invalidateProvidesCache();
403
		} catch (\InvalidArgumentException $ex) {
404
			// A plugin must have been deactivated due to missing folder
405
			// without proper cleanup
406
			elgg_flush_caches();
1 ignored issue
show
Deprecated Code introduced by
The function elgg_flush_caches() has been deprecated: 3.3 use elgg_clear_caches() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

406
			/** @scrutinizer ignore-deprecated */ elgg_flush_caches();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
407
		}
408
	}
409
410
	/**
411
	 * Returns an \ElggPlugin object with the path $path.
412
	 *
413
	 * @param string $plugin_id The id (dir name) of the plugin. NOT the guid.
414
	 *
415
	 * @return ElggPlugin|null
416
	 */
417
	public function get($plugin_id) {
418
		if (!$plugin_id) {
419
			return null;
420
		}
421
422
		$fallback = function () use ($plugin_id) {
423
			$plugins = elgg_get_entities([
424
				'type' => 'object',
425
				'subtype' => 'plugin',
426
				'metadata_name_value_pairs' => [
427
					'name' => 'title',
428
					'value' => $plugin_id,
429
				],
430
				'limit' => 1,
431
				'distinct' => false,
432
			]);
433
434
			if ($plugins) {
435
				return $plugins[0];
436
			}
437
438
			return null;
439
		};
440
441
		$plugin = $this->cache->load($plugin_id);
442
		if (!isset($plugin)) {
443
			$plugin = $fallback();
444
			if ($plugin instanceof ElggPlugin) {
445
				$plugin->cache();
446
			}
447
		}
448
449
		return $plugin;
450
	}
451
452
	/**
453
	 * Returns if a plugin exists in the system.
454
	 *
455
	 * @warning This checks only plugins that are registered in the system!
456
	 * If the plugin cache is outdated, be sure to regenerate it with
457
	 * {@link _elgg_generate_plugin_objects()} first.
458
	 *
459
	 * @param string $id The plugin ID.
460
	 *
461
	 * @return bool
462
	 */
463
	public function exists($id) {
464
		return $this->get($id) instanceof ElggPlugin;
465
	}
466
467
	/**
468
	 * Returns the highest priority of the plugins
469
	 *
470
	 * @return int
471
	 * @throws DatabaseException
472
	 */
473
	public function getMaxPriority() {
474
		$priority = $this->namespacePrivateSetting('internal', 'priority');
475
476
		$qb = Select::fromTable('entities', 'e');
477
		$qb->select('MAX(CAST(ps.value AS unsigned)) as max')
478
			->join('e', 'private_settings', 'ps', 'e.guid = ps.entity_guid')
479
			->where($qb->compare('ps.name', '=', $priority, ELGG_VALUE_STRING))
480
			->andWhere($qb->compare('e.type', '=', 'object', ELGG_VALUE_STRING))
481
			->andWhere($qb->compare('e.subtype', '=', 'plugin', ELGG_VALUE_STRING));
482
483
		$data = $this->db->getDataRow($qb);
484
		if (empty($data)) {
485
			return 1;
486
		}
487
488
		return max(1, (int) $data->max);
489
	}
490
491
	/**
492
	 * Returns if a plugin is active for a current site.
493
	 *
494
	 * @param string $plugin_id The plugin ID
495
	 *
496
	 * @return bool
497
	 */
498
	public function isActive($plugin_id) {
499
		if (isset($this->boot_plugins) && is_array($this->boot_plugins)) {
500
			return array_key_exists($plugin_id, $this->boot_plugins);
501
		}
502
		
503
		$plugin = $this->get($plugin_id);
504
		if (!$plugin) {
505
			return false;
506
		}
507
		
508
		return check_entity_relationship($plugin->guid, 'active_plugin', 1) instanceof \ElggRelationship;
509
	}
510
511
	/**
512
	 * Registers lifecycle hooks for all active plugins sorted by their priority
513
	 *
514
	 * @note   This is called on every page load. If a plugin is active and problematic, it
515
	 * will be disabled and a visible error emitted. This does not check the deps system because
516
	 * that was too slow.
517
	 *
518
	 * @return bool
519
	 */
520
	public function build() {
521
522
		$plugins_path = $this->getPath();
523
524
		// temporary disable all plugins if there is a file called 'disabled' in the plugin dir
525
		if (file_exists("$plugins_path/disabled")) {
526
			if ($this->session->isAdminLoggedIn() && $this->context->contains('admin')) {
527
				$this->system_messages->addSuccessMessage($this->translator->translate('plugins:disabled'));
528
			}
529
530
			return false;
531
		}
532
533
		$this->events->registerHandler('plugins_load', 'system', [$this, 'register']);
534
		$this->events->registerHandler('plugins_boot:before', 'system', [$this, 'boot']);
535
		$this->events->registerHandler('init', 'system', [$this, 'init']);
536
		$this->events->registerHandler('ready', 'system', [$this, 'ready']);
537
		$this->events->registerHandler('upgrade', 'system', [$this, 'upgrade']);
538
		$this->events->registerHandler('shutdown', 'system', [$this, 'shutdown']);
539
540
		return true;
541
	}
542
543
	/**
544
	 * Autoload plugin classes and files
545
	 * Register views, translations and custom entity types
546
	 *
547
	 * @elgg_event plugins_load system
548
	 * @return void
549
	 */
550
	public function register() {
551
		$plugins = $this->find('active');
552
		if (empty($plugins)) {
553
			return;
554
		}
555
556
		if ($this->timer) {
557
			$this->timer->begin([__METHOD__]);
558
		}
559
560
		foreach ($plugins as $plugin) {
561
			try {
562
				$plugin->register();
563
			} catch (Exception $ex) {
564
				$this->disable($plugin, $ex);
565
			}
566
		}
567
568
		$this->registerRoot();
569
570
		if ($this->timer) {
571
			$this->timer->end([__METHOD__]);
572
		}
573
	}
574
575
	/**
576
	 * Boot the plugins
577
	 *
578
	 * @elgg_event plugins_boot:before system
579
	 * @return void
580
	 */
581
	public function boot() {
582
		$plugins = $this->find('active');
583
		if (empty($plugins)) {
584
			return;
585
		}
586
587
		if ($this->timer) {
588
			$this->timer->begin([__METHOD__]);
589
		}
590
591
		foreach ($plugins as $plugin) {
592
			try {
593
				$setup = $plugin->boot();
594
				if ($setup instanceof Closure) {
595
					$setup();
596
				}
597
			} catch (Exception $ex) {
598
				$this->disable($plugin, $ex);
599
			}
600
		}
601
602
		$this->bootRoot();
603
604
		if ($this->timer) {
605
			$this->timer->end([__METHOD__]);
606
		}
607
	}
608
609
	/**
610
	 * Register root level plugin views and translations
611
	 * @return void
612
	 */
613
	protected function registerRoot() {
614
		if (Paths::project() === Paths::elgg()) {
615
			return;
616
		}
617
618
		// Elgg is installed as a composer dep, so try to treat the root directory
619
		// as a custom plugin that is always loaded last and can't be disabled...
620
		if (!$this->config->system_cache_loaded) {
621
			// configure view locations for the custom plugin (not Elgg core)
622
			$viewsFile = Paths::project() . 'views.php';
623
			if (is_file($viewsFile)) {
624
				$viewsSpec = Includer::includeFile($viewsFile);
625
				if (is_array($viewsSpec)) {
626
					$this->views->mergeViewsSpec($viewsSpec);
627
				}
628
			}
629
630
			// find views for the custom plugin (not Elgg core)
631
			$this->views->registerPluginViews(Paths::project());
632
		}
633
634
		if (!$this->config->i18n_loaded_from_cache) {
635
			$this->translator->registerTranslations(Paths::project() . 'languages');
636
		}
637
	}
638
	/**
639
	 * Boot root level custom plugin for starter-project installation
640
	 * @return void
641
	 */
642
	protected function bootRoot() {
643
		if (Paths::project() === Paths::elgg()) {
644
			return;
645
		}
646
647
		// This is root directory start.php
648
		$root_start = Paths::project() . "start.php";
649
		if (is_file($root_start)) {
650
			$setup = Application::requireSetupFileOnce($root_start);
651
			if ($setup instanceof \Closure) {
652
				$setup();
653
			}
654
		}
655
	}
656
657
	/**
658
	 * Initialize plugins
659
	 *
660
	 * @elgg_event init system
661
	 * @return void
662
	 */
663
	public function init() {
664
		$plugins = $this->find('active');
665
		if (empty($plugins)) {
666
			return;
667
		}
668
669
		if ($this->timer) {
670
			$this->timer->begin([__METHOD__]);
671
		}
672
673
		foreach ($plugins as $plugin) {
674
			try {
675
				$plugin->init();
676
			} catch (Exception $ex) {
677
				$this->disable($plugin, $ex);
678
			}
679
		}
680
681
		if ($this->timer) {
682
			$this->timer->end([__METHOD__]);
683
		}
684
	}
685
686
	/**
687
	 * Run plugin ready handlers
688
	 *
689
	 * @elgg_event ready system
690
	 * @return void
691
	 */
692
	public function ready() {
693
		$plugins = $this->find('active');
694
		if (empty($plugins)) {
695
			return;
696
		}
697
698
		if ($this->timer) {
699
			$this->timer->begin([__METHOD__]);
700
		}
701
702
		foreach ($plugins as $plugin) {
703
			try {
704
				$plugin->getBootstrap()->ready();
705
			} catch (Exception $ex) {
706
				$this->disable($plugin, $ex);
707
			}
708
		}
709
710
		if ($this->timer) {
711
			$this->timer->end([__METHOD__]);
712
		}
713
	}
714
715
	/**
716
	 * Run plugin upgrade handlers
717
	 *
718
	 * @elgg_event upgrade system
719
	 * @return void
720
	 */
721
	public function upgrade() {
722
		$plugins = $this->find('active');
723
		if (empty($plugins)) {
724
			return;
725
		}
726
727
		if ($this->timer) {
728
			$this->timer->begin([__METHOD__]);
729
		}
730
731
		foreach ($plugins as $plugin) {
732
			try {
733
				$plugin->getBootstrap()->upgrade();
734
			} catch (Exception $ex) {
735
				$this->disable($plugin, $ex);
736
			}
737
		}
738
739
		if ($this->timer) {
740
			$this->timer->end([__METHOD__]);
741
		}
742
	}
743
744
	/**
745
	 * Run plugin shutdown handlers
746
	 *
747
	 * @elgg_event shutdown system
748
	 * @return void
749
	 */
750
	public function shutdown() {
751
		$plugins = $this->find('active');
752
		if (empty($plugins)) {
753
			return;
754
		}
755
756
		if ($this->timer) {
757
			$this->timer->begin([__METHOD__]);
758
		}
759
760
		foreach ($plugins as $plugin) {
761
			try {
762
				$plugin->getBootstrap()->shutdown();
763
			} catch (Exception $ex) {
764
				$this->disable($plugin, $ex);
765
			}
766
		}
767
768
		if ($this->timer) {
769
			$this->timer->end([__METHOD__]);
770
		}
771
	}
772
773
	/**
774
	 * Disable a plugin upon exception
775
	 *
776
	 * @param ElggPlugin $plugin   Plugin entity to disable
777
	 * @param Exception  $previous Exception thrown
778
	 *
779
	 * @return void
780
	 */
781
	protected function disable(ElggPlugin $plugin, Exception $previous) {
782
		$this->getLogger()->log(LogLevel::ERROR, $previous, [
783
			'context' => [
784
				'plugin' => $plugin,
785
			],
786
		]);
787
788
		$disable_plugins = $this->config->auto_disable_plugins;
789
		if ($disable_plugins === null) {
790
			$disable_plugins = true;
791
		}
792
793
		if (!$disable_plugins) {
794
			return;
795
		}
796
797
		try {
798
			$id = $plugin->getID();
799
			$plugin->deactivate();
800
801
			$msg = $this->translator->translate(
802
				'PluginException:CannotStart',
803
				[$id, $plugin->guid, $previous->getMessage()]
804
			);
805
806
			elgg_add_admin_notice("cannot_start $id", $msg);
807
		} catch (\PluginException $ex) {
808
			$this->getLogger()->log(LogLevel::ERROR, $ex, [
809
				'context' => [
810
					'plugin' => $plugin,
811
				],
812
			]);
813
		}
814
	}
815
816
	/**
817
	 * Returns an ordered list of plugins
818
	 *
819
	 * @param string $status The status of the plugins. active, inactive, or all.
820
	 *
821
	 * @return ElggPlugin[]
822
	 */
823
	public function find($status = 'active') {
824
		if (!$this->db || !$this->config->installed) {
825
			return [];
826
		}
827
828
		if ($status === 'active' && isset($this->boot_plugins)) {
829
			// boot_plugins is an already ordered list of plugins
830
			return array_values($this->boot_plugins);
831
		}
832
		
833
		$volatile_data_name = null;
834
		$site_guid = 1;
835
836
		// grab plugins
837
		$options = [
838
			'type' => 'object',
839
			'subtype' => 'plugin',
840
			'limit' => false,
841
			// ORDER BY CAST(ps.value) is super slow. We custom sorting below.
842
			'order_by' => false,
843
			// preload private settings because private settings will probably be used, at least priority
844
			'preload_private_settings' => true,
845
		];
846
847
		switch ($status) {
848
			case 'active':
849
				$options['relationship'] = 'active_plugin';
850
				$options['relationship_guid'] = $site_guid;
851
				$options['inverse_relationship'] = true;
852
				
853
				// shorten callstack
854
				$volatile_data_name = 'select:value';
855
				$options['select'] = ['ps.value'];
856
				$options['private_setting_names'] = [
857
					$this->namespacePrivateSetting('internal', 'priority'),
858
				];
859
				break;
860
861
			case 'inactive':
862
				$options['wheres'][] = function (QueryBuilder $qb, $main_alias) use ($site_guid) {
863
					$subquery = $qb->subquery('entity_relationships', 'active_er');
864
					$subquery->select('active_er.guid_one')
865
						->where($qb->compare('active_er.relationship', '=', 'active_plugin', ELGG_VALUE_STRING))
866
						->andWhere($qb->compare('active_er.guid_two', '=', $site_guid, ELGG_VALUE_GUID));
867
868
					return $qb->compare("{$main_alias}.guid", 'NOT IN', $subquery->getSQL());
869
				};
870
				break;
871
872
			case 'all':
873
			default:
874
				break;
875
		}
876
877
		$old_ia = $this->session->setIgnoreAccess(true);
878
		$plugins = elgg_get_entities($options) ? : [];
879
		$this->session->setIgnoreAccess($old_ia);
880
881
		$result = $this->orderPluginsByPriority($plugins, $volatile_data_name);
882
		
883
		if ($status === 'active' && !isset($this->boot_plugins)) {
884
			// populate local cache if for some reason this is not set yet
885
			$this->setBootPlugins($result, false);
886
		}
887
		
888
		return $result;
889
	}
890
	
891
	/**
892
	 * Sorts plugins by priority
893
	 *
894
	 * @param \ElggPlugin[] $plugins            Array of plugins
895
	 * @param string        $volatile_data_name Use an optional volatile data name to retrieve priority
896
	 *
897
	 * @return ElggPlugin[]
898
	 */
899
	protected function orderPluginsByPriority($plugins = [], $volatile_data_name = null) {
900
		$priorities = [];
901
		$sorted_plugins = [];
902
				
903
		foreach ($plugins as $plugin) {
904
			$priority = null;
905
			if (!empty($volatile_data_name)) {
906
				$priority = $plugin->getVolatileData($volatile_data_name);
907
			}
908
			
909
			if (!isset($priority)) {
910
				$priority = $plugin->getPriority();
911
			}
912
			
913
			$priorities[$plugin->guid] = (int) $priority;
914
			$sorted_plugins[$plugin->guid] = $plugin;
915
		}
916
		
917
		asort($priorities);
918
		
919
		return array_values(array_replace($priorities, $sorted_plugins));
920
	}
921
922
	/**
923
	 * Reorder plugins to an order specified by the array.
924
	 * Plugins not included in this array will be appended to the end.
925
	 *
926
	 * @note   This doesn't use the \ElggPlugin->setPriority() method because
927
	 *       all plugins are being changed and we don't want it to automatically
928
	 *       reorder plugins.
929
	 * @todo   Can this be done in a single sql command?
930
	 *
931
	 * @param array $order An array of plugin ids in the order to set them
932
	 *
933
	 * @return bool
934
	 */
935
	public function setPriorities(array $order) {
936
		$name = $this->namespacePrivateSetting('internal', 'priority');
937
938
		$plugins = $this->find('any');
939
		if (empty($plugins)) {
940
			return false;
941
		}
942
943
		// reindex to get standard counting. no need to increment by 10.
944
		// though we do start with 1
945
		$order = array_values($order);
946
947
		$missing_plugins = [];
948
		/* @var ElggPlugin[] $missing_plugins */
949
950
		$priority = 0;
951
		foreach ($plugins as $plugin) {
952
			$plugin_id = $plugin->getID();
953
954
			if (!in_array($plugin_id, $order)) {
955
				$missing_plugins[] = $plugin;
956
				continue;
957
			}
958
959
			$priority = array_search($plugin_id, $order) + 1;
960
961
			if (!$plugin->setPrivateSetting($name, $priority)) {
962
				return false;
963
			}
964
		}
965
966
		// set the missing plugins' priorities
967
		if (empty($missing_plugins)) {
968
			return true;
969
		}
970
971
		foreach ($missing_plugins as $plugin) {
972
			$priority++;
973
			if (!$plugin->setPrivateSetting($name, $priority)) {
974
				return false;
975
			}
976
		}
977
978
		return true;
979
	}
980
981
	/**
982
	 * Reindexes all plugin priorities starting at 1.
983
	 *
984
	 * @return bool
985
	 */
986
	public function reindexPriorities() {
987
		return $this->setPriorities([]);
988
	}
989
990
	/**
991
	 * Namespaces a string to be used as a private setting name for a plugin.
992
	 *
993
	 * For user_settings, two namespaces are added: a user setting namespace and the
994
	 * plugin id.
995
	 *
996
	 * For internal (plugin priority), there is a single internal namespace added.
997
	 *
998
	 * @param string $type The type of setting: user_setting or internal.
999
	 * @param string $name The name to namespace.
1000
	 * @param string $id   The plugin's ID to namespace with.  Required for user_setting.
1001
	 *
1002
	 * @return string
1003
	 */
1004
	public function namespacePrivateSetting($type, $name, $id = null) {
1005
		switch ($type) {
1006
			case 'user_setting':
1007
				if (!$id) {
1008
					throw new \InvalidArgumentException("You must pass the plugin id for user settings");
1009
				}
1010
				$name = ELGG_PLUGIN_USER_SETTING_PREFIX . "$id:$name";
1011
				break;
1012
1013
			case 'internal':
1014
				$name = ELGG_PLUGIN_INTERNAL_PREFIX . $name;
1015
				break;
1016
		}
1017
1018
		return $name;
1019
	}
1020
1021
1022
	/**
1023
	 * Returns an array of all provides from all active plugins.
1024
	 *
1025
	 * Array in the form array(
1026
	 *    'provide_type' => array(
1027
	 *        'provided_name' => array(
1028
	 *            'version' => '1.8',
1029
	 *            'provided_by' => 'provider_plugin_id'
1030
	 *    )
1031
	 *  )
1032
	 * )
1033
	 *
1034
	 * @param string $type The type of provides to return
1035
	 * @param string $name A specific provided name to return. Requires $provide_type.
1036
	 *
1037
	 * @return array|false
1038
	 */
1039
	public function getProvides($type = null, $name = null) {
1040
		if ($this->provides_cache === null) {
1041
			$active_plugins = $this->find('active');
1042
1043
			$provides = [];
1044
1045
			foreach ($active_plugins as $plugin) {
1046
				$plugin_provides = [];
1047
				$manifest = $plugin->getManifest();
1048
				if ($manifest instanceof \ElggPluginManifest) {
1049
					$plugin_provides = $plugin->getManifest()->getProvides();
1050
				}
1051
				if ($plugin_provides) {
1052
					foreach ($plugin_provides as $provided) {
1053
						$provides[$provided['type']][$provided['name']] = [
1054
							'version' => $provided['version'],
1055
							'provided_by' => $plugin->getID()
1056
						];
1057
					}
1058
				}
1059
			}
1060
1061
			$this->provides_cache = $provides;
1062
		}
1063
1064
		if ($type && $name) {
1065
			if (isset($this->provides_cache[$type][$name])) {
1066
				return $this->provides_cache[$type][$name];
1067
			} else {
1068
				return false;
1069
			}
1070
		} else if ($type) {
1071
			if (isset($this->provides_cache[$type])) {
1072
				return $this->provides_cache[$type];
1073
			} else {
1074
				return false;
1075
			}
1076
		}
1077
1078
		return $this->provides_cache;
1079
	}
1080
1081
	/**
1082
	 * Deletes all cached data on plugins being provided.
1083
	 *
1084
	 * @return boolean
1085
	 */
1086
	public function invalidateProvidesCache() {
1087
		$this->provides_cache = null;
1088
1089
		return true;
1090
	}
1091
1092
	/**
1093
	 * Checks if a plugin is currently providing $type and $name, and optionally
1094
	 * checking a version.
1095
	 *
1096
	 * @param string $type       The type of the provide
1097
	 * @param string $name       The name of the provide
1098
	 * @param string $version    A version to check against
1099
	 * @param string $comparison The comparison operator to use in version_compare()
1100
	 *
1101
	 * @return array An array in the form array(
1102
	 *    'status' => bool Does the provide exist?,
1103
	 *    'value' => string The version provided
1104
	 * )
1105
	 */
1106
	public function checkProvides($type, $name, $version = null, $comparison = 'ge') {
1107
		$provided = $this->getProvides($type, $name);
1108
		if (!$provided) {
1109
			return [
1110
				'status' => false,
1111
				'value' => ''
1112
			];
1113
		}
1114
1115
		if ($version) {
1116
			$status = version_compare($provided['version'], $version, $comparison);
1117
		} else {
1118
			$status = true;
1119
		}
1120
1121
		return [
1122
			'status' => $status,
1123
			'value' => $provided['version']
1124
		];
1125
	}
1126
1127
	/**
1128
	 * Returns an array of parsed strings for a dependency in the
1129
	 * format: array(
1130
	 *    'type'            =>    requires, conflicts, or provides.
1131
	 *    'name'            =>    The name of the requirement / conflict
1132
	 *    'value'            =>    A string representing the expected value: <1, >=3, !=enabled
1133
	 *    'local_value'    =>    The current value, ("Not installed")
1134
	 *    'comment'        =>    Free form text to help resovle the problem ("Enable / Search for plugin <link>")
1135
	 * )
1136
	 *
1137
	 * @param array $dep An \ElggPluginPackage dependency array
1138
	 *
1139
	 * @return false|array
1140
	 */
1141
	public function getDependencyStrings($dep) {
1142
		$translator = $this->translator;
1143
		$dep_system = elgg_extract('type', $dep);
1144
		$info = elgg_extract('dep', $dep);
1145
		$type = elgg_extract('type', $info);
1146
1147
		if (!$dep_system || !$info || !$type) {
1148
			return false;
1149
		}
1150
1151
		// rewrite some of these to be more readable
1152
		$comparison = elgg_extract('comparison', $info);
1153
		switch ($comparison) {
1154
			case 'lt':
1155
				$comparison = '<';
1156
				break;
1157
			case 'gt':
1158
				$comparison = '>';
1159
				break;
1160
			case 'ge':
1161
				$comparison = '>=';
1162
				break;
1163
			case 'le':
1164
				$comparison = '<=';
1165
				break;
1166
			default:
1167
				//keep $comparison value intact
1168
				break;
1169
		}
1170
1171
		/*
1172
		'requires'	'plugin oauth_lib'	<1.3	1.3		'downgrade'
1173
		'requires'	'php setting bob'	>3		3		'change it'
1174
		'conflicts'	'php setting'		>3		4		'change it'
1175
		'conflicted''plugin profile'	any		1.8		'disable profile'
1176
		'provides'	'plugin oauth_lib'	1.3		--		--
1177
		'priority'	'before blog'		--		after	'move it'
1178
		*/
1179
		$strings = [];
1180
		$strings['type'] = $translator->translate('ElggPlugin:Dependencies:' . ucwords($dep_system));
1181
1182
		switch ($type) {
1183
			case 'elgg_release':
1184
				// 'Elgg Version'
1185
				$strings['name'] = $translator->translate('ElggPlugin:Dependencies:Elgg');
1186
				$strings['expected_value'] = "$comparison {$info['version']}";
1187
				$strings['local_value'] = $dep['value'];
1188
				$strings['comment'] = '';
1189
				break;
1190
1191
			case 'php_version':
1192
				// 'PHP version'
1193
				$strings['name'] = $translator->translate('ElggPlugin:Dependencies:PhpVersion');
1194
				$strings['expected_value'] = "$comparison {$info['version']}";
1195
				$strings['local_value'] = $dep['value'];
1196
				$strings['comment'] = '';
1197
				break;
1198
1199
			case 'php_extension':
1200
				// PHP Extension %s [version]
1201
				$strings['name'] = $translator->translate('ElggPlugin:Dependencies:PhpExtension', [$info['name']]);
1202
				if ($info['version']) {
1203
					$strings['expected_value'] = "$comparison {$info['version']}";
1204
					$strings['local_value'] = $dep['value'];
1205
				} else {
1206
					$strings['expected_value'] = '';
1207
					$strings['local_value'] = '';
1208
				}
1209
				$strings['comment'] = '';
1210
				break;
1211
1212
			case 'php_ini':
1213
				$strings['name'] = $translator->translate('ElggPlugin:Dependencies:PhpIni', [$info['name']]);
1214
				$strings['expected_value'] = "$comparison {$info['value']}";
1215
				$strings['local_value'] = $dep['value'];
1216
				$strings['comment'] = '';
1217
				break;
1218
1219
			case 'plugin':
1220
				$strings['name'] = $translator->translate('ElggPlugin:Dependencies:Plugin', [$info['name']]);
1221
				$expected = $info['version'] ? "$comparison {$info['version']}" : $translator->translate('any');
1222
				$strings['expected_value'] = $expected;
1223
				$strings['local_value'] = $dep['value'] ? $dep['value'] : '--';
1224
				$strings['comment'] = '';
1225
				break;
1226
1227
			case 'priority':
1228
				$expected_priority = ucwords($info['priority']);
1229
				$real_priority = ucwords($dep['value']);
1230
				$strings['name'] = $translator->translate('ElggPlugin:Dependencies:Priority');
1231
				$strings['expected_value'] = $translator->translate("ElggPlugin:Dependencies:Priority:$expected_priority", [$info['plugin']]);
1232
				$strings['local_value'] = $translator->translate("ElggPlugin:Dependencies:Priority:$real_priority", [$info['plugin']]);
1233
				$strings['comment'] = '';
1234
				break;
1235
		}
1236
1237
		if ($dep['type'] == 'suggests') {
1238
			if ($dep['status']) {
1239
				$strings['comment'] = $translator->translate('ok');
1240
			} else {
1241
				$strings['comment'] = $translator->translate('ElggPlugin:Dependencies:Suggests:Unsatisfied');
1242
			}
1243
		} else {
1244
			if ($dep['status']) {
1245
				$strings['comment'] = $translator->translate('ok');
1246
			} else {
1247
				$strings['comment'] = $translator->translate('error');
1248
			}
1249
		}
1250
1251
		return $strings;
1252
	}
1253
1254
	/**
1255
	 * Get all settings (excluding user settings) for a plugin
1256
	 *
1257
	 * @param \ElggPlugin $plugin Plugin
1258
	 *
1259
	 * @return string[]
1260
	 * @throws DatabaseException
1261
	 */
1262
	public function getAllSettings(ElggPlugin $plugin) {
1263
		if (!$plugin->guid) {
1264
			return [];
1265
		}
1266
1267
		$values = $this->private_settings_cache->load($plugin->guid);
1268
		if (isset($values)) {
1269
			return $values;
1270
		}
1271
1272
		$us_prefix = $this->namespacePrivateSetting('user_setting', '', $plugin->getID());
1273
1274
		// Get private settings for user
1275
		$qb = Select::fromTable('private_settings');
1276
		$qb->select('name')
1277
			->addSelect('value')
1278
			->where($qb->compare('name', 'not like', "$us_prefix%", ELGG_VALUE_STRING))
1279
			->andWhere($qb->compare('entity_guid', '=', $plugin->guid, ELGG_VALUE_GUID));
1280
1281
		$rows = $this->db->getData($qb);
1282
1283
		$settings = [];
1284
1285
		if (!empty($rows)) {
1286
			foreach ($rows as $row) {
1287
				$settings[$row->name] = $row->value;
1288
			}
1289
		}
1290
1291
		$this->private_settings_cache->save($plugin->guid, $settings);
1292
1293
		return $settings;
1294
	}
1295
1296
	/**
1297
	 * Returns an array of all plugin user settings for a user
1298
	 *
1299
	 * @param ElggPlugin $plugin Plugin
1300
	 * @param ElggUser   $user   User
1301
	 *
1302
	 * @return array
1303
	 * @see  ElggPlugin::getAllUserSettings()
1304
	 * @throws DatabaseException
1305
	 */
1306
	public function getAllUserSettings(ElggPlugin $plugin, ElggUser $user = null) {
1307
1308
		// send an empty name so we just get the first part of the namespace
1309
		$prefix = $this->namespacePrivateSetting('user_setting', '', $plugin->getID());
1310
1311
		$qb = Select::fromTable('private_settings');
1312
		$qb->select('name')
1313
			->addSelect('value')
1314
			->where($qb->compare('name', 'like', "{$prefix}%"));
1315
1316
		if ($user) {
1317
			$qb->andWhere($qb->compare('entity_guid', '=', $user->guid, ELGG_VALUE_INTEGER));
1318
		}
1319
1320
		$rows = $this->db->getData($qb);
1321
1322
		$settings = [];
1323
1324
		if (!empty($rows)) {
1325
			foreach ($rows as $rows) {
1326
				$name = substr($rows->name, strlen($prefix));
1327
				$value = $rows->value;
1328
1329
				$settings[$name] = $value;
1330
			}
1331
		}
1332
1333
		return $settings;
1334
	}
1335
1336
	/**
1337
	 * Returns entities based upon plugin user settings.
1338
	 * Takes all the options for {@link elgg_get_entities_from_private_settings()}
1339
	 * in addition to the ones below.
1340
	 *
1341
	 * @param array $options Array in the format:
1342
	 *
1343
	 *    plugin_id => STR The plugin id. Required.
1344
	 *
1345
	 *    plugin_user_setting_names => null|ARR private setting names
1346
	 *
1347
	 *    plugin_user_setting_values => null|ARR metadata values
1348
	 *
1349
	 *    plugin_user_setting_name_value_pairs => null|ARR (
1350
	 *                                         name => 'name',
1351
	 *                                         value => 'value',
1352
	 *                                         'operand' => '=',
1353
	 *                                        )
1354
	 *                                 Currently if multiple values are sent via
1355
	 *                               an array (value => array('value1', 'value2')
1356
	 *                               the pair's operand will be forced to "IN".
1357
	 *
1358
	 *    plugin_user_setting_name_value_pairs_operator => null|STR The operator to use for combining
1359
	 *                                        (name = value) OPERATOR (name = value); default AND
1360
	 *
1361
	 * @return mixed int If count, int. If not count, array. false on errors.
1362
	 */
1363
	public function getEntitiesFromUserSettings(array $options = []) {
1364
		$singulars = [
1365
			'plugin_user_setting_name',
1366
			'plugin_user_setting_value',
1367
			'plugin_user_setting_name_value_pair'
1368
		];
1369
1370
		$options = LegacyQueryOptionsAdapter::normalizePluralOptions($options, $singulars);
1371
1372
		// rewrite plugin_user_setting_name_* to the right PS ones.
1373
		$map = [
1374
			'plugin_user_setting_names' => 'private_setting_names',
1375
			'plugin_user_setting_values' => 'private_setting_values',
1376
			'plugin_user_setting_name_value_pairs' => 'private_setting_name_value_pairs',
1377
			'plugin_user_setting_name_value_pairs_operator' => 'private_setting_name_value_pairs_operator',
1378
		];
1379
1380
		foreach ($map as $plugin => $private) {
1381
			if (!isset($options[$plugin])) {
1382
				continue;
1383
			}
1384
1385
			if (isset($options[$private])) {
1386
				if (!is_array($options[$private])) {
1387
					$options[$private] = [$options[$private]];
1388
				}
1389
1390
				$options[$private] = array_merge($options[$private], $options[$plugin]);
1391
			} else {
1392
				$options[$private] = $options[$plugin];
1393
			}
1394
		}
1395
1396
		$prefix = $this->namespacePrivateSetting('user_setting', '', $options['plugin_id']);
1397
		$options['private_setting_name_prefix'] = $prefix;
1398
1399
		return elgg_get_entities($options);
1400
	}
1401
1402
	/**
1403
	 * Set plugin priority and adjust the priorities of other plugins
1404
	 *
1405
	 * @param ElggPlugin $plugin   Plugin
1406
	 * @param int        $priority New priority
1407
	 *
1408
	 * @return int|false
1409
	 * @throws DatabaseException
1410
	 */
1411
	public function setPriority(ElggPlugin $plugin, $priority) {
1412
1413
		$old_priority = $plugin->getPriority() ? : 1;
1414
1415
		$name = $this->namespacePrivateSetting('internal', 'priority');
1416
1417
		if (!$plugin->setPrivateSetting($name, $priority)) {
1418
			return false;
1419
		}
1420
1421
		if (!$plugin->guid) {
1422
			return false;
1423
		}
1424
1425
		$qb = Update::table('private_settings');
1426
		$qb->where($qb->compare('name', '=', $name, ELGG_VALUE_STRING))
1427
			->andWhere($qb->compare('entity_guid', '!=', $plugin->guid, ELGG_VALUE_INTEGER));
1428
1429
		if ($priority > $old_priority) {
1430
			$qb->set('value', "CAST(value AS UNSIGNED) - 1");
1431
			$qb->andWhere($qb->between('CAST(value AS UNSIGNED)', $old_priority, $priority, ELGG_VALUE_INTEGER));
1432
		} else {
1433
			$qb->set('value', "CAST(value AS UNSIGNED) + 1");
1434
			$qb->andWhere($qb->between('CAST(value AS UNSIGNED)', $priority, $old_priority, ELGG_VALUE_INTEGER));
1435
		}
1436
1437
		if (!$this->db->updateData($qb)) {
1438
			return false;
1439
		}
1440
1441
		return $priority;
1442
	}
1443
}
1444