1: <?php
2: require_once 'resource1.php';
3: /**
4: * Allows formatting text in the command line.
5: */
6: class cli_formatter{
7: /**
8: * Fills the command line with a certain color.
9: *
10: * @param string $colour The color to fill the cmd window with.
11: * @param integer $width The number of columns in the cmd window.
12: * @param integer $height The number of rows in the cmd window.
13: * @return void
14: */
15: public static function fill(string $colour, int $width=120, int $height=29):void{
16: self::clear();
17: $string = '';
18: $i = 1;
19: while($i < $height+1){
20: $string .= str_repeat(" ",$width);
21: if($i < $height){
22: $string .= "\n";
23: }
24: $i++;
25: }
26: echo self::formatLine($string,$colour,false,true,"reverse");
27: }
28: /**
29: * Makes the windows warning noise.
30: *
31: * @return void
32: */
33: public static function ding():void{
34: echo "\007";
35: }
36: /**
37: * Formats a string using command line styles.
38: *
39: * @param string $string The string to format.
40: * @param string|false $colour The color of the text.
41: * @param string|false $background The background color for the text.
42: * @param boolean $newline Weather to add a newline character at the end of the string.
43: * @param string|false $attributes A comma seperated list with no spaces of style attributes.
44: * @return string The formatted string.
45: */
46: public static function formatLine(string $string, string|false $colour=false, string|false $background=false, bool $newline=true, string|false $attributes=false):string{
47:
48: //Based on https://gist.github.com/donatj/1315354
49:
50: $colourValues = array(
51: 'bold' => '1', 'dim' => '2',
52: 'black' => '0;30', 'dark_gray' => '1;30',
53: 'blue' => '0;34', 'light_blue' => '1;34',
54: 'green' => '0;32', 'light_green' => '1;32',
55: 'cyan' => '0;36', 'light_cyan' => '1;36',
56: 'red' => '0;31', 'light_red' => '1;31',
57: 'purple' => '0;35', 'light_purple' => '1;35',
58: 'brown' => '0;33', 'yellow' => '1;33',
59: 'light_gray' => '0;37', 'white' => '1;37',
60: 'normal' => '0;39'
61: );
62: $backgroundValues = array(
63: 'black' => '40', 'red' => '41',
64: 'green' => '42', 'yellow' => '43',
65: 'blue' => '44', 'magenta' => '45',
66: 'cyan' => '46', 'light_gray' => '47',
67: );
68: $attributeValues = array(
69: 'underline' => '4', 'blink' => '5',
70: 'reverse' => '7', 'hidden' => '8',
71: );
72:
73: $output = '';
74:
75: if($colour !== false){
76: if(isset($colourValues[$colour])){
77: $output .= "\033[";
78: $output .= $colourValues[$colour] . "m";
79: }
80: }
81:
82: if($background !== false){
83: if(isset($backgroundValues[$background])){
84: $output .= "\033[";
85: $output .= $backgroundValues[$background] . "m";
86: }
87: }
88:
89: if($attributes !== false){
90: $attributesArray = array();
91: if(strpos($attributes,",") !== false){
92: $attributesArray = explode(",",$attributes);
93: }
94: else{
95: $attributesArray[0] = $attributes;
96: }
97: foreach($attributesArray as $attribute){
98: if(isset($attributeValues[$attribute])){
99: $output .= "\033[";
100: $output .= $attributeValues[$attribute] . "m";
101: }
102: }
103: }
104:
105: $output .= $string . "\033[0m";
106:
107: if($newline){
108: $output .= "\n";
109: }
110:
111: return $output;
112: }
113: /**
114: * Clears the text in the cmd window.
115: *
116: * @return void
117: */
118: public static function clear():void{
119: echo chr(27) . chr(91) . 'H' . chr(27) . chr(91) . 'J';
120: }
121: }
122: /**
123: * Allows some basic command execution.
124: */
125: class cmd{
126: /**
127: * Runs a command in a new cmd window.
128: *
129: * @param string $command The command to run.
130: * @param boolean $keepOpen Weather to keep the new cmd window open.
131: * @return void
132: */
133: public static function newWindow(string $command, bool $keepOpen=false):void{
134: mklog(1, 'Starting process with command ' . $command);
135: $env = getEnvironment();
136: if($env['windows']){
137: pclose(popen('start cmd.exe /' . ($keepOpen ? "k" : "c") . ' "' . escapeshellcmd($command) . '"','r'));
138: }
139: else{
140: if(!$env['desktop']){
141: mklog(3, "You cannot run newWindow in this environment");
142: return;
143: }
144:
145: $terminal = null;
146:
147: foreach(['gnome-terminal', 'xterm', 'konsole', 'xfce4-terminal', 'lxterminal'] as $term){
148: if(shell_exec("which $term 2>/dev/null")){
149: $terminal = $term;
150: break;
151: }
152: }
153:
154: if($terminal){
155: $escapedCwd = escapeshellarg(getcwd());
156:
157: // Build the inner bash command
158: $innerCmd = $keepOpen ? "$command; bash" : $command;
159:
160: $escapedCmd = escapeshellarg($innerCmd);
161:
162: $launch = match($terminal){
163: 'gnome-terminal' => "gnome-terminal --working-directory=$escapedCwd -- bash -c $escapedCmd",
164: 'konsole' => "konsole --workdir $escapedCwd -e bash -c $escapedCmd",
165: 'xfce4-terminal' => "xfce4-terminal --working-directory=$escapedCwd -e bash -c $escapedCmd",
166: 'lxterminal' => "lxterminal --working-directory=$escapedCwd -e bash -c $escapedCmd",
167: 'xterm' => "cd $escapedCwd && xterm -e bash -c $escapedCmd",
168: default => null,
169: };
170:
171: if($launch){
172: shell_exec("$launch &");
173: return;
174: }
175: }
176: mklog(3, "Failed to find terminal program");
177: }
178: }
179: /**
180: * Runs a command using exec().
181: *
182: * @param string $command The command to be run.
183: * @param boolean $silent Weather to redirect the command stdout and stderr to null.
184: * @param boolean $returnOutput Weather to return the output of the command.
185: * @param int $expectedExit The expected exit code for the command.
186: * @return boolean|array If returnOutput is false, the return is a boolean that is true if the commands exit code was equal to the expected exit code, if returnOutput is true then an array of lines is returned.
187: */
188: public static function run(string $command, bool $silent=false, bool $returnOutput=false, int $expectedExit=0):bool|array{
189: if($silent){
190: if(PHP_OS_FAMILY === 'Linux'){
191: $command .= " > /dev/null 2>&1";
192: }
193: else{
194: $command .= " >nul 2>&1";
195: }
196: }
197:
198: mklog(0, "Running command " . $command);
199:
200: exec($command, $output, $result);
201:
202: if($returnOutput){
203: return $output;
204: }
205:
206: if($result === $expectedExit){
207: return true;
208: }
209:
210: return false;
211: }
212: /**
213: * @deprecated
214: */
215: public static function returnNewWindow(string $command):string|false|null{
216: return shell_exec($command);
217: }
218: /**
219: * Returns the name of the start script.
220: *
221: * @return string The name of the start script.
222: */
223: public static function startScript():string{
224: return $_SERVER['LAUNCHER_SCRIPT'] ?? 'start.' . (PHP_OS_FAMILY === 'Linux' ? 'sh' : 'bat');
225: }
226: }
227: /**
228: * Allows you to create command line tables.
229: */
230: class commandline_list{
231: /**
232: * Turns some data into a table.
233: *
234: * @param array $columnNames Either a list of column names or an array of column names as keys and column widths as values.
235: * @param array $rowsData A list containing row information, each row information is a list of strings to put into the table cells.
236: * @return string The table.
237: */
238: public static function table(array $columnNames=[], array $rowsData=[]):string{
239: $width = 1;
240: if(array_is_list($columnNames)){
241: foreach($columnNames as $index => $text){
242: $columnNames[$text] = strlen(trim($text));
243: unset($columnNames[$index]);
244: }
245: }
246:
247: $columnWidths = [];
248: foreach($columnNames as $text => $length){
249: $width += $length +4;
250: $columnWidths[] = $length;
251: }
252:
253: $output = self::tableLine($width);
254: $output .= self::tableRow($columnNames) . "\n";
255: $output .= preg_replace('/[^|]/', "-", self::tableRow($columnNames)) . "\n";
256:
257: foreach($rowsData as $rowData){
258: $output .= self::tableRow($rowData,$columnWidths) . "\n";
259: }
260:
261: $output .= self::tableLine($width);
262:
263: return $output;
264: }
265: /**
266: * Limits the length of a string, if the string is over the given length, the string will be cut off at -2 length and have ".." added so the total length is the given length.
267: *
268: * @param string $string The string to be limited.
269: * @param integer $length The desired length.
270: * @return string The limited string.
271: */
272: public static function stringLengthLimit(string $string, int $length):string{
273: if($length < 3){
274: $length = 3;
275: }
276: if(strlen(trim($string)) > $length){
277: $string = substr($string,0,$length-2) . "..";
278: }
279: return $string;
280: }
281: /**
282: * Takes in a row of information and turns it into a string representing the row.
283: *
284: * @param array $data Either a list of values to be put into the table, or an array where the keys are the values to put into the table and the values are the width of the columns.
285: * @param array $columnWidths A list of column widths, only used when $data is a list.
286: * @return string
287: */
288: public static function tableRow(array $data, array $columnWidths=[]):string{
289: if(!array_is_list($data)){
290: $columnWidths = array_values($data);
291: $data = array_keys($data);
292: }
293: elseif(count($columnWidths) < count($data)){
294: return "ERROR";
295: }
296:
297: $output = '|';
298: $i = 0;
299: foreach($columnWidths as $length){
300: if(isset($data[$i])){
301: $output .= ' ' . str_pad(self::stringLengthLimit(data_types::convert_to_string($data[$i]),$length),$length," ",STR_PAD_RIGHT) . ' |';
302: }
303: else{
304: $output .= ' ' . str_repeat(" ",$length) . ' |';
305: }
306: $i++;
307: }
308: return $output;
309: }
310: /**
311: * Generates a horizontal table seperator.
312: *
313: * @param integer $width The width of the table.
314: * @return string The seperator string.
315: */
316: public static function tableLine(int $width):string{
317: return "|" . str_repeat("-",$width-2) . "|\n";
318: }
319: }
320: /**
321: * Type management.
322: */
323: class data_types{
324: /**
325: * Converts a string to a fitting type.
326: *
327: * @param string $value The string to convert.
328: * @return integer|float|boolean|string The converted value.
329: */
330: public static function convert_string(string $value):int|float|bool|string{
331: $return = $value;
332: if(is_numeric($value)){
333: //Check if the string contains a point
334: if(strpos($value,'.')){
335: //Convert string to float
336: $return = floatval($value);
337: }
338: else{
339: //Convert string to integer
340: $return = intval($value);
341: }
342: }
343: elseif(in_array(strtolower($value), ["true","false"])){
344: //convert string to boolean
345: $return = strtolower($value) === "true";
346: }
347: return $return;
348: }
349: /**
350: * Converts a string/float/integer/boolean to a string.
351: *
352: * @param string|float|integer|boolean $value The value to be converted.
353: * @return string The value as a string.
354: */
355: public static function convert_to_string(string|float|int|bool $value):string{
356: $return = "";
357: if(is_string($value)){
358: $return = $value;
359: }
360: elseif(is_float($value) || is_int($value)){
361: $return = (string) $value;
362: }
363: elseif(is_bool($value)){
364: $return = $value ? "true" : "false";
365: }
366:
367: return $return;
368: }
369: /**
370: * Converts some xml into a readable array.
371: *
372: * @param string $xml The xml string.
373: * @return array The decoded values.
374: */
375: public static function xmlStringToArray(string $xml):array{
376: $xml1 = simplexml_load_string($xml);
377: return json_decode(json_encode($xml1),true);
378: }
379: /**
380: * Converts an array into a string representation that can be given to eval.
381: *
382: * @param array $data The data to convert.
383: * @return string The core string representation.
384: */
385: public static function array_to_eval_string(array $data):string{
386: $string = '[';
387: foreach($data as $key => $value){
388: if(is_int($key)){
389: $keytext = $key;
390: }
391: else{
392: $keytext = '"' . $key . '"';
393: }
394:
395: $valuetext = self::convert_to_eval_string($value);
396: $string .= $keytext . '=>' . $valuetext . ',';
397: }
398: $string = substr($string, 0, -1) . ']';
399: return $string;
400: }
401: /**
402: * Converts a value into a code string representation.
403: *
404: * @param array|string|float|integer|boolean $value The value to be converted.
405: * @return string The code string representation.
406: */
407: public static function convert_to_eval_string(array|string|float|int|bool $value):string{
408: $return = "";
409: if(is_array($value)){
410: $return = self::array_to_eval_string($value);
411: }
412: elseif(is_string($value)){
413: $return = '"' . $value . '"';
414: }
415: elseif(is_float($value) || is_int($value)){
416: $return = (string) $value;
417: }
418: elseif(is_bool($value)){
419: $return = $value ? "true" : "false";
420: }
421:
422: return $return;
423: }
424: /**
425: * Checks if a given array matches a template.
426: *
427: * @param array $data The data to test.
428: * @param array $expected An array with the same keys and structure as the expected data, but the values are either arrays or a string saying the type of data (from gettype()) that should be there.
429: * @return boolean Weather the data matched the expected layout and types.
430: */
431: public static function validateData(array $data, array $expected):bool{
432: foreach($expected as $expectedName => $expectedType){
433: if(!isset($data[$expectedName])){
434: return false;
435: }
436:
437: if(is_array($expectedType)){
438: if(!is_array($data[$expectedName])){
439: return false;
440: }
441:
442: if(!self::validateData($data[$expectedName], $expectedType)){
443: return false;
444: }
445:
446: continue;
447: }
448:
449: if(gettype($data[$expectedName]) !== $expectedType){
450: return false;
451: }
452: }
453:
454: return true;
455: }
456: }
457: /**
458: * Download a file.
459: */
460: class downloader{
461: /**
462: * Downloads a file and shows a progress bar.
463: *
464: * @param string $url The url to download from.
465: * @param string $outFile The file to save the data to.
466: * @return boolean Indicates success.
467: */
468: public static function downloadFile(string $url, string $outFile):bool{
469: mklog(1, 'Downloading file ' . basename($url));
470:
471: if(is_file($outFile)){
472: mklog(2, 'The download destination already exists');
473: return false;
474: }
475:
476: $destDir = files::getFileDir($outFile);
477: if(!empty($destDir)){
478: if(!files::ensureFolder($destDir)){
479: return false;
480: }
481: }
482:
483: $return = false;
484:
485: $curl = curl_init();
486: $file = fopen($outFile, 'wb');
487:
488: curl_setopt_array($curl, [
489: CURLOPT_URL => $url,
490: CURLOPT_HEADER => false,
491: CURLOPT_FOLLOWLOCATION => true,
492: CURLOPT_FILE => $file,
493: CURLOPT_PROGRESSFUNCTION => ['downloader', 'curlProgress'],
494: CURLOPT_NOPROGRESS => false,
495: CURLOPT_SSL_VERIFYPEER => false,
496: CURLOPT_FAILONERROR => true
497: ]);
498:
499: mklog(0, "Retreiving file from " . $url);
500: @curl_exec($curl);
501:
502: if(curl_errno($curl) === 0){
503: $return = true;
504: echo "\n";
505: }
506: else{
507: mklog(2, 'Download error: ' . curl_error($curl));
508: }
509:
510: if(!@fclose($file)){
511: mklog(2, 'Failed to close output file ' . $outFile);
512: }
513:
514: if(!is_file($outFile)){
515: mklog(2, 'The output file ' . $outFile . ' doesnt exist after downloading');
516: $return = false;
517: }
518:
519: return $return;
520: }
521: private static function curlProgress($resource, $download_size = 0, $downloaded = 0, $upload_size = 0, $uploaded = 0):void{
522: if($download_size > 0 && $downloaded > 0) {
523: echo files::progressTracker($download_size, $downloaded);
524: }
525: }
526: }
527: /**
528: * File management.
529: */
530: class files{
531: private static $progressStartTime = 0;
532: private static $progressLastTotal = 0;
533: private static $progressLocalStartTime = 0;
534: private static $progressLocalCurrent = 0;
535: private static $progressLastCurrent = 0;
536:
537: /**
538: * Similar to glob but also recursively calls glob on subfolders too.
539: *
540: * @param string $base The base path.
541: * @param string $pattern The pattern to test for inside.
542: * @param integer $flags glob() flags.
543: * @return array A list of all the files that were found, this does not include the sub folder names.
544: */
545: public static function globRecursive(string $base, string $pattern, int $flags=0):array{
546: mklog(1, "Recursivly globbing folder " . $base);
547:
548: $flags = $flags & ~GLOB_NOCHECK;
549:
550: if (substr($base, -1) !== DIRECTORY_SEPARATOR) {
551: $base .= DIRECTORY_SEPARATOR;
552: }
553:
554: $files = glob($base.$pattern, $flags);
555: if (!is_array($files)) {
556: $files = [];
557: }
558:
559: $dirs = glob($base.'*', GLOB_ONLYDIR|GLOB_NOSORT|GLOB_MARK);
560: if (!is_array($dirs)) {
561: return $files;
562: }
563:
564: foreach ($dirs as $dir) {
565: $dirFiles = self::globRecursive($dir, $pattern, $flags);
566: $files = array_merge($files, $dirFiles);
567: }
568:
569: return $files;
570: }
571: /**
572: * Makes sure a folder exists by creating it if it doesnt exist.
573: *
574: * @param string $dir The folder to check.
575: * @return boolean True if the folder already existed or exists now, false on failure.
576: */
577: public static function ensureFolder(string $dir):bool{
578: if(is_dir($dir)){
579: return true;
580: }
581: elseif(is_file($dir)){
582: return false;
583: }
584: else{
585: return self::mkFolder($dir);
586: }
587: }
588: /**
589: * Creates a folder.
590: *
591: * @param string $path The path of the folder to create, the folders parent does not need to exist to create it.
592: * @return boolean Indicates success.
593: */
594: public static function mkFolder(string $path):bool{
595: if(empty($path)){
596: return false;
597: }
598:
599: if(is_dir($path)){
600: mklog(2, 'Failed to create directory ' . $path . ' as it already exists');
601: return false;
602: }
603: if(is_file($path)){
604: mklog(2, 'Failed to create directory ' . $path . ' as a file exists with the same name');
605: }
606:
607: mklog(0, "Creating directory " . $path);
608:
609: return mkdir($path, 0777, true);
610: }
611: /**
612: * Creates a file, the parent directory does not need to exist to create the file.
613: *
614: * @param string $path The path of the file.
615: * @param string $data The data to put into the file.
616: * @param string $fopenMode What mode fopen should use.
617: * @param boolean $overwrite Weather to overwrite any existing file.
618: * @return boolean Indicates success.
619: */
620: public static function mkFile(string $path, string $data, string $fopenMode="w", bool $overwrite=true):bool{
621: if(empty($path)){
622: mklog(2, 'Cannot create a file with empty path');
623: return false;
624: }
625:
626: if(!in_array($fopenMode, ['w', 'wb', 'a', 'ab', 'x', 'xb', 'c', 'cb'])){
627: mklog(2, 'Invalid fopen mode: ' . $fopenMode);
628: return false;
629: }
630:
631: if(!$overwrite && is_file($path)){
632: mklog(2, 'Cannot create file ' . $path . ' as it already exists and overwrite is set to false');
633: return false;
634: }
635:
636: $dir = self::getFileDir($path);
637: if(!empty($dir) && !is_dir($dir)){
638: if(!self::mkFolder($dir)){
639: mklog(2, 'Failed to create ' . $dir . ' directory when creating file');
640: return false;
641: }
642: }
643:
644: mklog(0, 'Opening file ' . $path . ' with fopen mode ' . $fopenMode);
645:
646: $stream = fopen($path, $fopenMode);
647: if(!$stream){
648: mklog(2, 'Failed to open file ' . $path . ' with mode ' . $fopenMode);
649: return false;
650: }
651:
652: $return = true;
653: $length = strlen($data);
654: $bytes = fwrite($stream, $data);
655: if($bytes === false){
656: mklog(2, 'Failed to write data to file ' . $path);
657: $return = false;
658: }
659: elseif($bytes !== $length){
660: mklog(2, 'Failed to write correct amount of data to file ' . $path . ' (' . $bytes . ' out of ' . $length . ' bytes)');
661: $return = false;
662: }
663:
664: if(fclose($stream)){
665: mklog(0, 'Closed file ' . $path);
666: }
667: else{
668: mklog(2, 'Failed to close/save file ' . $path);
669: $return = false;
670: }
671:
672: return $return;
673: }
674: /**
675: * Removes the last name in a file path.
676: *
677: * @param string $path The full file path.
678: * @return string The path without the last name.
679: */
680: public static function getFileDir(string $path):string{
681: if(PHP_OS_FAMILY === 'Linux'){
682: $path = str_replace("\\","/",$path);
683: $pos = strripos($path,"/");
684: }
685: else{
686: $path = str_replace("/","\\",$path);
687: $pos = strripos($path,"\\");
688: }
689:
690: $dir = substr($path,0,$pos);
691: return $dir;
692: }
693: /**
694: * Use basename instead.
695: *
696: * @param string $path
697: * @return string
698: */
699: public static function getFileName(string $path):string{
700: return basename($path);
701: }
702: /**
703: * Copies a file from one place to another.
704: *
705: * @param string $pathFrom The source file.
706: * @param string $pathTo The destination file, the destination file's folder will be created if it doesnt already exist.
707: * @param boolean $showProgress Weather to show a progress bar, this changes the copy from copy() to many fread and fwrites.
708: * @return boolean Indicates success.
709: */
710: public static function copyFile(string $pathFrom, string $pathTo, bool $showProgress=true):bool{
711: mklog(1, 'Copying file ' . $pathFrom . ' to ' . $pathTo);
712:
713: if(!is_file($pathFrom)){
714: mklog(2, 'Cannot copy from nonexistant source ' . $pathFrom);
715: return false;
716: }
717:
718: if(is_file($pathTo)){
719: mklog(2, 'The destination file already exists ' . $pathTo);
720: return false;
721: }
722:
723: $dir = self::getFileDir($pathTo);
724: if(!empty($dir) && !is_dir($dir)){
725: if(!self::mkFolder($dir)){
726: mklog(2, 'Failed to create folder for destination file ' . $dir);
727: return false;
728: }
729: }
730:
731: if($showProgress){
732: mklog(0, 'Copying file in chunk/progress mode');
733:
734: $totalBytes = filesize($pathFrom);
735: if(!$totalBytes){
736: mklog(2, 'Failed to get size of file');
737: return false;
738: }
739:
740: $in = fopen($pathFrom, 'rb');
741: if(!$in){
742: mklog(2, 'Failed to open source stream');
743: return false;
744: }
745:
746: $out = fopen($pathTo, 'wb');
747: if(!$out){
748: mklog(2, 'Failed to open source stream');
749: @fclose($in);
750: return false;
751: }
752:
753: $bytesCopied = 0;
754: while(!feof($in)){
755: $chunk = fread($in, 1024*1024);
756:
757: if($chunk === false || !fwrite($out, $chunk)){
758:
759: mklog(2, ($chunk === false ? 'Failed to read chunk from source file' : 'Failed to write chunk to destination file'));
760:
761: if(fclose($in)){
762: mklog(2, 'Failed to close input file');
763: }
764:
765: @fclose($out);
766: @unlink($pathTo);
767: return false;
768: }
769:
770: $bytesCopied += strlen($chunk);
771:
772: echo self::progressTracker($totalBytes, $bytesCopied);
773: }
774:
775: if($bytesCopied !== $totalBytes){
776: mklog(2, 'Failed to copy all bytes');
777: return false;
778: }
779:
780: if(fclose($in)){
781: mklog(2, 'Failed to close input file');
782: }
783: if(fclose($out)){
784: mklog(2, 'Failed to close output file');
785: }
786:
787: return true;
788: }
789: else{
790: return copy($pathFrom, $pathTo);
791: }
792: }
793: /**
794: * Makes all slashes backslashes and adds quotes if enabled and needed.
795: *
796: * @param string $path The path to be made valid for a windows command.
797: * @param boolean $addquotes Weather to add quotes if needed.
798: * @return string The converted path.
799: */
800: public static function validatePath(string $path, bool $addquotes=false):string{
801: $path = PHP_OS_FAMILY === 'Linux' ? str_replace("\\","/",$path) : str_replace("/","\\",$path);
802: return $addquotes ? escapeshellarg($path) : $path;
803: }
804: /**
805: * Gets the extension from the last .xyz part of a path.
806: *
807: * @param string $fileName The path or name of the file.
808: * @return string The extension of the file.
809: */
810: public static function getFileExtension(string $fileName):string{
811: $ext = "";
812: $pos = strripos($fileName,".");
813: if($pos !== false){
814: $ext = substr($fileName,$pos+1);
815: }
816: return $ext;
817: }
818: /**
819: * Returns an array which has lowercase file extensions as keys and mime types as values.
820: *
821: * @return array
822: */
823: public static function fileExtensionMimeTypes():array{
824: return [
825: "bmp" => "image/bmp",
826: "gif" => "image/gif",
827: "ico" => "image/vnd.microsoft.icon",
828: "jpeg" => "image/jpeg",
829: "jpg" => "image/jpeg",
830: "png" => "image/png",
831: "svg" => "image/svg+xml",
832: "tiff" => "image/tiff",
833: "tif" => "image/tiff",
834: "webp" => "image/webp",
835:
836: "css" => "text/css",
837: "csv" => "text/csv",
838: "ics" => "text/calendar",
839: "html" => "text/html",
840: "java" => "text/x-java-source,java",
841: "js" => "text/javascript",
842: "txt" => "text/plain",
843:
844: "aac" => "audio/x-aac",
845: "m3u" => "audio/x-mpegurl",
846: "midi" => "audio/midi",
847: "mid" => "audio/midi",
848: "mp3" => "audio/mp3",
849: "mp4a" => "audio/mp4",
850: "oga" => "audio/ogg",
851: "ogg" => "audio/ogg",
852: "opus" => "audio/opus",
853: "wav" => "audio/x-wav",
854: "weba" => "audio/webm",
855:
856: "3gp" => "video/3gpp",
857: "3g2" => "video/3gpp2",
858: "avi" => "video/x-msvideo",
859: "flv" => "video/x-flv",
860: "h264" => "video/h264",
861: "jpgv" => "video/jpeg",
862: "m4v" => "video/x-m4v",
863: "mxu" => "video/vnd.mpegurl",
864: "mpeg" => "video/mpeg",
865: "mp4" => "video/mp4",
866: "ogv" => "video/ogg",
867: "webm" => "video/webm",
868:
869: "7z" => "application/x-7z-compressed",
870: "apk" => "application/vnd.android.package-archive",
871: "bin" => "application/octet-stream",
872: "bz" => "application/x-bzip",
873: "bz2" => "application/x-bzip2",
874: "cab" => "application/vnd.ms-cab-compressed",
875: "class" => "application/java-vm",
876: "csh" => "application/x-csh",
877: "deb" => "application/x-debian-package",
878: "doc" => "application/msword",
879: "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
880: "eot" => "application/vnd.ms-fontobject",
881: "epub" => "application/epub+zip",
882: "exe" => "application/x-msdownload",
883: "gz" => "application/gzip",
884: "jar" => "application/java-archive",
885: "json" => "application/json",
886: "mpkg" => "application/vnd.apple.installer+xml",
887: "odp" => "application/vnd.oasis.opendocument.presentation",
888: "ods" => "application/vnd.oasis.opendocument.spreadsheet",
889: "odt" => "application/vnd.oasis.opendocument.text",
890: "pdf" => "application/pdf",
891: "ppt" => "application/vnd.ms-powerpoint",
892: "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
893: "pub" => "application/x-mspublisher",
894: "rar" => "application/x-rar-compressed",
895: "sh" => "application/x-sh",
896: "swf" => "application/x-shockwave-flash",
897: "tar" => "application/x-tar",
898: "vsd" => "application/vnd.visio",
899: "xhtml" => "application/xhtml+xml",
900: "xls" => "application/vnd.ms-excel",
901: "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
902: "xml" => "application/xml",
903: "xul" => "application/vnd.mozilla.xul+xml",
904: "zip" => "application/zip",
905: ];
906: }
907: /**
908: * Converts a number of bytes into a string with an appropriate unit.
909: *
910: * @param integer $bytes The number of bytes.
911: * @return string The formatted string.
912: */
913: public static function formatBytes(int $bytes):string{
914: $digits = strlen(round($bytes));
915: $unit = "B ";
916:
917: if($digits > 12){
918: $bytes = $bytes / (1024**4);
919: $unit = "TB";
920: }
921: elseif($digits > 9){
922: $bytes = $bytes / (1024**3);
923: $unit = "GB";
924: }
925: elseif($digits > 6){
926: $bytes = $bytes / (1024**2);
927: $unit = "MB";
928: }
929: elseif($digits > 3){
930: $bytes = $bytes / 1024;
931: $unit = "KB";
932: }
933:
934: if(intval($bytes) < 10){
935: return round($bytes, 1) . $unit;
936: }
937: else{
938: return round($bytes) . $unit;
939: }
940: }
941: /**
942: * Makes a string that is a probress bar that can be printed to the command line.
943: *
944: * @param float $precentage The current percentage the bar should show.
945: * @param integer $barWidth The number of characters the bar should be made of.
946: * @param integer $totalBytes The total number of bytes, 0 to not show total.
947: * @param integer $bytesPerSecond The current speed to show, 0 to not show speed.
948: * @param integer $secondsLeft The number of seconds left, 0 to not show time left.
949: * @return string The progress bar string.
950: */
951: public static function progressBar(float $precentage, int $barWidth=30, int $totalBytes=0, int $bytesPerSecond=0, int $secondsLeft=0):string{
952: if($precentage > 100 || $precentage < 0 || $barWidth < 1 || $barWidth > 90){
953: return "";
954: }
955:
956: $barFilled = intval(floor(($precentage/100)*$barWidth));
957:
958: $string = "[" . str_repeat("#",$barFilled) . str_repeat(" ",$barWidth - $barFilled) . "] ";
959: $string .= $precentage . "% ";
960:
961: if($totalBytes){
962: $string .= files::formatBytes($totalBytes) . " ";
963: }
964: if($bytesPerSecond){
965: $string .= files::formatBytes($bytesPerSecond) . "/s ";
966: }
967: if($secondsLeft){
968: $string .= gmdate("H:i:s", $secondsLeft) . " ";
969: }
970:
971: return $string . " \r";
972: }
973: /**
974: * Repeatedly call this function and it will return a progress bar with calculated time left and speed.
975: *
976: * @param integer $total The total number of bytes in the operation.
977: * @param integer $current The current number of bytes done.
978: * @param integer $barWidth The width of the bar part.
979: * @param boolean $showTotal Weather to show the total number of bytes.
980: * @param boolean $showSpeed Weather to calculate the speed of current bytes change between calls.
981: * @param boolean $showEta Weather to calculate the time left between calls.
982: * @return string The current state of the progress bar.
983: */
984: public static function progressTracker(int $total, int $current, int $barWidth=30, bool $showTotal=true, bool $showSpeed=true, bool $showEta=true):string{
985: if($total < 1 || $current < 0 || $current > $total || $barWidth > 90){
986: return "";
987: }
988:
989: if(self::$progressLastTotal !== $total){
990: //Reset if total changes
991: self::$progressStartTime = microtime(true);
992: self::$progressLastTotal = $total;
993: self::$progressLocalStartTime = self::$progressStartTime;
994: self::$progressLocalCurrent = $current;
995: self::$progressLastCurrent = 0;
996: }
997:
998: $precentage = round(($current / $total) * 100);
999:
1000: if($showSpeed){
1001: $currentDifference = $current - self::$progressLastCurrent;
1002: self::$progressLocalCurrent += $currentDifference;
1003: $timeDiff = microtime(true) - self::$progressLocalStartTime;
1004: $bytesPerSecond = round(self::$progressLocalCurrent / $timeDiff);
1005:
1006: if($timeDiff > 10){
1007: self::$progressLocalCurrent = 0;
1008: self::$progressLocalStartTime = microtime(true);
1009: }
1010: }
1011: else{
1012: $bytesPerSecond = 0;
1013: }
1014:
1015: if($showEta && $bytesPerSecond){
1016: $eta = round(($total - $current) / $bytesPerSecond);
1017: }
1018: else{
1019: $eta = 0;
1020: }
1021:
1022: self::$progressLastCurrent = $current;
1023:
1024: return files::progressBar($precentage, $barWidth, ($showTotal ? $total : 0), $bytesPerSecond, $eta);
1025: }
1026: }
1027: /**
1028: * Read and write json files.
1029: */
1030: class json{
1031: private static $readCache = [];
1032:
1033: /**
1034: * Adds a value to an array inside a json file, this does not work with nested arrays.
1035: *
1036: * @param string $path The path to the json file.
1037: * @param integer|string $entryKey The new key to add to the json file.
1038: * @param mixed $entryValue The new value to put with the key.
1039: * @param boolean $addToTop Weather to add it before the existing data or after.
1040: * @return boolean Indicates success.
1041: */
1042: public static function addToFile(string $path, int|string $entryKey, mixed $entryValue, bool $addToTop=false):bool{
1043: $existing = self::readFile($path);
1044: $new = [];
1045: if($addToTop){
1046: $new[$entryKey] = $entryValue;
1047: }
1048: foreach($existing as $key => $value){
1049: $new[$key] = $value;
1050: }
1051: if(!$addToTop){
1052: $new[$entryKey] = $entryValue;
1053: }
1054: return self::writeFile($path,$new,true);
1055: }
1056: /**
1057: * Reads a json file and returns the decoded values.
1058: *
1059: * @param string $path The path to the json file.
1060: * @param boolean $createIfNonexistant Weather to create the file if it does not exist.
1061: * @param mixed $expectedValue The default value to return and put into the file if it doesnt already exist.
1062: * @return mixed The value stored in the json file or the default value.
1063: */
1064: public static function readFile(string $path, bool $createIfNonexistant=false, mixed $expectedValue=[]):mixed{
1065: global $arguments; //json-read-cache-timeout
1066: if(!is_int($arguments['json-read-cache-timeout'])){
1067: $arguments['json-read-cache-timeout'] = 1;
1068: }
1069: if(!is_int($arguments['json-url-read-cache-timeout'])){
1070: $arguments['json-url-read-cache-timeout'] = 5;
1071: }
1072:
1073: $url = strtolower(substr($path,0,4)) === "http";
1074: $timeout = $url ? $arguments['json-url-read-cache-timeout'] : $arguments['json-read-cache-timeout'];
1075:
1076: if(isset(self::$readCache[$path]) && microtime(true) - self::$readCache[$path]['lasttime'] < $timeout){
1077: mklog(0, 'Reading from cached ' . ($url ? 'URL ' : 'file ') . $path);
1078: return self::$readCache[$path]['contents'];
1079: }
1080:
1081: mklog(0, 'Reading from ' . ($url ? 'URL ' : 'file ') . $path);
1082:
1083: if($url){
1084: if(!extension_loaded("openssl")){
1085: mklog(2, 'Cannot open urls unless openssl is enabled');
1086: return false;
1087: }
1088:
1089: $context = stream_context_create(["http" => ["header" => "User-Agent: PHP-CLI " . $_SERVER['COMPUTERNAME'] . "\r\n"]]);
1090: $json = @file_get_contents($path, false, $context);
1091: }
1092: else{
1093: if(is_file($path)){
1094: $json = @file_get_contents($path);
1095: }
1096: else{
1097: if($createIfNonexistant){
1098: if(!txtrw::mktxt($path, json_encode($expectedValue, JSON_PRETTY_PRINT))){
1099: mklog(2, 'Failed to create file while reading file ' . $path);
1100: }
1101: return $expectedValue;
1102: }
1103: else{
1104: mklog(2, "Attempt made to read from nonexistant file " . $path);
1105: return false;
1106: }
1107: }
1108: }
1109:
1110: if(!is_string($json)){
1111: mklog(2, 'Failed to read from ' . $path);
1112: return false;
1113: }
1114:
1115: $decoded = json_decode($json, true);
1116:
1117: if($decoded === NULL){
1118: mklog(2, 'Failed to decode json in file ' . $path);
1119: return false;
1120: }
1121:
1122: self::$readCache[$path]['lasttime'] = microtime(true);
1123: self::$readCache[$path]['contents'] = $decoded;
1124:
1125: return $decoded;
1126: }
1127: /**
1128: * Writes a value into a json file.
1129: *
1130: * @param string $path The path to the json file.
1131: * @param mixed $value The value to but into the json file.
1132: * @param boolean $overwrite Weather to overwrite any existing file.
1133: * @return boolean Indicates success.
1134: */
1135: public static function writeFile(string $path, mixed $value, bool $overwrite=false):bool{
1136: mklog(0, 'Writing to file ' . $path);
1137:
1138: $json = json_encode($value, JSON_PRETTY_PRINT);
1139: if($json === false){
1140: return false;
1141: }
1142:
1143: if(!files::mkFile($path, $json, "w", $overwrite)){
1144: mklog(2, 'Failed to write to file ' . $path);
1145: return false;
1146: }
1147:
1148: self::$readCache[$path]['lasttime'] = microtime(true);
1149: self::$readCache[$path]['contents'] = $value;
1150:
1151: return true;
1152: }
1153: }
1154: /**
1155: * @deprecated
1156: */
1157: class time{
1158: /**
1159: * Use time() instead.
1160: * @deprecated
1161: */
1162: public static function stamp(){
1163: return time();
1164: }
1165: /**
1166: * Use microtime(true) instead.
1167: * @deprecated
1168: */
1169: public static function millistamp(){
1170: return floor(microtime(true)*1000);
1171: }
1172: }
1173: /**
1174: * @internal
1175: */
1176: class timetest{
1177: public static function command(string $line):void{
1178: $startTime = microtime(true);
1179:
1180: $line = str_replace("\\","\\\\",$line);
1181:
1182: $return = eval("return " . $line);
1183:
1184: echo "\nReturn: " . json_encode($return,JSON_PRETTY_PRINT) . "\n";
1185:
1186: $endTime = microtime(true);
1187: echo "\nTime Taken: " . round($endTime - $startTime,3) . " seconds.\n";
1188: }
1189: }
1190: /**
1191: * Read and write text files.
1192: */
1193: class txtrw{
1194: /**
1195: * Creates a text file.
1196: *
1197: * @param string $file The path to the file.
1198: * @param string $content The content to put into the file.
1199: * @param boolean $overwrite Weather to overwrite any existing file.
1200: * @return boolean Indicates success.
1201: */
1202: public static function mktxt(string $file, string $content, bool $overwrite=false):bool{
1203: return files::mkFile($file, $content, "w", $overwrite);
1204: }
1205: /**
1206: * Reads a text file.
1207: *
1208: * @param string $file The path to the file.
1209: * @param boolean $createIfNonexistant Weather to create the file if it does not exist.
1210: * @return string|false The contents of the file on success or false on failure.
1211: */
1212: public static function readtxt(string $file, bool $createIfNonexistant=false):string|false{
1213: if(!is_file($file)){
1214: if($createIfNonexistant){
1215: mklog(0, 'Creating nonexistant file for reading with no contents');
1216: if(self::mktxt($file, "")){
1217: return "";
1218: }
1219: else{
1220: mklog(2, 'Failed to create file to read ' . $file);
1221: return false;
1222: }
1223: }
1224: else{
1225: mklog(2, 'Failed to read from nonexistant file ' . $file);
1226: return false;
1227: }
1228: }
1229: else{
1230: $filecontents = file_get_contents($file);
1231: if(is_string($filecontents)){
1232: return $filecontents;
1233: }
1234: else{
1235: mklog(2, "Failed to read from file: " . $file);
1236: return false;
1237: }
1238: }
1239: }
1240: /**
1241: * Replace a specific line in a text file.
1242: *
1243: * @param string|array $input Either a list of lines or a path to a file.
1244: * @param string $starting What the line to be replaced starts with.
1245: * @param string $replacement The entire line replacement.
1246: * @param array $comments A list of symbols where lines beginning with any of these will be skipped when looking for which line to replace.
1247: * @return boolean|array If the input was a file path then a booleain indicating success will be returned, if the input was a list of lines, a list of lines will be returned.
1248: */
1249: public static function replaceLineBeginingWith(string|array $input, string $starting, string $replacement, array $comments=['#','//']):bool|array{
1250: if(is_string($input)){
1251: if(!is_file($input)){
1252: mklog(2, 'File ' . $input . ' does not exist');
1253: return false;
1254: }
1255:
1256: $lines = file($input);
1257: if(!is_array($lines)){
1258: mklog(2, 'Failed to read from file ' . $input);
1259: return false;
1260: }
1261: }
1262: else{
1263: if(!array_is_list($input)){
1264: mklog(2, 'Cannot use non list array for input');
1265: return false;
1266: }
1267: $lines = $input;
1268: }
1269:
1270: if(!array_is_list($comments)){
1271: mklog(2, 'Cannot use non list array for comments');
1272: return false;
1273: }
1274:
1275: $commentsLength = [];
1276: foreach($comments as $comment){
1277: $commentsLength[$comment] = strlen($comment);
1278: }
1279: unset($comments);
1280:
1281: $count = strlen($starting);
1282: $somethingHappened = false;
1283: foreach($lines as $index => $line){
1284: $line = trim($line);
1285: if(empty($line)){
1286: continue;
1287: }
1288:
1289: foreach($commentsLength as $comment => $commentLength){
1290: if(substr($line, 0, $commentLength) === $comment){
1291: continue;
1292: }
1293: }
1294:
1295: if(substr($line,0,$count) === $starting){
1296: $lines[$index] = $replacement . "\n";
1297: $somethingHappened = true;
1298: break;
1299: }
1300: }
1301:
1302: if(!$somethingHappened){
1303: return false;
1304: }
1305:
1306: if(is_string($input)){
1307: return self::mktxt($input, implode($lines), true);
1308: }
1309:
1310: return $lines;
1311: }
1312: }
1313: /**
1314: * Reading command line input.
1315: */
1316: class user_input{
1317: /**
1318: * Waits for the user to input something in the command line.
1319: *
1320: * @param boolean $newline Weather to output a newline for the user to type on.
1321: * @param boolean $returnArray Weather to parse the inputted line using cli::parseLine().
1322: * @return string|array The inputted string or the parseLine output.
1323: */
1324: public static function await($newline=false, $returnArray=false):string|array{
1325: if($newline){
1326: echo "\n";
1327: }
1328:
1329: $return = readline(">");
1330: readline_add_history($return);
1331:
1332: if($returnArray){
1333: $return = cli::parseLine($return);
1334: }
1335: return $return;
1336: }
1337: /**
1338: * Asks the user to input yes (y) or no (n).
1339: *
1340: * @return boolean Weather the input was yes.
1341: */
1342: public static function yesNo():bool{
1343: $res = "";
1344: while(true){
1345:
1346: echo "\ny/n >";
1347:
1348: $res = substr(trim(strtolower(self::await())), 0, 1);
1349:
1350: if($res === "y"){
1351: return true;
1352: }
1353: elseif($res === "n"){
1354: return false;
1355: }
1356: }
1357: }
1358: }
1359:
1360: /**
1361: * Enables an extension in php.ini config file. A restart of PHP-CLI is required to actually enable the extension after this is called.
1362: *
1363: * @param string $extension The extension to enable.
1364: * @return boolean True if the config file was edited, false otherwise.
1365: */
1366: function extension_enable(string $extension):bool{
1367: return extensions::setEnabled($extension, true);
1368: }
1369: /**
1370: * Checks weather an extension is loaded, and enables it if not allready loaded.
1371: *
1372: * @param string $extension The extension to check.
1373: * @return boolean True if the extension was allready loaded, false otherwise.
1374: */
1375: function extension_ensure(string $extension):bool{
1376: return extensions::ensure($extension);
1377: }