mikuli.cz

:)
git clone https://git.sr.ht/~ashymad/mikuli.cz
Log | Files | Refs

update.php (48615B)


      1 <?php
      2 // Update extension, https://github.com/annaesvensson/yellow-update
      3 
      4 class YellowUpdate {
      5     const VERSION = "0.9.10";
      6     const PRIORITY = "2";
      7     public $yellow;                 // access to API
      8     public $extensions;             // number of extensions
      9     
     10     // Handle initialisation
     11     public function onLoad($yellow) {
     12         $this->yellow = $yellow;
     13         $this->yellow->system->setDefault("updateCurrentRelease", "none");
     14         $this->yellow->system->setDefault("updateAvailableUrl", "auto");
     15         $this->yellow->system->setDefault("updateAvailableFile", "update-available.ini");
     16         $this->yellow->system->setDefault("updateInstalledFile", "update-installed.ini");
     17         $this->yellow->system->setDefault("updateExtensionFile", "extension.ini");
     18         $this->yellow->system->setDefault("updateEventPending", "none");
     19         $this->yellow->system->setDefault("updateEventDaily", "0");
     20         $this->yellow->system->setDefault("updateTrashTimeout", "7776660");
     21     }
     22     
     23     // Handle update
     24     public function onUpdate($action) {
     25         if ($action=="clean" || $action=="daily") {
     26             $statusCode = 200;
     27             $path = $this->yellow->system->get("coreExtensionDirectory");
     28             foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.download$/", false, false) as $entry) {
     29                 if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500;
     30             }
     31             if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!");
     32             $statusCode = 200;
     33             $path = $this->yellow->system->get("coreTrashDirectory");
     34             foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, false) as $entry) {
     35                 $expire = $this->yellow->toolbox->getFileDeleted($entry) + $this->yellow->system->get("updateTrashTimeout");
     36                 if ($expire<=time() && !$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500;
     37             }
     38             foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, true) as $entry) {
     39                 $expire = $this->yellow->toolbox->getFileDeleted($entry) + $this->yellow->system->get("updateTrashTimeout");
     40                 if ($expire<=time() && !$this->yellow->toolbox->deleteDirectory($entry)) $statusCode = 500;
     41             }
     42             if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!");
     43         }
     44     }
     45     
     46     // Handle request
     47     public function onRequest($scheme, $address, $base, $location, $fileName) {
     48         return $this->processRequestPending($scheme, $address, $base, $location, $fileName);
     49     }
     50     
     51     // Handle command
     52     public function onCommand($command, $text) {
     53         $statusCode = $this->processCommandPending();
     54         if ($statusCode==0) {
     55             switch ($command) {
     56                 case "about":       $statusCode = $this->processCommandAbout($command, $text); break;
     57                 case "install":     $statusCode = $this->processCommandInstall($command, $text); break;
     58                 case "uninstall":   $statusCode = $this->processCommandUninstall($command, $text); break;
     59                 case "update":      $statusCode = $this->processCommandUpdate($command, $text); break;
     60                 default:            $statusCode = 0; break;
     61             }
     62         }
     63         return $statusCode;
     64     }
     65     
     66     // Handle command help
     67     public function onCommandHelp() {
     68         return array("about [extension]", "install [extension]", "uninstall [extension]", "update [extension]");
     69     }
     70     
     71     // Handle page content element
     72     public function onParseContentElement($page, $name, $text, $attributes, $type) {
     73         $output = null;
     74         if ($name=="about" && $type=="inline") {
     75             list($dummy, $settingsCurrent) = $this->getExtensionSettings(true);
     76             $output = "Datenstrom Yellow ".YellowCore::RELEASE."<br />\n";
     77             foreach ($settingsCurrent as $key=>$value) {
     78                 $output .= ucfirst($key)." ".$value->get("version")."<br />\n";
     79             }
     80         }
     81         return $output;
     82     }
     83     
     84     // Process command to show current version
     85     public function processCommandAbout($command, $text) {
     86         $statusCode = 200;
     87         $extensions = $this->getExtensionsFromText($text);
     88         if (!is_array_empty($extensions)) {
     89             list($statusCode, $settings) = $this->getExtensionAboutInformation($extensions);
     90             if ($statusCode==200) {
     91                 foreach ($settings as $key=>$value) {
     92                     echo ucfirst($key)." ".$value->get("version")." - ".$this->getExtensionDescription($key, $value)."\n";
     93                     if ($value->isExisting("documentationUrl")) echo "Read more at ".$value->get("documentationUrl")."\n";
     94                 }
     95             }
     96             if ($statusCode>=400) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n";
     97         } else {
     98             echo "Datenstrom Yellow ".YellowCore::RELEASE."\n";
     99             list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true);
    100             foreach ($settingsCurrent as $key=>$value) {
    101                 echo ucfirst($key)." ".$value->get("version")."\n";
    102             }
    103         }
    104         return $statusCode;
    105     }
    106     
    107     // Process command to install extensions
    108     public function processCommandInstall($command, $text) {
    109         $extensions = $this->getExtensionsFromText($text);
    110         if (!is_array_empty($extensions)) {
    111             $this->extensions = 0;
    112             list($statusCode, $settings) = $this->getExtensionInstallInformation($extensions);
    113             if ($statusCode==200) $statusCode = $this->downloadExtensions($settings);
    114             if ($statusCode==200) $statusCode = $this->updateExtensions("install");
    115             if ($statusCode>=400) echo "ERROR installing files: ".$this->yellow->page->errorMessage."\n";
    116             echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated";
    117             echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." installed\n";
    118         } else {
    119             list($statusCode, $settingsAvailable) = $this->getExtensionSettings(false);
    120             foreach ($settingsAvailable as $key=>$value) {
    121                 echo ucfirst($key)." - ".$this->getExtensionDescription($key, $value)."\n";
    122             }
    123             if ($statusCode!=200) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n";
    124         }
    125         return $statusCode;
    126     }
    127     
    128     // Process command to uninstall extensions
    129     public function processCommandUninstall($command, $text) {
    130         $extensions = $this->getExtensionsFromText($text);
    131         if (!is_array_empty($extensions)) {
    132             $this->extensions = 0;
    133             list($statusCode, $settings) = $this->getExtensionUninstallInformation($extensions, "core, update");
    134             if ($statusCode==200) $statusCode = $this->removeExtensions($settings);
    135             if ($statusCode>=400) echo "ERROR uninstalling files: ".$this->yellow->page->errorMessage."\n";
    136             echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated";
    137             echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." uninstalled\n";
    138         } else {
    139             list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true);
    140             foreach ($settingsCurrent as $key=>$value) {
    141                 echo ucfirst($key)." - ".$this->getExtensionDescription($key, $value)."\n";
    142             }
    143             if ($statusCode!=200) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n";
    144         }
    145         return $statusCode;
    146     }
    147 
    148     // Process command to update website
    149     public function processCommandUpdate($command, $text) {
    150         $extensions = $this->getExtensionsFromText($text);
    151         if (!is_array_empty($extensions)) {
    152             list($statusCode, $settings) = $this->getExtensionUpdateInformation($extensions);
    153             if ($statusCode!=200 || !is_array_empty($settings)) {
    154                 $this->extensions = 0;
    155                 if ($statusCode==200) $statusCode = $this->downloadExtensions($settings);
    156                 if ($statusCode==200) $statusCode = $this->updateExtensions("update");
    157                 if ($statusCode>=400) echo "ERROR updating files: ".$this->yellow->page->errorMessage."\n";
    158                 echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated";
    159                 echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." updated\n";
    160             } else {
    161                 echo "Your website is up to date\n";
    162             }
    163         } else {
    164             list($statusCode, $settings) = $this->getExtensionUpdateInformation(array("all"));
    165             if (!is_array_empty($settings)) {
    166                 foreach ($settings as $key=>$value) {
    167                     echo ucfirst($key)." ".$value->get("version")."\n";
    168                 }
    169                 echo "Yellow $command: Updates are available. Please type 'php yellow.php update all'.\n";
    170             } elseif ($statusCode!=200) {
    171                 echo "ERROR updating files: ".$this->yellow->page->errorMessage."\n";
    172             } else {
    173                 echo "Your website is up to date\n";
    174             }
    175         }
    176         return $statusCode;
    177     }
    178     
    179     // Process command for pending events
    180     public function processCommandPending() {
    181         $statusCode = 0;
    182         $this->extensions = 0;
    183         $this->updatePatchPending();
    184         $this->updateEventPending();
    185         $statusCode = $this->updateExtensionPending();
    186         if ($statusCode==303) {
    187             echo "Yellow detected ZIP file".($this->extensions!=1 ? "s" : "");
    188             echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." installed. Please run command again.\n";
    189         }
    190         return $statusCode;
    191     }
    192     
    193     // Process request for pending events
    194     public function processRequestPending($scheme, $address, $base, $location, $fileName) {
    195         $statusCode = 0;
    196         if ($this->yellow->lookup->isContentFile($fileName)) {
    197             $this->updatePatchPending();
    198             $this->updateEventPending();
    199             $statusCode = $this->updateExtensionPending();
    200             if ($statusCode==303) {
    201                 $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
    202                 $statusCode = $this->yellow->sendStatus(303, $location);
    203             }
    204         }
    205         return $statusCode;
    206     }
    207     
    208     // Download extensions
    209     public function downloadExtensions($settings) {
    210         $statusCode = 200;
    211         $path = $this->yellow->system->get("coreExtensionDirectory");
    212         foreach ($settings as $key=>$value) {
    213             $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip";
    214             list($statusCode, $fileData) = $this->getExtensionFile($value->get("downloadUrl"));
    215             if ($statusCode==200 && !$this->yellow->toolbox->writeFile($fileName.".download", $fileData)) {
    216                 $statusCode = 500;
    217                 $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
    218             }
    219             if ($statusCode!=200) break;
    220         }
    221         if ($statusCode==200) {
    222             foreach ($settings as $key=>$value) {
    223                 $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip";
    224                 if (!$this->yellow->toolbox->renameFile($fileName.".download", $fileName)) {
    225                     $statusCode = 500;
    226                     $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
    227                 }
    228             }
    229         }
    230         return $statusCode;
    231     }
    232 
    233     // Update extensions
    234     public function updateExtensions($action) {
    235         $statusCode = 200;
    236         if (function_exists("opcache_reset")) opcache_reset();
    237         $this->yellow->page->setHeader("Clear-Site-Data", "cache");
    238         $path = $this->yellow->system->get("coreExtensionDirectory");
    239         foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry) {
    240             $statusCode = max($statusCode, $this->updateExtensionArchive($entry, $action));
    241             if (!$this->yellow->toolbox->deleteFile($entry)) {
    242                 $statusCode = 500;
    243                 $this->yellow->page->error($statusCode, "Can't delete file '$entry'!");
    244             }
    245         }
    246         return $statusCode;
    247     }
    248 
    249     // Update extension from archive
    250     public function updateExtensionArchive($path, $action) {
    251         $statusCode = 200;
    252         $zip = new ZipArchive();
    253         if ($zip->open($path)===true) {
    254             if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUpdate::updateExtensionArchive file:$path<br />\n";
    255             $pathBase = "";
    256             if (preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1];
    257             $fileData = $zip->getFromName($pathBase.$this->yellow->system->get("updateExtensionFile"));
    258             $settings = $this->yellow->toolbox->getTextSettings($fileData, "");
    259             list($extension, $version, $newModified, $oldModified) = $this->getExtensionInformation($settings);
    260             if (!is_string_empty($extension) && !is_string_empty($version)) {
    261                 $statusCode = max($statusCode, $this->updateExtensionSettings($extension, $action, $fileData));
    262                 $paths = $this->getExtensionDirectories($zip, $pathBase);
    263                 foreach ($this->getExtensionFileNames($settings) as $fileName) {
    264                     list($entry, $flags) = $this->yellow->toolbox->getTextList($settings[$fileName], ",", 2);
    265                     if (!$this->yellow->lookup->isContentFile($fileName)) {
    266                         $fileNameSource = $pathBase.$entry;
    267                         $fileData = $zip->getFromName($fileNameSource);
    268                         $lastModified = $this->yellow->toolbox->getFileModified($fileName);
    269                         $statusCode = max($statusCode, $this->updateExtensionFile($fileName, $fileData,
    270                             $newModified, $oldModified, $lastModified, $flags, $extension));
    271                     } else {
    272                         foreach ($this->getExtensionContentRootPages() as $page) {
    273                             list($fileNameSource, $fileNameDestination) = $this->getExtensionContentFileNames(
    274                                 $fileName, $pathBase, $entry, $flags, $paths, $page);
    275                             $fileData = $zip->getFromName($fileNameSource);
    276                             $lastModified = $this->yellow->toolbox->getFileModified($fileNameDestination);
    277                             $statusCode = max($statusCode, $this->updateExtensionFile($fileNameDestination, $fileData,
    278                                 $newModified, $oldModified, $lastModified, $flags, $extension));
    279                         }
    280                     }
    281                 }
    282                 $statusCode = max($statusCode, $this->updateExtensionNotification($extension, $action));
    283                 $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'");
    284                 ++$this->extensions;
    285             } else {
    286                 $statusCode = 500;
    287                 $this->yellow->page->error($statusCode, "Can't detect file '$path'!");
    288             }
    289             $zip->close();
    290         } else {
    291             $statusCode = 500;
    292             $this->yellow->page->error($statusCode, "Can't open file '$path'!");
    293         }
    294         return $statusCode;
    295     }
    296     
    297     // Update extension from file
    298     public function updateExtensionFile($fileName, $fileData, $newModified, $oldModified, $lastModified, $flags, $extension) {
    299         $statusCode = 200;
    300         $fileName = $this->yellow->lookup->normalisePath($fileName);
    301         if ($this->yellow->lookup->isValidFile($fileName)) {
    302             $create = $update = $delete = false;
    303             if (preg_match("/create/i", $flags) && !is_file($fileName) && !is_string_empty($fileData)) $create = true;
    304             if (preg_match("/update/i", $flags) && is_file($fileName) && !is_string_empty($fileData)) $update = true;
    305             if (preg_match("/delete/i", $flags) && is_file($fileName)) $delete = true;
    306             if (preg_match("/optional/i", $flags) && $this->yellow->extension->isExisting($extension)) $create = $update = $delete = false;
    307             if (preg_match("/careful/i", $flags) && is_file($fileName) && $lastModified!=$oldModified) $update = false;
    308             if ($create) {
    309                 if (!$this->yellow->toolbox->writeFile($fileName, $fileData, true) ||
    310                     !$this->yellow->toolbox->modifyFile($fileName, $newModified)) {
    311                     $statusCode = 500;
    312                     $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
    313                 }
    314             }
    315             if ($update) {
    316                 if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory")) ||
    317                     !$this->yellow->toolbox->writeFile($fileName, $fileData) ||
    318                     !$this->yellow->toolbox->modifyFile($fileName, $newModified)) {
    319                     $statusCode = 500;
    320                     $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
    321                 }
    322             }
    323             if ($delete) {
    324                 if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory"))) {
    325                     $statusCode = 500;
    326                     $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!");
    327                 }
    328             }
    329             if ($this->yellow->system->get("coreDebugMode")>=2) {
    330                 $debug = "action:".($create ? "create" : "").($update ? "update" : "").($delete ? "delete" : "");
    331                 if (!$create && !$update && !$delete) $debug = "action:none";
    332                 echo "YellowUpdate::updateExtensionFile file:$fileName $debug<br />\n";
    333             }
    334         }
    335         return $statusCode;
    336     }
    337 
    338     // Update pending patches
    339     public function updatePatchPending() {
    340         $fileName = $this->yellow->system->get("coreWorkerDirectory")."updatepatch.bin";
    341         if (is_file($fileName)) {
    342             if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUpdate::updatePatchPending file:$fileName<br />\n";
    343             if (!$this->yellow->extension->isExisting("updatepatch")) {
    344                 require_once($fileName);
    345                 $this->yellow->extension->register("updatepatch", "YellowUpdatePatch");
    346             }
    347             if ($this->yellow->extension->isExisting("updatepatch")) {
    348                 $value = $this->yellow->extension->data["updatepatch"];
    349                 if (method_exists($value["object"], "onLoad")) $value["object"]->onLoad($this->yellow);
    350                 if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("patch");
    351             }
    352             unset($this->yellow->extension->data["updatepatch"]);
    353             if (function_exists("opcache_reset")) opcache_reset();
    354             $this->yellow->page->setHeader("Clear-Site-Data", "cache");
    355             if (!$this->yellow->toolbox->deleteFile($fileName)) {
    356                 $this->yellow->toolbox->log("error", "Can't delete file '$fileName'!");
    357             }
    358         }
    359     }
    360 
    361     // Update pending events
    362     public function updateEventPending() {
    363         if ($this->yellow->system->get("updateCurrentRelease")!="none") {
    364             if ($this->yellow->system->get("updateCurrentRelease")!=YellowCore::RELEASE) {
    365                 $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
    366                 if (!$this->yellow->system->save($fileName, array("updateCurrentRelease" => YellowCore::RELEASE))) {
    367                     $this->yellow->toolbox->log("error", "Can't write file '$fileName'!");
    368                 } else {
    369                     list($name, $version, $os) = $this->yellow->toolbox->detectServerInformation();
    370                     $product = "Datenstrom Yellow ".YellowCore::RELEASE;
    371                     $this->yellow->toolbox->log("info", "Update $product, PHP ".PHP_VERSION.", $name $version, $os");
    372                 }
    373             }
    374             if ($this->yellow->system->get("updateEventPending")!="none") {
    375                 foreach (preg_split("/\s*,\s*/", $this->yellow->system->get("updateEventPending")) as $token) {
    376                     list($extension, $action) = $this->yellow->toolbox->getTextList($token, "/", 2);
    377                     if ($action!="uninstall") {
    378                         $this->updateSystemSettings($extension, $action);
    379                         $this->updateLanguageSettings($extension, $action);
    380                         if ($this->yellow->extension->isExisting($extension)) {
    381                             $value = $this->yellow->extension->data[$extension];
    382                             if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate($action);
    383                         }
    384                     }
    385                 }
    386                 $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
    387                 if (!$this->yellow->system->save($fileName, array("updateEventPending" => "none"))) {
    388                     $this->yellow->toolbox->log("error", "Can't write file '$fileName'!");
    389                 }
    390             }
    391             if ($this->yellow->system->get("updateEventDaily")<=time()) {
    392                 foreach ($this->yellow->extension->data as $key=>$value) {
    393                     if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("daily");
    394                 }
    395                 $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
    396                 if (!$this->yellow->system->save($fileName, array("updateEventDaily" => $this->getTimestampDaily()))) {
    397                     $this->yellow->toolbox->log("error", "Can't write file '$fileName'!");
    398                 }
    399             }
    400         }
    401     }
    402     
    403     // Update pending extensions
    404     public function updateExtensionPending() {
    405         $statusCode = 0;
    406         $path = $this->yellow->system->get("coreExtensionDirectory");
    407         if (!is_array_empty($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", false, false))) {
    408             $statusCode = $this->updateExtensions("install");
    409             if ($statusCode==200) $statusCode = 303;
    410             if ($statusCode>=400) {
    411                 $this->yellow->toolbox->log("error", $this->yellow->page->errorMessage);
    412                 $this->yellow->page->statusCode = 0;
    413                 $this->yellow->page->errorMessage = "";
    414                 $statusCode = 303;
    415             }
    416         }
    417         return $statusCode;
    418     }
    419     
    420     // Update extension settings
    421     public function updateExtensionSettings($extension, $action, $text = "") {
    422         $statusCode = 200;
    423         $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateInstalledFile");
    424         $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName);
    425         if ($action=="install" || $action=="update") {
    426             $settingsCurrent = $this->yellow->toolbox->getTextSettings($fileData, "extension");
    427             $settingsCurrent[$extension] = new YellowArray();
    428             $block = $this->yellow->toolbox->getTextSettings($text, "");
    429             foreach ($block as $key=>$value) $settingsCurrent[$extension][$key] = $value;
    430             $settingsCurrent->uksort("strnatcasecmp");
    431             $fileDataNew = "";
    432             foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
    433                 if (preg_match("/^\#/", $line)) $fileDataNew = $line;
    434                 break;
    435             }
    436             foreach ($settingsCurrent as $extension=>$block) {
    437                 if (!is_string_empty($fileDataNew)) $fileDataNew .= "\n";
    438                 foreach ($block as $key=>$value) {
    439                     $fileDataNew .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n";
    440                 }
    441             }
    442         } elseif ($action=="uninstall") {
    443             $fileDataNew = $this->yellow->toolbox->unsetTextSettings($fileData, "extension", $extension);
    444         }
    445         if ($fileData!=$fileDataNew && !$this->yellow->toolbox->writeFile($fileName, $fileDataNew)) {
    446             $statusCode = 500;
    447             $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
    448         }
    449         return $statusCode;
    450     }
    451     
    452     // Update system settings
    453     public function updateSystemSettings($extension, $action) {
    454         $statusCode = 200;
    455         $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
    456         $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName);
    457         if (!is_string_empty($extension)) {
    458             $regex = "/^".ucfirst($extension)."[A-Z]+/";
    459             if ($action=="install" || $action=="update") {
    460                 $fileDataStart = $fileDataSettings = "";
    461                 $settings = new YellowArray();
    462                 $settings->exchangeArray($this->yellow->system->settingsDefaults->getArrayCopy());
    463                 foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
    464                     if (preg_match("/^\#/", $line)) {
    465                         if (is_string_empty($fileDataStart)) $fileDataStart = $line."\n";
    466                         continue;
    467                     }
    468                     if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
    469                         if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
    470                             if (!preg_match($regex, $matches[1]) || $settings->isExisting($matches[1])) {
    471                                 $settings[$matches[1]] = $matches[2];
    472                             }
    473                         }
    474                     }
    475                 }
    476                 foreach ($settings as $key=>$value) {
    477                     $fileDataSettings .= ucfirst($key).(is_string_empty($value) ? ":\n" : ": $value\n");
    478                 }
    479                 $fileDataNew = $fileDataStart.$fileDataSettings;
    480             } elseif ($action=="uninstall") {
    481                 $fileDataNew = "";
    482                 foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
    483                     if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
    484                         if (!is_string_empty($matches[1]) && preg_match($regex, $matches[1])) continue;
    485                     }
    486                     $fileDataNew .= $line;
    487                 }
    488             }
    489         }
    490         if ($fileData!=$fileDataNew && !$this->yellow->toolbox->writeFile($fileName, $fileDataNew)) {
    491             $statusCode = 500;
    492             $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
    493         }
    494         return $statusCode;
    495     }
    496     
    497     // Update language settings
    498     public function updateLanguageSettings($extension, $action) {
    499         $statusCode = 200;
    500         $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreLanguageFile");
    501         $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName);
    502         if (!is_string_empty($extension) && ucfirst($extension)!="Language") {
    503             $regex = "/^".ucfirst($extension)."[A-Z]+/";
    504             if ($action=="install" || $action=="update") {
    505                 $fileDataStart = $fileDataSettings = $language = "";
    506                 $settings = new YellowArray();
    507                 foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
    508                     if (preg_match("/^\#/", $line)) {
    509                         if (is_string_empty($fileDataStart)) $fileDataStart = $line."\n";
    510                         continue;
    511                     }
    512                     if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
    513                         if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
    514                             if (lcfirst($matches[1])=="language") {
    515                                 if (!is_array_empty($settings)) {
    516                                     if (!is_string_empty($fileDataSettings)) $fileDataSettings .= "\n";
    517                                     foreach ($settings as $key=>$value) {
    518                                         $fileDataSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n";
    519                                     }
    520                                 }
    521                                 $language = $matches[2];
    522                                 $settings = new YellowArray();
    523                                 $settings["language"] = $language;
    524                                 $settings["languageLocale"] = "n/a";
    525                                 $settings["languageDescription"] = "n/a";
    526                                 $settings["languageTranslator"] = "Unknown";
    527                                 foreach ($this->yellow->language->settingsDefaults as $key=>$value) {
    528                                     $require = preg_match("/^([a-z]*)[A-Z]+/", $key, $tokens) ? $tokens[1] : "core";
    529                                     if ($require=="language") $require = "core";
    530                                     if ($this->yellow->extension->isExisting($require)) {
    531                                         if ($this->yellow->language->isText($key, $language)) {
    532                                             $settings[$key] = $this->yellow->language->getText($key, $language);
    533                                         } else {
    534                                             $settings[$key] = $this->yellow->language->getText($key, "en");
    535                                         }
    536                                     }
    537                                 }
    538                             }
    539                             if (!is_string_empty($language)) {
    540                                 if (!preg_match($regex, $matches[1]) || $settings->isExisting($matches[1])) {
    541                                     $settings[$matches[1]] = $matches[2];
    542                                 }
    543                             }
    544                         }
    545                     }
    546                 }
    547                 if (!is_array_empty($settings)) {
    548                     if (!is_string_empty($fileDataSettings)) $fileDataSettings .= "\n";
    549                     foreach ($settings as $key=>$value) {
    550                         $fileDataSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n";
    551                     }
    552                 }
    553                 $fileDataNew = $fileDataStart.$fileDataSettings;
    554             } elseif ($action=="uninstall") {
    555                 $fileDataNew = "";
    556                 foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
    557                     if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
    558                         if (!is_string_empty($matches[1]) && preg_match($regex, $matches[1])) continue;
    559                     }
    560                     $fileDataNew .= $line;
    561                 }
    562             }
    563         }
    564         if ($fileData!=$fileDataNew && !$this->yellow->toolbox->writeFile($fileName, $fileDataNew)) {
    565             $statusCode = 500;
    566             $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
    567         }
    568         return $statusCode;
    569     }
    570     
    571     // Update extension notification
    572     public function updateExtensionNotification($extension, $action) {
    573         $statusCode = 200;
    574         if ($this->yellow->extension->isExisting($extension) && $action=="uninstall") {
    575             $value = $this->yellow->extension->data[$extension];
    576             if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate($action);
    577         }
    578         $updateEventPending = $this->yellow->system->get("updateEventPending");
    579         if ($updateEventPending=="none") $updateEventPending = "";
    580         if (!is_string_empty($updateEventPending)) $updateEventPending .= ",";
    581         $updateEventPending .= "$extension/$action";
    582         $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile");
    583         if (!$this->yellow->system->save($fileName, array("updateEventPending" => $updateEventPending))) {
    584             $statusCode = 500;
    585             $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
    586         }
    587         return $statusCode;
    588     }
    589     
    590     // Remove extensions
    591     public function removeExtensions($settings) {
    592         $statusCode = 200;
    593         if (function_exists("opcache_reset")) opcache_reset();
    594         $this->yellow->page->setHeader("Clear-Site-Data", "cache");
    595         foreach ($settings as $extension=>$block) {
    596             $statusCode = max($statusCode, $this->removeExtensionArchive($extension, "uninstall", $block));
    597         }
    598         return $statusCode;
    599     }
    600 
    601     // Remove extension archive
    602     public function removeExtensionArchive($extension, $action, $settings) {
    603         $statusCode = 200;
    604         $fileNames = $this->getExtensionFileNames($settings, true);
    605         if (!is_array_empty($fileNames)) {
    606             $statusCode = max($statusCode, $this->updateExtensionNotification($extension, $action));
    607             foreach ($fileNames as $fileName) {
    608                 $statusCode = max($statusCode, $this->removeExtensionFile($fileName));
    609             }
    610             if ($statusCode==200) {
    611                 $statusCode = max($statusCode, $this->updateExtensionSettings($extension, $action));
    612                 $statusCode = max($statusCode, $this->updateSystemSettings($extension, $action));
    613                 $statusCode = max($statusCode, $this->updateLanguageSettings($extension, $action));
    614             }
    615             $version = $settings->get("version");
    616             $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'");
    617             ++$this->extensions;
    618         } else {
    619             $statusCode = 500;
    620             $this->yellow->page->error($statusCode, "Please delete extension '$extension' manually!");
    621         }
    622         return $statusCode;
    623     }
    624 
    625     // Remove extension file
    626     public function removeExtensionFile($fileName) {
    627         $statusCode = 200;
    628         $fileName = $this->yellow->lookup->normalisePath($fileName);
    629         if ($this->yellow->lookup->isValidFile($fileName) && is_file($fileName)) {
    630             if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory"))) {
    631                 $statusCode = 500;
    632                 $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!");
    633             }
    634             if ($this->yellow->system->get("coreDebugMode")>=2) {
    635                 echo "YellowUpdate::removeExtensionFile file:$fileName action:delete<br />\n";
    636             }
    637         }
    638         return $statusCode;
    639     }
    640 
    641     // Return extensions from text, space separated
    642     public function getExtensionsFromText($text) {
    643         return array_unique(array_filter($this->yellow->toolbox->getTextArguments($text), "strlen"));
    644     }
    645     
    646     // Return extension about information
    647     public function getExtensionAboutInformation($extensions) {
    648         $settings = array();
    649         list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true);
    650         $settingsCurrent["Datenstrom Yellow"] = new YellowArray();
    651         $settingsCurrent["Datenstrom Yellow"]["version"] = YellowCore::RELEASE;
    652         $settingsCurrent["Datenstrom Yellow"]["description"] = "Datenstrom Yellow is for people who make small websites.";
    653         $settingsCurrent["Datenstrom Yellow"]["documentationUrl"] = "https://datenstrom.se/yellow/";
    654         foreach ($extensions as $extension) {
    655             $found = false;
    656             if (strtoloweru($extension)=="yellow") $extension = "Datenstrom Yellow";
    657             foreach ($settingsCurrent as $key=>$value) {
    658                 if (strtoloweru($key)==strtoloweru($extension)) {
    659                     $settings[$key] = $settingsCurrent[$key];
    660                     $found = true;
    661                     break;
    662                 }
    663             }
    664             if (!$found) {
    665                 $statusCode = 500;
    666                 $this->yellow->page->error($statusCode, "Can't find extension '$extension'!");
    667             }
    668         }
    669         return array($statusCode, $settings);
    670     }
    671 
    672     // Return extension install information
    673     public function getExtensionInstallInformation($extensions) {
    674         $settings = array();
    675         list($statusCodeCurrent, $settingsCurrent) = $this->getExtensionSettings(true);
    676         list($statusCodeAvailable, $settingsAvailable) = $this->getExtensionSettings(false);
    677         $statusCode = max($statusCodeCurrent, $statusCodeAvailable);
    678         foreach ($extensions as $extension) {
    679             $found = false;
    680             foreach ($settingsAvailable as $key=>$value) {
    681                 if (strtoloweru($key)==strtoloweru($extension)) {
    682                     if (!$settingsCurrent->isExisting($key)) $settings[$key] = $settingsAvailable[$key];
    683                     $found = true;
    684                     break;
    685                 }
    686             }
    687             if (!$found) {
    688                 $statusCode = 500;
    689                 $this->yellow->page->error($statusCode, "Can't find extension '$extension'!");
    690             }
    691         }
    692         return array($statusCode, $settings);
    693     }
    694 
    695     // Return extension about information
    696     public function getExtensionUninstallInformation($extensions, $extensionsProtected = "") {
    697         $settings = array();
    698         list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true);
    699         foreach ($extensions as $extension) {
    700             $found = false;
    701             foreach ($settingsCurrent as $key=>$value) {
    702                 if (strtoloweru($key)==strtoloweru($extension)) {
    703                     $settings[$key] = $settingsCurrent[$key];
    704                     $found = true;
    705                     break;
    706                 }
    707             }
    708             if (!$found) {
    709                 $statusCode = 500;
    710                 $this->yellow->page->error($statusCode, "Can't find extension '$extension'!");
    711             }
    712         }
    713         $protected = preg_split("/\s*,\s*/", $extensionsProtected);
    714         foreach ($settings as $key=>$value) {
    715             if (in_array($key, $protected)) unset($settings[$key]);
    716         }
    717         return array($statusCode, $settings);
    718     }
    719     
    720     // Return extension update information
    721     public function getExtensionUpdateInformation($extensions) {
    722         $settings = array();
    723         list($statusCodeCurrent, $settingsCurrent) = $this->getExtensionSettings(true);
    724         list($statusCodeAvailable, $settingsAvailable) = $this->getExtensionSettings(false);
    725         $statusCode = max($statusCodeCurrent, $statusCodeAvailable);
    726         if (in_array("all", $extensions)) {
    727             foreach ($settingsCurrent as $key=>$value) {
    728                 if ($settingsAvailable->isExisting($key)) {
    729                     $versionCurrent = $settingsCurrent[$key]->get("version");
    730                     $versionAvailable = $settingsAvailable[$key]->get("version");
    731                     if (strnatcasecmp($versionCurrent, $versionAvailable)<0) {
    732                         $settings[$key] = $settingsAvailable[$key];
    733                     }
    734                 }
    735             }
    736         } else {
    737             foreach ($extensions as $extension) {
    738                 $found = false;
    739                 foreach ($settingsCurrent as $key=>$value) {
    740                     if (strtoloweru($key)==strtoloweru($extension) && $settingsAvailable->isExisting($key)) {
    741                         $versionCurrent = $settingsCurrent[$key]->get("version");
    742                         $versionAvailable = $settingsAvailable[$key]->get("version");
    743                         if (strnatcasecmp($versionCurrent, $versionAvailable)<0) {
    744                             $settings[$key] = $settingsAvailable[$key];
    745                         }
    746                         $found = true;
    747                         break;
    748                     }
    749                 }
    750                 if (!$found) {
    751                     $statusCode = 500;
    752                     $this->yellow->page->error($statusCode, "Can't find extension '$extension'!");
    753                 }
    754             }
    755         }
    756         return array($statusCode, $settings);
    757     }
    758 
    759     // Return extension settings
    760     public function getExtensionSettings($current) {
    761         $statusCode = 200;
    762         $settings = array();
    763         if ($current) {
    764             $fileNameCurrent = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateInstalledFile");
    765             $fileData = $this->yellow->toolbox->readFile($fileNameCurrent);
    766             $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension");
    767             foreach ($settings->getArrayCopy() as $key=>$value) {
    768                 if (!$this->yellow->extension->isExisting($key)) unset($settings[$key]);
    769             }
    770             foreach ($this->yellow->extension->data as $key=>$value) {
    771                 if (!$settings->isExisting($key)) $settings[$key] = new YellowArray();
    772                 $settings[$key]["extension"] = ucfirst($key);
    773                 $settings[$key]["version"] = $value["version"];
    774             }
    775         } else {
    776             $fileNameAvailable = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateAvailableFile");
    777             $expire = $this->yellow->toolbox->getFileModified($fileNameAvailable) + 60*10;
    778             if ($expire<=time()) {
    779                 $url = $this->yellow->system->get("updateAvailableUrl");
    780                 if ($url=="auto") $url = "https://raw.githubusercontent.com/datenstrom/yellow/main/system/extensions/update-available.ini";
    781                 list($statusCode, $fileData) = $this->getExtensionFile($url);
    782                 if ($statusCode==200 && !$this->yellow->toolbox->writeFile($fileNameAvailable, $fileData)) {
    783                     $statusCode = 500;
    784                     $this->yellow->page->error($statusCode, "Can't write file '$fileNameAvailable'!");
    785                 }
    786             }
    787             $fileData = $this->yellow->toolbox->readFile($fileNameAvailable);
    788             $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension");
    789         }
    790         $settings->uksort("strnatcasecmp");
    791         return array($statusCode, $settings);
    792     }
    793 
    794     // Return extension information
    795     public function getExtensionInformation($settings) {
    796         $extension = lcfirst($settings->get("extension"));
    797         $version = $settings->get("version");
    798         $newModified = strtotime($settings->get("published"));
    799         $oldModified = 0;
    800         $invalid = false;
    801         foreach ($settings as $key=>$value) {
    802             if (strposu($key, "/")) {
    803                 $fileName = $this->yellow->lookup->normalisePath($key);
    804                 if (!$this->yellow->lookup->isValidFile($fileName)) $invalid = true;
    805                 if ($oldModified==0) $oldModified = $this->yellow->toolbox->getFileModified($fileName);
    806             }
    807         }
    808         if ($invalid) $extension = $version = "";
    809         return array($extension, $version, $newModified, $oldModified);
    810     }
    811 
    812     // Return extension directories
    813     public function getExtensionDirectories($zip, $pathBase) {
    814         $paths = array();
    815         for ($index=0; $index<$zip->numFiles; ++$index) {
    816             $entry = substru($zip->getNameIndex($index), strlenu($pathBase));
    817             if (preg_match("#^(.*\/).*?$#", $entry, $matches)) {
    818                 array_push($paths, $matches[1]);
    819             }
    820         }
    821         return array_unique($paths);
    822     }
    823     
    824     // Return extension file names
    825     public function getExtensionFileNames($settings, $reverse = false) {
    826         $fileNames = array();
    827         foreach ($settings as $key=>$value) {
    828             if (strposu($key, "/")) array_push($fileNames, $key);
    829         }
    830         if ($reverse) $fileNames = array_reverse($fileNames);
    831         return $fileNames;
    832     }
    833 
    834     // Return extension root pages for content files
    835     public function getExtensionContentRootPages() {
    836         return $this->yellow->content->scanLocation("");
    837     }
    838 
    839     // Return extension files names for content files
    840     public function getExtensionContentFileNames($fileName, $pathBase, $entry, $flags, $paths, $page) {
    841         if (preg_match("/multi-language/i", $flags)) {
    842             $pathMultiLanguage = "";
    843             $languagesWanted = array($page->get("language"), "en");
    844             foreach ($languagesWanted as $language) {
    845                 foreach ($paths as $path) {
    846                     if ($this->yellow->lookup->normaliseToken(rtrim($path, "/"))==$language) {
    847                         $pathMultiLanguage = $path;
    848                         break;
    849                     }
    850                 }
    851                 if (!is_string_empty($pathMultiLanguage)) break;
    852             }
    853             $fileNameSource = $pathBase.$pathMultiLanguage.$entry;
    854         } else {
    855             $fileNameSource = $pathBase.$entry;
    856         }
    857         if ($this->yellow->system->get("coreMultiLanguageMode")) {
    858             $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory"));
    859             $fileNameDestination = $page->fileName.substru($fileName, $contentDirectoryLength);
    860         } else {
    861             $fileNameDestination = $fileName;
    862         }
    863         return array($fileNameSource, $fileNameDestination);
    864     }
    865     
    866     // Return extension description including responsible developer/designer/translator
    867     public function getExtensionDescription($key, $value) {
    868         $description = $responsible = "";
    869         if ($value->isExisting("description")) $description = $value->get("description");
    870         if ($value->isExisting("developer")) $responsible = "Developed by ".$value["developer"].".";
    871         if ($value->isExisting("designer")) $responsible = "Designed by ".$value["designer"].".";
    872         if ($value->isExisting("translator")) $responsible = "Translated by ".$value["translator"].".";
    873         if (is_string_empty($description)) $description = "No description available.";
    874         return "$description $responsible";
    875     }
    876     
    877     // Return extension file
    878     public function getExtensionFile($url) {
    879         $curlHandle = curl_init();
    880         curl_setopt($curlHandle, CURLOPT_URL, $this->getExtensionDownloadUrl($url));
    881         curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; YellowUpdate/".YellowUpdate::VERSION."; SoftwareUpdater)");
    882         curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1);
    883         curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30);
    884         $fileData = curl_exec($curlHandle);
    885         $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
    886         $redirectUrl = ($statusCode>=300 && $statusCode<=399) ? curl_getinfo($curlHandle, CURLINFO_REDIRECT_URL) : "";
    887         if (PHP_VERSION_ID<80000) curl_close($curlHandle);
    888         if ($statusCode==0) {
    889             $statusCode = 450;
    890             $this->yellow->page->error($statusCode, "Can't connect to the update server!");
    891         }
    892         if ($statusCode!=450 && $statusCode!=200) {
    893             $statusCode = 500;
    894             $this->yellow->page->error($statusCode, "Can't download file '$url'!");
    895         }
    896         if ($this->yellow->system->get("coreDebugMode")>=2 && !is_string_empty($redirectUrl)) {
    897             echo "YellowUpdate::getExtensionFile redirected to url:$redirectUrl<br />\n";
    898         }
    899         if ($this->yellow->system->get("coreDebugMode")>=2) {
    900             echo "YellowUpdate::getExtensionFile status:$statusCode url:$url<br />\n";
    901         }
    902         return array($statusCode, $fileData);
    903     }
    904     
    905     // Return extension download URL, redirect to known URL if necessary
    906     public function getExtensionDownloadUrl($url) {
    907         if (preg_match("#^https://github.com/(.+)/archive/refs/heads/main.zip$#", $url, $matches)) {
    908             $url = "https://codeload.github.com/".$matches[1]."/zip/refs/heads/main";
    909         }
    910         if (preg_match("#^https://github.com/(.+)/raw/main/(.+)$#", $url, $matches)) {
    911             $url = "https://raw.githubusercontent.com/".$matches[1]."/main/".$matches[2];
    912         }
    913         return $url;
    914     }
    915     
    916     // Return time of next daily update
    917     public function getTimestampDaily() {
    918         $timeOffset = 0;
    919         foreach (str_split($this->yellow->system->get("sitename")) as $char) {
    920             $timeOffset = ($timeOffset+ord($char)) % 60;
    921         }
    922         return mktime(0, 0, 0) + 60*60*24 + $timeOffset;
    923     }
    924 }