1: <?php
2: pkgmgr::init();
3:
4: /**
5: * The package manager.
6: */
7: class pkgmgr{
8: private static $downloadSite = 'https://www.tomgriffiths.net';
9: private static $downloadSiteFiles = 'https://files.tomgriffiths.net';
10: private static $packageCount = 0;
11: private static $packageInitCount = 0;
12: private static $packages = [];
13: private static $preloadedPackages = ["self","cli","pkgmgr","extensions","inimgmt","linuxcmd","cli_formatter","cmd","commandline_list","data_types","downloader","files","json","time","timetest","txtrw","user_input"];
14:
15: /**
16: * @internal description
17: */
18: public static function command(string $line):void{
19: $lines = explode(" ",$line);
20: if($lines[0] === "list"){
21: $columnNames["Package ID"] = 25;
22: $columnNames["Package Name"] = 35;
23: if(isset($lines[1])){
24: if($lines[1] === "desc"){
25: $columnNames["Package Description"] = 40;
26: }
27: elseif($lines[1] === "depend"){
28: $columnNames["Package Dependencies"] = 40;
29: }
30: }
31: $columnNames["Version"] = 8;
32:
33: $rowsData = array();
34: foreach(self::$packages as $packageId => $packageInfo){
35: $rowData = array();
36: $rowData[] = $packageId;
37: $rowData[] = $packageInfo['name'];
38: if(isset($lines[1])){
39: if($lines[1] === "desc"){
40: $rowData[] = $packageInfo['description'];
41: }
42: elseif($lines[1] === "depend"){
43: $dependenciesString = count($packageInfo['dependencies']) . ": ";
44: ksort($packageInfo['dependencies']);
45: foreach($packageInfo['dependencies'] as $dependencyId => $dependencyVersion){
46: $dependenciesString .= $dependencyId . ", ";
47: }
48: $rowData[] = substr($dependenciesString,0,-2);
49: }
50: }
51:
52: $rowData[] = $packageInfo['version'];
53: $rowsData[] = $rowData;
54: }
55: echo commandline_list::table($columnNames,$rowsData);
56: }
57: elseif($lines[0] === "install"){
58: if(!isset($lines[1])){
59: echo "pkgmgr: Package id not specified\n";
60: }
61: if(!self::downloadPackage($lines[1])){
62: echo "\npkgmgr: Failed to download package " . $lines[1] . "\n\n";
63: }
64: }
65: elseif($lines[0] === "update"){
66: if(isset($lines[1])){
67: if(self::doesPackageExist($lines[1],false)){
68: if(!self::downloadPackage($lines[1],false,false,false)){
69: mklog('warning','Failed to update package ' . $lines[1],false);
70: }
71: else{
72: mklog('general','Please restart for changes to take effect',false);
73: }
74: }
75: }
76: else{
77: self::updatePackages();
78: }
79: }
80: elseif($lines[0] === "update-core"){
81: mklog('general','Running Update',false);
82: files::ensureFolder("temp/coreupdates");
83: if(!downloader::downloadFile("https://files.tomgriffiths.net/php-cli/updates/latest.zip","temp/coreupdates/latest.zip")){
84: echo "Failed to download update file\n";
85: return;
86: }
87:
88: $zip = new ZipArchive;
89: $result = $zip->open("temp/coreupdates/latest.zip");
90: if($result === true){
91: $zip->extractTo(".");
92: $zip->close();
93: unlink("temp/coreupdates/latest.zip");
94: }
95: else{
96: echo "Failed to unzip update file\n";
97: @unlink("temp/coreupdates/latest.zip");
98: return;
99: }
100:
101: cli::command("reload");
102: exit;
103: }
104: else{
105: echo "pkgmgr: Command not found\n";
106: }
107: }
108: /**
109: * @internal
110: */
111: public static function init():void{
112: if(!is_dir('packages')){
113: if(!mkdir('packages',0777,true)){
114: mklog(3,'Failed to create packages directory');
115: }
116: }
117:
118: foreach(glob('packages/*') as $dir){
119: $package = substr($dir,strripos($dir,"/")+1);
120: if(!class_exists($package)){//Other packages may have loaded package as a dependency
121: if(!self::loadPackage($package)){
122: mklog(3,'Unable to load package ' . $package,false);
123: }
124: }
125: }
126:
127: mklog(1,'Loaded ' . self::$packageCount . ' packages, ' . self::$packageInitCount . ' initialized');
128: }
129:
130: /**
131: * Loads a package.
132: *
133: * @param string $package The id of the package to be loaded.
134: * @return boolean Indicates success.
135: */
136: public static function loadPackage(string $package):bool{
137: if(!self::validatePackageId($package)){
138: mklog(2, 'Invalid package id');
139: return false;
140: }
141:
142: if(in_array($package, self::$preloadedPackages)){
143: return true;
144: }
145:
146: if(class_exists($package)){
147: mklog(1,'Package ' . $package . ' is allready loaded');
148: return false;
149: }
150:
151: $info = self::getPackageInfo($package, false);
152: if($info === false){
153: mklog(2, 'Failed to get package info while loading package ' . $package);
154: return false;
155: }
156:
157: foreach($info['dependencies'] as $dependencyId => $dependencyVersion){
158: if(!in_array($dependencyId, self::$preloadedPackages)){
159: $dependencyInfo = self::getPackageInfo($dependencyId,false);
160: if(is_array($dependencyInfo)){
161: if($dependencyInfo['version'] < $dependencyVersion){
162: mklog(2,'Unable to load package ' . $package . ' as one of its dependencies (' . $dependencyId . ') has an incorrect version of ' . $dependencyVersion);
163: return false;
164: }
165: }
166: else{
167: mklog(2,'Unable to load package ' . $package . ' as one of its dependencies (' . $dependencyId . ') does not exist');
168: return false;
169: }
170: }
171: }
172: foreach($info['dependencies'] as $dependencyId => $dependencyVersion){
173: if(!class_exists($dependencyId)){
174: if(!self::loadPackage($dependencyId)){
175: mklog(2,'Unable to load dependency ' . $dependencyId . ' for ' . $package);
176: return false;
177: }
178: }
179: }
180:
181: mklog(0, 'Loading package ' . $package);
182:
183: if(!is_file($info['dir'] . "/main.php")){
184: return false;
185: }
186:
187: if($GLOBALS['arguments']['check-syntax']){
188: if(!self::isPhpFileSyntaxOk($info['dir'] . "/main.php")){//slow
189: mklog(2, "The package " . $package . " has invalid syntax");
190: }
191: }
192:
193: include_once $info['dir'] . "/main.php";
194: self::$packageCount++;
195: self::$packages[$package] = $info;
196:
197: if(method_exists($package,"init")){
198: mklog(1,'Running init for package ' . $info['name']);
199:
200: try{
201: $package::init();
202: self::$packageInitCount++;
203: }
204: catch(Throwable $throwable){
205: mklog(2, "Failed to run init for package " . $package . " (" . substr($throwable,0,strpos($throwable,"\n")) . ")");
206: return false;
207: }
208:
209: }
210:
211: return true;
212: }
213: /**
214: * Checks if a string could be a valid package id, this does not check package existance.
215: *
216: * @param string $packageId The string to be tested.
217: * @return boolean True if the string is valid, false otherwise.
218: */
219: public static function validatePackageId(string $packageId):bool{
220: return (preg_match("/^[a-zA-Z0-9_]+$/",$packageId) === 1);
221: }
222: /**
223: * Checks if some package info is valid.
224: *
225: * @param array $info The array to be checked.
226: * @return boolean Weather the array is valid package info.
227: */
228: public static function validatePackageInfo(array $info):bool{
229: if(!isset($info['id_name'])){
230: mklog(0, 'Package does not have id_name');
231: return false;
232: }
233: if(!is_string($info['id_name']) || !self::validatePackageId($info['id_name'])){
234: mklog(0, 'Package has an invalid id');
235: return false;
236: }
237:
238: foreach(['version', 'author', 'name', 'dependencies'] as $thing){
239: if(!isset($info[$thing])){
240: mklog(0, 'Package ' . $info['id_name'] . ' does not have ' . $thing);
241: return false;
242: }
243: }
244:
245: if(!is_int($info['version']) || $info['version'] < 1){
246: mklog(0, 'Package ' . $info['id_name'] . ' has an invalid version');
247: return false;
248: }
249: if(!is_string($info['author']) || !preg_match("/[a-z0-9_]/", $info['author'])){
250: mklog(0, 'Package ' . $info['id_name'] . ' has an invalid author');
251: return false;
252: }
253:
254: if(!is_string($info['name'])){
255: mklog(0, 'Package ' . $info['id_name'] . ' has an invalid name');
256: return false;
257: }
258:
259: if(!is_array($info['dependencies'])){
260: mklog(0, 'Package ' . $info['id_name'] . ' does not have a dependencies list');
261: return false;
262: }
263: foreach($info['dependencies'] as $key => $value){
264: if(!is_string($key) || !self::validatePackageId($key) || !is_int($value) || $value < 1){
265: mklog(0, 'Package ' . $info['id_name'] . ' has an invalid dependency');
266: return false;
267: }
268: }
269:
270: return true;
271: }
272: /**
273: * Gets the dependencies of a pakage.
274: *
275: * @param array $packageInfo The package info.
276: * @return array|false The dependencies on success or false on failure.
277: */
278: public static function getPackageDependencies(array $packageInfo):array|false{
279: if(self::validatePackageInfo($packageInfo)){
280: return $packageInfo['dependencies'];
281: }
282: return false;
283: }
284: /**
285: * Checks weather a package exists.
286: *
287: * @param string $packageId The id to check for.
288: * @param boolean $online Weather to check online or local.
289: * @return boolean Weather the package exists.
290: */
291: public static function doesPackageExist(string $packageId, bool $online):bool{
292: if(self::getPackageInfo($packageId, $online) !== false){
293: return true;
294: }
295: return false;
296: }
297: /**
298: * Gets a packages package info, see readme for package info.
299: *
300: * @param string $packageId The if of the package.
301: * @param boolean $online Weather to get the info from online or locally.
302: * @return array|false The package info on success or false on failure.
303: */
304: public static function getPackageInfo(string $packageId, bool $online):array|false{
305: if(!self::validatePackageId($packageId)){
306: mklog(2, 'Invalid package name ' . $packageId);
307: return false;
308: }
309:
310: if($online === true){
311: $result = json::readFile(self::$downloadSite . '/php-cli/api/?function=getPackageInfo&packageId=' . $packageId);
312: if(!is_array($result)){
313: mklog(2, 'Failed to download information for package ' . $packageId);
314: return false;
315: }
316:
317: if(!isset($result['success']) || !$result['success'] === true || !isset($result['data']) || !is_array($result['data'])){
318: mklog(2, 'Failed to download valid information for package ' . $packageId);
319: return false;
320: }
321:
322: return $result['data'];
323: }
324: else{
325: $infoFile = 'packages/' . $packageId . '/information.json';
326: $phpMainFile = 'packages/' . $packageId . '/main.php';
327: if(!is_file($infoFile) || !is_file($phpMainFile)){
328: mklog(2, 'The package ' . $packageId . ' has missing files');
329: return false;
330: }
331:
332: $packageInfo = json::readFile($infoFile);
333: if(!is_array($packageInfo)){
334: mklog(2,'Unable to load information for package ' . $packageId);
335: return false;
336: }
337:
338: $packageInfo['dir'] = getcwd() . '/packages/' . $packageId;
339:
340: if(!self::validatePackageInfo($packageInfo)){
341: mklog(2,'The package ' . $packageId . ' has invalid data');
342: return false;
343: }
344:
345: return $packageInfo;
346: }
347: }
348: /**
349: * Checks online for a specific package versions package info.
350: *
351: * @param string $packageId The id of the package.
352: * @param integer $version The version to check.
353: * @return array|false The package info on success or false on failure.
354: */
355: public static function getPackageVersionInfo(string $packageId, int $version):array|false{
356: if(!self::validatePackageId($packageId)){
357: mklog(2, 'Unable get version info for invalid package id ' . $packageId);
358: return false;
359: }
360:
361: $result = json::readFile(self::$downloadSite . '/php-cli/api/?function=getPackageVersionInfo&packageId=' . $packageId . '&version=' . $version);
362: if(!is_array($result)){
363: mklog(2, 'Unable to download information for package ' . $packageId . ' v' . $version);
364: return false;
365: }
366:
367: if(!isset($result['success']) || !$result['success'] || !isset($result['data']) || !is_array($result['data'])){
368: mklog(2, 'Unable to download valid information for package ' . $packageId . ' v' . $version);
369: return false;
370: }
371:
372: return $result['data'];
373: }
374: /**
375: * Downloads a package.
376: *
377: * @param string $packageId The id of the package to download.
378: * @param integer|boolean $version The version to download or false, which makes it download the latest version.
379: * @param boolean $getDependencies Weather to also download the packages dependencies.
380: * @param boolean $load Weather to load the package after it has been downloaded.
381: * @return boolean Indicates success.
382: */
383: public static function downloadPackage(string $packageId, int|bool $version=false, bool $getDependencies=true, bool $load=true):bool{
384: if(!self::validatePackageId($packageId)){
385: mklog(2, 'Invalid package id ' . $packageId);
386: return false;
387: }
388:
389: $info = self::getPackageInfo($packageId, true);
390: if(!is_array($info)){
391: mklog(2, 'Failed to download information about package ' . $packageId);
392: return false;
393: }
394:
395: foreach(['id_name','author','versions','name','latest_version'] as $thing){
396: if(!isset($info[$thing])){
397: mklog(2, 'Incomplete data for download for ' . $packageId);
398: return false;
399: }
400: }
401:
402: $downloadVersion = false;
403: if(is_int($version)){
404: if(in_array($version, $info['versions'])){
405: $downloadVersion = $version;
406: }
407: }
408: if(!is_int($downloadVersion)){
409: mklog(0, 'No download version set, assuming latest version ' . $info['latest_version']);
410: $downloadVersion = $info['latest_version'];
411: }
412:
413: if(is_file('packages/' . $packageId . '/information.json')){
414: $localInfo = json::readFile('packages/' . $packageId . '/information.json');
415: if(!is_array($localInfo)){
416: mklog(1, 'Local package information for package ' . $packageId . ' is not correct, overwriting');
417: }
418: if(isset($localInfo['version'])){
419: if($localInfo['version'] == $downloadVersion){
420: mklog(1,'Version of package ' . $packageId . ' already matches');
421: return true;
422: }
423: }
424: }
425:
426: if(is_file('packages/' . $packageId . '/.noupdate')){
427: mklog(1,'Package ' . $packageId . ' is marked for not updating (.noupdate file found)');
428: return false;
429: }
430:
431: if(is_dir('packages/' . $packageId . '/files')){
432: if(!cmd::run('rmdir "packages/' . $packageId . '/files" /S /Q')){
433: mklog(2,'Failed to remove old files dir for package ' . $packageId);
434: }
435: }
436:
437: $info2 = self::getPackageVersionInfo($packageId,$downloadVersion);
438: if($info2 === false){
439: mklog(2, 'Failed to download information about package ' . $packageId . ' v' . $downloadVersion);
440: return false;
441: }
442:
443: $info2['id_name'] = $info['id_name'];
444: $info2['author'] = $info['author'];
445:
446: foreach(['version', 'name'] as $thing){
447: if(!isset($info2[$thing])){
448: mklog(2,'Incomplete data for version download of ' . $packageId);
449: return false;
450: }
451: }
452:
453: $downloadFile = 'temp/pkgmgr/downloads/' . $packageId . '-' . time() . '.zip';
454: mklog(1,'Downloading package ' . $packageId . ' version ' . $downloadVersion);
455:
456: $downloadTries = 0;
457: retrydownload:
458: if(!downloader::downloadFile(self::$downloadSiteFiles . '/php-cli/packages/' . $packageId . '/' . $downloadVersion . '.zip',$downloadFile)){
459: mklog(2,'Failed to download zip file for package ' . $packageId);
460: return false;
461: }
462:
463: $downloadTries++;
464: $zip = new ZipArchive;
465: $result = $zip->open($downloadFile);
466: if($result !== true){
467: mklog(1, 'Failed to download valid package file, retrying');
468: if($downloadTries < 5){
469: goto retrydownload;
470: }
471: else{
472: mklog(2,'Failed to download a valid zip file for package ' . $packageId);
473: return false;
474: }
475: }
476: $zip->close();
477:
478: if($getDependencies){
479: mklog(0, 'Downloading dependencies for package ' . $packageId);
480: if(isset($info2['dependencies'])){
481: if(is_array($info2['dependencies'])){
482: foreach($info2['dependencies'] as $dependency => $dependencyVersion){
483: if(!class_exists($dependency) || (isset(self::$packages[$dependency]) && self::$packages[$dependency]['version'] < $dependencyVersion)){
484: if(!self::downloadPackage($dependency)){
485: mklog(2, 'Failed to download dependency for ' . $packageId . ' which requires ' . $dependency . ' v' . $dependencyVersion);
486: }
487: }
488: }
489: }
490: else{
491: mklog(2,'Unknown dependencies format for package ' . $packageId);
492: }
493: }
494: }
495:
496: files::ensureFolder('packages/' . $packageId);
497:
498: mklog(0, "Unpacking " . $packageId);
499: $zip = new ZipArchive;
500: $result = $zip->open($downloadFile);
501: if($result !== true){
502: mklog(2, 'Failed to open package zip file ' . $downloadFile);
503: return false;
504: }
505: if(!$zip->extractTo('packages/' . $packageId)){
506: mklog(2, 'Failed to extract contents from package zip file to packages/' . $packageId);
507: return false;
508: }
509: $zip->close();
510:
511: if(!json::writeFile('packages/' . $packageId . '/information.json',$info2,true)){
512: mklog(2,'Unable to write information file for package ' . $packageId);
513: return false;
514: }
515:
516: if(!unlink($downloadFile)){
517: mklog(0,'Unable to delete temporary download file ' . $downloadFile);
518: }
519:
520: if($load){
521: mklog(1, "Loading " . $packageId);
522: return self::loadPackage($packageId);
523: }
524:
525: mklog(1, 'Please restart PHP-CLI for the update to apply');
526: return true;
527: }
528: /**
529: * Updates all the packages.
530: *
531: * @return boolean Indicates success.
532: */
533: public static function updatePackages():bool{
534: $return = false;
535:
536: $glob = glob('packages/*');
537: if(!is_array($glob)){
538: mklog(2, 'Unable to read packages folder');
539: return false;
540: }
541:
542: foreach($glob as $dir){
543: $packageId = files::getFileName($dir);
544: if(self::doesPackageExist($packageId, false)){
545: if(self::downloadPackage($packageId, false, true, false)){
546: $return = true;
547: }
548: else{
549: mklog(2, 'Failed to update package ' . $packageId,false);
550: }
551: }
552: }
553: return $return;
554: }
555: /**
556: * Reads the main.php file in a package and checks for what classes it uses.
557: *
558: * @param string $packageId The id of the package.
559: * @param boolean $getLatestVersions Weather to check the latest versions of the dependencies online.
560: * @return array|false An estimation of the dependencies on success or false on failure.
561: */
562: public static function getPackageFileDependencies(string $packageId, bool $getLatestVersions=true):array|false{
563: if(!self::validatePackageId($packageId)){
564: return false;
565: }
566:
567: if(in_array($packageId, self::$preloadedPackages)){
568: return [];
569: }
570:
571: $file = 'packages/' . $packageId . '/main.php';
572: if(!is_file($file)){
573: return false;
574: }
575:
576: mklog(0, 'Reading ' . $file);
577:
578: $text = file_get_contents($file);
579: if($text === false){
580: mklog(2,'Unable to read package file ' . $file);
581: }
582: $offset = 0;
583: $dependencies = array();
584: while(true){
585: $pos = strpos($text,"::",$offset);
586: if($pos === false){
587: break;
588: }
589: $pos2 = 1;
590: while(true){
591: $dependency = substr($text,$pos - $pos2,$pos2);
592: if(preg_match("/^[a-zA-Z0-9_]+$/", $dependency) === 1){
593: $pos2++;
594: }
595: else{
596: break;
597: }
598: }
599: $dependency = trim(substr($dependency,1));
600: if(!in_array($dependency, self::$preloadedPackages) && is_file('packages/' . $dependency . '/main.php')){
601: $leftfromdependency = 1;
602: $makedependency = true;
603: while(true){
604: $firsttwochars = substr($text,$pos - $pos2 - $leftfromdependency,2);
605: if($firsttwochars === "//"){
606: $makedependency = false;
607: break;
608: }
609: elseif(strpos($firsttwochars,"\n") !== false){
610: break;
611: }
612: else{
613: $leftfromdependency++;
614: }
615: }
616:
617: if($makedependency){
618: if($getLatestVersions){
619: if(!isset($dependencies[$dependency])){
620: mklog(1, 'Checking latest version of ' . $dependency);
621: $onlineInfo = self::getPackageInfo($dependency, true);
622: if(is_array($onlineInfo) && isset($onlineInfo['latest_version'])){
623: $dependencies[$dependency] = $onlineInfo['latest_version'];
624: }
625: else{
626: $dependencies[$dependency] = 1;
627: mklog(2, 'Failed to get latest version for ' . $dependency);
628: }
629: }
630: }
631: else{
632: if(!in_array($dependency, $dependencies)){
633: $dependencies[] = $dependency;
634: }
635: }
636: }
637: }
638: $offset = $pos+2;
639: }
640:
641: return $dependencies;
642: }
643: /**
644: * Gets all the loaded packages and their versions.
645: *
646: * @return array
647: */
648: public static function getLoadedPackages():array{
649: $return = [];
650: foreach(self::$packages as $packageId => $packageInfo){
651: $return[$packageId] = $packageInfo['version'];
652: }
653: return $return;
654: }
655: /**
656: * Gets the download url for the latest thread safe x64 windows build of php from windows.php.net.
657: *
658: * @return string|false The url of the latest zip or false on failure.
659: */
660: public static function getLatestPhpUrl():string|false{
661: $data = json::readFile('https://downloads.php.net/~windows/releases/releases.json');
662:
663: if(!is_array($data)){
664: mklog(2,'Unable to read php releases information');
665: return false;
666: }
667:
668: //Major version search
669: $currentVersion = "0";
670: foreach($data as $version => $versionData){
671: if(floatval($version) > floatval($currentVersion)){
672: $currentVersion = $version;
673: }
674: }
675:
676: if(!isset($data[$currentVersion])){
677: return false;
678: }
679: $data = $data[$currentVersion];
680:
681: //Type search
682: $currentVersion = "";
683: foreach($data as $version => $versionData){
684: if(substr($version,0,3) === "ts-" && substr($version, -4) === "-x64"){
685: $currentVersion = $version;
686: break;
687: }
688: }
689:
690: if(!isset($data[$currentVersion])){
691: return false;
692: }
693: $data = $data[$currentVersion];
694:
695: //Final check
696: if(!isset($data['zip'])){
697: return false;
698: }
699: if(!isset($data['zip']['path'])){
700: return false;
701: }
702:
703: return 'https://downloads.php.net/~windows/releases/' . $data['zip']['path'];
704: }
705: /**
706: * Checks the syntax of a php file with php -l.
707: *
708: * @param string $file The php file to be checked.
709: * @return boolean Weather the php file has valid syntax.
710: */
711: public static function isPhpFileSyntaxOk(string $file):bool{
712:
713: $output = shell_exec("php -l " . escapeshellarg($file) . " 2>&1");
714:
715: return str_contains($output, 'No syntax errors');
716: }
717: }