diff --git a/install.php b/install.php new file mode 100644 index 0000000..42fb7b2 --- /dev/null +++ b/install.php @@ -0,0 +1,30 @@ +addColumn(new rex_sql_column('id', 'int(10) unsigned', false, null, 'auto_increment')) + ->addColumn(new rex_sql_column('item_type', 'varchar(50)', false)) + ->addColumn(new rex_sql_column('item_key', 'varchar(255)', false)) + ->addColumn(new rex_sql_column('item_name', 'varchar(255)', false)) + ->addColumn(new rex_sql_column('repo_owner', 'varchar(255)', false)) + ->addColumn(new rex_sql_column('repo_name', 'varchar(255)', false)) + ->addColumn(new rex_sql_column('repo_branch', 'varchar(255)', false, 'main')) + ->addColumn(new rex_sql_column('repo_path', 'varchar(500)', false)) + ->addColumn(new rex_sql_column('installed_at', 'datetime', false)) + ->addColumn(new rex_sql_column('github_last_update', 'datetime', true)) + ->addColumn(new rex_sql_column('github_last_commit_sha', 'varchar(255)', true)) + ->addColumn(new rex_sql_column('github_last_commit_message', 'text', true)) + ->addColumn(new rex_sql_column('github_cache_updated_at', 'datetime', true)) + ->setPrimaryKey('id') + ->addIndex(new rex_sql_index('unique_item', ['item_type', 'item_key'], rex_sql_index::UNIQUE)) + ->addIndex(new rex_sql_index('item_type', ['item_type'])) + ->addIndex(new rex_sql_index('repo_lookup', ['repo_owner', 'repo_name'])) + ->ensure(); diff --git a/lang/de_de.lang b/lang/de_de.lang index 0d27d69..9238e03 100644 --- a/lang/de_de.lang +++ b/lang/de_de.lang @@ -39,16 +39,18 @@ modules_install = Installieren modules_install_confirm = Modul "{0}" installieren? modules_installed_success = Modul erfolgreich installiert modules_install_error = Fehler beim Installieren des Moduls -modules_update = Neu laden -modules_update_confirm = Modul "{0}" neu laden? -modules_updated_success = Modul erfolgreich neu geladen -modules_update_error = Fehler beim Neu-Laden des Moduls +modules_update = Aktualisieren +modules_update_confirm = Modul "{0}" aktualisieren? +modules_reload = Neu laden +modules_updated_success = Modul erfolgreich aktualisiert +modules_update_error = Fehler beim Aktualisieren des Moduls module_name = Name module_title = Titel module_description = Beschreibung module_version = Version module_author = Autor module_key = Key +module_sync_status = Sync-Status module_assets = Assets module_assets_target = Ziel-Verzeichnis module_actions = Aktionen @@ -92,7 +94,8 @@ templates_install = Installieren templates_install_confirm = Template "{0}" installieren? templates_installed_success = Template erfolgreich installiert templates_install_error = Fehler beim Installieren des Templates -templates_update = Neu laden +templates_update = Aktualisieren +templates_reload = Neu laden templates_update_confirm = Template "{0}" neu laden? templates_updated_success = Template erfolgreich neu geladen templates_update_error = Fehler beim Neu-Laden des Templates @@ -102,6 +105,7 @@ template_description = Beschreibung template_version = Version template_author = Autor template_key = Key +template_sync_status = Sync-Status template_assets = Assets template_assets_target = Ziel-Verzeichnis template_actions = Aktionen diff --git a/lang/en_gb.lang b/lang/en_gb.lang index a3c5680..9134a53 100644 --- a/lang/en_gb.lang +++ b/lang/en_gb.lang @@ -37,12 +37,18 @@ modules_install = Install modules_install_confirm = Install module "{0}"? modules_installed_success = Module installed successfully modules_install_error = Error installing module +modules_update = Update +modules_update_confirm = Update module "{0}"? +modules_reload = Reload +modules_updated_success = Module updated successfully +modules_update_error = Error updating module module_name = Name module_title = Title module_description = Description module_version = Version module_author = Author module_key = Key +module_sync_status = Sync Status module_assets = Assets module_assets_target = Target Directory module_actions = Actions @@ -80,13 +86,14 @@ templates_title = Templates templates_select_repo = Select Repository templates_choose_repo = -- Choose Repository -- templates_no_repos = No repositories configured. Please add repositories first. -templates_loading = Loading templates... +templates_loading = Loading Templates... templates_no_templates = No templates found in this repository templates_install = Install templates_install_confirm = Install template "{0}"? templates_installed_success = Template installed successfully templates_install_error = Error installing template -templates_update = Reload +templates_update = Update +templates_reload = Reload templates_update_confirm = Reload template "{0}"? templates_updated_success = Template successfully reloaded templates_update_error = Error reloading template @@ -96,6 +103,7 @@ template_description = Description template_version = Version template_author = Author template_key = Key +template_sync_status = Sync Status template_assets = Assets template_assets_target = Target Directory template_actions = Actions diff --git a/lib/GitHubApi.php b/lib/GitHubApi.php index c51c8e0..b540511 100644 --- a/lib/GitHubApi.php +++ b/lib/GitHubApi.php @@ -248,4 +248,36 @@ public function uploadFile(string $owner, string $repo, string $path, string $co { return $this->createOrUpdateFile($owner, $repo, $path, $content, $message, $branch); } + + /** + * Letztes Commit-Datum für eine Datei/Ordner abrufen + * + * @param string $owner Repository Owner + * @param string $repo Repository Name + * @param string $path Pfad zur Datei/Ordner + * @param string $branch Branch Name + * @return string|null Datum im Format 'Y-m-d H:i:s' oder null bei Fehler + */ + public function getLastCommitDate(string $owner, string $repo, string $path, string $branch = 'main'): ?string + { + try { + $endpoint = "repos/{$owner}/{$repo}/commits"; + $endpoint .= "?path=" . urlencode($path); + $endpoint .= "&sha=" . urlencode($branch); + $endpoint .= "&per_page=1"; + + $commits = $this->makeRequest($endpoint); + + if (!empty($commits) && isset($commits[0]['commit']['committer']['date'])) { + $githubDate = $commits[0]['commit']['committer']['date']; + // GitHub gibt ISO 8601 zurück: 2024-11-16T10:30:00Z + $timestamp = strtotime($githubDate); + return date('Y-m-d H:i:s', $timestamp); + } + } catch (\Exception $e) { + // Bei Fehler null zurückgeben + } + + return null; + } } diff --git a/lib/GitHubItemCache.php b/lib/GitHubItemCache.php new file mode 100644 index 0000000..1c09c58 --- /dev/null +++ b/lib/GitHubItemCache.php @@ -0,0 +1,241 @@ +setTable(\rex::getTable('github_installer_items')); + + // Prüfen ob bereits vorhanden + $existing = self::get($itemType, $itemKey); + + $sql->setValue('item_type', $itemType); + $sql->setValue('item_key', $itemKey); + $sql->setValue('item_name', $itemName); + $sql->setValue('repo_owner', $repoOwner); + $sql->setValue('repo_name', $repoName); + $sql->setValue('repo_branch', $repoBranch); + $sql->setValue('repo_path', $repoPath); + + if ($githubLastUpdate) { + $sql->setValue('github_last_update', $githubLastUpdate); + } + if ($commitSha) { + $sql->setValue('github_last_commit_sha', $commitSha); + } + if ($commitMessage) { + $sql->setValue('github_last_commit_message', $commitMessage); + } + + $sql->setValue('github_cache_updated_at', date('Y-m-d H:i:s')); + + if ($existing) { + // Update + $sql->setWhere('id = :id', ['id' => $existing['id']]); + $sql->update(); + } else { + // Insert + $sql->setValue('installed_at', date('Y-m-d H:i:s')); + $sql->insert(); + } + } + + /** + * Holt GitHub-Item-Informationen aus dem Cache + * + * @param string $itemType Type: 'module', 'template', 'action' + * @param string $itemKey REDAXO key field + * @return array|null Item data oder null wenn nicht gefunden + */ + public static function get(string $itemType, string $itemKey): ?array + { + $sql = \rex_sql::factory(); + $sql->setQuery( + 'SELECT * FROM ' . \rex::getTable('github_installer_items') . ' + WHERE item_type = :type AND item_key = :key', + ['type' => $itemType, 'key' => $itemKey] + ); + + if ($sql->getRows() > 0) { + return [ + 'id' => $sql->getValue('id'), + 'item_type' => $sql->getValue('item_type'), + 'item_key' => $sql->getValue('item_key'), + 'item_name' => $sql->getValue('item_name'), + 'repo_owner' => $sql->getValue('repo_owner'), + 'repo_name' => $sql->getValue('repo_name'), + 'repo_branch' => $sql->getValue('repo_branch'), + 'repo_path' => $sql->getValue('repo_path'), + 'installed_at' => $sql->getValue('installed_at'), + 'github_last_update' => $sql->getValue('github_last_update'), + 'github_last_commit_sha' => $sql->getValue('github_last_commit_sha'), + 'github_last_commit_message' => $sql->getValue('github_last_commit_message'), + 'github_cache_updated_at' => $sql->getValue('github_cache_updated_at'), + ]; + } + + return null; + } + + /** + * Prüft ob der Cache für ein Item noch gültig ist + * + * @param string $itemType Type: 'module', 'template', 'action' + * @param string $itemKey REDAXO key field + * @param int $cacheLifetime Cache-Lebensdauer in Sekunden + * @return bool True wenn Cache noch gültig + */ + public static function isCacheValid(string $itemType, string $itemKey, int $cacheLifetime = self::CACHE_LIFETIME): bool + { + $item = self::get($itemType, $itemKey); + + if (!$item || !$item['github_cache_updated_at']) { + return false; + } + + $cacheTime = strtotime($item['github_cache_updated_at']); + $now = time(); + + return ($now - $cacheTime) < $cacheLifetime; + } + + /** + * Aktualisiert die GitHub-Informationen für ein Item + * + * @param string $itemType Type: 'module', 'template', 'action' + * @param string $itemKey REDAXO key field + * @param GitHubApi $github GitHub API instance + * @return bool True wenn erfolgreich aktualisiert + */ + public static function refreshGitHubData(string $itemType, string $itemKey, GitHubApi $github): bool + { + $item = self::get($itemType, $itemKey); + + if (!$item) { + return false; + } + + try { + // Letztes Commit-Datum holen + $lastUpdate = $github->getLastCommitDate( + $item['repo_owner'], + $item['repo_name'], + $item['repo_path'], + $item['repo_branch'] + ); + + if ($lastUpdate) { + $sql = \rex_sql::factory(); + $sql->setTable(\rex::getTable('github_installer_items')); + $sql->setValue('github_last_update', $lastUpdate); + $sql->setValue('github_cache_updated_at', date('Y-m-d H:i:s')); + $sql->setWhere('id = :id', ['id' => $item['id']]); + $sql->update(); + + return true; + } + } catch (\Exception $e) { + error_log("GitHub Cache Refresh Error: " . $e->getMessage()); + } + + return false; + } + + /** + * Löscht einen Cache-Eintrag + * + * @param string $itemType Type: 'module', 'template', 'action' + * @param string $itemKey REDAXO key field + */ + public static function delete(string $itemType, string $itemKey): void + { + $sql = \rex_sql::factory(); + $sql->setQuery( + 'DELETE FROM ' . \rex::getTable('github_installer_items') . ' + WHERE item_type = :type AND item_key = :key', + ['type' => $itemType, 'key' => $itemKey] + ); + } + + /** + * Holt alle Items eines bestimmten Typs + * + * @param string $itemType Type: 'module', 'template', 'action' + * @return array Liste von Items + */ + public static function getAllByType(string $itemType): array + { + $sql = \rex_sql::factory(); + $sql->setQuery( + 'SELECT * FROM ' . \rex::getTable('github_installer_items') . ' + WHERE item_type = :type', + ['type' => $itemType] + ); + + $items = []; + while ($sql->hasNext()) { + $items[] = [ + 'id' => $sql->getValue('id'), + 'item_type' => $sql->getValue('item_type'), + 'item_key' => $sql->getValue('item_key'), + 'item_name' => $sql->getValue('item_name'), + 'repo_owner' => $sql->getValue('repo_owner'), + 'repo_name' => $sql->getValue('repo_name'), + 'repo_branch' => $sql->getValue('repo_branch'), + 'repo_path' => $sql->getValue('repo_path'), + 'installed_at' => $sql->getValue('installed_at'), + 'github_last_update' => $sql->getValue('github_last_update'), + 'github_last_commit_sha' => $sql->getValue('github_last_commit_sha'), + 'github_last_commit_message' => $sql->getValue('github_last_commit_message'), + 'github_cache_updated_at' => $sql->getValue('github_cache_updated_at'), + ]; + $sql->next(); + } + + return $items; + } + + /** + * Leert den gesamten Cache + */ + public static function clearAll(): void + { + $sql = \rex_sql::factory(); + $sql->setQuery('TRUNCATE TABLE ' . \rex::getTable('github_installer_items')); + } +} diff --git a/lib/NewInstallManager.php b/lib/NewInstallManager.php index 174097a..981a37d 100644 --- a/lib/NewInstallManager.php +++ b/lib/NewInstallManager.php @@ -93,6 +93,27 @@ public function installNewModule(string $repoKey, string $moduleName, string $ke try { $sql->insert(); + // GitHub-Cache speichern + if ($moduleKey) { + $githubDate = $this->github->getLastCommitDate( + $repo['owner'], + $repo['repo'], + "modules/{$moduleName}", + $repo['branch'] + ); + + GitHubItemCache::save( + 'module', + $moduleKey, + $moduleTitle, + $repo['owner'], + $repo['repo'], + $repo['branch'], + "modules/{$moduleName}", + $githubDate + ); + } + // REDAXO Cache löschen \rex_delete_cache(); @@ -168,6 +189,27 @@ public function installNewTemplate(string $repoKey, string $templateName, string try { $sql->insert(); + // GitHub-Cache speichern + if ($templateKey) { + $githubDate = $this->github->getLastCommitDate( + $repo['owner'], + $repo['repo'], + "templates/{$templateName}", + $repo['branch'] + ); + + GitHubItemCache::save( + 'template', + $templateKey, + $templateTitle, + $repo['owner'], + $repo['repo'], + $repo['branch'], + "templates/{$templateName}", + $githubDate + ); + } + // REDAXO Cache löschen \rex_delete_cache(); diff --git a/lib/RepositoryManager.php b/lib/RepositoryManager.php index 5c05ed9..ef00f2e 100644 --- a/lib/RepositoryManager.php +++ b/lib/RepositoryManager.php @@ -100,7 +100,10 @@ public function removeRepository(string $key): bool } /** - * Module aus Repository abrufen + * Module mit Status-Information abrufen + * + * @param string $key Repository-Key + * @return array Module mit Status (installed/new/update_available) */ public function getModules(string $key): array { @@ -175,6 +178,90 @@ public function getTemplates(string $key): array return $templates; } + /** + * Templates mit GitHub-Sync-Status abrufen + * + * @param string $key Repository-Key + * @return array Templates mit Installationsstatus und GitHub-Sync-Informationen + */ + public function getTemplatesWithStatus(string $key): array + { + $templates = $this->getTemplates($key); + $repositories = $this->getRepositories(); + + if (!isset($repositories[$key])) { + return []; + } + + $repo = $repositories[$key]; + + foreach ($templates as &$template) { + // Installationsstatus prüfen + $status = $this->getTemplateInstallStatus($template['key'], $template['title']); + $template['status'] = $status['status']; + + // GitHub-Commit-Datum holen + $template['github_date'] = null; + $template['db_date'] = null; + $template['update_available'] = false; + + if ($status['status'] === 'installed' && $template['key']) { + // Cache prüfen + $cached = GitHubItemCache::get('template', $template['key']); + + if ($cached && GitHubItemCache::isCacheValid('template', $template['key'], 3600)) { + // Aus Cache holen + $template['github_date'] = $cached['github_last_update']; + } else { + // Von GitHub API holen + $template['github_date'] = $this->github->getLastCommitDate( + $repo['owner'], + $repo['repo'], + "templates/{$template['name']}", + $repo['branch'] + ); + + // Cache aktualisieren + if ($template['github_date']) { + GitHubItemCache::save( + 'template', + $template['key'], + $template['title'], + $repo['owner'], + $repo['repo'], + $repo['branch'], + "templates/{$template['name']}", + $template['github_date'] + ); + } + } + + // DB updatedate holen + $sql = \rex_sql::factory(); + $sql->setQuery( + 'SELECT updatedate FROM ' . \rex::getTable('template') . ' WHERE id = ?', + [$status['existing_data']['id']] + ); + + if ($sql->getRows() > 0) { + $template['db_date'] = $sql->getValue('updatedate'); + } + + // Vergleich: Ist GitHub neuer als DB? + if ($template['github_date'] && $template['db_date']) { + $githubTimestamp = strtotime($template['github_date']); + $dbTimestamp = strtotime($template['db_date']); + + if ($githubTimestamp > $dbTimestamp) { + $template['update_available'] = true; + } + } + } + } + + return $templates; + } + /** * Einfacher YAML-Parser für Basis-Konfigurationen */ @@ -456,35 +543,87 @@ private function extractDescriptionFromPhp(string $content): string } /** - * Module mit Status-Informationen abrufen + * Module mit GitHub-Sync-Status abrufen + * + * @param string $key Repository-Key + * @return array Module mit Installationsstatus und GitHub-Sync-Informationen */ - public function getModulesWithStatus(string $repoKey): array + public function getModulesWithStatus(string $key): array { - $modules = $this->getModules($repoKey); + $modules = $this->getModules($key); + $repositories = $this->getRepositories(); - foreach ($modules as &$module) { - $status = $this->getModuleInstallStatus($module['key'] ?? '', $module['title'] ?? $module['name']); - $module['status'] = $status['status']; // 'new', 'installed', 'updatable' - $module['existing_data'] = $status['existing_data']; + if (!isset($repositories[$key])) { + return []; } - return $modules; - } - - /** - * Templates mit Status-Informationen abrufen - */ - public function getTemplatesWithStatus(string $repoKey): array - { - $templates = $this->getTemplates($repoKey); + $repo = $repositories[$key]; - foreach ($templates as &$template) { - $status = $this->getTemplateInstallStatus($template['key'] ?? '', $template['title'] ?? $template['name']); - $template['status'] = $status['status']; - $template['existing_data'] = $status['existing_data']; + foreach ($modules as &$module) { + // Installationsstatus prüfen + $status = $this->getModuleInstallStatus($module['key'], $module['title']); + $module['status'] = $status['status']; + + // GitHub-Commit-Datum holen + $module['github_date'] = null; + $module['db_date'] = null; + $module['update_available'] = false; + + if ($status['status'] === 'installed' && $module['key']) { + // Cache prüfen + $cached = GitHubItemCache::get('module', $module['key']); + + if ($cached && GitHubItemCache::isCacheValid('module', $module['key'], 3600)) { + // Aus Cache holen + $module['github_date'] = $cached['github_last_update']; + } else { + // Von GitHub API holen + $module['github_date'] = $this->github->getLastCommitDate( + $repo['owner'], + $repo['repo'], + "modules/{$module['name']}", + $repo['branch'] + ); + + // Cache aktualisieren + if ($module['github_date']) { + GitHubItemCache::save( + 'module', + $module['key'], + $module['title'], + $repo['owner'], + $repo['repo'], + $repo['branch'], + "modules/{$module['name']}", + $module['github_date'] + ); + } + } + + // DB updatedate holen + $sql = \rex_sql::factory(); + $sql->setQuery( + 'SELECT updatedate FROM ' . \rex::getTable('module') . ' WHERE id = ?', + [$status['existing_data']['id']] + ); + + if ($sql->getRows() > 0) { + $module['db_date'] = $sql->getValue('updatedate'); + } + + // Vergleich: Ist GitHub neuer als DB? + if ($module['github_date'] && $module['db_date']) { + $githubTimestamp = strtotime($module['github_date']); + $dbTimestamp = strtotime($module['db_date']); + + if ($githubTimestamp > $dbTimestamp) { + $module['update_available'] = true; + } + } + } } - return $templates; + return $modules; } /** diff --git a/lib/UpdateManager.php b/lib/UpdateManager.php index 80c4347..8ff6da8 100644 --- a/lib/UpdateManager.php +++ b/lib/UpdateManager.php @@ -149,10 +149,34 @@ public function updateModule(string $repoKey, string $moduleName, string $key = } else { $updateSql->setWhere('id = :whereid', ['whereid' => $status['existing_data']['id']]); } + + // WICHTIG: Update-Datum setzen für GitHub-Sync + $updateSql->addGlobalUpdateFields(); try { $updateSql->update(); + // GitHub-Cache aktualisieren + if ($moduleKey) { + $githubDate = $this->github->getLastCommitDate( + $repo['owner'], + $repo['repo'], + "modules/{$moduleName}", + $repo['branch'] + ); + + GitHubItemCache::save( + 'module', + $moduleKey, + $moduleTitle, + $repo['owner'], + $repo['repo'], + $repo['branch'], + "modules/{$moduleName}", + $githubDate + ); + } + // REDAXO Cache löschen \rex_delete_cache(); @@ -282,10 +306,34 @@ public function updateTemplate(string $repoKey, string $templateName, string $ke } else { $updateSql->setWhere('id = :whereid', ['whereid' => $status['existing_data']['id']]); } + + // WICHTIG: Update-Datum setzen für GitHub-Sync + $updateSql->addGlobalUpdateFields(); try { $updateSql->update(); + // GitHub-Cache aktualisieren + if ($templateKey) { + $githubDate = $this->github->getLastCommitDate( + $repo['owner'], + $repo['repo'], + "templates/{$templateName}", + $repo['branch'] + ); + + GitHubItemCache::save( + 'template', + $templateKey, + $templateTitle, + $repo['owner'], + $repo['repo'], + $repo['branch'], + "templates/{$templateName}", + $githubDate + ); + } + // REDAXO Cache löschen \rex_delete_cache(); diff --git a/package.yml b/package.yml index e9ac249..18becfc 100644 --- a/package.yml +++ b/package.yml @@ -1,5 +1,5 @@ package: github_installer -version: '1.4.0' +version: '1.5.0' author: 'Friends Of REDAXO' supportpage: 'https://github.com/FriendsOfREDAXO/github_installer' info: 'Bidirectional GitHub integration - Install modules/templates from GitHub and upload your own' diff --git a/pages/modules.php b/pages/modules.php index b6e9334..4be2939 100644 --- a/pages/modules.php +++ b/pages/modules.php @@ -102,6 +102,7 @@