├── .gitignore ├── boot.php ├── assets ├── images │ └── repo-placeholder.svg └── css │ └── zip_install.css ├── install.php ├── .github └── workflows │ └── publish-to-redaxo-org.yml ├── package.yml ├── update.php ├── LICENSE ├── lang ├── en_gb.lang └── de_de.lang ├── README.md ├── pages └── install.packages.zip_install.php └── lib └── zip_install.php /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | vendor/autoload.php 4 | vendor/composer/* -------------------------------------------------------------------------------- /boot.php: -------------------------------------------------------------------------------- 1 | getAssetsUrl('css/zip_install.css')); 6 | } 7 | -------------------------------------------------------------------------------- /assets/images/repo-placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /install.php: -------------------------------------------------------------------------------- 1 | getCachePath(); 7 | rex_dir::create($cacheDir); 8 | 9 | // Temp Upload Ordner erstellen 10 | $tmpDir = $addon->getCachePath('tmp_uploads'); 11 | rex_dir::create($tmpDir); 12 | 13 | // htaccess zum Schutz des Cache-Verzeichnisses erstellen 14 | $htaccess = $cacheDir . '/.htaccess'; 15 | if (!file_exists($htaccess)) { 16 | $content = "Order deny,allow\nDeny from all"; 17 | rex_file::put($htaccess, $content); 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-redaxo-org.yml: -------------------------------------------------------------------------------- 1 | # Instructions: https://github.com/FriendsOfREDAXO/installer-action/ 2 | 3 | name: Publish to REDAXO.org 4 | on: 5 | release: 6 | types: 7 | - published 8 | 9 | jobs: 10 | redaxo_publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: FriendsOfREDAXO/installer-action@v1 15 | with: 16 | myredaxo-username: ${{ secrets.MYREDAXO_USERNAME }} 17 | myredaxo-api-key: ${{ secrets.MYREDAXO_API_KEY }} 18 | description: ${{ github.event.release.body }} 19 | -------------------------------------------------------------------------------- /package.yml: -------------------------------------------------------------------------------- 1 | package: zip_install 2 | version: '2.2.4' 3 | author: Friends Of REDAXO 4 | supportpage: github.com/FriendsOfREDAXO/zip_install 5 | live_mode: false 6 | 7 | requires: 8 | redaxo: '^5.18.0' 9 | php: 10 | version: '>=8.1' 11 | extensions: [fileinfo, zip] 12 | 13 | pages: 14 | install/packages/zip_install: 15 | title: translate:title 16 | icon: rex-icon fa-upload 17 | perm: admin[] 18 | pjax: true 19 | 20 | conflicts: 21 | packages: 22 | install/packages/upload: '<2.0.0' 23 | 24 | default_config: 25 | upload_max_size: 50 26 | github_token: '' # Optional GitHub API Token für höheres Rate Limit 27 | -------------------------------------------------------------------------------- /update.php: -------------------------------------------------------------------------------- 1 | getVersion(), '2.0.0', '<')) { 6 | $tmpDir = $addon->getCachePath('tmp_uploads'); 7 | 8 | if (is_dir($tmpDir)) { 9 | // Try to delete the directory 10 | if (!rex_dir::delete($tmpDir)) { 11 | // If deletion fails, log a warning 12 | rex_logger::factory()->warning('The temporary directory ' . $tmpDir . ' could not be deleted.'); 13 | } else { 14 | // Log successful deletion as a notice 15 | rex_logger::factory()->notice('The temporary directory ' . $tmpDir . ' has been successfully deleted.'); 16 | } 17 | 18 | // Create new tmp directory 19 | rex_dir::create($tmpDir); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Friends Of REDAXO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lang/en_gb.lang: -------------------------------------------------------------------------------- 1 | zip_install_previous = Previous 2 | zip_install_next = Next 3 | 4 | zip_install = Zip Install 5 | zip_install_title = ZIP Upload / GitHub 6 | 7 | zip_install_settings = Settings 8 | zip_install_settings_save = Save Settings 9 | 10 | zip_install_file = ZIP File 11 | zip_install_file_upload = Upload ZIP File 12 | zip_install_url = URL to ZIP File or GitHub Repository 13 | zip_install_github_user = GitHub User/Organization 14 | zip_install_github_search = Search 15 | zip_install_github_search_placeholder = e.g. FriendsOfREDAXO 16 | zip_install_github_info = After entering the GitHub username or organization, the available repositories will be displayed. 17 | zip_install_install = Install 18 | zip_install_download = Download ZIP 19 | 20 | zip_install_choose_file = Select ZIP File 21 | zip_install_upload_file = Upload File 22 | zip_install_choose_info = AddOns and plugins possible. If the folder already exists, it will be overwritten.
Please create a backup before uploading if necessary.

Note: The folder will be automatically renamed to the package name. 23 | 24 | zip_install_upload_failed = Upload failed 25 | zip_install_invalid_addon = The package could not be extracted because it was invalid. 26 | zip_install_invalid_file = The uploaded file is not a valid ZIP file. 27 | zip_install_invalid_url = The entered URL does not appear to point to a ZIP file or a GitHub repository. 28 | zip_install_invalid_github = The entered GitHub user/organization could not be found. 29 | zip_install_missing_addon = The plugin could not be installed because the associated AddOn is missing! 30 | zip_install_extension_error = The uploaded file does not have a valid ZIP extension. 31 | zip_install_mime_error = The uploaded file is not a valid ZIP file. 32 | zip_install_url_file_not_loaded = The ZIP file could not be loaded from the URL. 33 | 34 | zip_install_size_error = The file exceeds the maximum upload size of {0} MB. 35 | 36 | zip_install_install_succeed = The AddOn was successfully extracted. Go to AddOns 37 | zip_install_plugin_install_succeed = The plugin was successfully extracted. Go to AddOns 38 | zip_install_plugin_parent_missing = The plugin could not be installed because the associated AddOn is missing! 39 | 40 | zip_install_security_warning = Please note: Only install AddOns from trusted sources! 41 | 42 | zip_install_settings_github_token = GitHub Token 43 | zip_install_settings_github_token_info = Optional. A GitHub token increases the hourly API limit from 60 to 5000 requests. 44 | zip_install_settings_upload_max_size = Maximum Upload Size (MB) 45 | zip_install_settings_upload_max_size_info = Maximum size of ZIP files that can be uploaded. Default: 50 MB. 46 | 47 | #permissions 48 | perm_general_zip_install[] = Permissions for ZIP Upload/GitHub Installation 49 | 50 | # Messages for download when addon already exists 51 | zip_install_downloaded_installed_title = AddOn already installed 52 | zip_install_downloaded_installed_message = The AddOn {0} is already installed. You can reinstall it if needed. 53 | -------------------------------------------------------------------------------- /lang/de_de.lang: -------------------------------------------------------------------------------- 1 | zip_install_previous = Vorherige 2 | zip_install_next = Nächste 3 | 4 | zip_install = Zip Install 5 | zip_install_title = ZIP Upload / GitHub 6 | 7 | zip_install_settings = Einstellungen 8 | zip_install_settings_save = Einstellungen speichern 9 | 10 | zip_install_file = ZIP-Datei 11 | zip_install_file_upload = ZIP-Datei hochladen 12 | zip_install_url = URL zu ZIP-Datei oder GitHub Repository 13 | zip_install_github_user = GitHub Benutzer/Organisation 14 | zip_install_github_search = Suchen 15 | zip_install_github_search_placeholder = z.B. FriendsOfREDAXO 16 | zip_install_github_info = Nach Eingabe des GitHub Benutzernamens oder der Organisation werden die verfügbaren Repositories angezeigt. 17 | zip_install_install = Installieren 18 | zip_install_download = ZIP herunterladen 19 | 20 | zip_install_choose_file = ZIP-Datei auswählen 21 | zip_install_upload_file = Datei hochladen 22 | zip_install_choose_info = AddOns und Plugins möglich. Falls der Ordner bereits existiert wird dieser überschrieben.
Bitte gegebenenfalls vor dem Upload ein Backup anlegen.

Hinweis: Der Ordner wird automatisch auf den Package-Namen umbenannt. 23 | 24 | zip_install_upload_failed = Upload fehlgeschlagen 25 | zip_install_invalid_addon = Das Paket wurde nicht entpackt, da es ungültig war. 26 | zip_install_invalid_file = Die hochgeladene Datei ist keine gültige ZIP-Datei. 27 | zip_install_invalid_url = Die eingegebene URL scheint nicht auf eine ZIP-Datei oder ein GitHub Repository zu verweisen. 28 | zip_install_invalid_github = Der eingegebene GitHub Benutzer/Organisation konnte nicht gefunden werden. 29 | zip_install_missing_addon = Das Plugin konnte nicht installiert werden, da das zugehörige AddOn fehlt! 30 | zip_install_extension_error = Die hochgeladene Datei hat keine gültige ZIP-Endung. 31 | zip_install_mime_error = Die hochgeladene Datei ist keine gültige ZIP-Datei. 32 | zip_install_url_file_not_loaded = Die ZIP-Datei konnte nicht von der URL geladen werden. 33 | 34 | zip_install_size_error = Die Datei überschreitet die maximale Upload-Größe von {0} MB. 35 | 36 | zip_install_install_succeed = Das AddOn wurde erfolgreich entpackt. Zu den AddOns 37 | zip_install_plugin_install_succeed = Das Plugin wurde erfolgreich entpackt. Zu den AddOns 38 | zip_install_plugin_parent_missing = Das Plugin konnte nicht installiert werden, da das zugehörige AddOn fehlt! 39 | 40 | zip_install_security_warning = Bitte beachten: Installieren Sie nur AddOns aus vertrauenswürdigen Quellen! 41 | 42 | zip_install_settings_github_token = GitHub Token 43 | zip_install_settings_github_token_info = Optional. Ein GitHub Token erhöht das stündliche API-Limit von 60 auf 5000 Anfragen. 44 | zip_install_settings_upload_max_size = Maximale Upload-Größe (MB) 45 | zip_install_settings_upload_max_size_info = Maximale Größe von ZIP-Dateien, die hochgeladen werden können. Standard: 50 MB. 46 | 47 | #permissions 48 | perm_general_zip_install[] = Berechtigungen für ZIP Upload/GitHub Installation 49 | 50 | # Messages for download when addon already exists 51 | zip_install_downloaded_installed_title = AddOn bereits installiert 52 | zip_install_downloaded_installed_message = Das AddOn {0} ist bereits installiert. Sie können es neu installieren, falls erforderlich. 53 | -------------------------------------------------------------------------------- /assets/css/zip_install.css: -------------------------------------------------------------------------------- 1 | /* CSS-Variablen für Light/Dark Mode */ 2 | :root, 3 | .rex-theme-light { 4 | --zip-bg-color: #fff; 5 | --zip-border-color: #e9ecef; 6 | --zip-shadow-color: rgba(0, 0, 0, 0.1); 7 | --zip-text-color: #324050; 8 | --zip-text-light: #66727d; 9 | --zip-hover-bg: rgba(0, 0, 0, 0.03); 10 | --zip-card-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 11 | --zip-transition: 0.3s ease; 12 | } 13 | 14 | /* Dark Mode Variablen */ 15 | @media (prefers-color-scheme: dark) { 16 | :root:not(.rex-theme-light) { 17 | --zip-bg-color: #32373c; 18 | --zip-border-color: #404850; 19 | --zip-shadow-color: rgba(0, 0, 0, 0.25); 20 | --zip-text-color: #f0f3f6; 21 | --zip-text-light: #bbc5ce; 22 | --zip-hover-bg: rgba(255, 255, 255, 0.05); 23 | --zip-card-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 24 | } 25 | } 26 | 27 | /* REDAXO Dark Mode */ 28 | .rex-theme-dark { 29 | --zip-bg-color: #32373c; 30 | --zip-border-color: #404850; 31 | --zip-shadow-color: rgba(0, 0, 0, 0.25); 32 | --zip-text-color: #f0f3f6; 33 | --zip-text-light: #bbc5ce; 34 | --zip-hover-bg: rgba(255, 255, 255, 0.05); 35 | --zip-card-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 36 | } 37 | 38 | /* Layout */ 39 | #zip_install_repos { 40 | margin-top: 20px; 41 | } 42 | 43 | /* Repository Cards */ 44 | .zip-panel { 45 | background: var(--zip-bg-color); 46 | border: 1px solid var(--zip-border-color); 47 | box-shadow: var(--zip-card-shadow); 48 | border-radius: 4px; 49 | margin-bottom: 20px; 50 | transition: var(--zip-transition); 51 | } 52 | 53 | .zip-panel:hover { 54 | box-shadow: 0 4px 8px var(--zip-shadow-color); 55 | } 56 | 57 | .zip-panel-header { 58 | padding: 12px 15px; 59 | border-bottom: 1px solid var(--zip-border-color); 60 | background: var(--zip-bg-color); 61 | } 62 | 63 | .zip-panel-header-content { 64 | display: flex; 65 | justify-content: space-between; 66 | align-items: center; 67 | } 68 | 69 | .zip-panel-title { 70 | font-size: 14px; 71 | font-weight: 600; 72 | color: var(--zip-text-color); 73 | margin: 0; 74 | } 75 | 76 | .zip-panel-title a { 77 | color: var(--zip-text-color); 78 | text-decoration: none; 79 | transition: var(--zip-transition); 80 | } 81 | 82 | .zip-panel-title a:hover { 83 | opacity: 0.8; 84 | } 85 | 86 | .zip-panel-body { 87 | padding: 15px; 88 | background: var(--zip-bg-color); 89 | } 90 | 91 | .zip-description { 92 | color: var(--zip-text-light); 93 | margin: 0; 94 | min-height: 40px; 95 | } 96 | 97 | /* Form Elements */ 98 | .form-group label { 99 | color: var(--zip-text-color); 100 | font-weight: 600; 101 | } 102 | 103 | .form-group .help-block { 104 | color: var(--zip-text-light); 105 | } 106 | 107 | /* Search Input and Datalist */ 108 | input[list="authors"] { 109 | background-color: var(--zip-bg-color); 110 | border: 1px solid var(--zip-border-color); 111 | color: var(--zip-text-color); 112 | } 113 | 114 | input[list="authors"]:focus { 115 | border-color: var(--zip-text-light); 116 | box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25); 117 | } 118 | 119 | /* Download Button */ 120 | .zip-download-form { 121 | margin: 0; 122 | padding: 0; 123 | } 124 | 125 | .zip-download-form .btn { 126 | padding: 4px 8px; 127 | font-size: 12px; 128 | } 129 | 130 | /* Input Group Enhancements */ 131 | .input-group-lg > .input-group-addon { 132 | font-size: 18px; 133 | padding: 10px 16px; 134 | } 135 | 136 | /* Spacing Utilities */ 137 | .mb-4 { 138 | margin-bottom: 1.5rem; 139 | } 140 | 141 | /* Mobile Anpassungen */ 142 | @media (max-width: 767px) { 143 | .zip-panel-body .btn { 144 | width: 100%; 145 | margin-bottom: 5px; 146 | } 147 | 148 | .zip-panel-header-content { 149 | flex-direction: column; 150 | align-items: flex-start; 151 | } 152 | 153 | .zip-download-form { 154 | margin-top: 10px; 155 | width: 100%; 156 | } 157 | 158 | .zip-download-form .btn { 159 | width: 100%; 160 | } 161 | 162 | .input-group-lg { 163 | margin-bottom: 10px; 164 | } 165 | 166 | .input-group-btn { 167 | display: block; 168 | width: 100%; 169 | } 170 | 171 | .input-group-btn .btn { 172 | width: 100%; 173 | margin-top: 10px; 174 | border-radius: 4px; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REDAXO AddOn-Installation via ZIP & GitHub 2 | 3 | Dieses AddOn ermöglicht die einfache Installation von AddOns oder Plugins durch Hochladen von ZIP-Dateien, Installation über eine URL oder direkt von GitHub. 4 | 5 | ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/FriendsOfREDAXO/zip_install?labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit%20Reviews) 6 | 7 | **WICHTIGER HINWEIS: Dieses AddOn ist ausschließlich für erfahrene Systemadministrator:innen bestimmt. Die unsachgemäße Anwendung kann zu unerwartetem Verhalten oder Schäden führen.** 8 | 9 | **Will man ein vorhandenes AddOn ersetzen, sollte dieses für eine saubere Installation vorher deinstalliert werden, sonst bleiben evtl. alte Dateien erhalten** 10 | 11 | ![Screenshot](https://raw.githubusercontent.com/FriendsOfREDAXO/zip_install/assets/screenshot.png) 12 | 13 | ## Funktionen 14 | 15 | * ZIP-Upload über den Browser 16 | * Installation über eine URL, die direkt zu einer ZIP-Datei führt 17 | * GitHub-Integration: 18 | * Installation direkt von GitHub-Repositories 19 | * Repository-Suche nach Benutzer/Organisation 20 | * Anzeige von Beschreibungen und Details der Repositories 21 | 22 | ## Installation 23 | 24 | * Download und Installation über den Installer oder 25 | * Download der ZIP-Datei 26 | * Entpacken in den AddOns-Ordner als `/redaxo/src/addons/zip_install/` 27 | * Installation und Aktivierung in REDAXO 28 | 29 | ## Verwendung 30 | 31 | Das AddOn ist im Installer unter "ZIP Upload/GitHub" zu finden und bietet drei Möglichkeiten zur Installation: 32 | 33 | 1. **ZIP-Upload**: Direkter Upload einer ZIP-Datei eines AddOns/Plugins 34 | 2. **URL-Installation**: Eingabe eines Links zu einer ZIP-Datei oder einem GitHub-Repository 35 | 3. **GitHub-Integration**: Suche nach GitHub-Repositories und direkte Installation 36 | 37 | ### GitHub-URLs 38 | 39 | Folgende GitHub-URL-Formate werden unterstützt: 40 | 41 | * Repository-URL: `https://github.com/FriendsOfREDAXO/demo_base` 42 | * Spezifischer Branch: `https://github.com/FriendsOfREDAXO/demo_base/tree/main` 43 | 44 | ### Plugins 45 | 46 | Plugins werden automatisch in das entsprechende Verzeichnis des zugehörigen AddOns installiert. Der Name wird dabei automatisch aus der `package.yml` übernommen. 47 | 48 | ## Wichtige Hinweise 49 | 50 | * Das AddOn überschreibt vorhandene Dateien ohne Rückfrage. 51 | * Es wird keine Installation oder Neuinstallation durchgeführt. 52 | * Abhängigkeiten werden nicht geprüft. 53 | * Die `update.php` des AddOns wird nicht ausgeführt. 54 | * Der Upload ist auf 50 MB begrenzt. 55 | 56 | ## GitHub API-Token setzen 57 | 58 | Das Add-on liefert keinen Token für die GitHub-API mit. Ohne Token sind die API-Abfragen auf 60 pro Stunde begrenzt. 59 | Ein persönlicher Zugriffstoken kann unter GitHub > Settings > Developer settings > Personal access tokens erstellt werden. 60 | Der Token benötigt mindestens 'public_repo' Berechtigung für öffentliche Repositories. 61 | Der Token kann z.B. in der install.php des project-Add-ons oder einem eigenen wie folgt updatesicher gesetzt werden: 62 | 63 | ``` 64 | $addon = rex_addon::get('zip_install'); 65 | $addon->setConfig('github_token', 'GitHubToken'); 66 | ``` 67 | 68 | ## Voraussetzungen 69 | 70 | * REDAXO >= 5.18 71 | * PHP >= 8.1 72 | * PHP-Erweiterungen: zip, fileinfo 73 | 74 | ## API Dokumentation für die `ZipInstall` Klasse 75 | 76 | Diese Dokumentation beschreibt die `ZipInstall` Klasse, die zum Installieren von REDAXO Addons und Plugins aus ZIP-Archiven verwendet wird. Die Klasse bietet Funktionen zum Hochladen von ZIP-Dateien, zum Herunterladen von ZIP-Dateien von URLs (inkl. GitHub), sowie zum Extrahieren und Installieren der Addons/Plugins. 77 | 78 | **Klassenname:** `ZipInstall` 79 | 80 | **Namespace:** `FriendsOfRedaxo\ZipInstall` 81 | 82 | ### Konstruktor 83 | 84 | ```php 85 | public function __construct() 86 | ``` 87 | 88 | **Beschreibung:** 89 | 90 | Initialisiert die `ZipInstall` Klasse. Erstellt einen temporären Ordner im Cache-Verzeichnis des Addons, falls dieser nicht existiert. 91 | 92 | **Parameter:** 93 | 94 | * Keine 95 | 96 | **Rückgabewert:** 97 | 98 | * Keiner 99 | 100 | ### Methoden 101 | 102 | #### `handleFileUpload()` 103 | 104 | ```php 105 | public function handleFileUpload(): string 106 | ``` 107 | 108 | **Beschreibung:** 109 | 110 | Verarbeitet den Upload einer ZIP-Datei, die über ein HTML-Formular hochgeladen wurde. 111 | 112 | **Parameter:** 113 | 114 | * Keine 115 | 116 | **Rückgabewert:** 117 | 118 | * `string`: Gibt einen HTML-String für eine Erfolgs- oder Fehlermeldung zurück. 119 | 120 | **Funktionsweise:** 121 | 122 | 1. Prüft, ob eine Datei über `$_FILES['zip_file']` hochgeladen wurde. 123 | 2. Überprüft den MIME-Type der hochgeladenen Datei (erlaubt sind `application/zip` und `application/octet-stream`). 124 | 3. Überprüft die Dateigröße anhand der Konfigurationseinstellung `upload_max_size`. 125 | 4. Verschiebt die hochgeladene Datei in den temporären Ordner mit einem eindeutigen Dateinamen. 126 | 5. Ruft die Methode `installZip()` auf, um die Installation durchzuführen. 127 | 128 | **Fehlermeldungen:** 129 | 130 | * `zip_install_upload_failed`: Upload fehlgeschlagen. 131 | * `zip_install_mime_error`: Ungültiger Dateityp. Bitte laden Sie eine ZIP-Datei hoch. 132 | * `zip_install_size_error`: Die Dateigröße überschreitet das Limit von `%%size%%` MB. 133 | 134 | #### `handleUrlInput()` 135 | 136 | ```php 137 | public function handleUrlInput(string $url): string 138 | ``` 139 | 140 | **Beschreibung:** 141 | 142 | Verarbeitet eine URL, die auf eine ZIP-Datei oder ein GitHub-Repository verweist. 143 | 144 | **Parameter:** 145 | 146 | * `$url` (`string`): Die URL, die verarbeitet werden soll. 147 | 148 | **Rückgabewert:** 149 | 150 | * `string`: Gibt einen HTML-String für eine Erfolgs- oder Fehlermeldung zurück. 151 | 152 | **Funktionsweise:** 153 | 154 | 1. Überprüft, ob die URL nicht leer ist. 155 | 2. Entfernt den abschließenden Slash von der URL. 156 | 3. Prüft, ob die URL ein GitHub-Repository ist und generiert die Download-URL der ZIP-Datei. 157 | 4. Lädt die ZIP-Datei in den temporären Ordner mit einem eindeutigen Dateinamen herunter. 158 | 5. Ruft die Methode `installZip()` auf, um die Installation durchzuführen. 159 | 160 | **Fehlermeldungen:** 161 | 162 | * `zip_install_invalid_url`: Ungültige URL. 163 | * `zip_install_url_file_not_loaded`: Die Datei konnte von der angegebenen URL nicht geladen werden. 164 | 165 | #### `installZip()` 166 | 167 | ```php 168 | protected function installZip(string $tmpFile): string 169 | ``` 170 | 171 | **Beschreibung:** 172 | 173 | Extrahiert und installiert ein Addon oder Plugin aus einer temporären ZIP-Datei. 174 | 175 | **Parameter:** 176 | 177 | * `$tmpFile` (`string`): Der Pfad zur temporären ZIP-Datei. 178 | 179 | **Rückgabewert:** 180 | 181 | * `string`: Gibt einen HTML-String für eine Erfolgs- oder Fehlermeldung zurück. 182 | 183 | **Funktionsweise:** 184 | 185 | 1. Öffnet die ZIP-Datei mit der `ZipArchive`-Klasse. 186 | 2. Sucht die `package.yml` Datei im ZIP-Archiv. 187 | 3. Extrahiert den Inhalt des ZIP-Archivs in einen temporären Ordner. 188 | 4. Liest die `package.yml` Datei, um die Addon-/Plugin-Informationen zu erhalten. 189 | 5. Kopiert die Dateien an den entsprechenden Speicherort im REDAXO-System. 190 | 6. Löscht den temporären Ordner und die temporäre ZIP-Datei. 191 | 192 | **Fehlermeldungen:** 193 | 194 | * `zip_install_invalid_addon`: Das Addon/Plugin ist ungültig oder konnte nicht installiert werden. 195 | * `zip_install_plugin_parent_missing`: Das Parent-Addon für dieses Plugin ist nicht vorhanden. 196 | 197 | #### `getGitHubRepos()` 198 | 199 | ```php 200 | public function getGitHubRepos(string $username): array 201 | ``` 202 | 203 | **Beschreibung:** 204 | 205 | Holt eine Liste von GitHub-Repositories für einen bestimmten Benutzer oder eine Organisation. 206 | 207 | **Parameter:** 208 | 209 | * `$username` (`string`): Der GitHub-Benutzername oder Name der Organisation. 210 | 211 | **Rückgabewert:** 212 | 213 | * `array`: Gibt ein Array von GitHub-Repositories zurück. Jedes Repository enthält Name, Beschreibung, URL, Download-URL und den Default-Branch. 214 | 215 | **Funktionsweise:** 216 | 217 | 1. Erstellt eine API-Anfrage an GitHub für die Repositories. 218 | 2. Filtert Fork, archivierte und deaktivierte Repositories heraus. 219 | 3. Formatiert die Repositories in ein einfach zu handhabendes Array. 220 | 221 | #### `isValidUrl()` 222 | 223 | ```php 224 | protected function isValidUrl(string $url): bool 225 | ``` 226 | 227 | **Beschreibung:** 228 | 229 | Überprüft, ob eine URL gültig und erreichbar ist. 230 | 231 | **Parameter:** 232 | 233 | * `$url` (`string`): Die zu überprüfende URL. 234 | 235 | **Rückgabewert:** 236 | 237 | * `bool`: Gibt `true` zurück, wenn die URL gültig und erreichbar ist, sonst `false`. 238 | 239 | **Funktionsweise:** 240 | 241 | 1. Führt eine `get_headers()` Anfrage durch. 242 | 2. Überprüft, ob der Statuscode `200` enthalten ist. 243 | 244 | #### `downloadFile()` 245 | 246 | ```php 247 | protected function downloadFile(string $url, string $destination): bool 248 | ``` 249 | 250 | **Beschreibung:** 251 | 252 | Lädt eine Datei von einer URL herunter und speichert sie auf dem Server. 253 | 254 | **Parameter:** 255 | 256 | * `$url` (`string`): Die URL der herunterzuladenden Datei. 257 | * `$destination` (`string`): Der Dateipfad zum Speichern der heruntergeladenen Datei. 258 | 259 | **Rückgabewert:** 260 | 261 | * `bool`: Gibt `true` zurück, wenn die Datei erfolgreich heruntergeladen und gespeichert wurde, sonst `false`. 262 | 263 | **Funktionsweise:** 264 | 265 | 1. Verwendet `file_get_contents()` um den Inhalt der URL abzurufen. 266 | 2. Speichert den Inhalt in die angegebene Datei mit `rex_file::put()`. 267 | 268 | ### Zusammenfassung 269 | 270 | Die `ZipInstall` Klasse bietet eine umfassende Möglichkeit zur Installation von REDAXO Addons und Plugins per ZIP-Upload oder URL. Sie enthält Sicherheitsvorkehrungen (MIME-Type Überprüfung, eindeutige Dateinamen), um das Risiko von Sicherheitslücken zu minimieren und die Stabilität der Installation zu gewährleisten. 271 | 272 | 273 | 274 | ## Lizenz 275 | 276 | MIT Lizenz, siehe [LICENSE.md](LICENSE.md) 277 | 278 | ## Autor 279 | 280 | * [Friends Of REDAXO](https://github.com/FriendsOfREDAXO) 281 | 282 | ## Lead 283 | [Thomas Skerbis](https://github.com/skerbis) 284 | 285 | 286 | 287 | ## Danksagung 288 | 289 | * Ursprüngliches AddOn von [@aeberhard](https://github.com/aeberhard) 290 | -------------------------------------------------------------------------------- /pages/install.packages.zip_install.php: -------------------------------------------------------------------------------- 1 | isValid()) { 26 | if ($zipFile = rex_files('zip_file')) { 27 | $result = $installer->handleFileUploadWithResult(); 28 | echo $result['message']; 29 | 30 | // Add direct installation link if addon was successfully installed 31 | if (isset($result['success']) && $result['success'] && $result['addon_key']) { 32 | // Always show installation link if addon was successfully extracted 33 | if (rex_addon::exists($result['addon_key'])) { 34 | $addon = rex_addon::get($result['addon_key']); 35 | 36 | if (!$addon->isInstalled()) { 37 | // Generate the correct installation link like the install addon 38 | $installUrl = rex_url::currentBackendPage([ 39 | 'page' => 'packages', 40 | 'package' => $result['addon_key'], 41 | 'function' => 'install', 42 | 'rex-api-call' => 'package', 43 | '_csrf_token' => rex_csrf_token::factory('rex_api_package')->getValue() 44 | ]); 45 | 46 | echo '
'; 47 | echo '

AddOn erfolgreich heruntergeladen

'; 48 | echo '

Das AddOn ' . rex_escape($result['addon_key']) . ' wurde erfolgreich entpackt und ist bereit zur Installation.

'; 49 | echo '

AddOn jetzt installieren

'; 50 | echo '
'; 51 | } else { 52 | // Offer a direct reinstall action (same underlying action as install, different wording) 53 | $reinstallUrl = rex_url::currentBackendPage([ 54 | 'page' => 'packages', 55 | 'package' => $result['addon_key'], 56 | 'function' => 'install', 57 | 'rex-api-call' => 'package', 58 | '_csrf_token' => rex_csrf_token::factory('rex_api_package')->getValue() 59 | ]); 60 | 61 | echo '
'; 62 | echo '

' . rex_i18n::msg('zip_install_downloaded_installed_title') . '

'; 63 | echo '

' . rex_i18n::msg('zip_install_downloaded_installed_message', rex_escape($result['addon_key'])) . '

'; 64 | echo '

' . rex_i18n::msg('package_reinstall') . '

'; 65 | echo '
'; 66 | } 67 | } else { 68 | // Even if addon doesn't exist in the system check, still try to provide a link 69 | // This can happen with GitHub repos that have different folder structures 70 | $installUrl = rex_url::currentBackendPage([ 71 | 'page' => 'packages', 72 | 'package' => $result['addon_key'], 73 | 'function' => 'install', 74 | 'rex-api-call' => 'package', 75 | '_csrf_token' => rex_csrf_token::factory('rex_api_package')->getValue() 76 | ]); 77 | 78 | echo '
'; 79 | echo '

AddOn heruntergeladen

'; 80 | echo '

Das AddOn wurde entpackt, konnte aber nicht automatisch erkannt werden.

'; 81 | echo '

AddOn installieren (falls verfügbar)

'; 82 | echo '
'; 83 | } 84 | } 85 | } elseif ($url = rex_post('zip_url', 'string', '')) { 86 | $result = $installer->handleUrlInputWithResult($url); 87 | echo $result['message']; 88 | 89 | // Add direct installation link if addon was successfully installed 90 | if (isset($result['success']) && $result['success'] && $result['addon_key']) { 91 | // Force addon detection refresh to ensure newly extracted addons are recognized 92 | rex_package_manager::synchronizeWithFileSystem(); 93 | 94 | // Always show installation link if addon was successfully extracted 95 | if (rex_addon::exists($result['addon_key'])) { 96 | $addon = rex_addon::get($result['addon_key']); 97 | 98 | if (!$addon->isInstalled()) { 99 | // Generate the correct installation link like the install addon 100 | $installUrl = rex_url::currentBackendPage([ 101 | 'page' => 'packages', 102 | 'package' => $result['addon_key'], 103 | 'function' => 'install', 104 | 'rex-api-call' => 'package', 105 | '_csrf_token' => rex_csrf_token::factory('rex_api_package')->getValue() 106 | ]); 107 | 108 | echo '
'; 109 | echo '

AddOn erfolgreich heruntergeladen

'; 110 | echo '

Das AddOn ' . rex_escape($result['addon_key']) . ' wurde erfolgreich entpackt und ist bereit zur Installation.

'; 111 | echo '

AddOn jetzt installieren

'; 112 | echo '
'; 113 | } else { 114 | // Offer a direct reinstall action (same underlying action as install, different wording) 115 | $reinstallUrl = rex_url::currentBackendPage([ 116 | 'page' => 'packages', 117 | 'package' => $result['addon_key'], 118 | 'function' => 'install', 119 | 'rex-api-call' => 'package', 120 | '_csrf_token' => rex_csrf_token::factory('rex_api_package')->getValue() 121 | ]); 122 | 123 | echo '
'; 124 | echo '

' . rex_i18n::msg('zip_install_downloaded_installed_title') . '

'; 125 | echo '

' . rex_i18n::msg('zip_install_downloaded_installed_message', rex_escape($result['addon_key'])) . '

'; 126 | echo '

' . rex_i18n::msg('package_reinstall') . '

'; 127 | echo '
'; 128 | } 129 | } else { 130 | // Even if addon doesn't exist in the system check, still try to provide a link 131 | // This can happen with GitHub repos that have different folder structures 132 | $installUrl = rex_url::currentBackendPage([ 133 | 'page' => 'packages', 134 | 'package' => $result['addon_key'], 135 | 'function' => 'install', 136 | 'rex-api-call' => 'package', 137 | '_csrf_token' => rex_csrf_token::factory('rex_api_package')->getValue() 138 | ]); 139 | 140 | echo '
'; 141 | echo '

AddOn heruntergeladen

'; 142 | echo '

Das AddOn wurde entpackt, konnte aber nicht automatisch erkannt werden.

'; 143 | echo '

AddOn installieren (falls verfügbar)

'; 144 | echo '
'; 145 | } 146 | } 147 | } elseif ($githubUser = rex_post('github_user', 'string', '')) { 148 | $repos = $installer->getGitHubRepos($githubUser); 149 | } 150 | } else { 151 | echo rex_view::error(rex_i18n::msg('csrf_token_invalid')); 152 | } 153 | } 154 | 155 | $csrfField = rex_csrf_token::factory('zip_install')->getHiddenField(); 156 | 157 | // Create datalist HTML 158 | $datalistHtml = ''; 159 | foreach ($commonAuthors as $author) { 160 | $datalistHtml .= ''; 161 | } 162 | $datalistHtml .= ''; 163 | 164 | // Main content with new layout 165 | $content = '
'; 166 | 167 | // Left column (8 columns) - GitHub search 168 | $content .= '
'; 169 | 170 | // Get the current search term 171 | $currentSearch = rex_post('github_user', 'string', ''); 172 | 173 | // GitHub section 174 | $githubContent = ' 175 |
176 | ' . $csrfField . ' 177 |
178 | 179 | 182 | 183 | 184 | 185 |
186 | ' . $datalistHtml . ' 187 |
'; 188 | 189 | // Show repos if we have results 190 | if (isset($repos) && is_array($repos)) { 191 | $githubContent .= '
'; 192 | foreach ($repos as $repo) { 193 | $githubContent .= ' 194 |
195 |
196 |
197 |
198 |

199 | ' . rex_escape($repo['name']) . ' 200 |

201 |
202 | ' . $csrfField . ' 203 | 204 | 207 |
208 |
209 |
210 |
211 |
' . rex_escape($repo['description']) . '
212 |
213 |
214 |
'; 215 | } 216 | $githubContent .= '
'; 217 | } elseif (isset($repos)) { 218 | $githubContent .= rex_view::info(rex_i18n::msg('zip_install_invalid_github')); 219 | } else { 220 | $githubContent .= '

' . rex_i18n::msg('zip_install_github_info') . '

'; 221 | } 222 | 223 | $content .= $githubContent . '
'; 224 | 225 | // Right column (4 columns) - URL and Upload forms 226 | $content .= '
'; 227 | 228 | // URL form 229 | $urlContent = ' 230 |
231 | ' . $csrfField . ' 232 |
233 | 234 | 236 |
237 | 238 |
'; 239 | 240 | $fragment = new rex_fragment(); 241 | $fragment->setVar('title', 'ZIP URL'); 242 | $fragment->setVar('body', $urlContent, false); 243 | $content .= $fragment->parse('core/page/section.php'); 244 | 245 | // Upload form 246 | $uploadContent = ' 247 |
248 | ' . $csrfField . ' 249 |
250 | 251 | 252 |

' . rex_i18n::rawMsg('zip_install_choose_info') . '

253 |
254 | 255 |
'; 256 | 257 | $fragment = new rex_fragment(); 258 | $fragment->setVar('title', rex_i18n::msg('zip_install_file_upload')); 259 | $fragment->setVar('body', $uploadContent, false); 260 | $content .= $fragment->parse('core/page/section.php'); 261 | 262 | $content .= '
'; // End right column 263 | $content .= '
'; // End row 264 | 265 | // Output the full content 266 | $fragment = new rex_fragment(); 267 | $fragment->setVar('title', rex_i18n::msg('zip_install_title')); 268 | $fragment->setVar('body', $content, false); 269 | echo $fragment->parse('core/page/section.php'); 270 | -------------------------------------------------------------------------------- /lib/zip_install.php: -------------------------------------------------------------------------------- 1 | addon = rex_addon::get('zip_install'); 34 | $this->tmpFolder = $this->addon->getCachePath('tmp_uploads'); 35 | 36 | // Ensure temp folder exists 37 | if (!is_dir($this->tmpFolder)) { 38 | try { 39 | rex_dir::create($this->tmpFolder); 40 | } catch (Exception $e) { 41 | // Log the exception or handle it as needed 42 | trigger_error('Error creating temp directory: ' . $e->getMessage(), E_USER_WARNING); 43 | // Possibly throw another exception or return an error message 44 | return; 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * Handle file upload from form 51 | * 52 | * @return string Returns a HTML string for a view message. 53 | */ 54 | public function handleFileUpload(): string 55 | { 56 | $result = $this->handleFileUploadWithResult(); 57 | return $result['message']; 58 | } 59 | 60 | /** 61 | * Handle URL input (direct ZIP or GitHub URL) 62 | * 63 | * @param string $url The URL to process. 64 | * @return string Returns a HTML string for a view message. 65 | */ 66 | public function handleUrlInput(string $url): string 67 | { 68 | $result = $this->handleUrlInputWithResult($url); 69 | return $result['message']; 70 | } 71 | 72 | /** 73 | * Install ZIP file 74 | * 75 | * @param string $tmpFile Path to the temporary ZIP file. 76 | * @return array Returns an array with status and addon key. 77 | */ 78 | protected function installZip(string $tmpFile): array 79 | { 80 | $error = false; 81 | $isPlugin = false; 82 | $parentIsMissing = false; 83 | $folderName = ''; 84 | /** @var string|false $packageFile */ 85 | $packageFile = false; 86 | /** @var array{package: string, version: string} $config */ 87 | $config = ['package' => '', 'version' => '']; 88 | $extractPath = $this->tmpFolder . '/extract/'; // Define here to ensure its existence for the finally block 89 | 90 | try { 91 | $zip = new ZipArchive(); 92 | if ($zip->open($tmpFile) !== true) { 93 | throw new Exception(rex_i18n::msg('zip_install_invalid_addon')); 94 | } 95 | 96 | // Check first entry and look for package.yml 97 | $i = 1; 98 | for ($i = 0; $i < $zip->numFiles; $i++) { 99 | /** @var array{name: string, index: int, size: int, mtime: int, crc: int, comp_size: int, comp_method: int} $stat */ 100 | $stat = $zip->statIndex($i); 101 | $filename = $stat['name']; 102 | 103 | // Normalisiere Pfadtrenner für plattformübergreifende Kompatibilität 104 | $filename = str_replace('\\', '/', $filename); 105 | 106 | if ($i == 0) { 107 | // First entry must be a directory 108 | if (!str_ends_with($filename, '/')) { 109 | $error = true; 110 | break; 111 | } 112 | $folderName = $filename; 113 | } 114 | 115 | // Find first package.yml 116 | if (!$packageFile && str_contains($filename, 'package.yml')) { 117 | $packageFile = $filename; 118 | } 119 | } 120 | 121 | if ($error || !$packageFile) { 122 | throw new Exception(rex_i18n::msg('zip_install_invalid_addon')); 123 | } 124 | 125 | // Extract to temp folder 126 | if (!is_dir($extractPath)) { 127 | rex_dir::create($extractPath); 128 | } 129 | 130 | if (!$zip->extractTo($extractPath)) { 131 | throw new Exception(rex_i18n::msg('zip_install_invalid_addon')); 132 | } 133 | $zip->close(); 134 | 135 | // Normalisiere den Pfad 136 | $packageFilePath = $extractPath . str_replace('\\', '/', $packageFile); 137 | 138 | /** @var array{package: string, version: string} $config */ 139 | // Read package.yml 140 | $config = rex_file::getConfig($packageFilePath); 141 | if (empty($config['package'])) { 142 | throw new Exception(rex_i18n::msg('zip_install_invalid_addon')); 143 | } 144 | 145 | // Handle plugins 146 | $pluginCheck = explode('/', $config['package']); 147 | if (count($pluginCheck) > 1) { 148 | $isPlugin = true; 149 | // Check if parent exists 150 | if (rex_dir::isWritable(rex_path::addon($pluginCheck[0]))) { 151 | // Copy plugin to correct location 152 | $sourcePath = $extractPath . rtrim($folderName, '/'); 153 | $destPath = rex_path::addon($pluginCheck[0], 'plugins/' . $pluginCheck[1]); 154 | 155 | if (!rex_dir::copy($sourcePath, $destPath)) { 156 | $error = true; 157 | } 158 | } else { 159 | $parentIsMissing = true; 160 | $error = true; 161 | } 162 | } else { 163 | // Copy addon 164 | $sourcePath = $extractPath . rtrim($folderName, '/'); 165 | $destPath = rex_path::addon($config['package']); 166 | 167 | if (!rex_dir::copy($sourcePath, $destPath)) { 168 | $error = true; 169 | } 170 | } 171 | 172 | } catch (Exception $e) { 173 | $error = true; 174 | trigger_error('Error during installation: ' . $e->getMessage(), E_USER_WARNING); 175 | } finally { 176 | // Cleanup 177 | rex_dir::delete($extractPath); 178 | @unlink($tmpFile); // Use @ to suppress warnings if unlink fails 179 | } 180 | 181 | if (!$error) { 182 | if ($isPlugin) { 183 | return [ 184 | 'success' => true, 185 | 'message' => rex_view::success(str_replace( 186 | '%%plugin%%', 187 | $config['package'], 188 | rex_i18n::rawMsg('zip_install_plugin_install_succeed') 189 | )), 190 | 'addon_key' => $config['package'] 191 | ]; 192 | } 193 | return [ 194 | 'success' => true, 195 | 'message' => rex_view::success(str_replace( 196 | '%%addon%%', 197 | $config['package'], 198 | rex_i18n::rawMsg('zip_install_install_succeed') 199 | )), 200 | 'addon_key' => $config['package'] 201 | ]; 202 | } 203 | 204 | if ($parentIsMissing) { 205 | return [ 206 | 'success' => false, 207 | 'message' => rex_view::error(rex_i18n::msg('zip_install_plugin_parent_missing')), 208 | 'addon_key' => null 209 | ]; 210 | } 211 | return [ 212 | 'success' => false, 213 | 'message' => rex_view::error(rex_i18n::msg('zip_install_invalid_addon')), 214 | 'addon_key' => null 215 | ]; 216 | } 217 | 218 | /** 219 | * Get GitHub repositories for user/organization 220 | * 221 | * @param string $username The GitHub username or organization name. 222 | * @return array Returns an array of GitHub repositories. 223 | */ 224 | public function getGitHubRepos(string $username): array 225 | { 226 | $username = trim($username, '@/ '); 227 | $allRepos = []; 228 | $page = 1; 229 | $perPage = 100; 230 | 231 | // Get GitHub token from config if available 232 | $token = $this->addon->getConfig('github_token'); 233 | 234 | $headers = [ 235 | 'User-Agent: REDAXOZipInstall/2.0', 236 | 'Accept: application/vnd.github.v3+json' 237 | ]; 238 | 239 | // Add authorization header if token is available 240 | if ($token) { 241 | $headers[] = 'Authorization: Bearer ' . $token; 242 | } 243 | 244 | $options = [ 245 | 'http' => [ 246 | 'method' => 'GET', 247 | 'header' => implode("\r\n", $headers) 248 | ] 249 | ]; 250 | 251 | $context = stream_context_create($options); 252 | 253 | while (count($allRepos) < 200) { 254 | $url = sprintf( 255 | 'https://api.github.com/users/%s/repos?per_page=%d&page=%d', 256 | urlencode($username), 257 | $perPage, 258 | $page 259 | ); 260 | 261 | try { 262 | $response = @file_get_contents($url, false, $context); 263 | 264 | // Check for rate limit headers 265 | if (isset($http_response_header)) { 266 | foreach ($http_response_header as $header) { 267 | if (stripos($header, 'X-RateLimit-Remaining:') === 0) { 268 | $remaining = intval(trim(substr($header, 21))); 269 | if ($remaining <= 5) { 270 | // Using warning level for rate limit notifications 271 | \rex_logger::factory()->log(\Psr\Log\LogLevel::WARNING, 272 | 'GitHub API Rate Limit is getting low: ' . $remaining . ' requests remaining' 273 | ); 274 | } 275 | } 276 | } 277 | } 278 | 279 | if ($response === false) { 280 | // Using error level for failed requests 281 | \rex_logger::factory()->log(\Psr\Log\LogLevel::ERROR, 282 | 'Failed to fetch GitHub repos from: ' . $url 283 | ); 284 | break; 285 | } 286 | 287 | $repos = json_decode($response, true); 288 | if (!is_array($repos) || empty($repos)) { 289 | break; 290 | } 291 | 292 | foreach ($repos as $repo) { 293 | if (count($allRepos) >= 200) { 294 | break 2; 295 | } 296 | 297 | if (str_starts_with($repo['name'], '.') || $repo['fork'] || $repo['archived'] || $repo['disabled']) { 298 | continue; 299 | } 300 | 301 | $downloadUrl = $repo['default_branch'] === 'main' 302 | ? $repo['html_url'] . '/archive/refs/heads/main.zip' 303 | : $repo['html_url'] . '/archive/refs/heads/master.zip'; 304 | 305 | $allRepos[] = [ 306 | 'name' => $repo['name'], 307 | 'description' => $repo['description'], 308 | 'url' => $repo['html_url'], 309 | 'download_url' => $downloadUrl, 310 | 'default_branch' => $repo['default_branch'], 311 | 'topics' => $repo['topics'] ?? [], 312 | 'homepage' => $repo['homepage'] ?? null 313 | ]; 314 | } 315 | 316 | } catch (Exception $e) { 317 | // Using logException for caught exceptions 318 | \rex_logger::logException($e); 319 | break; 320 | } 321 | 322 | $page++; 323 | } 324 | 325 | return $allRepos; 326 | } 327 | 328 | /** 329 | * Check if URL is valid and accessible 330 | * 331 | * @param string $url The URL to check. 332 | * @return bool True if the URL is valid and accessible, false otherwise. 333 | */ 334 | protected function isValidUrl(string $url): bool 335 | { 336 | try { 337 | // Check if cURL is available 338 | if (!function_exists('curl_init')) { 339 | return false; 340 | } 341 | 342 | $ch = curl_init(); 343 | curl_setopt($ch, CURLOPT_URL, $url); 344 | curl_setopt($ch, CURLOPT_NOBODY, true); // HEAD request 345 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 346 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 347 | curl_setopt($ch, CURLOPT_MAXREDIRS, 5); 348 | curl_setopt($ch, CURLOPT_TIMEOUT, 10); 349 | curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); 350 | curl_setopt($ch, CURLOPT_USERAGENT, 'REDAXOZipInstall/2.2.1'); 351 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); 352 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); 353 | 354 | curl_exec($ch); 355 | $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 356 | $error = curl_error($ch); 357 | curl_close($ch); 358 | 359 | return empty($error) && ($httpCode === 200 || $httpCode === 302); 360 | } catch (Exception $e) { 361 | trigger_error('Error checking URL validity: ' . $e->getMessage(), E_USER_WARNING); 362 | return false; // In case of an exception consider the URL invalid 363 | } 364 | } 365 | 366 | /** 367 | * Download file from URL 368 | * 369 | * @param string $url The URL of the file to download. 370 | * @param string $destination The destination path to save the downloaded file. 371 | * @return bool True if the file was downloaded successfully, false otherwise. 372 | */ 373 | protected function downloadFile(string $url, string $destination): bool 374 | { 375 | try { 376 | // Check if cURL is available 377 | if (!function_exists('curl_init')) { 378 | trigger_error('cURL extension is required for downloading files', E_USER_WARNING); 379 | return false; 380 | } 381 | 382 | $ch = curl_init(); 383 | curl_setopt($ch, CURLOPT_URL, $url); 384 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 385 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 386 | curl_setopt($ch, CURLOPT_MAXREDIRS, 5); 387 | curl_setopt($ch, CURLOPT_TIMEOUT, 60); 388 | curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); 389 | curl_setopt($ch, CURLOPT_USERAGENT, 'REDAXOZipInstall/2.2.1'); 390 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); 391 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); 392 | curl_setopt($ch, CURLOPT_HTTPHEADER, [ 393 | 'Accept: application/zip, application/octet-stream, */*', 394 | 'Cache-Control: no-cache' 395 | ]); 396 | 397 | $content = curl_exec($ch); 398 | $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 399 | $error = curl_error($ch); 400 | curl_close($ch); 401 | 402 | if ($content === false || $httpCode !== 200 || !empty($error)) { 403 | if (!empty($error)) { 404 | trigger_error('cURL error: ' . $error, E_USER_WARNING); 405 | } 406 | return false; 407 | } 408 | 409 | return rex_file::put($destination, $content); 410 | } catch (Exception $e) { 411 | trigger_error('Error downloading file: ' . $e->getMessage(), E_USER_WARNING); 412 | return false; 413 | } 414 | } 415 | 416 | /** 417 | * Handle file upload from form and return result with addon key 418 | * 419 | * @return array Returns an array with message and addon key. 420 | */ 421 | public function handleFileUploadWithResult(): array 422 | { 423 | if (!isset($_FILES['zip_file'])) { 424 | return [ 425 | 'message' => rex_view::error(rex_i18n::msg('zip_install_upload_failed')), 426 | 'addon_key' => null 427 | ]; 428 | } 429 | 430 | /** @var array{name: string, type: string, tmp_name: string, error: int, size: int} $uploadedFile */ 431 | $uploadedFile = $_FILES['zip_file']; 432 | 433 | // Check for upload errors 434 | if ($uploadedFile['error'] !== UPLOAD_ERR_OK) { 435 | return [ 436 | 'message' => rex_view::error(rex_i18n::msg('zip_install_upload_failed')), 437 | 'addon_key' => null 438 | ]; 439 | } 440 | 441 | // Check if tmp_name is not empty 442 | if (empty($uploadedFile['tmp_name']) || !is_uploaded_file($uploadedFile['tmp_name'])) { 443 | return [ 444 | 'message' => rex_view::error(rex_i18n::msg('zip_install_upload_failed')), 445 | 'addon_key' => null 446 | ]; 447 | } 448 | 449 | // Validate file extension 450 | $fileExtension = strtolower(pathinfo($uploadedFile['name'], PATHINFO_EXTENSION)); 451 | if ($fileExtension !== 'zip') { 452 | return [ 453 | 'message' => rex_view::error(rex_i18n::msg('zip_install_extension_error')), 454 | 'addon_key' => null 455 | ]; 456 | } 457 | 458 | // Check mime type (as before) 459 | $allowedMimeTypes = ['application/zip', 'application/octet-stream']; 460 | if (!in_array($uploadedFile['type'], $allowedMimeTypes)) { 461 | 462 | // Check actual mime type with fileinfo extension 463 | if (function_exists('finfo_open')) { 464 | $finfo = finfo_open(FILEINFO_MIME_TYPE); 465 | $actualMimeType = finfo_file($finfo, $uploadedFile['tmp_name']); 466 | finfo_close($finfo); 467 | if (!in_array($actualMimeType, $allowedMimeTypes)) { 468 | return [ 469 | 'message' => rex_view::error(rex_i18n::msg('zip_install_mime_error')), 470 | 'addon_key' => null 471 | ]; 472 | } 473 | } 474 | else { 475 | return [ 476 | 'message' => rex_view::error(rex_i18n::msg('zip_install_mime_error')), 477 | 'addon_key' => null 478 | ]; 479 | } 480 | } 481 | 482 | // Check filesize 483 | $maxSize = $this->addon->getConfig('upload_max_size', 50) * 1024 * 1024; // Convert MB to bytes 484 | if ($uploadedFile['size'] > $maxSize) { 485 | return [ 486 | 'message' => rex_view::error(rex_i18n::msg('zip_install_size_error', $this->addon->getConfig('upload_max_size', 20))), 487 | 'addon_key' => null 488 | ]; 489 | } 490 | 491 | $tmpFile = $this->tmpFolder . '/' . uniqid('upload_') . '.zip'; // Generate unique filename 492 | 493 | try { 494 | 495 | // Verify file content before moving 496 | $zip = new ZipArchive(); 497 | if ($zip->open($uploadedFile['tmp_name']) !== true) { 498 | throw new Exception(rex_i18n::msg('zip_install_invalid_zip')); 499 | } 500 | $zip->close(); 501 | 502 | 503 | if (!move_uploaded_file($uploadedFile['tmp_name'], $tmpFile)) { 504 | throw new Exception(rex_i18n::msg('zip_install_upload_failed')); 505 | } 506 | } catch (Exception $e) { 507 | return [ 508 | 'message' => rex_view::error(rex_i18n::msg('zip_install_upload_failed') . ' ' . $e->getMessage()), 509 | 'addon_key' => null 510 | ]; 511 | } 512 | 513 | return $this->installZip($tmpFile); 514 | } 515 | 516 | /** 517 | * Handle URL input and return result with addon key 518 | * 519 | * @param string $url The URL to process. 520 | * @return array Returns an array with message and addon key. 521 | */ 522 | public function handleUrlInputWithResult(string $url): array 523 | { 524 | if (empty($url)) { 525 | return [ 526 | 'message' => rex_view::error(rex_i18n::msg('zip_install_invalid_url')), 527 | 'addon_key' => null 528 | ]; 529 | } 530 | 531 | // Remove trailing slash if exists 532 | $url = rtrim($url, '/'); 533 | 534 | // Check if it's a GitHub repository URL 535 | if (preg_match('/^https:\/\/github\.com\/([^\/]+)\/([^\/]+)(\/tree\/([^\/]+))?$/i', $url, $matches)) { 536 | $owner = $matches[1]; 537 | $repo = $matches[2]; 538 | $branch = $matches[4] ?? null; 539 | 540 | if ($branch) { 541 | $downloadUrl = "https://github.com/$owner/$repo/archive/refs/heads/$branch.zip"; 542 | 543 | // Check if the specified branch exists, if not fall back to default branches 544 | if (!$this->isValidUrl($downloadUrl)) { 545 | // Try main branch as fallback 546 | $mainUrl = "https://github.com/$owner/$repo/archive/refs/heads/main.zip"; 547 | if ($this->isValidUrl($mainUrl)) { 548 | $downloadUrl = $mainUrl; 549 | } else { 550 | // Try master branch as fallback 551 | $masterUrl = "https://github.com/$owner/$repo/archive/refs/heads/master.zip"; 552 | if ($this->isValidUrl($masterUrl)) { 553 | $downloadUrl = $masterUrl; 554 | } 555 | // If neither main nor master exists, keep the original URL and let it fail with a proper error 556 | } 557 | } 558 | } else { 559 | // Try main/master branch 560 | $downloadUrl = "https://github.com/$owner/$repo/archive/refs/heads/main.zip"; 561 | 562 | // If main doesn't exist, try master 563 | if (!$this->isValidUrl($downloadUrl)) { 564 | $downloadUrl = "https://github.com/$owner/$repo/archive/refs/heads/master.zip"; 565 | } 566 | } 567 | $url = $downloadUrl; 568 | } 569 | 570 | // Download file 571 | $tmpFile = $this->tmpFolder . '/' . uniqid('download_') . '.zip'; // Generate unique filename 572 | if (!$this->downloadFile($url, $tmpFile)) { 573 | return [ 574 | 'message' => rex_view::error(rex_i18n::msg('zip_install_url_file_not_loaded')), 575 | 'addon_key' => null 576 | ]; 577 | } 578 | 579 | return $this->installZip($tmpFile); 580 | } 581 | 582 | /** 583 | * Extracts the AddOn key from a ZIP file. 584 | * 585 | * @param string $zipFile Path to the ZIP file. 586 | * @return string|null AddOn key or null if not found. 587 | */ 588 | public function getAddonKeyFromZip(string $zipFile): ?string 589 | { 590 | $zip = new ZipArchive(); 591 | if ($zip->open($zipFile) === true) { 592 | for ($i = 0; $i < $zip->numFiles; $i++) { 593 | $stat = $zip->statIndex($i); 594 | if (preg_match('/addons\/([^\/]+)\//', $stat['name'], $matches)) { 595 | $zip->close(); 596 | return $matches[1]; 597 | } 598 | } 599 | $zip->close(); 600 | } 601 | return null; 602 | } 603 | 604 | /** 605 | * Extracts the AddOn key from a URL. 606 | * 607 | * @param string $url The URL of the ZIP file. 608 | * @return string|null AddOn key or null if not found. 609 | */ 610 | public function getAddonKeyFromUrl(string $url): ?string 611 | { 612 | $path = parse_url($url, PHP_URL_PATH); 613 | if ($path && preg_match('/addons\/([^\/]+)\//', $path, $matches)) { 614 | return $matches[1]; 615 | } 616 | return null; 617 | } 618 | } 619 | --------------------------------------------------------------------------------