Test Setup Failed
Push — 17.1 ( 6edd18...88f1e2 )
by Ralf
08:41
created

install-cli.php ➔ run_git()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 6
nop 1
dl 0
loc 21
rs 8.9617
c 0
b 0
f 0
1
#!/usr/bin/env php
2
<?php
3
/**
4
 * Install / update EGroupware - Command line interface
5
 *
6
 * Usage: install-cli.php [-v|--verbose] [--use-prerelease] (master|bugfix|release|<branch>|<tag>)
7
 *        install-cli.php --git <arguments>	# runs git with given arguments in all app-dirs
8
 *                                          # e.g. tag -a 17.1.20190214 -m 'tagging release'
9
 *
10
 * EGroupware main directory should be either git cloned:
11
 *
12
 *	git clone -b <branch> https://github.com/EGroupware/egroupware [<target>]
13
 *
14
 * or created via composer create-project
15
 *
16
 *	composer create-project --prefer-source --keep-vcs egroupware/egroupware[:(dev-master|17.1.x-dev|<tag>)] <target>
17
 *
18
 * Both will create a git clone, which can be further updated by calling this tool without argument.
19
 *
20
 * We currently use 3 "channels":
21
 * - release: taged maintenance releases only eg. 17.1.20190214
22
 * - bugfix:  release-branch incl. latest bugfixes eg. 17.1 or 17.1.x-dev for composer
23
 * - master:  latest development for next release
24
 * To change the channel, call install-cli.php <channel-to-update-to>.
25
 *
26
 * This tool requires the following binaries installed at the usually places or in your path:
27
 * - php & git: apt/yum/zypper install php-cli git
28
 * - composer: see https://getcomposer.org/download/ for installation instructions
29
 * The following binaries are needed to minify JavaScript and CSS
30
 * - npm: apt/yum/zypper install npm
31
 * - grunt: npm install -g grunt-cli
32
 *
33
 * @link http://www.egroupware.org
34
 * @package api
35
 * @author Ralf Becker <[email protected]>
36
 * @copyright (c) 2019 by Ralf Becker <[email protected]>
37
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
38
 */
39
40
chdir(__DIR__);	// to enable relative pathes to work
41
42
if (php_sapi_name() !== 'cli')	// security precaution: forbit calling setup-cli as web-page
43
{
44
	die('<h1>install-cli.php must NOT be called as web-page --> exiting !!!</h1>');
45
}
46
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT);
47
48
// parse arguments
49
$verbose = $use_prerelease = $run_git = false;
50
51
$argv = $_SERVER['argv'];
52
$cmd  = array_shift($argv);
53
54
foreach($argv as $n => $arg)
55
{
56
	if ($arg[0] === '-')
57
	{
58
		switch($arg)
59
		{
60
			case '-v':
61
			case '--verbose':
62
				$verbose = true;
63
				unset($argv[$n]);
64
				break;
65
66
			case '--use-prerelease':
67
				$use_prerelease = true;
68
				unset($argv[$n]);
69
				break;
70
71
			case '-h':
72
			case '--help':
73
				usage();
74
75
			case '--git':
76
				$run_git = true;
77
				unset($argv[$n]);
78
				break 2;	// no further argument processing, as they are for git
79
80
			default:
81
				usage("Unknown argument '$arg'!");
82
		}
83
	}
84
}
85
86
if (!$run_git && count($argv) > 1) usage("Too many arguments!");
87
88 View Code Duplication
function usage($err=null)
0 ignored issues
show
Best Practice introduced by
The function usage() has been defined more than once; this definition is ignored, only the first definition in admin/admin-cli.php (L311-349) is considered.

This check looks for functions that have already been defined in other files.

Some Codebases, like WordPress, make a practice of defining functions multiple times. This may lead to problems with the detection of function parameters and types. If you really need to do this, you can mark the duplicate definition with the @ignore annotation.

/**
 * @ignore
 */
function getUser() {

}

function getUser($id, $realm) {

}

See also the PhpDoc documentation for @ignore.

Loading history...
89
{
90
	global $cmd;
91
92
	if ($err)
93
	{
94
		echo "$err\n\n";
95
	}
96
	die("Usage:\t$cmd [-v|--verbose] [--use-prerelease] (master|bugfix|release|<branch>|<tag>)\n".
97
		"\t$cmd --git <arguments>\t	runs git with given arguments in all app-dirs, e.g. tag -a 17.1.20190214 -m 'tagging release'\n");
98
}
99
100
$bins = array(
101
	'php'      => PHP_BINARY,
102
	'git'      => '/usr/bin/git',
103
	'composer' => ['/usr/bin/composer', '/usr/bin/composer.phar'],
104
	// npm and grunt are no hard requirement and should be the last in the list!
105
	'npm'      => '/usr/bin/npm',
106
	'grunt'    => '/usr/bin/grunt',
107
);
108
109
// check if the necessary binaries are installed
110
foreach($bins as $name => $binaries)
111
{
112
	foreach((array)$binaries as $bin)
113
	{
114
		if (file_exists($bin) && is_executable($bin))
115
		{
116
			$bins[$name] = $$name = $bin;
117
			continue 2;
118
		}
119
	}
120
	$output = $ret = null;
121
	if (($bin = exec('which '.$name, $output, $ret)) && !$ret &&
122
		(file_exists($bin)) && is_executable($bin))
123
	{
124
		$bins[$name] = $$name = $bin;
125
	}
126
	// check if we can just run it, because it's in the path
127
	elseif (exec($name.' -v', $output, $ret) && !$ret)
128
	{
129
		$bins[$name] = $$name = $num;
130
	}
131
	else
132
	{
133
		$bins[$name] = $$name = false;
134
		error_log("Could not find $name command!");
135
		if (!in_array($name, ['npm','grunt']))
136
		{
137
			exit(1);
138
		}
139
		else
140
		{
141
			error_log("npm and grunt are required to minify JavaScript and CSS files to improve performance.");
142
			break;
143
		}
144
	}
145
}
146
147
if ($verbose) echo "Using following binaries: ".json_encode ($bins, JSON_UNESCAPED_SLASHES)."\n";
148
149
if (!extension_loaded('curl')) die("Required PHP extesion 'curl' missing! You need to install php-curl package.\n\n");
150
151
// check if we are on a git clone
152
$output = array();
153
if (!file_exists(__DIR__.'/.git') || !is_dir(__DIR__.'/.git'))
154
{
155
	error_log("Could not identify git branch (you need to use git clone or composer create-project --prefer-source --keep-vcs egroupware/egroupware)!");
156
	exit(1);
157
}
158
159
// should we only run a git command
160
if ($run_git)
161
{
162
	exit (run_git($argv));
163
}
164
165
if (!exec($git.' branch --no-color', $output, $ret) || $ret)
166
{
167
	foreach($output as $line)
0 ignored issues
show
Bug introduced by
The expression $output of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
168
	{
169
		error_log($line);
170
	}
171
	exit($ret);
172
}
173
foreach($output as $line)
0 ignored issues
show
Bug introduced by
The expression $output of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
174
{
175
	if ($line[0] == '*')
176
	{
177
		$branch = substr($line, 2);
178
		// are we on a tag
179
		if (preg_match('/^\(HEAD .* ([0-9.]+)\)$/', $branch, $matches))
180
		{
181
			$branch = $matches[1];
182
		}
183
		break;
184
	}
185
}
186
$channel = 'development';
187
if (preg_match('/^\d+\.\d+(\.\d{8})?/', $branch, $machtes))
188
{
189
	$channel = isset($matches[1]) ? 'release' : 'bugfix';
190
}
191
if ($verbose) echo "Currently using branch: $branch --> $channel channel\n";
192
193
if ($argv)
194
{
195
	$target = array_shift($argv);
196
197
	if ($target === 'release')
198
	{
199
		$target = get_latest_release($use_prerelease);
200
	}
201
	elseif ($target === 'bugfix')
202
	{
203
		$target = (string)(float)get_latest_release($use_prerelease);
204
	}
205
}
206
else
207
{
208
	$target = $branch;
209
210
	// find latest release
211
	if ($channel == 'release')
212
	{
213
		$target = get_latest_release($use_prerelease);
214
	}
215
}
216
217
echo "Updating to: $target\n";
218
219
// Update EGroupware itself and further apps installed via git
220
foreach(scandir(__DIR__) as $dir)
221
{
222
	if ($dir !== '..' && file_exists(__DIR__.'/'.$dir.'/.git') &&
223
		// these apps / dirs are managed by composer, no need to run manual updates
224
		!in_array($dir, ['vendor', 'activesync', 'collabora', 'projectmanager', 'tracker']))
225
	{
226
		$cmd = "cd $dir ; $git stash -q";
227
		// switch message about detached head off for release-channel/tags
228
		if (preg_match('/^\d+\.\d+\.\d{8}/', $target))
229
		{
230
			$cmd .= "; $git config advice.detachedHead false";
231
		}
232
		if ($branch != $target)
233
		{
234
			$cmd .= "; $git checkout $target";
235
		}
236
		// no need to pull for release-channel/tags
237
		if (!preg_match('/^\d+\.\d+\.\d{8}/', $target))
238
		{
239
			$cmd .= "; $git pull --rebase";
240
		}
241
		$cmd .= "; test -z \"$($git stash list)\" || $git stash pop";
242
		if ($verbose) echo "$cmd\n";
243
		system($cmd);
244
	}
245
}
246
247
// update composer managed dependencies
248
$cmd = $composer.' install';
249
if ($verbose) echo "$cmd\n";
250
system($cmd);
251
252
// update npm dependencies and run grunt to minify javascript and css
253
if ($npm && $grunt)
254
{
255
	$cmd = $npm.' install';
256
	if ($verbose) echo "$cmd\n";
257
	system($cmd);
258
259
	if ($verbose) echo "$grunt\n";
260
	system($grunt);
261
}
262
263
/**
264
 * Run git command with given arguments in install-dir and all app-dirs
265
 *
266
 * cd and git command is echoed to stderr
267
 *
268
 * @param array $argv
269
 * @return int exit-code of last git command, breaks on first non-zero exit-code
270
 */
271
function run_git(array $argv)
272
{
273
	global $git;
274
275
	$git_cmd = $git.' '.implode(' ', array_map('escapeshellarg', $argv));
276
277
	$ret = 0;
278
	foreach(scandir(__DIR__) as $dir)
279
	{
280
		if ($dir !== '..' && file_exists(__DIR__.'/'.$dir.'/.git'))
281
		{
282
			$cmd = ($dir !== '.' ? "cd $dir; " : '').$git_cmd;
283
284
			error_log("\n>>> ".$cmd."\n");
285
			system($cmd, $ret);
286
			// break if command is not successful
287
			if ($ret) return $ret;
288
		}
289
	}
290
	return $ret;
291
}
292
293
/**
294
 * Get latest release
295
 *
296
 * @param boolean $prerelease =false include releases taged as prerelease
297
 * @param boolean $return_name =true true: just return name, false: full release object
298
 * @return array|string|null null if no release found
299
 */
300
function get_latest_release($prerelease=false, $return_name=true)
301
{
302
	foreach(github_api('/repos/egroupware/egroupware/releases', [], 'GET') as $release)
303
	{
304
		if ($prerelease || $release['prerelease'] === false)
305
		{
306
			return $return_name ? $release['tag_name'] : $release;
307
		}
308
	}
309
	return null;
310
}
311
/**
312
 * Sending a Github API request
313
 *
314
 * @param string $_url url of just path where to send request to (https://api.github.com is added automatic)
315
 * @param string|array $data payload, array get automatic added as get-parameter or json_encoded for POST
316
 * @param string $method ='POST'
317
 * @param string $upload =null path of file to upload, payload for request with $method='FILE'
318
 * @param string $content_type =null
319
 * @throws Exception
320
 * @return array with response
321
 */
322
function github_api($_url, $data, $method='POST', $upload=null, $content_type=null)
0 ignored issues
show
Best Practice introduced by
The function github_api() has been defined more than once; this definition is ignored, only the first definition in doc/rpm-build/checkout-build-archives.php (L463-514) is considered.

This check looks for functions that have already been defined in other files.

Some Codebases, like WordPress, make a practice of defining functions multiple times. This may lead to problems with the detection of function parameters and types. If you really need to do this, you can mark the duplicate definition with the @ignore annotation.

/**
 * @ignore
 */
function getUser() {

}

function getUser($id, $realm) {

}

See also the PhpDoc documentation for @ignore.

Loading history...
323
{
324
	global /*$config,*/ $verbose;
325
326
	$url = $_url[0] == '/' ? 'https://api.github.com'.$_url : $_url;
327
	$c = curl_init();
328
	curl_setopt($c, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
329
	//curl_setopt($c, CURLOPT_USERPWD, $config['github_user'].':'.$config['github_token']);
330
	curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
331
	curl_setopt($c, CURLOPT_USERAGENT, basename(__FILE__));
332
	curl_setopt($c, CURLOPT_TIMEOUT, 240);
333
	curl_setopt($c, CURLOPT_FOLLOWLOCATION, true);
334
335 View Code Duplication
	switch($method)
336
	{
337
		case 'POST':
338
			curl_setopt($c, CURLOPT_POST, true);
339
			if (is_array($data)) $data = json_encode($data, JSON_FORCE_OBJECT);
340
			curl_setopt($c, CURLOPT_POSTFIELDS, $data);
341
			break;
342
		case 'GET':
343
			if(count($data)) $url .= '?' . http_build_query($data);
344
			break;
345
		case 'FILE':
346
			curl_setopt($c, CURLOPT_HTTPHEADER, array("Content-type: $content_type"));
347
			curl_setopt($c, CURLOPT_POST, true);
348
			curl_setopt($c, CURLOPT_POSTFIELDS, file_get_contents($upload));
349
			if(count($data)) $url .= '?' . http_build_query($data);
350
			break;
351
		default:
352
			throw new Exception(__FUNCTION__.": Unknown/unimplemented method=$method!");
353
	}
354
	curl_setopt($c, CURLOPT_URL, $url);
355
356 View Code Duplication
	if (is_string($data)) $short_data = strlen($data) > 100 ? substr($data, 0, 100).' ...' : $data;
357 View Code Duplication
	if ($verbose) echo "Sending $method request to $url ".(isset($short_data)&&$method!='GET'?$short_data:'')."\n";
358
359 View Code Duplication
	if (($response = curl_exec($c)) === false)
360
	{
361
		// run failed request again to display response including headers
362
		curl_setopt($c, CURLOPT_HEADER, true);
363
		curl_setopt($c, CURLOPT_RETURNTRANSFER, false);
364
		curl_exec($c);
365
		throw new Exception("$method request to $url failed ".(isset($short_data)&&$method!='GET'?$short_data:''));
366
	}
367
368 View Code Duplication
	if ($verbose) echo (strlen($response) > 200 ? substr($response, 0, 200).' ...' : $response)."\n";
369
370
	curl_close($c);
371
372
	return json_decode($response, true);
373
}
374