<?php
/*********************************************************************************
 *
 * TimeTrex is a Workforce Management program developed by
 * TimeTrex Software Inc. Copyright (C) 2003 - 2021 TimeTrex Software Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by
 * the Free Software Foundation with the addition of the following permission
 * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
 * WORK IN WHICH THE COPYRIGHT IS OWNED BY TIMETREX, TIMETREX DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
 * details.
 *
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program; if not, see http://www.gnu.org/licenses or write to the Free
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 *
 *
 * You can contact TimeTrex headquarters at Unit 22 - 2475 Dobbin Rd. Suite
 * #292 West Kelowna, BC V4T 2E9, Canada or at email address info@timetrex.com.
 *
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * "Powered by TimeTrex" logo. If the display of the logo is not reasonably
 * feasible for technical reasons, the Appropriate Legal Notices must display
 * the words "Powered by TimeTrex".
 *
 ********************************************************************************/


/**
 * @package Other\SystemDiagnostic
 */
class SystemDiagnostic {

	private $cli_mode = false;
	private $progress_bar_obj = null;
	private $api_message_id = null;
	private $curl_last_progress = null; //Progress of last time CURL reported.
	private $curl_progress_start = null; //Time CURL transfer started.

	/**
	 * @param $toggle
	 * @return bool
	 */
	static function setSystemDiagnostic( $toggle ) {
		$install_obj = new Install();
		$install_obj->writeConfigFile( [ 'debug' => [ 'enable' => $toggle, 'enable_log' => $toggle, 'verbosity' => 10 ] ] );

		return true;
	}

	/**
	 * @param $c_obj
	 * @param $cli_mode
	 * @return bool
	 */
	function uploadSystemDiagnostic( $c_obj, $cli_mode ) {
		$install_obj = new Install();
		if ( $install_obj->checkDiskSpace() !== 0 ) {
			Debug::Text( 'ERROR: Not enough disk space.', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		$this->cli_mode = $cli_mode;

		//Set script execution time to 24 hours. Users may have slow upload speed and large log files.
		set_time_limit( 86400 );

		global $config_vars;
		if ( !isset( $config_vars['cache']['dir'] ) ) { //Just in case the cache directory is not set.
			$config_vars['cache']['dir'] = Environment::getBasePath();
		}
		$temp_dir = $config_vars['cache']['dir'] . DIRECTORY_SEPARATOR . 'system_diagnostics' . DIRECTORY_SEPARATOR;
		//Check if tempdir is writable.
		if ( $install_obj->checkWritableCacheDirectory() !== 0 ) {
			Debug::Text( 'ERROR: Cache directory is not writable.', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		//Create lock file so that this function cannot be ran more than once simultaneously.
		$lock_file = new LockFile( $config_vars['cache']['dir'] . DIRECTORY_SEPARATOR . 'system_diagnostic.lock' );

		if ( $lock_file->exists() == false ) {
			if ( $lock_file->create() == true ) {
				Debug::text( 'System diagnostic lock file created. Starting process of gathering system logs.', __FILE__, __LINE__, __METHOD__, 10 );

				$this->getProgressBarObject()->start( $this->getAPIMessageID(), 104, null, TTi18n::getText( 'Starting Upload Process...' ) ); //104 = 4 steps plus 100 for upload progress.

				$this->cleanUpTempDir( $temp_dir, false );

				$registration_key = SystemSettingFactory::getSystemSettingValueByKey( 'registration_key' );
				$zip_name = Misc::sanitizeFileName( $c_obj->getName() . '-' . date( 'Ymd-Hmi' ) .'-'. ( ( $registration_key != '' ) ? $registration_key : '0000000000000000000000000000000000000000' ) );
				$zip_file = $temp_dir . $zip_name . '.zip';

				if ( OPERATING_SYSTEM === 'WIN' ) {
					$apache_log_name = Environment::getBasePath() . '..' . DIRECTORY_SEPARATOR . 'apache2'. DIRECTORY_SEPARATOR . 'log' . DIRECTORY_SEPARATOR .'error.log';
				} else {
					$apache_log_name = '/var/log/apache2/error.log';
				}

				if ( OPERATING_SYSTEM === 'WIN' ) {
					$installer_log_name = Environment::getBasePath() . '..' . DIRECTORY_SEPARATOR .'install.log';
				} else {
					$installer_log_name = null;
				}

				if ( OPERATING_SYSTEM === 'WIN' ) {
					$sql_log_name = Environment::getBasePath() . '..' . DIRECTORY_SEPARATOR .'upgrade_sql_error_timetrex.log';
				} else {
					$sql_log_name = null;
				}

				$timetrex_log_name = $config_vars['path']['log'] . DIRECTORY_SEPARATOR . 'timetrex.log';

				//Using file_exists instead of is_dir as file_exists checks for both files and directories incase of name collision.
				if ( !file_exists( $temp_dir ) ) {
					mkdir( $temp_dir );
				}

				if ( $this->cli_mode === true ) {
					echo "Collecting Log Files...\n";
				} else {
					$this->getProgressBarObject()->set( $this->getAPIMessageID(), 1, TTi18n::getText( 'Collecting Log Files...' ) );
				}

				$zip = new ZipArchive;
				if ( $zip->open( $zip_file, ZipArchive::CREATE ) === true ) {
					//Create phpInfo() log.
					ob_start();
					phpinfo();
					$php_info = ob_get_contents();
					ob_end_clean();
					//Remove HTML formatting.
					$php_info = strip_tags( $php_info );
					//Remove CSS styling from top of output.
					$php_info = strstr( $php_info, 'PHP Version' );
					$zip->addFromString( 'php_info.log', $php_info );

					global $db;

					//Create system_info.log
					$system_info = 'Operating System: '. php_uname() . PHP_EOL;
					$system_info .= 'PostgreSQL: ' . Debug::varDump( $db->ServerInfo( true ) ) . PHP_EOL;
					$system_info .= 'Total Disk Space: ' . round( disk_total_space( dirname( __FILE__ ) ) / 1024 / 1024 / 1024 ) . 'gb Free Space: ' . round( disk_free_space( dirname( __FILE__ ) ) / 1024 / 1024 / 1024 ) . 'gb' . PHP_EOL;
					$zip->addFromString( 'system_info.log', $system_info );

					//Zip apache log files.
					if ( file_exists( $apache_log_name ) ) {
						$zip->addFile( $apache_log_name, basename( $apache_log_name ) );
					} else {
						Debug::Text( 'Unable to locate Apache log files. May be caused by permission issues, or it doesn\'t exist: ' . $apache_log_name, __FILE__, __LINE__, __METHOD__, 10 );
					}

					if ( file_exists( $installer_log_name ) ) {
						$zip->addFile( $installer_log_name, basename( $installer_log_name ) );
					} else {
						Debug::Text( 'Unable to locate Windows Installer log files. May be caused by permission issues, or it doesn\'t exist: ' . $installer_log_name, __FILE__, __LINE__, __METHOD__, 10 );
					}

					if ( file_exists( $sql_log_name ) ) {
						$zip->addFile( $sql_log_name, basename( $sql_log_name ) );
					} else {
						Debug::Text( 'Unable to locate SQL error log files. May be caused by permission issues, or it doesn\'t exist: ' . $sql_log_name, __FILE__, __LINE__, __METHOD__, 10 );
					}

					//Include all TimeTrex files.
					$root_path = realpath( Environment::getBasePath() );
					$files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $root_path ), RecursiveIteratorIterator::LEAVES_ONLY );
					foreach ( $files as $name => $file ) {
						// Skip directories (they would be added automatically)
						if ( !$file->isDir() ) {
							// Add current file to archive under the 'timetrex' directory.
							$zip->addFile( $file->getRealPath(), str_replace( '\\', '/', 'timetrex'. DIRECTORY_SEPARATOR . substr( $file->getRealPath(), strlen( $root_path ) + 1 ) ) ); //Replace all backslashes from Windows with forward slashes as recommended in: https://www.php.net/manual/en/ziparchive.addfile.php
						}
					}
					unset( $files, $file_path, $relative_path, $root_path );

					//Zip TimeTrex log files -- Do this last so we can get as many log lines into it as possible.
					if ( file_exists( $timetrex_log_name ) ) {
						Debug::Text( '  Writing debug buffer to log prior to uploading...', __FILE__, __LINE__, __METHOD__, 10 );
						Debug::writeToLog(); //Write all previous debug lines to log before uploading it. **Keep this here, even though its in TTShutdown() as well**

						$zip->addFile( $timetrex_log_name, basename( $timetrex_log_name ) );
					} else {
						Debug::Text( 'Unable to locate TimeTrex log files. May be caused by permission issues.' . $timetrex_log_name, __FILE__, __LINE__, __METHOD__, 10 );
					}

					if ( $this->cli_mode === true ) {
						echo "Compressing Logs...\n";
					} else {
						$this->getProgressBarObject()->set( $this->getAPIMessageID(), 2, TTi18n::getText( 'Compressing Logs...' ) );
					}

					$retval = $zip->close();

					if ( $retval === true && file_exists( $zip_file ) ) {

						if ( $this->cli_mode === true ) {
							echo "Securely Uploading Logs...\n";
						} else {
							$this->getProgressBarObject()->set( $this->getAPIMessageID(), 3, TTi18n::getText( 'Securely Uploading Logs...' ) );
						}

						$fp = fopen( $zip_file, 'r' );

						$curl_handler = curl_init();
						curl_setopt( $curl_handler, CURLOPT_URL, 'https://nextcloud.timetrex.com/s/6NePJBkdqGeLaDM' );
						curl_setopt( $curl_handler, CURLOPT_SSL_VERIFYPEER, false ); //False to avoid curl error - "unable to get local issuer certificate."
						curl_setopt( $curl_handler, CURLOPT_RETURNTRANSFER, true );
						$curl_retval = curl_exec( $curl_handler );

						$doc = new DOMDocument();
						//LIBXML_NOERROR to ignore HTML errors on the page we are loading.
						$doc->loadHTML( $curl_retval, LIBXML_NOERROR );
						$head = $doc->getElementsByTagName( 'head' );
						if ( isset( $head[0] ) ) {
							$request_token = $head[0]->getAttribute( 'data-requesttoken' );
							$basic_authorization_token = base64_encode( $doc->getElementById( 'sharingToken' )->getAttribute( 'value' ) . ':' );
						}

						curl_setopt( $curl_handler, CURLOPT_URL, 'https://nextcloud.timetrex.com/public.php/webdav/' . $zip_name . '.zip' );
						curl_setopt( $curl_handler, CURLOPT_HTTPHEADER, [
								'requesttoken: ' . $request_token,
								'authorization: Basic ' . $basic_authorization_token,
						] );
						curl_setopt( $curl_handler, CURLOPT_PUT, true );
						curl_setopt( $curl_handler, CURLOPT_INFILESIZE, filesize( $zip_file ) );
						curl_setopt( $curl_handler, CURLOPT_INFILE, $fp );
						curl_setopt( $curl_handler, CURLOPT_NOPROGRESS, false );
						curl_setopt( $curl_handler, CURLOPT_PROGRESSFUNCTION, [ $this, 'updateUploadProgress' ] );
						curl_setopt( $curl_handler, CURLOPT_TIMEOUT, 0 );
						curl_setopt( $curl_handler, CURLINFO_HEADER_OUT, true );

						$this->curl_progress_start = microtime( true );
						$curl_retval = curl_exec( $curl_handler );
						Debug::Text( 'CURL Return Response: ' . Debug::varDump( $curl_retval ), __FILE__, __LINE__, __METHOD__, 10 );

						if ( $this->cli_mode === true ) {
							echo 'Finished Uploading Logs...'."\n";
						} else {
							$this->getProgressBarObject()->set( $this->getAPIMessageID(), 104, TTi18n::getText( 'Finished Uploading Logs...' ) );
						}

						if ( curl_errno( $curl_handler ) ) {
							$error_msg = curl_error( $curl_handler );
							Debug::Text( 'CURL ERROR: ' . $error_msg, __FILE__, __LINE__, __METHOD__, 10 );
						} else {
							Debug::Text( 'CURL Upload success.', __FILE__, __LINE__, __METHOD__, 10 );
						}
						curl_close( $curl_handler );

						$this->cleanUpTempDir( $temp_dir, true );
					}
				} else {
					return false;
				}
			}
			$lock_file->delete();
		} else {
			Debug::text( 'Skipping... uploading system diagnostics. Lock file exists...', __FILE__, __LINE__, __METHOD__, 10 );
			if ( $this->cli_mode === true ) {
				echo "NOTICE: Lock file exists, diagnostics already being collected elsewhere, skipping...\n";
			}

			return false;
		}

		return true;
	}


	/**
	 * @param $temp_dir
	 * @param $remove_temp_dir
	 * @return bool
	 */
	function cleanUpTempDir( $temp_dir, $remove_temp_dir ) {
		Debug::Text( 'Cleaning up Temp Dir: ' . $temp_dir, __FILE__, __LINE__, __METHOD__, 10 );
		if ( is_dir( $temp_dir ) ) {
			if ( $this->cli_mode === true ) {
				echo "Cleaning Up...\n";
			} else {
				$this->getProgressBarObject()->set( $this->getAPIMessageID(), 3, TTi18n::getText( 'Cleaning Up' ) );
			}

			$files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $temp_dir, RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST );
			foreach ( $files as $fileinfo ) {
				if ( $fileinfo->isDir() === true ) {
					@rmdir( $fileinfo->getRealPath() );
				} else {
					Misc::unlink( $fileinfo->getRealPath() );
				}
			}

			if ( $remove_temp_dir === true ) {
				//Since windows deleting files is async, its likely rare that the directory can be deleted, even after a sleeping for 10 seconds.
				@rmdir( $temp_dir );
			}

			clearstatcache(); //Clear any stat cache when done.
			Debug::Text( 'Done cleaning up Temp Dir: ' . $temp_dir, __FILE__, __LINE__, __METHOD__, 10 );
		}

		return true;
	}

	/**
	 *    Updates API or CLI with upload progress as a percent.
	 */
	function updateUploadProgress( $curl_handler, $download_file_size, $downloaded, $upload_file_size, $uploaded ) {
		//Debug::text( '  Download File Size: '. $download_file_size .' Downloaded: '. $downloaded .' Upload File Size: '. $upload_file_size .' Uploaded: '. $uploaded, __FILE__, __LINE__, __METHOD__, 10 );
		//echo '  Download File Size: '. $download_file_size .' Downloaded: '. $downloaded .' Upload File Size: '. $upload_file_size .' Uploaded: '. $uploaded ."\n";
		if ( $upload_file_size > 0 ) { //Prevent division by 0.
			$elapsed_upload_time = ( microtime( true ) - $this->curl_progress_start );
			if ( $elapsed_upload_time == 0 ) {
				$bytes_per_second = $uploaded;
			} else {
				$bytes_per_second = ( $uploaded / $elapsed_upload_time );
			}
			$kilobytes_per_second = round( $bytes_per_second / 1024, 2 );

			$progress = round( ( $uploaded / $upload_file_size ) * 100 );

			if ( $this->curl_last_progress === null || $progress !== $this->curl_last_progress ) { //Only update progress when it changes.
				if ( $this->cli_mode === true ) {
					if ( $progress % 10 == 0 ) {
						echo 'Securely Uploading Logs... ' . $progress . '% - ' . $kilobytes_per_second . "KB/s\n";
					}
				} else {
					//Add 3 to the progress because there are 3 steps before it.
					$this->getProgressBarObject()->set( $this->getAPIMessageID(), ( floor( $progress ) + 3 ), TTi18n::getText( 'Securely Uploading Logs...' ) . ' ' . $progress . '% - '. $kilobytes_per_second .'KB/s' );
				}
			}

			$this->curl_last_progress = $progress;
		}
	}

	/**
	 * @param object $obj
	 * @return bool
	 */
	function setProgressBarObject( $obj ) {
		if ( is_object( $obj ) ) {
			$this->progress_bar_obj = $obj;

			return true;
		}

		return false;
	}

	/**
	 * @return null|ProgressBar
	 */
	function getProgressBarObject() {
		if ( !is_object( $this->progress_bar_obj ) ) {
			$this->progress_bar_obj = new ProgressBar();
		}

		return $this->progress_bar_obj;
	}

	/**
	 * Returns the API messageID for each individual call.
	 * @return bool|null
	 */
	function getAPIMessageID() {
		if ( $this->api_message_id != null ) {
			return $this->api_message_id;
		}

		return false;
	}

	/**
	 * @param string $id UUID
	 * @return bool
	 */
	function setAPIMessageID( $id ) {
		Debug::Text( 'API Message ID: ' . $id, __FILE__, __LINE__, __METHOD__, 10 );
		if ( $id != '' ) {
			$this->api_message_id = $id;

			return true;
		}

		return false;
	}
}

?>