Passed
Pull Request — master (#988)
by
unknown
05:03
created

TWebServerAction::setAddress()   B

Complexity

Conditions 7
Paths 11

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 17
c 1
b 0
f 0
nc 11
nop 1
dl 0
loc 28
rs 8.8333
1
<?php
2
/**
3
 * TWebServerAction class file
4
 *
5
 * @author Brad Anderson <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 */
9
10
namespace Prado\Shell\Actions;
11
12
use Prado\Prado;
13
use Prado\Shell\TShellAction;
14
use Prado\Shell\TShellApplication;
15
use Prado\TPropertyValue;
16
use Prado\Util\Helpers\TProcessHelper;
17
18
/**
19
 * TWebServerAction class
20
 *
21
 * This class serves the application with the built-in PHP testing web server.
22
 *
23
 * When no network address is specified, the web server will listen on the default
24
 * 127.0.0.1 network interface and on port 8080.  The application is accessible on
25
 * the machine web browser at web address "http://127.0.0.1:8080/".
26
 *
27
 * The command option `--address=localhost` can specify the network address both
28
 * with and without a port.  A port can also be specified with the network address,
29
 * eg `--address=localhost:8777`.
30
 *
31
 * if the machine "hosts" file maps a domain name to 127.0.0.1/localhost, then that
32
 * domain name can be specified with `--address=testdomain.com` and is accessible
33
 * on the machine web browser at "http://testdomain.com/".  In this example, testdomain.com
34
 * maps to 127.0.0.1 in the system's "hosts" file.
35
 *
36
 * The network address can be changed to IPv6 with the command line option `--ipv6`.
37
 * To serve pages on all network addresses, including any internet IP address, include
38
 * the option `--all`.  These options only work when there is no address specified.
39
 *
40
 * The command line option `--port=8777` can be used to change the port; in this example
41
 * to port 8777.  Ports 1023 and below are typically reserved for application and
42
 * system use and cannot be specified without administration access.
43
 *
44
 * To have more than one worker (to handle multiple requests), specify the `--workers=8`
45
 * command option (with the number of page workers need for you).  In this example,
46
 * eight concurrent workers are created.
47
 *
48
 * The TWebServerAction is only available when the application is in "Debug" mode.
49
 * In other Application modes, this action can be enabled with the Prado Application
50
 * Parameter "Prado:PhpWebServer" set to "true". eg. Within the Application configuration:
51
 *
52
 * ```xml
53
 * <parameters>
54
 *     <parameter id="Prado:PhpWebServer" value="true" />
55
 * </parameters>
56
 * ```
57
 *
58
 * @author Brad Anderson <[email protected]>
59
 * @since 4.2.3
60
 */
61
class TWebServerAction extends TShellAction
62
{
63
	public const DEV_WEBSERVER_ENV = 'PRADO_DEV_WEBSERVER';
64
	public const WORKERS_ENV = 'PHP_CLI_SERVER_WORKERS';
65
66
	public const DEV_WEBSERVER_PARAM = 'Prado:PhpWebServer';
67
68
	protected $action = 'http';
69
	protected $methods = ['serve'];
70
	protected $parameters = [[]];
71
	protected $optional = [['router-filepath']];
72
	protected $description = [
73
		'Provides a Test PHP Web Server to serve the application.',
74
		'Runs a PHP Web Server after Initializing the Application.'];
75
76
	/** @var bool Listen on all network addresses assigned to the computer, when one is not provided. */
77
	private bool $_all = false;
78
79
	/** @var ?string The specific address to listen on. */
80
	private ?string $_address = null;
81
82
	/** @var int The port to listen on, default 8080 */
83
	private int $_port = 8080;
84
85
	/** @var bool Use a direct ip v6 address, when one is not provided. */
86
	private bool $_ipv6 = false;
87
88
	/** @var int the number of workers for the Web Server */
89
	private int $_workers = 1;
90
91
	/**
92
	 * This option is only used when no network interface is specified.
93
	 * @return bool Respond on all network addresses.
94
	 */
95
	public function getAll(): bool
96
	{
97
		return $this->_all;
98
	}
99
100
	/**
101
	 * This option is only used when no network interface is specified.
102
	 * @param mixed $value
103
	 * @return bool Respond on all network addresses.
104
	 * @return static The current object.
105
	 */
106
	public function setAll($value): static
107
	{
108
		if ($value === null || $value === '') {
109
			$this->_all = true;
110
		} else {
111
			$this->_all = TPropertyValue::ensureBoolean($value);
112
		}
113
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Prado\Shell\Actions\TWebServerAction which is incompatible with the documented return type boolean.
Loading history...
114
	}
115
116
	/**
117
	 * Gets the network address to serve pages from.  When no network address is specified
118
	 * then this will return the proper network address based upon {@see self::getIpv6()}
119
	 * and {@see self::getAll()}.
120
	 * @return string The network address to serve pages.
121
	 */
122
	public function getAddress(): string
123
	{
124
		if(!$this->_address) {
125
			if ($this->getIpv6()) {
126
				if ($this->getAll()) {
127
					return '[::0]';
128
				} else {
129
					return 'localhost';
130
				}
131
			} else {
132
				if ($this->getAll()) {
133
					return '0.0.0.0';
134
				} else {
135
					return '127.0.0.1';
136
				}
137
			}
138
		}
139
		return $this->_address;
140
	}
141
142
	/**
143
	 * @param ?string $address The network address to serve pages from.
144
	 * @return static The current object.
145
	 */
146
	public function setAddress($address): static
147
	{
148
		if ($address) {
149
			$address = TPropertyValue::ensureString($address);
150
			$port = null;
151
152
			if (($address[0] ?? '') === '[') {
153
				if ($pos = strrpos($address, ']')) {
154
					if ($pos = strrpos($address, ':', $pos)) {
155
						$port = substr($address, $pos + 1);
156
					}
157
				}
158
			} else {
159
				if (($pos = strrpos($address, ':')) !== false) {
160
					$port = substr($address, $pos + 1);
161
				}
162
			}
163
164
			if (is_numeric($port)) {
165
				$this->_port = (int) $port;
166
				$address = substr($address, 0, $pos);
167
			}
168
			$this->_address = $address;
169
		} else {
170
			$this->_address = null;
171
		}
172
173
		return $this;
174
	}
175
176
	/**
177
	 * @return int The port to serve pages.
178
	 */
179
	public function getPort(): int
180
	{
181
		return $this->_port;
182
	}
183
184
	/**
185
	 * @param null|int|string $port The port to serve pages, default 8080.
186
	 * @return static The current object.
187
	 */
188
	public function setPort($port): static
189
	{
190
		$this->_port = TPropertyValue::ensureInteger($port);
191
192
		return $this;
193
	}
194
195
	/**
196
	 * @return bool Use an IPv6 network address.
197
	 */
198
	public function getIpv6(): bool
199
	{
200
		return $this->_ipv6;
201
	}
202
203
	/**
204
	 * @param null|bool|string $ipv6
205
	 * @return static The current object.
206
	 */
207
	public function setIpv6($ipv6): static
208
	{
209
		if ($ipv6 === null || $ipv6 === '') {
210
			$ipv6 = true;
211
		}
212
		$this->_ipv6 = TPropertyValue::ensureBoolean($ipv6);
213
214
		return $this;
215
	}
216
217
	/**
218
	 * @return int The number of web server requests workers.
219
	 */
220
	public function getWorkers(): int
221
	{
222
		return $this->_workers;
223
	}
224
225
	/**
226
	 * @param null|int|string $value The number of web server requests workers.
227
	 * @return static The current object.
228
	 */
229
	public function setWorkers($value): static
230
	{
231
		if ($value === null || $value === '') {
232
			$this->_workers = 8;
233
		} else {
234
			$this->_workers = max(1, TPropertyValue::ensureInteger($value));
235
		}
236
237
		return $this;
238
	}
239
240
	/**
241
	 * Properties for the action set by parameter.
242
	 * @param string $methodID the action being executed
243
	 * @return array properties for the $actionID
244
	 */
245
	public function options($methodID): array
246
	{
247
		if ($methodID === 'serve') {
248
			return ['address', 'port', 'workers', 'ipv6', 'all'];
249
		}
250
		return [];
251
	}
252
253
	/**
254
	 * Aliases for the properties to be set by parameter.  'i' is for 'interface'.
255
	 * @return array<string, string> alias => property for the $actionID
256
	 */
257
	public function optionAliases(): array
258
	{
259
		return ['a' => 'address', 'p' => 'port', 'w' => 'workers', '6' => 'ipv6', 'i' => 'all'];
260
	}
261
262
263
	/**
264
	 * This runs the PHP Development Web Server.
265
	 * @param array $args parameters
266
	 * @return bool
267
	 */
268
	public function actionServe($args)
269
	{
270
		array_shift($args);
271
272
		$env = getenv();
273
		$env[static::DEV_WEBSERVER_ENV] = '1';
274
275
		if (($workers = $this->getWorkers()) > 1) {
276
			$env[static::WORKERS_ENV] = $workers;
277
		}
278
279
		$address = $this->getAddress();
280
		$port = $this->getPort();
281
		$documentRoot = dirname($_SERVER['SCRIPT_FILENAME']);
282
283
		if ($router = array_shift($args)) {
284
			if ($r = realpath($router)) {
285
				$router = $r;
286
			}
287
		}
288
289
		$app = Prado::getApplication();
290
		$quiet = ($app instanceof TShellApplication) ? $app->getQuietMode() : 0;
291
292
		$writer = $this->getWriter();
293
294
		if (!is_dir($documentRoot)) {
295
			if ($quiet !== 3) {
296
				$writer->writeError("Document root \"$documentRoot\" does not exist.");
297
				$writer->flush();
298
			}
299
			return true;
300
		}
301
302
		if ($this->isAddressTaken($address, $port)) {
303
			if ($quiet !== 3) {
304
				$writer->writeError("http://$address is taken by another process.");
305
				$writer->flush();
306
			}
307
			return true;
308
		}
309
310
		if ($router !== null && !file_exists($router)) {
311
			if ($quiet !== 3) {
312
				$writer->writeError("Routing file \"$router\" does not exist.");
313
				$writer->flush();
314
			}
315
			return true;
316
		}
317
318
		$nullFile = null;
319
		if ($quiet >= 2) {
320
			$nullFile = TProcessHelper::isSystemWindows() ? 'NUL' : '/dev/null';
321
			$descriptors = [STDIN, ['file', $nullFile, 'w'], ['file', $nullFile, 'w']];
322
		} else {
323
			$writer->writeline();
324
			$writer->write("Document root is \"{$documentRoot}\"\n");
325
			if ($router) {
326
				$writer->write("Routing file is \"$router\"\n");
327
			}
328
			$writer->writeline();
329
			$writer->write("To quit press CTRL-C or COMMAND-C.\n");
330
			$writer->flush();
331
332
			$descriptors = [STDIN, STDOUT, STDERR];
333
		}
334
335
		$command = $this->generateCommand($address . ':' . $port, $documentRoot, $router);
336
		$cwd = null;
337
338
		$process = proc_open($command, $descriptors, $pipes, $cwd, $env);
339
		proc_close($process);
340
341
		if ($nullFile) {
342
			fclose($descriptors[1]);
343
			fclose($descriptors[2]);
344
		}
345
346
		return true;
347
	}
348
349
	/**
350
	 * @param string $address The web server address and port.
351
	 * @param string $documentRoot The path of the application.
352
	 * @param ?string $router The router file
353
	 * @return array
354
	 */
355
	public function generateCommand(string $address, string $documentRoot, $router): array
356
	{
357
		return TProcessHelper::filterCommand(array_merge(['@php', '-S', $address, '-t', $documentRoot], $router ? [$router] : []));
358
	}
359
360
361
	/**
362
	 * This checks if a specific hostname and port are currently being used in the system
363
	 * @param string $hostname server address
364
	 * @param int $port server port
365
	 * @return bool if address is already in use
366
	 */
367
	protected function isAddressTaken($hostname, $port)
368
	{
369
		$fp = @fsockopen($hostname, $port, $errno, $errstr, 3);
370
		if ($fp === false) {
371
			return false;
372
		}
373
		fclose($fp);
374
		return true;
375
	}
376
}
377