Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

UpgradeService   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 277
Duplicated Lines 0 %

Test Coverage

Coverage 7.29%

Importance

Changes 0
Metric Value
dl 0
loc 277
ccs 7
cts 96
cp 0.0729
rs 10
c 0
b 0
f 0
wmc 30

9 Methods

Rating   Name   Duplication   Size   Complexity  
A setProcessedUpgrade() 0 5 1
A __construct() 0 11 1
A getUnprocessedUpgrades() 0 14 4
A getProcessedUpgrades() 0 2 1
B run() 0 30 3
A processUpgrades() 0 10 2
B getUpgradeFiles() 0 28 6
C upgradeCode() 0 61 10
A getUpgradeFileVersion() 0 8 2
1
<?php
2
namespace Elgg;
3
use Elgg\Filesystem\Directory\Local;
4
use Phinx\Console\PhinxApplication;
5
use Phinx\Wrapper\TextWrapper;
6
7
/**
8
 * Upgrade service for Elgg
9
 *
10
 * @access private
11
 */
12
class UpgradeService {
13
14
	/**
15
	 * @var \Elgg\i18n\Translator
16
	 */
17
	private $translator;
18
19
	/**
20
	 * @var \Elgg\EventsService
21
	 */
22
	private $events;
0 ignored issues
show
introduced by
The private property $events is not used, and could be removed.
Loading history...
23
24
	/**
25
	 * @var \Elgg\PluginHooksService
26
	 */
27
	private $hooks;
28
29
	/**
30
	 * @var \Elgg\Config
31
	 */
32
	private $config;
33
34
	/**
35
	 * @var \Elgg\Logger
36
	 */
37
	private $logger;
38
39
	/**
40
	 * @var \Elgg\Database\Mutex
41
	 */
42
	private $mutex;
43
44
	/**
45
	 * Constructor
46
	 *
47
	 * @param \Elgg\i18n\Translator    $translator Translation service
48
	 * @param \Elgg\PluginHooksService $hooks      Plugin hook service
49
	 * @param \Elgg\Config             $config     Config
50
	 * @param \Elgg\Logger             $logger     Logger
51
	 * @param \Elgg\Database\Mutex     $mutex      Database mutex service
52
	 */
53 1
	public function __construct(
54
			\Elgg\i18n\Translator $translator,
55
			\Elgg\PluginHooksService $hooks,
56
			\Elgg\Config $config,
57
			\Elgg\Logger $logger,
58
			\Elgg\Database\Mutex $mutex) {
59 1
		$this->translator = $translator;
60 1
		$this->hooks = $hooks;
61 1
		$this->config = $config;
62 1
		$this->logger = $logger;
63 1
		$this->mutex = $mutex;
64 1
	}
65
66
	/**
67
	 * Run the upgrade process
68
	 *
69
	 * @return array $result Associative array containing possible errors
70
	 */
71
	public function run() {
72
		$result = [
73
			'failure' => false,
74
			'reason' => '',
75
		];
76
77
		// prevent someone from running the upgrade script in parallel (see #4643)
78
		if (!$this->mutex->lock('upgrade')) {
79
			$result['failure'] = true;
80
			$result['reason'] = $this->translator->translate('upgrade:locked');
81
			return $result;
82
		}
83
84
		// disable the system log for upgrades to avoid exceptions when the schema changes.
85
		$this->hooks->getEvents()->unregisterHandler('log', 'systemlog', 'system_log_default_logger');
86
		$this->hooks->getEvents()->unregisterHandler('all', 'all', 'system_log_listener');
87
88
		// turn off time limit
89
		set_time_limit(0);
90
91
		if ($this->getUnprocessedUpgrades()) {
92
			$this->processUpgrades();
93
		}
94
95
		$this->hooks->getEvents()->trigger('upgrade', 'system', null);
96
		elgg_flush_caches();
97
98
		$this->mutex->unlock('upgrade');
99
100
		return $result;
101
	}
102
103
	/**
104
	 * Run any php upgrade scripts which are required
105
	 *
106
	 * @param int  $version Version upgrading from.
107
	 * @param bool $quiet   Suppress errors.  Don't use this.
108
	 *
109
	 * @return bool
110
	 */
111
	protected function upgradeCode($version, $quiet = false) {
112
		$version = (int) $version;
113
		$upgrade_path = elgg_get_engine_path() . '/lib/upgrades/';
114
		$processed_upgrades = $this->getProcessedUpgrades();
115
116
		$upgrade_files = $this->getUpgradeFiles($upgrade_path);
117
118
		if ($upgrade_files === false) {
119
			return false;
120
		}
121
122
		$upgrades = $this->getUnprocessedUpgrades($upgrade_files, $processed_upgrades);
123
124
		// Sort and execute
125
		sort($upgrades);
126
127
		foreach ($upgrades as $upgrade) {
128
			$upgrade_version = $this->getUpgradeFileVersion($upgrade);
129
			$success = true;
130
131
			if ($upgrade_version <= $version) {
132
				// skip upgrade files from before the installation version of Elgg
133
				// because the upgrade files from before the installation version aren't
134
				// added to the database.
135
				continue;
136
			}
137
138
			// hide all errors.
139
			if ($quiet) {
140
				// hide include errors as well as any exceptions that might happen
141
				try {
142
					if (!@Includer::includeFile("$upgrade_path/$upgrade")) {
143
						$success = false;
144
						$this->logger->error("Could not include $upgrade_path/$upgrade");
145
					}
146
				} catch (\Exception $e) {
147
					$success = false;
148
					$this->logger->error($e->getMessage());
149
				}
150
			} else {
151
				if (!Includer::includeFile("$upgrade_path/$upgrade")) {
152
					$success = false;
153
					$this->logger->error("Could not include $upgrade_path/$upgrade");
154
				}
155
			}
156
157
			if ($success) {
158
				// don't set the version to a lower number in instances where an upgrade
159
				// has been merged from a lower version of Elgg
160
				if ($upgrade_version > $version) {
161
					$this->config->save('version', $upgrade_version);
162
				}
163
164
				// incrementally set upgrade so we know where to start if something fails.
165
				$this->setProcessedUpgrade($upgrade);
166
			} else {
167
				return false;
168
			}
169
		}
170
171
		return true;
172
	}
173
174
	/**
175
	 * Saves a processed upgrade to a dataset.
176
	 *
177
	 * @param string $upgrade Filename of the processed upgrade
178
	 *                        (not the path, just the file)
179
	 * @return bool
180
	 */
181
	protected function setProcessedUpgrade($upgrade) {
182
		$processed_upgrades = $this->getProcessedUpgrades();
183
		$processed_upgrades[] = $upgrade;
184
		$processed_upgrades = array_unique($processed_upgrades);
185
		return $this->config->save('processed_upgrades', $processed_upgrades);
186
	}
187
188
	/**
189
	 * Gets a list of processes upgrades
190
	 *
191
	 * @return mixed Array of processed upgrade filenames or false
192
	 */
193
	protected function getProcessedUpgrades() {
194
		return $this->config->processed_upgrades;
195
	}
196
197
	/**
198
	 * Returns the version of the upgrade filename.
199
	 *
200
	 * @param string $filename The upgrade filename. No full path.
201
	 * @return int|false
202
	 * @since 1.8.0
203
	 */
204
	protected function getUpgradeFileVersion($filename) {
205
		preg_match('/^([0-9]{10})([\.a-z0-9-_]+)?\.(php)$/i', $filename, $matches);
206
207
		if (isset($matches[1])) {
208
			return (int) $matches[1];
209
		}
210
211
		return false;
212
	}
213
214
	/**
215
	 * Returns a list of upgrade files relative to the $upgrade_path dir.
216
	 *
217
	 * @param string $upgrade_path The up
218
	 * @return array|false
219
	 */
220
	protected function getUpgradeFiles($upgrade_path = null) {
221
		if (!$upgrade_path) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $upgrade_path of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
222
			$upgrade_path = elgg_get_engine_path() . '/lib/upgrades/';
223
		}
224
		$upgrade_path = \Elgg\Project\Paths::sanitize($upgrade_path);
225
		$handle = opendir($upgrade_path);
226
227
		if (!$handle) {
228
			return false;
229
		}
230
231
		$upgrade_files = [];
232
233
		while ($upgrade_file = readdir($handle)) {
234
			// make sure this is a wellformed upgrade.
235
			if (is_dir($upgrade_path . '$upgrade_file')) {
236
				continue;
237
			}
238
			$upgrade_version = $this->getUpgradeFileVersion($upgrade_file);
239
			if (!$upgrade_version) {
240
				continue;
241
			}
242
			$upgrade_files[] = $upgrade_file;
243
		}
244
245
		sort($upgrade_files);
246
247
		return $upgrade_files;
248
	}
249
250
	/**
251
	 * Checks if any upgrades need to be run.
252
	 *
253
	 * @param null|array $upgrade_files      Optional upgrade files
254
	 * @param null|array $processed_upgrades Optional processed upgrades
255
	 *
256
	 * @return array
257
	 */
258
	protected function getUnprocessedUpgrades($upgrade_files = null, $processed_upgrades = null) {
259
		if ($upgrade_files === null) {
260
			$upgrade_files = $this->getUpgradeFiles();
261
		}
262
263
		if ($processed_upgrades === null) {
264
			$processed_upgrades = $this->config->processed_upgrades;
265
			if (!is_array($processed_upgrades)) {
266
				$processed_upgrades = [];
267
			}
268
		}
269
270
		$unprocessed = array_diff($upgrade_files, $processed_upgrades);
0 ignored issues
show
Bug introduced by
It seems like $upgrade_files can also be of type false; however, parameter $array1 of array_diff() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

270
		$unprocessed = array_diff(/** @scrutinizer ignore-type */ $upgrade_files, $processed_upgrades);
Loading history...
271
		return $unprocessed;
272
	}
273
274
	/**
275
	 * Upgrades Elgg Database and code
276
	 *
277
	 * @return bool
278
	 */
279
	protected function processUpgrades() {
280
		$dbversion = (int) $this->config->version;
281
282
		if ($this->upgradeCode($dbversion)) {
283
			system_message($this->translator->translate('upgrade:core'));
284
			
285
			return true;
286
		}
287
288
		return false;
289
	}
290
291
}
292