Completed
Pull Request — development (#2329)
by Joshua
09:15
created

Hooks::include_hook()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 8.125
Metric Value
dl 0
loc 20
ccs 7
cts 14
cp 0.5
rs 8.8571
cc 5
eloc 10
nc 8
nop 1
crap 8.125
1
<?php
2
3
/**
4
 * This file has all the main functions in it that relate to adding
5
 * removing, etc on hooks.
6
 *
7
 * @name      ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
10
 *
11
 * This software is a derived product, based on:
12
 *
13
 * Simple Machines Forum (SMF)
14
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
15
 * license:		BSD, See included LICENSE.TXT for terms and conditions.
16
 *
17
 * @version 1.1 dev Release Candidate 1
18
 *
19
 */
20
21
if (!defined('ELK'))
22
	die('No access...');
23
24
/**
25
 * Class Hooks
26
 */
27
class Hooks
28
{
29
	/**
30
	 * The instance of the class
31
	 * @var object
32
	 */
33
	private static $_instance = null;
34
35
	/**
36
	 * Holds our standard path replacement array
37
	 * @var array
38
	 */
39
	protected $_path_replacements = array();
40
41
	/**
42
	 * Holds the database instance
43
	 * @var null|database
44
	 */
45
	protected $_db = null;
46
47
	/**
48
	 * If holds instance of debug class
49
	 * @var object|null
50
	 */
51
	protected $_debug = null;
52
53
	/**
54
	 * The class constructor, loads globals in to the class object
55
	 *
56
	 * @param Database $db
57
	 * @param Debug $debug
58
	 * @param string[]|string|null $paths - additional paths to add to the replacement array
59
	 */
60
	private function __construct($db, $debug, $paths = null)
61
	{
62
		$this->_path_replacements = array(
63
			'BOARDDIR' => BOARDDIR,
64
			'SOURCEDIR' => SOURCEDIR,
65
			'EXTDIR' => EXTDIR,
66
			'LANGUAGEDIR' => LANGUAGEDIR,
67
			'ADMINDIR' => ADMINDIR,
68
			'CONTROLLERDIR' => CONTROLLERDIR,
69
			'SUBSDIR' => SUBSDIR,
70
		);
71
		$this->_db = $db;
72
		$this->_debug = $debug;
73
74
		if ($paths !== null)
75
			$this->newPath($paths);
76
	}
77
78
	/**
79
	 * Allows to set a new replacement path.
80
	 *
81
	 * @param string[]|string $path an array consisting of pairs "search" => "replace with"
82
	 */
83 1
	public function newPath($path)
84
	{
85 1
		$this->_path_replacements = array_merge($this->_path_replacements, (array) $path);
86 1
	}
87
88
	/**
89
	 * Process functions of an integration hook.
90
	 *
91
	 * What it does:
92
	 * - calls all functions of the given hook.
93
	 * - supports static class method calls.
94
	 *
95
	 * @param string $hook
96
	 * @param mixed[] $parameters = array()
97
	 * @return mixed[] the results of the functions
98
	 */
99 19
	public function hook($hook, $parameters = array())
100
	{
101 19
		global $modSettings;
102
103 19
		if ($this->_debug !== null)
104 19
			$this->_debug->add('hooks', $hook);
105
106 19
		$results = array();
107 19
		if (empty($modSettings[$hook]))
108 19
			return $results;
109
110
		// Loop through each function.
111 1
		$functions = $this->_prepare_hooks($modSettings[$hook]);
112 1
		foreach ($functions as $function => $call)
113 1
			$results[$function] = call_user_func_array($call, $parameters);
114
115 1
		return $results;
116
	}
117
118
	/**
119
	 * Splits up strings from $modSettings into functions and files to include.
120
	 *
121
	 * @param string $hook_calls
122
	 */
123 1
	protected function _prepare_hooks($hook_calls)
124
	{
125
		// Loop through each function.
126 1
		$functions = explode(',', $hook_calls);
127 1
		$returns = array();
128
129 1
		foreach ($functions as $function)
130
		{
131 1
			$function = trim($function);
132
133 1
			if (strpos($function, '|') !== false)
134 1
				list ($call, $file) = explode('|', $function);
135
			else
136 1
				$call = $function;
137
138
			// OOP static method
139 1
			if (strpos($call, '::') !== false)
140 1
				$call = explode('::', $call);
141
142 1
			if (!empty($file))
143 1
			{
144 1
				$absPath = strtr(trim($file), $this->_path_replacements);
145
146 1
				if (file_exists($absPath))
147 1
					require_once($absPath);
148 1
			}
149
150
			// Is it valid?
151 1
			if (is_callable($call))
152 1
				$returns[$function] = $call;
153 1
		}
154
155 1
		return $returns;
156
	}
157
158
	/**
159
	 * Includes files for hooks that only do that (i.e. integrate_pre_include)
160
	 *
161
	 * @param string $hook
162
	 */
163 1
	public function include_hook($hook)
164
	{
165 1
		global $modSettings;
166
167 1
		if ($this->_debug !== null)
168 1
			$this->_debug->add('hooks', $hook);
169
170
		// Any file to include?
171 1
		if (!empty($modSettings[$hook]))
172 1
		{
173
			$pre_includes = explode(',', $modSettings[$hook]);
174
			foreach ($pre_includes as $include)
175
			{
176
				$include = strtr(trim($include), $this->_path_replacements);
177
178
				if (file_exists($include))
179
					require_once($include);
180
			}
181
		}
182 1
	}
183
184
	/**
185
	 * Special hook call executed during obExit
186
	 */
187
	public function buffer_hook()
188
	{
189
		global $modSettings;
190
191
		if ($this->_debug !== null)
192
			$this->_debug->add('hooks', 'integrate_buffer');
193
194
		if (empty($modSettings['integrate_buffer']))
195
			return;
196
197
		$buffers = $this->_prepare_hooks($modSettings['integrate_buffer']);
198
199
		foreach ($buffers as $call)
200
			ob_start($call);
201
	}
202
203
	/**
204
	 * Add a function for integration hook.
205
	 *
206
	 * - does nothing if the function is already added.
207
	 *
208
	 * @param string $hook
209
	 * @param string $function
210
	 * @param string $file
211
	 * @param bool $permanent = true if true, updates the value in settings table
212
	 */
213 2
	public function add($hook, $function, $file = '', $permanent = true)
214
	{
215 2
		global $modSettings;
216
217 2
		$integration_call = (!empty($file) && $file !== true) ? $function . '|' . $file : $function;
218
219
		// Is it going to be permanent?
220
		if ($permanent)
221 2
			$this->_store($hook, $integration_call);
222
223
		// Make current function list usable.
224 2
		$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
225
226
		// Do nothing, if it's already there.
227 2
		if (in_array($integration_call, $functions))
228 2
			return;
229
230 2
		$functions[] = $integration_call;
231 2
		$modSettings[$hook] = implode(',', $functions);
232 2
	}
233
234
	/**
235
	 * Automatically loads all the integrations enabled and that can be found.
236
	 */
237
	public function loadIntegrations()
238
	{
239
		$enabled = $this->_get_enabled_integrations();
240
241
		foreach ($enabled as $class)
242
		{
243
			if (is_callable(array($class, 'register')))
244
			{
245
				$hooks = $class::register();
246
247
				if (empty($hooks))
248
					continue;
249
250
				foreach ($hooks as $hook)
251
					$this->add($hook[0], $hook[1], isset($hook[2]) ? $hook[2] : '', false);
252
			}
253
		}
254
	}
255
256
	/**
257
	 * @todo
258
	 */
259
	public function loadIntegrationsSettings()
260
	{
261
		$enabled = $this->_get_enabled_integrations();
262
263
		foreach ($enabled as $class)
264
		{
265
			if (is_callable(array($class, 'settingsRegister')))
266
			{
267
				$hooks = $class::settingsRegister();
268
269
				if (empty($hooks))
270
					continue;
271
272
				foreach ($hooks as $hook)
273
					$this->add($hook[0], $hook[1], isset($hook[2]) ? $hook[2] : '', false);
274
			}
275
		}
276
	}
277
278
	/**
279
	 * Find all integration files (default is *.integrate.php) in the supplied directory
280
	 *
281
	 * @param string $basepath
282
	 * @param string $ext
283
	 *
284
	 */
285
	public function discoverIntegrations($basepath, $ext = '.integrate.php')
286
	{
287
		$path = $basepath . '/*/*' . $ext;
288
		$names = array();
289
290
		$glob = new GlobIterator($path, FilesystemIterator::SKIP_DOTS);
291
292
		// Find all integration files
293
		foreach ($glob as $file)
294
		{
295
			$name = str_replace($ext, '', $file->getBasename());
296
			$composer_file = $file->getPath() . '/composer.json';
297
298
			// Already have the integration compose file, then use it, otherwise create one
299
			if (file_exists($composer_file))
300
				$composer_data = json_decode(file_get_contents($composer_file));
301
			else
302
				$composer_data = json_decode('{
303
    "name": "' . $name . '",
304
    "description": "' . $name . '",
305
    "version": "1.0.0",
306
    "type": "addon",
307
    "homepage": "http://www.elkarte.net",
308
    "time": "",
309
    "license": "",
310
    "authors": [
311
        {
312
            "name": "Unknown",
313
            "email": "notprovided",
314
            "homepage": "http://www.elkarte.net",
315
            "role": "Developer"
316
        }
317
    ],
318
    "support": {
319
        "email": "",
320
        "issues": "http://www.elkarte.net/community",
321
        "forum": "http://www.elkarte.net/community",
322
        "wiki": "",
323
        "irc": "",
324
        "source": ""
325
    },
326
    "require": {
327
        "elkarte/elkarte": "' . substr(FORUM_VERSION, 0, -5) . '"
328
    },
329
    "repositories": [
330
        {
331
            "type": "composer",
332
            "url": "http://packages.example.com"
333
        }
334
    ],
335
    "extra": {
336
        "setting_url": ""
337
    }
338
}');
339
340
			$names[] = array(
341
				'id' => $name,
342
				'class' => str_replace('.integrate.php', '_Integrate', $file->getBasename()),
343
				'title' => $composer_data->name,
344
				'description' => $composer_data->description,
345
				'path' => str_replace($basepath, '', $file->getPathname()),
346
				'details' => $composer_data,
347
			);
348
		}
349
350
		return $names;
351
	}
352
353
	/**
354
	 * Enables the autoloading of a certain addon.
355
	 *
356
	 * @param string $call A string consisting of "path/filename.integrate.php"
357
	 */
358
	public function enableIntegration($call)
359
	{
360
		$existing = $this->_get_enabled_integrations();
361
362
		$existing[] = $call;
363
364
		$this->_store_autoload_integrate($existing);
365
	}
366
367
	/**
368
	 * Disables the autoloading of a certain addon.
369
	 *
370
	 * @param string $call A string consisting of "path/filename.integrate.php"
371
	 */
372
	public function disableIntegration($call)
373
	{
374
		$existing = $this->_get_enabled_integrations();
375
376
		$existing = array_diff($existing, (array) $call);
377
378
		$this->_store_autoload_integrate($existing);
379
	}
380
381
	/**
382
	 * Retrieves from the database a set of references to files containing addons.
383
	 *
384
	 * @return string[] An array of strings consisting of "path/filename.integrate.php"
385
	 */
386
	protected function _get_enabled_integrations()
387
	{
388
		global $modSettings;
389
390
		if (!empty($modSettings['autoload_integrate']))
391
			$existing = explode(',', $modSettings['autoload_integrate']);
392
		else
393
			$existing = array();
394
395
		return $existing;
396
	}
397
398
	/**
399
	 * Saves into the database a set of references to files containing addons.
400
	 *
401
	 * @param string[] $existing An array of strings consisting of "path/filename.integrate.php"
402
	 */
403
	protected function _store_autoload_integrate($existing)
404
	{
405
		$existing = array_filter(array_unique($existing));
406
		updateSettings(array('autoload_integrate' => implode(',', $existing)));
407
	}
408
409
	/**
410
	 * Stores a function into the database.
411
	 *
412
	 * - does nothing if the function is already added.
413
	 *
414
	 * @param string $hook
415
	 * @param string $integration_call
416
	 */
417 1
	protected function _store($hook, $integration_call)
418
	{
419 1
		$request = $this->_db->query('', '
420
			SELECT value
421
			FROM {db_prefix}settings
422 1
			WHERE variable = {string:variable}',
423
			array(
424 1
				'variable' => $hook,
425
			)
426 1
		);
427 1
		list ($current_functions) = $this->_db->fetch_row($request);
428 1
		$this->_db->free_result($request);
429
430 1
		if (!empty($current_functions))
431 1
		{
432 1
			$current_functions = explode(',', $current_functions);
433 1
			if (in_array($integration_call, $current_functions))
434 1
				return;
435
436 1
			$permanent_functions = array_merge($current_functions, array($integration_call));
437 1
		}
438
		else
439 1
			$permanent_functions = array($integration_call);
440
441 1
		updateSettings(array($hook => implode(',', $permanent_functions)));
442 1
	}
443
444
	/**
445
	 * Remove an integration hook function.
446
	 *
447
	 * What it does:
448
	 * - Removes the given function from the given hook.
449
	 * - Does nothing if the function is not available.
450
	 *
451
	 * @param string $hook
452
	 * @param string $function
453
	 * @param string $file
454
	 */
455 1
	public function remove($hook, $function, $file = '')
456
	{
457 1
		global $modSettings;
458
459 1
		$integration_call = (!empty($file) && $file !== true) ? $function . '|' . $file : $function;
460
461
		// Get the permanent functions.
462 1
		$request = $this->_db->query('', '
463
			SELECT value
464
			FROM {db_prefix}settings
465 1
			WHERE variable = {string:variable}',
466
			array(
467 1
				'variable' => $hook,
468
			)
469 1
		);
470 1
		list ($current_functions) = $this->_db->fetch_row($request);
471 1
		$this->_db->free_result($request);
472
473
		// If we found entries for this hook
474 1
		if (!empty($current_functions))
475 1
		{
476 1
			$current_functions = explode(',', $current_functions);
477
478 1
			if (in_array($integration_call, $current_functions))
479 1
			{
480 1
				updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
481 1
				if (empty($modSettings[$hook]))
482 1
					removeSettings($hook);
483 1
			}
484 1
		}
485
486
		// Turn the function list into something usable.
487 1
		$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
488
489
		// You can only remove it if it's available.
490 1
		if (!in_array($integration_call, $functions))
491 1
			return;
492
493
		$functions = array_diff($functions, array($integration_call));
494
		$modSettings[$hook] = implode(',', $functions);
495
	}
496
497
	/**
498
	 * Instantiation is a bit more complex, so let's give it a custom function
499
	 *
500
	 * @param Database|null $db A database connection
501
	 * @param Debug|null $debug A class for debugging
502
	 * @param string[]|null $paths An array of paths for replacement
503
	 */
504
	public static function init($db = null, $debug = null, $paths = null)
505
	{
506
		if ($db === null)
507
			$db = database();
508
509
		if ($debug === null)
510
			$debug = Debug::get();
511
512
		self::$_instance = new Hooks($db, $debug, $paths);
513
	}
514
515
	/**
516
	 * Being a singleton, this is the static method to retrieve the instance of the class
517
	 *
518
	 * @param Database|null $db A database connection
519
	 * @param Debug|null $debug A class for debugging
520
	 * @param string[]|null $paths An array of paths for replacement
521
	 * @return Hooks An instance of the class.
522
	 */
523 22
	public static function get($db = null, $debug = null, $paths = null)
524
	{
525 22
		if (self::$_instance === null)
526 22
			Hooks::init($db, $debug, $paths);
527 22
		elseif ($paths !== null)
528
			self::$_instance->newPaths($paths);
529
530 22
		return self::$_instance;
531
	}
532
}