<?php namespace gp\install; class Installer{ /** * Whether or not installation checks have passed * 2 = All checks have passed * 1 = One or more checks passed with partial availability * 0 = One or more checks failed but have common solutions * -1 = One or more checks failed * * @var int */ private $can_install = 2; public $can_write_data = true; public $ftp_root = false; public $root_mode; private $lang = 'en'; public $statuses = []; private $ftp_connection; public function __construct(){ global $languages; //language preferences if( isset($_GET['lang']) && isset($languages[$_GET['lang']]) ){ $this->lang = $_GET['lang']; setcookie('lang',$this->lang); }elseif( isset($_COOKIE['lang']) && isset($languages[$_COOKIE['lang']]) ){ $this->lang = $_COOKIE['lang']; } \gp\tool::GetLangFile('main.inc',$this->lang); // installation checks $this->CheckDataFolder(); $this->CheckPHPVersion(); $this->CheckEnv(); $this->CheckMemory(); $this->CheckImages(); $this->CheckArchives(); $this->CheckPath(); $this->CheckIndexHtml(); } public function Run(){ global $langmessage; echo '<h1>'; echo $langmessage['Installation']; echo ' - v'.\gpversion; echo '</h1>'; $installed = false; $cmd = \gp\tool::GetCommand(); switch($cmd){ case 'Continue': $this->FTP_Prepare(); break; case 'Install': $installed = $this->Install_Normal(); break; } if( !$installed ){ $this->LanguageForm(); $this->DisplayStatus(); }else{ $this->Installed(); } } /** * Display check statuses * */ public function DisplayStatus(){ global $langmessage; echo '<h2>'.$langmessage['Checking_server'].'...</h2>'; echo '<table class="styledtable fullwidth">'; echo '<thead>'; echo '<tr>'; echo '<th>'.$langmessage['Checking'].'...</th>'; echo '<th>'.$langmessage['Status'].'</th>'; echo '<th>'.$langmessage['Current_Value'].'</th>'; echo '<th>'.$langmessage['Expected_Value'].'</th>'; echo '</tr>'; echo '</thead>'; echo '<tbody>'; foreach($this->statuses as $row){ echo '<tr><td>'; echo $row['checking']; echo '</td>'; if( $row['can_install'] === 2 ){ $class = 'passed'; $label = 'Passed'; }elseif( $row['can_install'] === 1 ){ $class = 'passed_orange'; $label = 'Passed'; }elseif( $row['can_install'] === 0 ){ $class = 'passed_orange'; $label = 'Failed'; }else{ $class = 'passed_orange'; $label = 'Failed'; } if( !empty($row['label']) ){ $label = $row['label']; } echo '<td class="'.$class.'">'.$label.'</td>'; echo '<td class="'.$class.'">'.$row['curr_value'].'</td>'; echo '<td>'.$row['expected'].'</td>'; echo '</tr>'; } echo '</tbody>'; echo '</table>'; echo '<p>'; echo \gp\tool::Link('',$langmessage['Refresh']); echo '</p>'; echo '<br/>'; if( $this->can_install > 0 ){ $this->Form_Entry(); return; } if( !$this->can_write_data ){ $this->Form_Permissions(); }else{ echo '<h3>'.$langmessage['Notes'].'</h3>'; echo '<div>'; echo $langmessage['Install_Conflict']; echo '</div>'; echo '<p>'; echo sprintf($langmessage['Install_Fix'],''); echo '</p>'; } } public function SetStatus($checking, $can_install, $curr_value, $expected = '', $status_label = ''){ if( $can_install < $this->can_install ){ $this->can_install = $can_install; } $this->statuses[] = [ 'checking' => $checking, 'can_install' => $can_install, 'curr_value' => $curr_value, 'expected' => $expected, 'label' => $status_label ]; } /** * Check the data folder to see if it's writable * */ public function CheckDataFolder(){ global $dataDir,$langmessage; $folder = $dataDir.'/data'; if( strlen($folder) > 33 ){ $show = '...'.substr($folder,-30); }else{ $show = $folder; } $status = null; $class = 'passed'; $can_install = 2; if( !is_dir($folder)){ if( !@mkdir($folder) ){ $status = $langmessage['See_Below'].' (0)'; $this->can_write_data = false; } }elseif( !gp_is_writable($folder) ){ $status = $langmessage['See_Below'].' (1)'; $this->can_write_data = false; } if( $this->can_write_data ){ $current = $langmessage['Writable']; }else{ $current = $langmessage['Not Writable']; $can_install = 0; } $this->SetStatus( $show, $can_install, $current, $langmessage['Writable'], $status); } /** * Check the php version * */ private function CheckPHPVersion(){ global $langmessage; $version = phpversion(); $can_install = 2; if( version_compare($version,'7.3','<') ){ $can_install = -1; } $this->SetStatus($langmessage['PHP_Version'], $can_install, $version, '7.3+'); } /** * Check the env for server variables * */ private function CheckEnv(){ global $langmessage; //make sure $_SERVER['SCRIPT_NAME'] is set $checking = '<a href="https://www.php.net/manual/reserved.variables.server.php" target="_blank">SCRIPT_NAME or PHP_SELF</a>'; $can_install = 2; $expected = $langmessage['Set']; $curr = $langmessage['Set']; if( !\gp\tool::GetEnv('SCRIPT_NAME','index.php') && !\gp\tool::GetEnv('PHP_SELF','index.php') ){ $curr = $langmessage['Not_Set']; $can_install = -1; } $this->SetStatus($checking, $can_install, $curr, $expected); } /** * Check php's memory limit * LESS compilation uses a fair amount of memory */ private function CheckMemory(){ $checkValue = ini_get('memory_limit'); $expected = '16M+ or Adjustable'; $checking = '<a href="https://php.net/manual/ini.core.php#ini.memory-limit" target="_blank">Memory Limit</a>'; // adjustable if( @ini_set('memory_limit','96M') !== false ){ $this->SetStatus( $checking, 2, $checkValue .' and adjustable', $expected); return; } // cant check memory if( !$checkValue ){ $this->SetStatus( $checking, 1, '???', $expected); return; } $byte_value = \gp\tool::getByteValue($checkValue); $mb_16 = \gp\tool::getByteValue('16M'); if( $byte_value > 100663296 ){ $this->SetStatus( $checking, 2, $checkValue, $expected); }elseif( $byte_value >= $mb_16 ){ $this->SetStatus( $checking, 1, $checkValue, $expected); }else{ $this->SetStatus( $checking, 0, $checkValue, $expected); } } /** * Very unlikely, ".php" cannot be in the directory name. see SetGlobalPaths() * */ public function CheckPath(){ global $langmessage; $dir = dirname(__FILE__); $checking = 'Install Directory'; $curr = str_replace('.php','<b>.php</b>',$dir); if( strpos($dir,'.php') === false ){ $this->SetStatus( $checking, 2, $curr ); return; } $this->SetStatus( $checking, 0, $curr , 'Rename your file structure so that directories do not use ".php".'); } /** * Warn user if there's an index.html file * */ public function CheckIndexHtml(){ global $langmessage, $dataDir; $show = 'Existing index.html'; $index = $dataDir.'/index.html'; if( file_exists($index) ){ $this->SetStatus( $show, 1, $index, $langmessage['index.html exists']); }else{ $this->SetStatus( $show, 2, '', ''); } } /** * Check for image manipulation functions * */ public function CheckImages(){ global $langmessage; $supported = array(); if( function_exists('imagetypes') ){ $supported_types = imagetypes(); if( $supported_types & IMG_JPG ){ $supported[] = 'jpg'; } if( $supported_types & IMG_PNG){ $supported[] = 'png'; } if( $supported_types & IMG_WBMP){ $supported[] = 'bmp'; } if( $supported_types & IMG_GIF){ $supported[] = 'gif'; } if( defined('IMG_WEBP') && ($supported_types & IMG_WEBP) ){ $supported[] = 'webp'; } } $checking = '<a href="https://www.php.net/manual/en/book.image.php" target="_blank">'.$langmessage['image_functions'].'</a>'; $supported_string = implode(', ',$supported); if( count($supported) >= 4 ){ $this->SetStatus( $checking, 2, $supported_string); }elseif( count($supported) > 0 ){ $this->SetStatus( $checking, 1, $supported_string,'',$langmessage['partially_available']); }else{ $this->SetStatus( $checking, 1, $supported_string,'',$langmessage['unavailable']); } } /** * Check for archive processing capabilities * */ public function CheckArchives(){ global $langmessage; $supported = array(); if( class_exists('\ZipArchive') ){ $supported['zip'] = 'zip'; } if( class_exists('\PharData') ){ if( !defined('HHVM_VERSION') || !ini_get('phar.readonly') ){ if( function_exists('gzopen') ){ $supported['tgz'] = 'gzip'; } if( function_exists('bzopen') ){ $supported['tbz'] = 'bzip'; } $supported['tar'] = 'tar'; } } $checking = '<a href="https://www.php.net/manual/en/refs.compression.php" target="_blank">Archive Extensions</a>'; $supported_string = implode(', ',$supported); if( count($supported) == 4 ){ $this->SetStatus( $checking, 2, $supported_string); }elseif( count($supported) > 0 ){ $this->SetStatus( $checking, 1, $supported_string, '', $langmessage['partially_available'] ); }else{ $this->SetStatus( $checking, 1, $supported_string, '', $langmessage['unavailable'] ); } } /** * Change the permissions of the data directory * */ public function FTP_Prepare(){ global $langmessage; echo '<h2>'.$langmessage['Using_FTP'].'...</h2>'; echo '<ul>'; $this->_FTP_Prepare(); $this->FTP_RestoreMode(); echo '</ul>'; } public function _FTP_Prepare(){ if( !$this->FTPConnection() ){ return; } if( $this->FTP_DataFolder() ){ return; } } /** * Create the data folder with appropriate permissions * * 1) make parent directory writable * 2) delete existing /data folder * 3) create /data folder with php's mkdir() (so it has the correct owner) * 4) restore parent directory * */ public function FTP_DataFolder(){ global $dataDir, $langmessage; $this->root_mode = fileperms($dataDir); if( !$this->root_mode ){ return false; } // (1) $modDir = ftp_site($this->ftp_connection, 'CHMOD 0777 '. $this->ftp_root ); if( !$modDir ){ echo '<li><span class="failed">'; echo sprintf($langmessage['Could_Not_'],'<em>CHMOD 0777 '. $this->ftp_root.'</em>'); echo '</span></li>'; return false; } // (2) use rename instead of trying to delete recursively $php_dir = $dataDir.'/data'; $php_del = false; if( file_exists($php_dir) ){ $ftp_dir = rtrim($this->ftp_root,'/').'/data'; $del_name = '/data-delete-'.rand(0,10000); $ftp_del = rtrim($this->ftp_root,'/').$del_name; $php_del = $dataDir.$del_name; $changed = ftp_rename($this->ftp_connection, $ftp_dir , $ftp_del ); if( !$changed ){ echo '<li><span class="failed">'; echo sprintf($langmessage['Could_Not_'],'<em>Remove '. $this->ftp_root.'/data</em>'); echo '</span></li>'; return false; } } // (3) use rename instead of trying to delete recursively $mode = 0755; if( defined(gp_chmod_dir) ){ $mode = gp_chmod_dir; } if( !mkdir($php_dir,$mode) ){ echo '<li><span class="failed">'; echo sprintf($langmessage['Could_Not_'],'<em>mkdir('.$php_dir.')</em>'); echo '</span></li>'; return false; } // (4) will be done afterwards // make sure it's writable ? clearstatcache(); if( !gp_is_writable($php_dir) ){ return false; } echo '<li><span class="passed"><b>'; echo $langmessage['Success_continue_below']; echo '</b></span></li>'; if( $php_del ){ $this->CopyData($php_del, $php_dir); } return true; } /** * Copy files from the "deleted" data folder to the new data folder * */ public function CopyData($from_dir, $to_dir){ $files = scandir($from_dir); foreach($files as $file){ if( $file === '..' || $file === '.' ){ continue; } $from = $from_dir.'/'.$file; //no directories if( is_dir($from) ){ continue; } $to = $to_dir.'/'.$file; copy($from,$to); } } /** * Restore the mode of the root directory to it's original mode * */ public function FTP_RestoreMode(){ global $langmessage; if( !$this->root_mode || !$this->ftp_connection ){ return; } $mode = $this->root_mode & 0777; $mode = '0'.decoct($mode); $ftp_cmd = 'CHMOD '.$mode.' '.$this->ftp_root; if( !ftp_site($this->ftp_connection, $ftp_cmd ) ){ echo '<li><span class="failed">'; echo sprintf($langmessage['Could_Not_'],'<em>Restore mode for '. $this->ftp_root.': '.$ftp_cmd.'</em>'); echo '</span></li>'; return; } } /** * Establish an FTP connection to be used by the installer * */ public function FTPConnection(){ global $dataDir, $langmessage; //test for functions if( !function_exists('ftp_connect') ){ echo '<li><span class="failed">'; echo $langmessage['FTP_UNAVAILABLE']; echo '</span></li>'; return false; } //Try to connect $this->ftp_connection = @ftp_connect($_POST['ftp_server'],21,6); if( !$this->ftp_connection ){ echo '<li><span class="failed">'; echo sprintf($langmessage['FAILED_TO_CONNECT'],'<em>'.htmlspecialchars($_POST['ftp_server']).'</em>'); echo '</span></li>'; return false; } echo '<li><span class="passed">'; echo sprintf($langmessage['CONNECTED_TO'],'<em>'.htmlspecialchars($_POST['ftp_server']).'</em>'); echo '</span></li>'; //Log in $login_result = @ftp_login($this->ftp_connection, $_POST['ftp_user'], $_POST['ftp_pass']); if( !$login_result ){ echo '<li><span class="failed">'; echo sprintf($langmessage['NOT_LOOGED_IN'],'<em>'.htmlspecialchars($_POST['ftp_user']).'</em>'); echo '</span></li>'; return false; } echo '<li><span class="passed">'; echo sprintf($langmessage['LOGGED_IN'],'<em>'.htmlspecialchars($_POST['ftp_user']).'</em>'); echo '</span></li>'; //Get FTP Root $this->ftp_root = \gp\tool\FileSystemFtp::GetFTPRoot($this->ftp_connection,$dataDir); if( $this->ftp_root === false ){ echo '<li><span class="failed">'; echo $langmessage['ROOT_DIRECTORY_NOT_FOUND']; echo '</span></li>'; return false; } echo '<li><span class="passed">'; echo sprintf($langmessage['FTP_ROOT'],'<em>'.$this->ftp_root.'</em>'); echo '</span></li>'; return true; } public function Form_Permissions(){ global $langmessage,$dataDir; echo '<div>'; echo '<h2>'.$langmessage['Changing_File_Permissions'].'</h2>'; echo '<p>'; echo $langmessage['REFRESH_AFTER_CHANGE']; echo '</p>'; echo '<table class="styledtable fullwidth">'; //manual method echo '<tr><th>'; echo $langmessage['manual_method']; echo '</th></tr>'; echo '<tr><td><p>'; echo $langmessage['LINUX_CHOWN']; echo '</p>'; $owner = $this->GetPHPOwner(); if( is_null($owner) ){ echo '<tt class="code">chown ?? "'.$dataDir.'/data"</tt>'; echo '<small>Replace ?? with the owner uid of PHP on your server</small>'; }else{ echo '<tt class="code">chown '.$owner.' "'.$dataDir.'/data"</tt>'; echo '<small>Note: "'.$owner.'" appears to be the owner uid of PHP on your server</small>'; } echo '<p><a href="">'.$langmessage['Refresh'].'</a></p>'; echo '</td></tr>'; //ftp echo '<tr><th>FTP</th></tr>'; echo '<tr><td><p>'; echo $langmessage['MOST_FTP_CLIENTS']; echo '</p>'; echo '<p>Using your FTP client, we recommend the following steps to make the data directory writable</p>'; echo '<ol>'; echo '<li>Make "'.$dataDir.'" writable</li>'; echo '<li>Delete "'.$dataDir.'/data"</li>'; echo '<li>Run '.\CMS_NAME.' Installer by refreshing this page</li>'; echo '<li>Restore the permissions of "'.$dataDir.'"</li>'; echo '</ol>'; echo '</td></tr>'; // if( function_exists('ftp_connect') ){ echo '<tr><th>'; echo $langmessage['Installer']; echo '</th></tr>'; echo '<tr><td>'; echo '<p>'; echo $langmessage['FTP_CHMOD']; echo '</p>'; $this->Form_FTPDetails(); echo '</td></tr>'; } echo '</table>'; echo '</div>'; } /** * Attempt to get the owner of php * */ public function GetPHPOwner(){ global $dataDir; if( !function_exists('fileowner') ){ return; } $name = tempnam( sys_get_temp_dir(), 'gpinstall-' ); if( !$name ){ return; } return fileowner($name); } public function Form_FTPDetails(){ global $langmessage; $_POST += array('ftp_server'=>\gp\tool\FileSystemFtp::GetFTPServer(),'ftp_user'=>''); echo '<form action="'.\gp\tool::GetUrl('').'" method="post">'; echo '<table class="padded_table">'; echo '<tr><td align="left">'.$langmessage['FTP_Server'].' </td><td>'; echo '<input type="text" class="text" size="20" name="ftp_server" value="'. htmlspecialchars($_POST['ftp_server']) .'" required />'; echo '</td></tr>'; echo '<tr><td align="left">'.$langmessage['FTP_Username'].' </td><td>'; echo '<input type="text" class="text" size="20" name="ftp_user" value="'. htmlspecialchars($_POST['ftp_user']) .'" />'; echo '</td></tr>'; echo '<tr><td align="left">'.$langmessage['FTP_Password'].' </td><td>'; echo '<input type="password" class="text" size="20" name="ftp_pass" value="" />'; echo '</td></tr>'; echo '<tr><td align="left"> </td><td>'; echo '<input type="hidden" name="cmd" value="Continue" />'; echo '<input type="submit" class="submit" name="aaa" value="'.$langmessage['continue'].'" />'; echo '</td></tr>'; echo '</table>'; echo '</form>'; } public function LanguageForm(){ global $languages; echo '<div class="lang_select">'; echo '<form action="'.\gp\tool::GetUrl('').'" method="get">'; echo '<select name="lang" onchange="this.form.submit()">'; foreach($languages as $lang => $label){ if( $lang === $this->lang ){ echo '<option value="'.$lang.'" selected="selected">'; }else{ echo '<option value="'.$lang.'">'; } echo ' '.$label.' ('.$lang.')'; echo '</option>'; } echo '</select>'; echo '<div class="sm">'; echo '<a href="https://github.com/Typesetter/Typesetter/tree/master/include/languages" target="_blank">Help translate '.\CMS_NAME.'</a>'; echo '</div>'; echo '</form>'; echo '</div>'; } public function Installed(){ global $langmessage; echo '<h4>'.$langmessage['Installation_Was_Successfull'].'</h4>'; echo '<h2>'; echo \gp\tool::Link('',$langmessage['View_your_web_site']); echo '</h2>'; echo '</ul>'; echo '<p>'; echo 'For added security, you may delete the /include/install/install.php file from your server.'; echo '</p>'; } public function Form_Entry(){ global $langmessage; echo '<form action="'.\gp\tool::GetUrl('').'" method="post">'; echo '<table class="styledtable">'; \gp\install\Tools::Form_UserDetails(); \gp\install\Tools::Form_Configuration(); echo '</table>'; echo '<p>'; echo '<input type="hidden" name="cmd" value="Install" />'; echo '<input type="submit" class="submit install_button" name="aaa" value="'.$langmessage['Install'].'" />'; echo '</p>'; echo '</form>'; } public function Install_Normal(){ global $langmessage; echo '<h2>'.$langmessage['Installing'].'</h2>'; echo '<ul class="install_status">'; $config = []; $config['language'] = $this->lang; $success = false; if( \gp\install\Tools::gpInstall_Check() ){ $success = \gp\install\Tools::Install_DataFiles_New(false, $config); } echo '</ul>'; return $success; } }