├── LICENSE
├── README.md
└── index.php
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Chevereto
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | PHP repo installer
2 | =
3 |
4 |
5 |
6 | ### About this repo
7 | This is web installer file that allows you to easily install any given GitHub repo. It uses pure PHP and it doesn't require you to install GIT on your server, enable PHP shell access or anything extra.
8 |
9 | ### How to use it
10 | 1. Download and edit the `$settings` array that you will find at the beginning of the file
11 | 2. Upload this file to your target destination
12 | 3. Open your website target destination URL and follow up the install process
13 |
14 | By default this installs [Chevereto/Chevereto-Free](https://github.com/Chevereto/Chevereto-Free) but the file includes examples to install Drupal, WordPress, PrestaShop, etc. It can be used to install any public GitHub repo.
15 |
16 | ### Why?
17 | I wanted a hassle free universal PHP alternative for those that wanted to install Chevereto-Free so I figured out that a web installer should be perfect for this. Is all made using standard libraries so it doesn't require anything fancy.
18 |
19 | ### License
20 | Copyright Rodolfo Berríos - Released under MIT License. You can use this freely in any project you want no strings attached.
--------------------------------------------------------------------------------
/index.php:
--------------------------------------------------------------------------------
1 | 'Chevereto/Chevereto-Free',
4 | 'videoId' => 'vDw1K-Spm_E',
5 | 'logoUrl' => 'https://chevereto.com/src/img/chevereto-free-logo-8bits.png',
6 | 'timeout' => array(
7 | 'start' => 10,
8 | 'redirection' => 10
9 | ),
10 | 'redirect' => TRUE,
11 | );
12 | /*
13 | // Example settings for other repos
14 | $settings = array(
15 | 'repo' => 'opencart/opencart',
16 | 'repoPath' => 'upload', // Indicate which repo path to extract
17 | 'videoId' => 'vDw1K-Spm_E',
18 | 'logoUrl' => 'http://www.opencart.com/opencart/application/view/image/logo.png',
19 | 'timeout' => array(
20 | 'start' => 5,
21 | 'redirection' => 5
22 | ),
23 | 'redirect' => FALSE,
24 | );
25 | $settings = array(
26 | 'repo' => 'WordPress/WordPress',
27 | 'videoId' => 'vDw1K-Spm_E',
28 | 'logoUrl' => 'https://s.w.org/about/images/logos/wordpress-logo-notext-rgb.png',
29 | 'timeout' => array(
30 | 'start' => 5,
31 | 'redirection' => 5
32 | ),
33 | 'redirect' => FALSE,
34 | );
35 | $settings = array(
36 | 'repo' => 'drupal/drupal',
37 | 'videoId' => 'vDw1K-Spm_E',
38 | 'logoUrl' => 'https://www.drupal.org/files/druplicon-small.png',
39 | 'timeout' => array(
40 | 'start' => 5,
41 | 'redirection' => 5
42 | ),
43 | 'redirect' => FALSE,
44 | );
45 | $settings = array(
46 | 'repo' => 'PrestaShop/PrestaShop',
47 | 'videoId' => 'vDw1K-Spm_E',
48 | 'logoUrl' => 'http://img-cdn.prestashop.com/logo.png',
49 | 'timeout' => array(
50 | 'start' => 5,
51 | 'redirection' => 5
52 | ),
53 | 'redirect' => FALSE,
54 | );
55 | */
56 | error_reporting(E_ALL ^ E_NOTICE);
57 | define('ROOT_PATH', rtrim(str_replace('\\','/', __DIR__), '/') . '/');
58 | define('ROOT_PATH_RELATIVE', rtrim(dirname($_SERVER['SCRIPT_NAME']), '\/') . '/');
59 | define('HTTP_HOST', $_SERVER['HTTP_HOST']);
60 | define('HTTP_PROTOCOL', ((!empty($_SERVER['HTTPS']) and strtolower($_SERVER['HTTPS']) == 'on') or $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') ? 'https' : 'http');
61 | define('ROOT_URL', HTTP_PROTOCOL . "://".HTTP_HOST . ROOT_PATH_RELATIVE); // http(s)://www.mysite.com/chevereto/
62 | define('SELF', ROOT_PATH . basename(__FILE__));
63 | if (class_exists('ZipArchive')) {
64 | class my_ZipArchive extends ZipArchive {
65 | public function extractSubdirTo($destination, $subdir) {
66 | $errors = array();
67 |
68 | // Prepare dirs
69 | $destination = str_replace(array("/", "\\"), DIRECTORY_SEPARATOR, $destination);
70 | $subdir = str_replace(array("/", "\\"), "/", $subdir);
71 |
72 | if (substr($destination, mb_strlen(DIRECTORY_SEPARATOR, "UTF-8") * -1) != DIRECTORY_SEPARATOR) {
73 | $destination .= DIRECTORY_SEPARATOR;
74 | }
75 |
76 | if (substr($subdir, -1) != "/") {
77 | $subdir .= "/";
78 | }
79 |
80 | // Extract files
81 | for ($i = 0; $i < $this->numFiles; $i++) {
82 | $filename = $this->getNameIndex($i);
83 |
84 | if (substr($filename, 0, mb_strlen($subdir, "UTF-8")) == $subdir) {
85 | $relativePath = substr($filename, mb_strlen($subdir, "UTF-8"));
86 | $relativePath = str_replace(array("/", "\\"), DIRECTORY_SEPARATOR, $relativePath);
87 |
88 | if (mb_strlen($relativePath, "UTF-8") > 0) {
89 | if (substr($filename, -1) == "/") { // Directory
90 | // New dir
91 | if (!is_dir($destination . $relativePath)) {
92 | if (!@mkdir($destination . $relativePath, 0755, true)) {
93 | $errors[$i] = $filename;
94 | }
95 | }
96 | } else {
97 | if (dirname($relativePath) != ".") {
98 | if (!is_dir($destination . dirname($relativePath))) {
99 | // New dir (for file)
100 | @mkdir($destination . dirname($relativePath), 0755, true);
101 | }
102 | }
103 | // New file
104 | if (@file_put_contents($destination . $relativePath, $this->getFromIndex($i)) === false) {
105 | $errors[$i] = $filename;
106 | }
107 | }
108 | }
109 | }
110 | }
111 |
112 | return $errors;
113 | }
114 | }
115 | }
116 | function debug($arguments) {
117 | if(empty($arguments)) return;
118 | echo '
'; 119 | foreach(func_get_args() as $value) { 120 | print_r($value); 121 | } 122 | echo ''; 123 | } 124 | function json_error($args) { 125 | if(func_num_args($args) == 1 and is_object($args)) { 126 | if(method_exists($args, 'getMessage') and method_exists($args, 'getCode')) { 127 | $message = $args->getMessage(); 128 | $code = $args->getCode(); 129 | $context = get_class($args); 130 | error_log($message); // log class errors 131 | } else { 132 | return; 133 | } 134 | } else { 135 | if(func_num_args($args) == 1) { 136 | $message = $args; 137 | $code = NULL; 138 | $context = NULL; 139 | } else { 140 | $message = func_get_arg(0); 141 | $code = func_get_arg(1); 142 | $context = NULL; 143 | } 144 | } 145 | return [ 146 | 'status_code' => 400, 147 | 'error' => [ 148 | 'message' => $message, 149 | 'code' => $code, 150 | 'context' => $context 151 | ] 152 | ]; 153 | } 154 | function json_output($data=[]) { 155 | error_reporting(0); 156 | @ini_set('display_errors', false); 157 | if(ob_get_level() === 0 and !ob_start('ob_gzhandler')) ob_start(); 158 | header('Last-Modified: '.gmdate('D, d M Y H:i:s').'GMT'); 159 | header('Cache-Control: no-cache, must-revalidate'); 160 | header('Pragma: no-cache'); 161 | header('Content-type: application/json; charset=UTF-8'); 162 | 163 | // Invalid json request 164 | if(empty($data)) { 165 | set_status_header(400); 166 | $json_fail = [ 167 | 'status_code' => 400, 168 | 'status_txt' => get_set_status_header_desc(400), 169 | 'error' => [ 170 | 'message' => 'no request data present', 171 | 'code' => NULL 172 | ] 173 | ]; 174 | die(json_encode($json_fail)); 175 | } 176 | 177 | // Populate missing values 178 | if($data['status_code'] && !$data['status_txt']){ 179 | $data['status_txt'] = get_set_status_header_desc($data['status_code']); 180 | } 181 | 182 | $json_encode = json_encode($data); 183 | 184 | if(!$json_encode) { // Json failed 185 | set_status_header(500); 186 | $json_fail = [ 187 | 'status_code' => 500, 188 | 'status_txt' => get_set_status_header_desc(500), 189 | 'error' => [ 190 | 'message' => "data couldn't be encoded into json", 191 | 'code' => NULL 192 | ] 193 | ]; 194 | die(json_encode($json_fail)); 195 | } 196 | set_status_header($data['status_code']); 197 | 198 | print $json_encode; 199 | die(); 200 | } 201 | function set_status_header($code) { 202 | $desc = get_set_status_header_desc($code); 203 | if(empty($desc)) return false; 204 | $protocol = $_SERVER['SERVER_PROTOCOL']; 205 | if('HTTP/1.1' != $protocol && 'HTTP/1.0' != $protocol) $protocol = 'HTTP/1.0'; 206 | $set_status_header = "$protocol $code $desc"; 207 | return @header($set_status_header, true, $code); 208 | } 209 | function get_set_status_header_desc($code) { 210 | $codes_to_desc = array( 211 | 100 => 'Continue', 212 | 101 => 'Switching Protocols', 213 | 102 => 'Processing', 214 | 200 => 'OK', 215 | 201 => 'Created', 216 | 202 => 'Accepted', 217 | 203 => 'Non-Authoritative Information', 218 | 204 => 'No Content', 219 | 205 => 'Reset Content', 220 | 206 => 'Partial Content', 221 | 207 => 'Multi-Status', 222 | 226 => 'IM Used', 223 | 300 => 'Multiple Choices', 224 | 301 => 'Moved Permanently', 225 | 302 => 'Found', 226 | 303 => 'See Other', 227 | 304 => 'Not Modified', 228 | 305 => 'Use Proxy', 229 | 306 => 'Reserved', 230 | 307 => 'Temporary Redirect', 231 | 400 => 'Bad Request', 232 | 401 => 'Unauthorized', 233 | 402 => 'Payment Required', 234 | 403 => 'Forbidden', 235 | 404 => 'Not Found', 236 | 405 => 'Method Not Allowed', 237 | 406 => 'Not Acceptable', 238 | 407 => 'Proxy Authentication Required', 239 | 408 => 'Request Timeout', 240 | 409 => 'Conflict', 241 | 410 => 'Gone', 242 | 411 => 'Length Required', 243 | 412 => 'Precondition Failed', 244 | 413 => 'Request Entity Too Large', 245 | 414 => 'Request-URI Too Long', 246 | 415 => 'Unsupported Media Type', 247 | 416 => 'Requested Range Not Satisfiable', 248 | 417 => 'Expectation Failed', 249 | 422 => 'Unprocessable Entity', 250 | 423 => 'Locked', 251 | 424 => 'Failed Dependency', 252 | 426 => 'Upgrade Required', 253 | 500 => 'Internal Server Error', 254 | 501 => 'Not Implemented', 255 | 502 => 'Bad Gateway', 256 | 503 => 'Service Unavailable', 257 | 504 => 'Gateway Timeout', 258 | 505 => 'HTTP Version Not Supported', 259 | 506 => 'Variant Also Negotiates', 260 | 507 => 'Insufficient Storage', 261 | 510 => 'Not Extended' 262 | ); 263 | if(array_key_exists($code, $codes_to_desc)) { 264 | return $codes_to_desc[$code]; 265 | } 266 | } 267 | function str_replace_last($search, $replace, $subject) { 268 | $pos = strrpos($subject, $search); 269 | if($pos !== false) { 270 | $subject = substr_replace($subject, $replace, $pos, strlen($search)); 271 | } 272 | return $subject; 273 | } 274 | function random_string($length) { 275 | switch(true) { 276 | case function_exists('random_bytes') : 277 | $r = random_bytes($length); 278 | break; 279 | case function_exists('openssl_random_pseudo_bytes') : 280 | $r = openssl_random_pseudo_bytes($length); 281 | break; 282 | case is_readable('/dev/urandom') : // deceze 283 | $r = file_get_contents('/dev/urandom', false, null, 0, $length); 284 | break; 285 | default : 286 | $i = 0; 287 | $r = ''; 288 | while($i ++ < $length) { 289 | $r .= chr(mt_rand(0, 255)); 290 | } 291 | break; 292 | } 293 | return substr(bin2hex($r), 0, $length); 294 | } 295 | function fetch_url($url, $file=NULL) { 296 | if(!$url) { 297 | throw new Exception('missing $url in ' . __FUNCTION__); 298 | } 299 | if(ini_get('allow_url_fopen') !== 1 && !function_exists('curl_init')) { 300 | throw new Exception("Fatal error in " .__FUNCTION__. ": cURL isn't installed and allow_url_fopen is disabled. Can't perform HTTP requests."); 301 | } 302 | 303 | if(ini_get('allow_url_fopen') !== 1 && !function_exists('curl_init')) { 304 | throw new Exception("Fatal error in " .__FUNCTION__. ": cURL isn't installed and allow_url_fopen is disabled. Can't perform HTTP requests."); 305 | } 306 | 307 | // File get contents is the failover fn 308 | $fn = (!function_exists('curl_init') ? 'fgc' : 'curl'); 309 | 310 | $content_disposition_regex = '/\bContent-Disposition:.*filename=(?:["\']?)(.*)(?:["\']?)\b/i'; 311 | 312 | if(is_dir($file)) { 313 | $dir = rtrim($file, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; 314 | $filename = 'download_' . random_string(8) . '-' . time(); 315 | $file = $dir . $filename; 316 | } 317 | 318 | if($fn == 'curl') { 319 | $ch = curl_init(); 320 | curl_setopt($ch, CURLOPT_URL, $url); 321 | curl_setopt($ch, CURLOPT_USERAGENT, 'Chevereto/php-repo-installer'); 322 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); 323 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 324 | curl_setopt($ch, CURLOPT_AUTOREFERER, 1); 325 | curl_setopt($ch, CURLOPT_TIMEOUT, 120); 326 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); 327 | curl_setopt($ch, CURLOPT_HEADER, 0); 328 | curl_setopt($ch, CURLOPT_FAILONERROR, 0); 329 | curl_setopt($ch, CURLOPT_ENCODING, 'gzip'); // this needs zlib output compression enabled (php) 330 | curl_setopt($ch, CURLOPT_VERBOSE, 0); 331 | 332 | if($file) { 333 | if(isset($dir)) { 334 | $header_filename = $filename . '-header.txt'; 335 | $header_file = $dir . $header_filename; 336 | $header_out = fopen($header_file, 'w'); 337 | curl_setopt($ch, CURLOPT_WRITEHEADER, $header_out); 338 | } 339 | // Save the file to $file destination 340 | $out = @fopen($file, 'wb'); 341 | if(!$out) { 342 | throw new Exception("Can't open " . __FUNCTION__ . "() file for read and write"); 343 | } 344 | curl_setopt($ch, CURLOPT_FILE, $out); 345 | $result = @curl_exec($ch); 346 | if(is_resource($out)) { 347 | @fclose($out); 348 | } 349 | if(is_resource($header_out)) { 350 | @fclose($header_out); 351 | } 352 | if(is_file($header_file)) { 353 | if(isset($dir)) { 354 | $headers = @file_get_contents($header_file); 355 | if($headers && preg_match($content_disposition_regex, $headers, $matches)) { 356 | $downloaded_file = $dir . $matches[1]; 357 | @rename($file, $downloaded_file); 358 | } 359 | } 360 | @unlink($header_file); 361 | } 362 | return $downloaded_file; // file || null 363 | } else { 364 | // Return the file string 365 | $file_get_contents = @curl_exec($ch); 366 | } 367 | if(curl_errno($ch)) { 368 | $curl_error = curl_error($ch); 369 | curl_close($ch); 370 | throw new Exception('Curl error: ' . $curl_error); 371 | } 372 | if($file == NULL) { 373 | curl_close($ch); 374 | return $file_get_contents; 375 | } 376 | } else { 377 | $context = stream_context_create([ 378 | 'http' => [ 379 | 'ignore_errors' => TRUE, 380 | 'method' => 'GET', 381 | 'header' => 'User-agent: Chevereto/php-repo-installer' . "\r\n" 382 | ], 383 | ]); 384 | $result = file_get_contents($url, FALSE, $context); 385 | if(!$result) { 386 | throw new Exception("file_get_contents: can't fetch target URL"); 387 | } 388 | if($file) { 389 | if(isset($dir)) { 390 | // Get file content-disposition header 391 | foreach($http_response_header as $header) { 392 | if(preg_match($content_disposition_regex, $header, $matches)) { 393 | $file = $dir . $matches[1]; 394 | break; 395 | } 396 | } 397 | } 398 | if(file_put_contents($file, $result) === FALSE) { 399 | throw new Exception("file_put_contents: can't fetch target URL"); 400 | } 401 | return $file; 402 | } else { 403 | return $result; 404 | } 405 | } 406 | } 407 | try { 408 | error_reporting(0); 409 | if(isset($_REQUEST['action'])) { 410 | set_time_limit(600); // Allow up to five minutes... 411 | $temp_dir = ROOT_PATH; 412 | // Detect writting permissions 413 | if(!is_writable($temp_dir)) { 414 | throw new Exception(sprintf("Can't write into %s path", $temp_dir)); 415 | } 416 | if(!is_writable(SELF)) { 417 | throw new Exception(sprintf("Can't write into %s file", $temp_dir)); 418 | } 419 | switch($_REQUEST['action']) { 420 | case 'download': 421 | $zipball_url = 'https://api.github.com/repos/' . $settings['repo'] . '/zipball'; 422 | $download = fetch_url($zipball_url, __DIR__); 423 | if($download === FALSE || is_null($download)) { 424 | throw new Exception(sprintf("Can't fetch %s from GitHub (fetch_url)", $settings['repo']), 400); 425 | } 426 | $zip_local_filename = str_replace_last('.zip', '_' . random_string(8) . time() . '.zip', $download); 427 | @rename($download, $zip_local_filename); 428 | $json_array = [ 429 | 'success' => [ 430 | 'message' => 'Download completed', 431 | 'code' => 200 432 | ], 433 | 'download' => [ 434 | 'filename' => basename($zip_local_filename) 435 | ] 436 | ]; 437 | break; 438 | case 'extract': 439 | $error_catch = []; 440 | foreach(['ZipArchive', 'RecursiveIteratorIterator', 'RecursiveDirectoryIterator'] as $k => $v) { 441 | if(!class_exists($v)) { 442 | $error_catch[] = strtr('%c class [http://php.net/manual/en/class.%l.php] not found', [ 443 | '%c' => $v, 444 | '%l' => strtolower($v) 445 | ]); 446 | } 447 | } 448 | if($error_catch) { 449 | throw new Exception(join("