├── 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("
", $error_catch), 100); 450 | } 451 | 452 | $repo_canonized_name = str_replace('/', '-', $settings['repo']); 453 | 454 | $zip_file = $temp_dir . $_REQUEST['file']; 455 | 456 | $explode_base = explode('_', $_REQUEST['file']); 457 | $expode_sub = explode('-', $explode_base[0]); 458 | $etag_short = end($expode_sub); 459 | 460 | // To be honest I don't know why GitHub prefix a "g" and sometimes prefix an "e" 461 | if($etag_short[0] == 'g') { 462 | $etag_short = substr($etag_short, 1); 463 | } 464 | 465 | if(empty($etag_short)) { 466 | throw new Exception("Can't detect zipball short etag"); 467 | } 468 | 469 | // Test .zip 470 | if(!is_readable($zip_file)) { 471 | throw new Exception('Missing '.$zip_file.' file', 400); 472 | } 473 | // Unzip .zip 474 | $zip = new my_ZipArchive; 475 | if ($zip->open($zip_file) === TRUE) { 476 | $folder = $repo_canonized_name . '-' . $etag_short . '/'; 477 | if(!empty($settings['repoPath'])) { 478 | $folder .= $settings['repoPath']; 479 | } 480 | $zip->extractSubdirTo($temp_dir, $folder); 481 | $zip->close(); 482 | @unlink($zip_file); 483 | } else { 484 | throw new Exception(strtr("Can't extract %f (%m)", ['%f' => $zip_file, '%m' => 'Zip open error']), 401); 485 | } 486 | $json_array['success'] = ['message' => 'OK', 'code' => 200]; 487 | break; 488 | } 489 | // Inject any missing status_code 490 | if(isset($json_array['success']) && !isset($json_array['status_code'])) { 491 | $json_array['status_code'] = 200; 492 | } 493 | $json_array['request'] = $_REQUEST; 494 | json_output($json_array); 495 | } 496 | } catch(Exception $e) { 497 | $json_array = json_error($e); 498 | $json_array['request'] = $_REQUEST; 499 | json_output($json_array); 500 | } 501 | ?> 502 | 503 | 504 | 505 | 506 | <?php echo $settings['repo']; ?> Installer 507 | 508 | 509 | 662 | 663 | 664 | 665 |
666 | 667 | 668 |
669 | 670 |
PHP GITHUB REPO INSTALLER v1.4 671 | https://github.com/Chevereto/php-repo-installer 672 | -- 673 | 674 | This script will install in 675 | To use another path close this tab and move this file somewhere else. 676 | 677 |
678 | 679 | 680 | 681 | 871 | 872 | --------------------------------------------------------------------------------