Completed
Branch 17.1 (152c57)
by Nathan
09:38
created

install-cli.php ➔ get_latest_release()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 4
nop 2
dl 0
loc 11
rs 9.6111
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
 *
8
 * EGroupware main directory should be either git cloned:
9
 *
10
 *	git clone -b <branch> https://github.com/EGroupware/egroupware [<target>]
11
 *
12
 * or created via composer create-project
13
 *
14
 *	composer create-project --prefer-source --keep-vcs egroupware/egroupware[:(dev-master|17.1.x-dev|<tag>)] <target>
15
 *
16
 * Both will create a git clone, which can be further updated by calling this tool without argument.
17
 *
18
 * We currently use 3 "channels":
19
 * - release: taged maintenance releases only eg. 17.1.20190214
20
 * - bugfix:  release-branch incl. latest bugfixes eg. 17.1 or 17.1.x-dev for composer
21
 * - master:  latest development for next release
22
 * To change the channel, call install-cli.php <channel-to-update-to>.
23
 *
24
 * This tool requires the following binaries installed at the usually places or in your path:
25
 * - php & git: apt/yum/zypper install php-cli git
26
 * - composer: see https://getcomposer.org/download/ for installation instructions
27
 * The following binaries are needed to minify JavaScript and CSS
28
 * - npm: apt/yum/zypper install npm
29
 * - grunt: npm install -g grunt-cli
30
 *
31
 * @link http://www.egroupware.org
32
 * @package api
33
 * @author Ralf Becker <[email protected]>
34
 * @copyright (c) 2019 by Ralf Becker <[email protected]>
35
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
36
 */
37
38
chdir(__DIR__);	// to enable relative pathes to work
39
40
if (php_sapi_name() !== 'cli')	// security precaution: forbit calling setup-cli as web-page
41
{
42
	die('<h1>install-cli.php must NOT be called as web-page --> exiting !!!</h1>');
43
}
44
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT);
45
46
// parse arguments
47
$verbose = $use_prerelease = false;
48
49
$argv = $_SERVER['argv'];
50
$cmd  = array_shift($argv);
51
52
foreach($argv as $n => $arg)
53
{
54
	if ($arg[0] === '-')
55
	{
56
		switch($arg)
57
		{
58
			case '-v':
59
			case '--verbose':
60
				$verbose = true;
61
				unset($argv[$n]);
62
				break;
63
64
			case '--use-prerelease':
65
				$use_prerelease = true;
66
				unset($argv[$n]);
67
				break;
68
69
			case '-h':
70
			case '--help':
71
				usage();
72
73
			default:
74
				usage("Unknown argument '$arg'!");
75
		}
76
	}
77
}
78
79
if (count($argv) > 1) usage("Too many arguments!");
80
81
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...
82
{
83
	global $cmd;
84
85
	if ($err)
86
	{
87
		echo "$err\n\n";
88
	}
89
	die("Usage: $cmd [-v|--verbose] [--use-prerelease] (master|bugfix|release|<branch>|<tag>)\n\n");
90
}
91
92
$bins = array(
93
	'php'      => PHP_BINARY,
94
	'git'      => '/usr/bin/git',
95
	'composer' => ['/usr/bin/composer', '/usr/bin/composer.phar'],
96
	// npm and grunt are no hard requirement and should be the last in the list!
97
	'npm'      => '/usr/bin/npm',
98
	'grunt'    => '/usr/bin/grunt',
99
);
100
101
// check if the necessary binaries are installed
102
foreach($bins as $name => $binaries)
103
{
104
	foreach((array)$binaries as $bin)
105
	{
106
		if (file_exists($bin) && is_executable($bin))
107
		{
108
			$bins[$name] = $$name = $bin;
109
			continue 2;
110
		}
111
	}
112
	$output = null;
113
	if (($bin = exec('which '.$name, $output, $ret)) && !$ret &&
114
		(file_exists($bin)) && is_executable($bin))
115
	{
116
		$bins[$name] = $$name = $bin;
117
	}
118
	else
119
	{
120
		$bins[$name] = $$name = false;
121
		error_log("Could not find $name command!");
122
		if (!in_array($name, ['npm','grunt']))
123
		{
124
			exit(1);
125
		}
126
		else
127
		{
128
			error_log("npm and grunt are required to minify JavaScript and CSS files to improve performance.");
129
			break;
130
		}
131
	}
132
}
133
134
if ($verbose) echo "Using following binaries: ".json_encode ($bins, JSON_UNESCAPED_SLASHES)."\n";
135
136
if (!extension_loaded('curl')) die("Required PHP extesion 'curl' missing! You need to install php-curl package.\n\n");
137
138
// check if we are on a git clone
139
$output = array();
140
if (!$git || !file_exists(__DIR__.'/.git') || !is_dir(__DIR__.'/.git') ||
141
	!exec($git.' branch --no-color', $output, $ret) || $ret)
142
{
143
	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...
144
	{
145
		error_log($line);
146
	}
147
	error_log("Could not identify git branch (you need to use git clone or composer create-project --prefer-source --keep-vcs egroupware/egroupware)!");
148
	exit(1);
149
}
150
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...
151
{
152
	if ($line[0] == '*')
153
	{
154
		$branch = substr($line, 2);
155
		// are we on a tag
156
		if (preg_match('/^\(HEAD .* ([0-9.]+)\)$/', $branch, $matches))
157
		{
158
			$branch = $matches[1];
159
		}
160
		break;
161
	}
162
}
163
$channel = 'development';
164
if (preg_match('/^\d+\.\d+(\.\d{8})?/', $branch, $machtes))
165
{
166
	$channel = isset($matches[1]) ? 'release' : 'bugfix';
167
}
168
if ($verbose) echo "Currently using branch: $branch --> $channel channel\n";
169
170
if ($argv)
171
{
172
	$target = array_shift($argv);
173
174
	if ($target === 'release')
175
	{
176
		$target = get_latest_release($use_prerelease);
177
	}
178
	elseif ($target === 'bugfix')
179
	{
180
		$target = (string)(float)get_latest_release($use_prerelease);
181
	}
182
}
183
else
184
{
185
	$target = $branch;
186
187
	// find latest release
188
	if ($channel == 'release')
189
	{
190
		$target = get_latest_release($use_prerelease);
191
	}
192
}
193
194
echo "Updating to: $target\n";
195
196
// Update EGroupware itself and further apps installed via git
197
foreach(scandir(__DIR__) as $dir)
198
{
199
	if ($dir !== '..' && file_exists(__DIR__.'/'.$dir.'/.git') &&
200
		// these apps / dirs are managed by composer, no need to run manual updates
201
		!in_array($dir, ['vendor', 'activesync', 'collabora', 'projectmanager', 'tracker']))
202
	{
203
		$cmd = "cd $dir ; $git stash -q";
204
		// switch message about detached head off for release-channel/tags
205
		if (preg_match('/^\d+\.\d+\.\d{8}/', $target))
206
		{
207
			$cmd .= "; $git config advice.detachedHead false";
208
		}
209
		if ($branch != $target)
210
		{
211
			$cmd .= "; $git checkout $target";
212
		}
213
		// no need to pull for release-channel/tags
214
		if (!preg_match('/^\d+\.\d+\.\d{8}/', $target))
215
		{
216
			$cmd .= "; $git pull --rebase";
217
		}
218
		$cmd .= "; test -z $($git stash list) || echo $git stash pop";
219
		if ($verbose) echo "$cmd\n";
220
		system($cmd);
221
	}
222
}
223
224
// update composer managed dependencies
225
$cmd = $composer.' install';
226
if ($verbose) echo "$cmd\n";
227
system($cmd);
228
229
// update npm dependencies and run grunt to minify javascript and css
230
if ($npm && $grunt)
231
{
232
	$cmd = $npm.' install';
233
	if ($verbose) echo "$cmd\n";
234
	system($cmd);
235
236
	if ($verbose) echo "$grunt\n";
237
	system($grunt);
238
}
239
240
/**
241
 * Get latest release
242
 *
243
 * @param boolean $prerelease =false include releases taged as prerelease
244
 * @param boolean $return_name =true true: just return name, false: full release object
245
 * @return array|string|null null if no release found
246
 */
247
function get_latest_release($prerelease=false, $return_name=true)
248
{
249
	foreach(github_api('/repos/egroupware/egroupware/releases', [], 'GET') as $release)
250
	{
251
		if ($prerelease || $release['prerelease'] === false)
252
		{
253
			return $return_name ? $release['tag_name'] : $release;
254
		}
255
	}
256
	return null;
257
}
258
/**
259
 * Sending a Github API request
260
 *
261
 * @param string $_url url of just path where to send request to (https://api.github.com is added automatic)
262
 * @param string|array $data payload, array get automatic added as get-parameter or json_encoded for POST
263
 * @param string $method ='POST'
264
 * @param string $upload =null path of file to upload, payload for request with $method='FILE'
265
 * @param string $content_type =null
266
 * @throws Exception
267
 * @return array with response
268
 */
269
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...
270
{
271
	global /*$config,*/ $verbose;
272
273
	$url = $_url[0] == '/' ? 'https://api.github.com'.$_url : $_url;
274
	$c = curl_init();
275
	curl_setopt($c, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
276
	//curl_setopt($c, CURLOPT_USERPWD, $config['github_user'].':'.$config['github_token']);
277
	curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
278
	curl_setopt($c, CURLOPT_USERAGENT, basename(__FILE__));
279
	curl_setopt($c, CURLOPT_TIMEOUT, 240);
280
	curl_setopt($c, CURLOPT_FOLLOWLOCATION, true);
281
282 View Code Duplication
	switch($method)
283
	{
284
		case 'POST':
285
			curl_setopt($c, CURLOPT_POST, true);
286
			if (is_array($data)) $data = json_encode($data, JSON_FORCE_OBJECT);
287
			curl_setopt($c, CURLOPT_POSTFIELDS, $data);
288
			break;
289
		case 'GET':
290
			if(count($data)) $url .= '?' . http_build_query($data);
291
			break;
292
		case 'FILE':
293
			curl_setopt($c, CURLOPT_HTTPHEADER, array("Content-type: $content_type"));
294
			curl_setopt($c, CURLOPT_POST, true);
295
			curl_setopt($c, CURLOPT_POSTFIELDS, file_get_contents($upload));
296
			if(count($data)) $url .= '?' . http_build_query($data);
297
			break;
298
		default:
299
			throw new Exception(__FUNCTION__.": Unknown/unimplemented method=$method!");
300
	}
301
	curl_setopt($c, CURLOPT_URL, $url);
302
303 View Code Duplication
	if (is_string($data)) $short_data = strlen($data) > 100 ? substr($data, 0, 100).' ...' : $data;
304 View Code Duplication
	if ($verbose) echo "Sending $method request to $url ".(isset($short_data)&&$method!='GET'?$short_data:'')."\n";
305
306 View Code Duplication
	if (($response = curl_exec($c)) === false)
307
	{
308
		// run failed request again to display response including headers
309
		curl_setopt($c, CURLOPT_HEADER, true);
310
		curl_setopt($c, CURLOPT_RETURNTRANSFER, false);
311
		curl_exec($c);
312
		throw new Exception("$method request to $url failed ".(isset($short_data)&&$method!='GET'?$short_data:''));
313
	}
314
315 View Code Duplication
	if ($verbose) echo (strlen($response) > 200 ? substr($response, 0, 200).' ...' : $response)."\n";
316
317
	curl_close($c);
318
319
	return json_decode($response, true);
320
}
321