Completed
Pull Request — master (#858)
by Mateusz
05:36 queued 01:54
created

CapistranoDeploymentBackend::disableMaintenance()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 9

Duplication

Lines 12
Ratio 100 %

Importance

Changes 0
Metric Value
dl 12
loc 12
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 9
nc 2
nop 3
1
<?php
2
3
class CapistranoDeploymentBackend extends Object implements DeploymentBackend {
4
5
	protected $packageGenerator;
6
7
	public function getPackageGenerator() {
8
		return $this->packageGenerator;
9
	}
10
11
	public function setPackageGenerator(PackageGenerator $packageGenerator) {
12
		$this->packageGenerator = $packageGenerator;
13
	}
14
15
	/**
16
	 * Create a deployment strategy.
17
	 *
18
	 * @param \DNEnvironment $environment
19
	 * @param array $options
20
	 *
21
	 * @return DeploymentStrategy
22
	 */
23
	public function planDeploy(\DNEnvironment $environment, $options) {
24
		$strategy = new DeploymentStrategy($environment, $options);
25
26
		$currentBuild = $environment->CurrentBuild();
27
		$currentSha = $currentBuild ? $currentBuild->SHA : '-';
28
		if($currentSha !== $options['sha']) {
29
			$strategy->setChange('Code version', $currentSha, $options['sha']);
30
		}
31
		$strategy->setActionTitle('Confirm deployment');
32
		$strategy->setActionCode('fast');
33
		$strategy->setEstimatedTime('2');
34
35
		return $strategy;
36
	}
37
38
	/**
39
	 * Deploy the given build to the given environment.
40
	 *
41
	 * @param \DNEnvironment $environment
42
	 * @param \DeploynautLogFile $log
43
	 * @param \DNProject $project
44
	 * @param array $options
45
	 */
46
	public function deploy(
47
		\DNEnvironment $environment,
48
		\DeploynautLogFile $log,
49
		\DNProject $project,
50
		$options
51
	) {
52
		$name = $environment->getFullName();
53
		$repository = $project->getLocalCVSPath();
54
		$sha = $options['sha'];
55
56
		$args = array(
57
			'branch' => $sha,
58
			'repository' => $repository,
59
		);
60
61
		$this->extend('deployStart', $environment, $sha, $log, $project);
62
63
		$log->write(sprintf('Deploying "%s" to "%s"', $sha, $name));
64
65
		$this->enableMaintenance($environment, $log, $project);
66
67
		// Use a package generator if specified, otherwise run a direct deploy, which is the default behaviour
68
		// if build_filename isn't specified
69
		if($this->packageGenerator) {
70
			$log->write(sprintf('Using package generator "%s"', get_class($this->packageGenerator)));
71
72
			try {
73
				$args['build_filename'] = $this->packageGenerator->getPackageFilename($project->Name, $sha, $repository, $log);
74
			} catch (Exception $e) {
75
				$log->write($e->getMessage());
76
				throw $e;
77
			}
78
79
			if(empty($args['build_filename'])) {
80
				throw new RuntimeException('Failed to generate package.');
81
			}
82
		}
83
84
		$command = $this->getCommand('deploy', 'web', $environment, $args, $log);
85
		$command->run(function($type, $buffer) use($log) {
86
			$log->write($buffer);
87
		});
88
89
		$error = null;
90
91
		$deploySuccessful = $command->isSuccessful();
92
		if ($deploySuccessful) {
93
			// Deployment automatically removes .htaccess, i.e. disables maintenance. Fine to smoketest.
94
			$deploySuccessful = $this->smokeTest($environment, $log);
95
		}
96
97
		if (!$deploySuccessful) {
98
			$this->enableMaintenance($environment, $log, $project);
99
100
			$rollbackSuccessful = $this->deployRollback($environment, $log, $project, $options, $args);
101
			if ($rollbackSuccessful) {
102
				// Again, .htaccess removed, maintenance off.
103
				$rollbackSuccessful = $this->smokeTest($environment, $log);
104
			}
105
106
			if (!$rollbackSuccessful) {
107
				$this->enableMaintenance($environment, $log, $project);
108
				$this->extend('deployRollbackFailure', $environment, $currentBuild->SHA, $log, $project);
0 ignored issues
show
Bug introduced by
The variable $currentBuild does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
109
				$log->write('Rollback failed');
110
111
				$error = $command->getErrorOutput();
112
			} else {
113
				$error = 'Rollback successful';
114
			}
115
		}
116
117
		// Regardless of outcome, try to clean up.
118
		$cleanup = $this->getCommand('deploy:cleanup', 'web', $environment, $args, $log);
119
		$cleanup->run(function($type, $buffer) use($log) {
120
			$log->write($buffer);
121
		});
122
		if(!$cleanup->isSuccessful()) {
123
			$this->extend('cleanupFailure', $environment, $sha, $log, $project);
124
			$log->write('Warning: Cleanup failed, but fine to continue. Needs manual cleanup sometime.');
125
		}
126
127
		$this->extend('deployEnd', $environment, $sha, $log, $project);
128
129
		if ($error) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $error of type string|null is loosely compared to true; 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...
130
			throw new RuntimeException($error);
131
		}
132
133
		$log->write(sprintf('Deploy of "%s" to "%s" finished', $sha, $name));
134
	}
135
136
	private function deployRollback(
137
		\DNEnvironment $environment,
138
		\DeploynautLogFile $log,
139
		\DNProject $project,
140
		$options,
141
		$args
142
	) {
143
		$sha = $options['sha'];
144
145
		$this->extend('deployFailure', $environment, $sha, $log, $project);
146
147
		$currentBuild = $environment->CurrentBuild();
148
		if (empty($currentBuild) || (!empty($options['no_rollback']) && $options['no_rollback'] !== 'false')) {
149
			throw new RuntimeException($command->getErrorOutput());
150
		}
151
152
		// re-run deploy with the current build sha to rollback
153
		$log->write('Deploy failed. Rolling back');
154
		$rollbackArgs = array_merge($args, ['branch' => $currentBuild->SHA]);
155
		$command = $this->getCommand('deploy', 'web', $environment, $rollbackArgs, $log);
156
		$command->run(function($type, $buffer) use($log) {
157
			$log->write($buffer);
158
		});
159
160
		return $command->isSuccessful();
161
	}
162
163
	/**
164
	 * @param \DNEnvironment $environment
165
	 * @return ArrayList
166
	 */
167
	public function getDeployOptions(\DNEnvironment $environment) {
168
		return new ArrayList([
169
			new PredeployBackupOption($environment->Usage === DNEnvironment::PRODUCTION),
170
			new NoRollbackDeployOption()
171
		]);
172
	}
173
174
	/**
175
	 * Enable a maintenance page for the given environment using the maintenance:enable Capistrano task.
176
	 */
177 View Code Duplication
	public function enableMaintenance(\DNEnvironment $environment, \DeploynautLogFile $log, \DNProject $project) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
178
		$name = $environment->getFullName();
179
		$command = $this->getCommand('maintenance:enable', 'web', $environment, null, $log);
180
		$command->run(function($type, $buffer) use($log) {
181
			$log->write($buffer);
182
		});
183
		if(!$command->isSuccessful()) {
184
			$this->extend('maintenanceEnableFailure', $environment, $log);
185
			throw new RuntimeException($command->getErrorOutput());
186
		}
187
		$log->write(sprintf('Maintenance page enabled on "%s"', $name));
188
	}
189
190
	/**
191
	 * Disable the maintenance page for the given environment using the maintenance:disable Capistrano task.
192
	 */
193 View Code Duplication
	public function disableMaintenance(\DNEnvironment $environment, \DeploynautLogFile $log, \DNProject $project) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
194
		$name = $environment->getFullName();
195
		$command = $this->getCommand('maintenance:disable', 'web', $environment, null, $log);
196
		$command->run(function($type, $buffer) use($log) {
197
			$log->write($buffer);
198
		});
199
		if(!$command->isSuccessful()) {
200
			$this->extend('maintenanceDisableFailure', $environment, $log);
201
			throw new RuntimeException($command->getErrorOutput());
202
		}
203
		$log->write(sprintf('Maintenance page disabled on "%s"', $name));
204
	}
205
206
	/**
207
	 * Check the status using the deploy:check capistrano method
208
	 */
209
	public function ping(\DNEnvironment $environment, \DeploynautLogFile $log, \DNProject $project) {
210
		$command = $this->getCommand('deploy:check', 'web', $environment, null, $log);
211
		$command->run(function($type, $buffer) use($log) {
212
			$log->write($buffer);
213
			echo $buffer;
214
		});
215
	}
216
217
	/**
218
	 * @inheritdoc
219
	 */
220
	public function dataTransfer(\DNDataTransfer $dataTransfer, \DeploynautLogFile $log) {
221
		if($dataTransfer->Direction == 'get') {
222
			$this->dataTransferBackup($dataTransfer, $log);
223
		} else {
224
			$environment = $dataTransfer->Environment();
225
			$project = $environment->Project();
226
			$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
227
			$archive = $dataTransfer->DataArchive();
228
229
			// extract the sspak contents, we'll need these so capistrano can restore that content
230
			try {
231
				$archive->extractArchive($workingDir);
232
			} catch(Exception $e) {
233
				$log->write($e->getMessage());
234
				throw new RuntimeException($e->getMessage());
235
			}
236
237
			// validate the contents match the requested transfer mode
238
			$result = $archive->validateArchiveContents($dataTransfer->Mode);
239
			if(!$result->valid()) {
240
				// do some cleaning, get rid of the extracted archive lying around
241
				$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
242
				$process->setTimeout(120);
243
				$process->run();
244
245
				// log the reason why we can't restore the snapshot and halt the process
246
				$log->write($result->message());
247
				throw new RuntimeException($result->message());
248
			}
249
250
			// Put up a maintenance page during a restore of db or assets.
251
			$this->enableMaintenance($environment, $log, $project);
252
			$this->dataTransferRestore($workingDir, $dataTransfer, $log);
253
			$this->disableMaintenance($environment, $log, $project);
254
		}
255
	}
256
257
	/**
258
	 * @param string $action Capistrano action to be executed
259
	 * @param string $roles Defining a server role is required to target only the required servers.
260
	 * @param \DNEnvironment $environment
261
	 * @param array<string>|null $args Additional arguments for process
262
	 * @param \DeploynautLogFile $log
263
	 * @return \Symfony\Component\Process\Process
264
	 */
265
	public function getCommand($action, $roles, \DNEnvironment $environment, $args = null, \DeploynautLogFile $log) {
266
		$name = $environment->getFullName();
267
		$env = $environment->Project()->getProcessEnv();
268
269
		if(!$args) {
270
			$args = array();
271
		}
272
		$args['history_path'] = realpath(DEPLOYNAUT_LOG_PATH . '/');
273
		$args['environment_id'] = $environment->ID;
274
275
		// Inject env string directly into the command.
276
		// Capistrano doesn't like the $process->setEnv($env) we'd normally do below.
277
		$envString = '';
278
		if(!empty($env)) {
279
			$envString .= 'env ';
280
			foreach($env as $key => $value) {
281
				$envString .= "$key=\"$value\" ";
282
			}
283
		}
284
285
		$data = DNData::inst();
286
		// Generate a capfile from a template
287
		$capTemplate = file_get_contents(BASE_PATH . '/deploynaut/Capfile.template');
288
		$cap = str_replace(
289
			array('<config root>', '<ssh key>', '<base path>'),
290
			array($data->getEnvironmentDir(), DEPLOYNAUT_SSH_KEY, BASE_PATH),
291
			$capTemplate
292
		);
293
294
		if(defined('DEPLOYNAUT_CAPFILE')) {
295
			$capFile = DEPLOYNAUT_CAPFILE;
296
		} else {
297
			$capFile = ASSETS_PATH . '/Capfile';
298
		}
299
		file_put_contents($capFile, $cap);
300
301
		$command = "{$envString}cap -f " . escapeshellarg($capFile) . " -vv $name $action ROLES=$roles";
302
		foreach($args as $argName => $argVal) {
303
			$command .= ' -s ' . escapeshellarg($argName) . '=' . escapeshellarg($argVal);
304
		}
305
306
		$log->write(sprintf('Running command: %s', $command));
307
308
		$process = new AbortableProcess($command);
309
		$process->setTimeout(3600);
310
		return $process;
311
	}
312
313
	/**
314
	 * Backs up database and/or assets to a designated folder,
315
	 * and packs up the files into a single sspak.
316
	 *
317
	 * @param \DNDataTransfer    $dataTransfer
318
	 * @param DeploynautLogFile $log
319
	 */
320
	protected function dataTransferBackup(\DNDataTransfer $dataTransfer, \DeploynautLogFile $log) {
321
		$environment = $dataTransfer->Environment();
322
		$name = $environment->getFullName();
323
324
		// Associate a new archive with the transfer.
325
		// Doesn't retrieve a filepath just yet, need to generate the files first.
326
		$dataArchive = DNDataArchive::create();
327
		$dataArchive->Mode = $dataTransfer->Mode;
328
		$dataArchive->AuthorID = $dataTransfer->AuthorID;
329
		$dataArchive->OriginalEnvironmentID = $environment->ID;
330
		$dataArchive->EnvironmentID = $environment->ID;
331
		$dataArchive->IsBackup = $dataTransfer->IsBackupDataTransfer();
332
333
		// Generate directory structure with strict permissions (contains very sensitive data)
334
		$filepathBase = $dataArchive->generateFilepath($dataTransfer);
335
		mkdir($filepathBase, 0700, true);
336
337
		$databasePath = $filepathBase . DIRECTORY_SEPARATOR . 'database.sql.gz';
338
339
		// Backup database
340 View Code Duplication
		if(in_array($dataTransfer->Mode, array('all', 'db'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
341
			$log->write(sprintf('Backup of database from "%s" started', $name));
342
			$command = $this->getCommand('data:getdb', 'db', $environment, array('data_path' => $databasePath), $log);
343
			$command->run(function($type, $buffer) use($log) {
344
				$log->write($buffer);
345
			});
346
			if(!$command->isSuccessful()) {
347
				$this->extend('dataTransferFailure', $environment, $log);
348
				throw new RuntimeException($command->getErrorOutput());
349
			}
350
			$log->write(sprintf('Backup of database from "%s" done', $name));
351
		}
352
353
		// Backup assets
354 View Code Duplication
		if(in_array($dataTransfer->Mode, array('all', 'assets'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
355
			$log->write(sprintf('Backup of assets from "%s" started', $name));
356
			$command = $this->getCommand('data:getassets', 'web', $environment, array('data_path' => $filepathBase), $log);
357
			$command->run(function($type, $buffer) use($log) {
358
				$log->write($buffer);
359
			});
360
			if(!$command->isSuccessful()) {
361
				$this->extend('dataTransferFailure', $environment, $log);
362
				throw new RuntimeException($command->getErrorOutput());
363
			}
364
			$log->write(sprintf('Backup of assets from "%s" done', $name));
365
		}
366
367
		// ensure the database connection is re-initialised, which is needed if the transfer
368
		// above took a really long time because the handle to the db may have become invalid.
369
		global $databaseConfig;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
370
		DB::connect($databaseConfig);
371
372
		$log->write('Creating sspak...');
373
374
		$sspakFilename = sprintf('%s.sspak', $dataArchive->generateFilename($dataTransfer));
375
		$sspakFilepath = $filepathBase . DIRECTORY_SEPARATOR . $sspakFilename;
376
377
		try {
378
			$dataArchive->attachFile($sspakFilepath, $dataTransfer);
379
			$dataArchive->setArchiveFromFiles($filepathBase);
380
		} catch(Exception $e) {
381
			$log->write($e->getMessage());
382
			throw new RuntimeException($e->getMessage());
383
		}
384
385
		// Remove any assets and db files lying around, they're not longer needed as they're now part
386
		// of the sspak file we just generated. Use --force to avoid errors when files don't exist,
387
		// e.g. when just an assets backup has been requested and no database.sql exists.
388
		$process = new AbortableProcess(sprintf('rm -rf %s/assets && rm -f %s', escapeshellarg($filepathBase), escapeshellarg($databasePath)));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 137 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
389
		$process->setTimeout(120);
390
		$process->run();
391
		if(!$process->isSuccessful()) {
392
			$log->write('Could not delete temporary files');
393
			throw new RuntimeException($process->getErrorOutput());
394
		}
395
396
		$log->write(sprintf('Creating sspak file done: %s', $dataArchive->ArchiveFile()->getAbsoluteURL()));
397
	}
398
399
	/**
400
	 * Utility function for triggering the db rebuild and flush.
401
	 * Also cleans up and generates new error pages.
402
	 * @param DeploynautLogFile $log
403
	 */
404 View Code Duplication
	public function rebuild(\DNEnvironment $environment, $log) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
405
		$name = $environment->getFullName();
406
		$command = $this->getCommand('deploy:migrate', 'web', $environment, null, $log);
407
		$command->run(function($type, $buffer) use($log) {
408
			$log->write($buffer);
409
		});
410
		if(!$command->isSuccessful()) {
411
			$log->write(sprintf('Rebuild of "%s" failed: %s', $name, $command->getErrorOutput()));
412
			throw new RuntimeException($command->getErrorOutput());
413
		}
414
		$log->write(sprintf('Rebuild of "%s" done', $name));
415
	}
416
417
	/**
418
	 * Extracts a *.sspak file referenced through the passed in $dataTransfer
419
	 * and pushes it to the environment referenced in $dataTransfer.
420
	 *
421
	 * @param string $workingDir Directory for the unpacked files.
422
	 * @param DNDataTransfer $dataTransfer
423
	 * @param DeploynautLogFile $log
424
	 */
425
	protected function dataTransferRestore($workingDir, \DNDataTransfer $dataTransfer, \DeploynautLogFile $log) {
426
		$environment = $dataTransfer->Environment();
427
		$name = $environment->getFullName();
428
429
		// Rollback cleanup.
430
		$self = $this;
431 View Code Duplication
		$cleanupFn = function() use($self, $workingDir, $environment, $log) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
432
			// Rebuild makes sense even if failed - maybe we can at least partly recover.
433
			$self->rebuild($environment, $log);
434
			$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
435
			$process->setTimeout(120);
436
			$process->run();
437
		};
438
439
		// Restore database into target environment
440 View Code Duplication
		if(in_array($dataTransfer->Mode, array('all', 'db'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
441
			$log->write(sprintf('Restore of database to "%s" started', $name));
442
			$args = array('data_path' => $workingDir . DIRECTORY_SEPARATOR . 'database.sql');
443
			$command = $this->getCommand('data:pushdb', 'db', $environment, $args, $log);
444
			$command->run(function($type, $buffer) use($log) {
445
				$log->write($buffer);
446
			});
447
			if(!$command->isSuccessful()) {
448
				$cleanupFn();
449
				$log->write(sprintf('Restore of database to "%s" failed: %s', $name, $command->getErrorOutput()));
450
				$this->extend('dataTransferFailure', $environment, $log);
451
				throw new RuntimeException($command->getErrorOutput());
452
			}
453
			$log->write(sprintf('Restore of database to "%s" done', $name));
454
		}
455
456
		// Restore assets into target environment
457 View Code Duplication
		if(in_array($dataTransfer->Mode, array('all', 'assets'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
458
			$log->write(sprintf('Restore of assets to "%s" started', $name));
459
			$args = array('data_path' => $workingDir . DIRECTORY_SEPARATOR . 'assets');
460
			$command = $this->getCommand('data:pushassets', 'web', $environment, $args, $log);
461
			$command->run(function($type, $buffer) use($log) {
462
				$log->write($buffer);
463
			});
464
			if(!$command->isSuccessful()) {
465
				$cleanupFn();
466
				$log->write(sprintf('Restore of assets to "%s" failed: %s', $name, $command->getErrorOutput()));
467
				$this->extend('dataTransferFailure', $environment, $log);
468
				throw new RuntimeException($command->getErrorOutput());
469
			}
470
			$log->write(sprintf('Restore of assets to "%s" done', $name));
471
		}
472
473
		$log->write('Rebuilding and cleaning up');
474
		$cleanupFn();
475
	}
476
477
	/**
478
	 * This is mostly copy-pasted from Anthill/Smoketest.
479
	 *
480
	 * @param \DNEnvironment $environment
481
	 * @param \DeploynautLogFile $log
482
	 * @return bool
483
	 */
484
	protected function smokeTest(\DNEnvironment $environment, \DeploynautLogFile $log) {
485
		$url = $environment->getBareURL();
486
		$timeout = 600;
487
		$tick = 60;
488
489
		if(!$url) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $url of type string|null 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...
490
			$log->write('Skipping site accessible check: no URL found.');
491
			return true;
492
		}
493
494
		$start = time();
495
		$infoTick = time() + $tick;
496
497
		$log->write(sprintf(
498
			'Waiting for "%s" to become accessible... (timeout: %smin)',
499
			$url,
500
			$timeout / 60
501
		));
502
503
		// configure curl so that curl_exec doesn't wait a long time for a response
504
		$ch = curl_init();
505
		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
506
		curl_setopt($ch, CURLOPT_TIMEOUT, 5);
507
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
508
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
509
		curl_setopt($ch, CURLOPT_MAXREDIRS, 10); // set a high number of max redirects (but not infinite amount) to avoid a potential infinite loop
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 141 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
510
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
511
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
512
		curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
513
		curl_setopt($ch, CURLOPT_URL, $url);
514
		curl_setopt($ch, CURLOPT_USERAGENT, 'Rainforest');
515
		$success = false;
516
517
		// query the site every second. Note that if the URL doesn't respond,
518
		// curl_exec will take 5 seconds to timeout (see CURLOPT_CONNECTTIMEOUT and CURLOPT_TIMEOUT above)
519
		do {
520
			if(time() > $start + $timeout) {
521
				$log->write(sprintf(' * Failed: check for %s timed out after %smin', $url, $timeout / 60));
522
				return false;
523
			}
524
525
			$response = curl_exec($ch);
526
527
			// check the HTTP response code for HTTP protocols
528
			$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
529
			if($status && !in_array($status, [500, 501, 502, 503, 504])) {
530
				$success = true;
531
			}
532
533
			// check for any curl errors, mostly for checking the response state of non-HTTP protocols,
534
			// but applies to checks of any protocol
535
			if($response && !curl_errno($ch)) {
536
				$success = true;
537
			}
538
539
			// Produce an informational ticker roughly every $tick
540
			if (time() > $infoTick) {
541
				$message = [];
542
543
				// Collect status information from different sources.
544
				if ($status) {
545
					$message[] = sprintf('HTTP status code is %s', $status);
546
				}
547
				if (!$response) {
548
					$message[] = 'response is empty';
549
				}
550
				if ($error = curl_error($ch)) {
551
					$message[] = sprintf('request error: %s', $error);
552
				}
553
554
				$log->write(sprintf(
555
					' * Still waiting: %s...',
556
					implode(', ', $message)
557
				));
558
559
				$infoTick = time() + $tick;
560
			}
561
562
			sleep(1);
563
		} while(!$success);
564
565
		curl_close($ch);
566
		$log->write(' * Success: site is accessible!');
567
		return true;
568
	}
569
570
}
571