diff --git a/README.md b/README.md index b02ea13..a0616d7 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,16 @@ This plugin is designed for: ## Features +- **Multi-Project Support**: Save and manage multiple GitHub projects in one place +- **Project Cards**: Visual card-based interface showing all your saved projects +- **One-Click Sync**: Synchronize any saved project to pull the latest version from Git - Install plugins directly from public or private GitHub repositories - Update existing plugins installed from GitHub - Preview repository contents before installation - Select specific versions (tags) of a plugin to install - Support for private repositories using GitHub Personal Access Tokens - Automatic plugin folder naming based on the repository name +- Track last synchronization time for each project ## Installation @@ -29,12 +33,33 @@ This plugin is designed for: ## Usage +### Installing a New Plugin + 1. In the WordPress admin panel, go to Plugins > GitHub Installer. -2. Enter the GitHub repository URL of the plugin you want to install. -3. If it's a private repository, check the "Private Repository?" box and enter your GitHub Personal Access Token. -4. The plugin will fetch available versions and provide a preview of the repository contents. -5. Select the version you want to install from the dropdown menu. -6. Click "Install/Update Plugin" to proceed with the installation or update. +2. Enter a project name (optional) to save the project for later use. +3. Enter the GitHub repository URL of the plugin you want to install. +4. If it's a private repository, check the "Private Repository?" box and enter your GitHub Personal Access Token. +5. The plugin will fetch available versions and provide a preview of the repository contents. +6. Select the version you want to install from the dropdown menu. +7. Click "Install/Update Plugin" to proceed with the installation or update. +8. Optionally, click "Als Projekt speichern" (Save as Project) to save this configuration for future use. + +### Managing Saved Projects + +Once you've saved projects, they will appear as cards below the installation form: + +- **View Project Details**: Each card shows the project name, repository URL, version, privacy status, and last sync time. +- **Synchronize**: Click the "Synchronisieren" button to pull the latest version from Git for that specific project. +- **Delete**: Click the "Löschen" button to remove a saved project from your list (this won't uninstall the plugin, just removes it from the saved projects). + +### Synchronizing Projects + +The synchronization feature allows you to quickly update any saved project: + +1. Find the project card you want to sync. +2. Click the "Synchronisieren" (Synchronize) button. +3. The plugin will automatically fetch the latest version from Git and update the plugin. +4. You'll see a status message indicating success or failure. ## Requirements @@ -57,8 +82,10 @@ No additional configuration is required after installation. However, for private ## Security Considerations - The plugin uses nonces and capability checks to ensure only authorized users can install plugins. -- Personal Access Tokens are not stored by the plugin and must be entered each time for private repositories. +- **Personal Access Tokens are stored in the WordPress database** when you save a project. Ensure your WordPress installation is secure. +- Only users with the `manage_options` capability (typically administrators) can access the plugin. - Always review the contents of a repository before installing to ensure it's from a trusted source. +- Saved projects and their tokens are stored using WordPress options API with proper sanitization. ## Limitations diff --git a/advanced-github-plugin-installer.php b/advanced-github-plugin-installer.php index 95552d0..ba9fb8d 100644 --- a/advanced-github-plugin-installer.php +++ b/advanced-github-plugin-installer.php @@ -1,8 +1,8 @@ $project_id, + 'name' => sanitize_text_field($project_data['name']), + 'repo_url' => esc_url_raw($project_data['repo_url']), + 'is_private' => (bool) $project_data['is_private'], + 'access_token' => !empty($project_data['access_token']) ? sanitize_text_field($project_data['access_token']) : '', + 'version' => sanitize_text_field($project_data['version']), + 'last_synced' => current_time('mysql') + ); + update_option('github_installer_projects', $projects); + return $project_id; +} + +function delete_project($project_id) { + $projects = get_saved_projects(); + if (isset($projects[$project_id])) { + unset($projects[$project_id]); + update_option('github_installer_projects', $projects); + return true; + } + return false; +} + function github_plugin_installer_page() { if (!current_user_can('manage_options')) { return; @@ -38,46 +73,201 @@ function github_plugin_installer_page() { $is_private = isset($_POST['is_private']) ? true : false; $access_token = $is_private ? sanitize_text_field($_POST['access_token']) : ''; $selected_version = sanitize_text_field($_POST['version']); - + install_update_github_plugin($repo_url, $access_token, $selected_version); } + $saved_projects = get_saved_projects(); + ?> +

-
- - - - - - - - - - - - - - - - - -
-
- -
- &1"; + exec($set_url_command, $url_output, $url_return); + } - if ($return_var !== 0) { - wp_die('Failed to update the plugin. Error: ' . implode("\n", $output)); + // Validate version before checkout + if (!empty($selected_version)) { + $update_command = "cd " . escapeshellarg($plugin_dir) . " && git fetch --all && git checkout " . escapeshellarg($selected_version) . " 2>&1"; + exec($update_command, $output, $return_var); + + if ($return_var !== 0) { + // Reset remote URL to original (without token) for security + if (!empty($access_token)) { + $reset_url_command = "cd " . escapeshellarg($plugin_dir) . " && git remote set-url origin " . escapeshellarg($repo_url) . " 2>&1"; + exec($reset_url_command, $reset_output, $reset_return); + } + wp_die('Failed to update the plugin. Error: ' . implode("\n", $output)); + } + } else { + // If no version specified, just fetch and pull the default branch + $update_command = "cd " . escapeshellarg($plugin_dir) . " && git fetch --all && git pull 2>&1"; + exec($update_command, $output, $return_var); + + if ($return_var !== 0) { + // Reset remote URL to original (without token) for security + if (!empty($access_token)) { + $reset_url_command = "cd " . escapeshellarg($plugin_dir) . " && git remote set-url origin " . escapeshellarg($repo_url) . " 2>&1"; + exec($reset_url_command, $reset_output, $reset_return); + } + wp_die('Failed to update the plugin. Error: ' . implode("\n", $output)); + } + } + + // Reset remote URL to original (without token) for security + if (!empty($access_token)) { + $reset_url_command = "cd " . escapeshellarg($plugin_dir) . " && git remote set-url origin " . escapeshellarg($repo_url) . " 2>&1"; + exec($reset_url_command, $reset_output, $reset_return); } } else { // Install new plugin $clone_command = "git clone "; if (!empty($access_token)) { - $repo_url = str_replace('https://', "https://{$access_token}@", $repo_url); + $repo_url_with_token = str_replace('https://', "https://{$access_token}@", $repo_url); + $clone_command .= escapeshellarg($repo_url_with_token) . " " . escapeshellarg($plugin_dir); + } else { + $clone_command .= escapeshellarg($repo_url) . " " . escapeshellarg($plugin_dir); } - $clone_command .= escapeshellarg($repo_url) . " " . escapeshellarg($plugin_dir); exec($clone_command, $output, $return_var); @@ -115,6 +341,12 @@ function install_update_github_plugin($repo_url, $access_token, $selected_versio wp_die('Failed to clone the repository. Error: ' . implode("\n", $output)); } + // Reset remote URL to original (without token) for security + if (!empty($access_token)) { + $reset_url_command = "cd " . escapeshellarg($plugin_dir) . " && git remote set-url origin " . escapeshellarg($repo_url) . " 2>&1"; + exec($reset_url_command, $reset_output, $reset_return); + } + // Checkout the selected version if (!empty($selected_version)) { $checkout_command = "cd " . escapeshellarg($plugin_dir) . " && git checkout " . escapeshellarg($selected_version); @@ -274,4 +506,124 @@ function check_plugin_status() { } else { wp_send_json_success(array('status' => 'not_installed')); } +} + +function save_github_project() { + check_ajax_referer('github_installer_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error('Insufficient permissions.'); + } + + $project_data = array( + 'name' => sanitize_text_field($_POST['name']), + 'repo_url' => esc_url_raw($_POST['repo_url']), + 'is_private' => !empty($_POST['is_private']) && $_POST['is_private'] !== 'false' && $_POST['is_private'] !== '0', + 'access_token' => isset($_POST['access_token']) ? sanitize_text_field($_POST['access_token']) : '', + 'version' => sanitize_text_field($_POST['version']) + ); + + if (empty($project_data['name']) || empty($project_data['repo_url'])) { + wp_send_json_error('Name und Repository URL sind erforderlich.'); + } + + $project_id = save_project($project_data); + wp_send_json_success(array('message' => 'Projekt erfolgreich gespeichert!', 'project_id' => $project_id)); +} + +function delete_github_project() { + check_ajax_referer('github_installer_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error('Insufficient permissions.'); + } + + $project_id = sanitize_text_field($_POST['project_id']); + + if (delete_project($project_id)) { + wp_send_json_success('Projekt erfolgreich gelöscht!'); + } else { + wp_send_json_error('Projekt konnte nicht gelöscht werden.'); + } +} + +function sync_github_project() { + check_ajax_referer('github_installer_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error('Insufficient permissions.'); + } + + $project_id = sanitize_text_field($_POST['project_id']); + $projects = get_saved_projects(); + + if (!isset($projects[$project_id])) { + wp_send_json_error('Projekt nicht gefunden.'); + } + + $project = $projects[$project_id]; + + // Perform the sync + try { + $repo_url = $project['repo_url']; + $access_token = $project['access_token']; + $version = $project['version']; + + $repo_name = strtolower(basename(parse_url($repo_url, PHP_URL_PATH), '.git')); + $plugin_dir = WP_PLUGIN_DIR . '/' . $repo_name; + + if (!file_exists($plugin_dir)) { + wp_send_json_error('Plugin ist nicht installiert. Bitte installieren Sie es zuerst über das Formular oben.'); + } + + // Update remote URL with access token if private repository + if ($project['is_private'] && !empty($access_token)) { + $auth_repo_url = str_replace('https://', "https://{$access_token}@", $repo_url); + $set_url_command = "cd " . escapeshellarg($plugin_dir) . " && git remote set-url origin " . escapeshellarg($auth_repo_url) . " 2>&1"; + exec($set_url_command, $url_output, $url_return); + } + + // Update existing plugin - validate version before checkout + if (!empty($version)) { + $update_command = "cd " . escapeshellarg($plugin_dir) . " && git fetch --all && git checkout " . escapeshellarg($version) . " 2>&1"; + exec($update_command, $output, $return_var); + + if ($return_var !== 0) { + // Reset remote URL to original (without token) for security + if ($project['is_private'] && !empty($access_token)) { + $reset_url_command = "cd " . escapeshellarg($plugin_dir) . " && git remote set-url origin " . escapeshellarg($repo_url) . " 2>&1"; + exec($reset_url_command, $reset_output, $reset_return); + } + wp_send_json_error('Synchronisierung fehlgeschlagen: ' . implode("\n", $output)); + } + } else { + // If no version specified, just fetch and pull the default branch + $update_command = "cd " . escapeshellarg($plugin_dir) . " && git fetch --all && git pull 2>&1"; + exec($update_command, $output, $return_var); + + if ($return_var !== 0) { + // Reset remote URL to original (without token) for security + if ($project['is_private'] && !empty($access_token)) { + $reset_url_command = "cd " . escapeshellarg($plugin_dir) . " && git remote set-url origin " . escapeshellarg($repo_url) . " 2>&1"; + exec($reset_url_command, $reset_output, $reset_return); + } + wp_send_json_error('Synchronisierung fehlgeschlagen: ' . implode("\n", $output)); + } + } + + // Reset remote URL to original (without token) for security + if ($project['is_private'] && !empty($access_token)) { + $reset_url_command = "cd " . escapeshellarg($plugin_dir) . " && git remote set-url origin " . escapeshellarg($repo_url) . " 2>&1"; + exec($reset_url_command, $reset_output, $reset_return); + } + + // Update last_synced timestamp + $project['last_synced'] = current_time('mysql'); + $projects[$project_id] = $project; + update_option('github_installer_projects', $projects); + + wp_send_json_success('Projekt erfolgreich synchronisiert!'); + } catch (Exception $e) { + wp_send_json_error('Fehler bei der Synchronisierung: ' . $e->getMessage()); + } } \ No newline at end of file diff --git a/installer-script.js b/installer-script.js index d47614d..30a4f97 100644 --- a/installer-script.js +++ b/installer-script.js @@ -17,6 +17,15 @@ jQuery(document).ready(function() { previewTimer = setTimeout(updatePreviewAndVersions, 500); }); + // Show "Save as Project" button when version is selected + jQuery('#version').on('change', function() { + if(jQuery(this).val()) { + jQuery('#save_project_btn').show(); + } else { + jQuery('#save_project_btn').hide(); + } + }); + function updatePreviewAndVersions() { previewRepo(); getVersions(); @@ -78,22 +87,137 @@ jQuery(document).ready(function() { var versions = response.data; var versionSelect = jQuery('#version'); versionSelect.empty(); + versionSelect.append(jQuery('').attr('value', '').text('-- Wählen Sie eine Version --')); jQuery.each(versions, function(index, version) { versionSelect.append(jQuery('').attr('value', version).text(version)); }); jQuery('#version_row').show(); } else { jQuery('#version_row').hide(); + jQuery('#save_project_btn').hide(); console.error('Failed to fetch versions:', response.data); } }, error: function() { jQuery('#version_row').hide(); + jQuery('#save_project_btn').hide(); console.error('An error occurred while fetching the repository versions.'); } }); } else { jQuery('#version_row').hide(); + jQuery('#save_project_btn').hide(); } } + + // Save project button handler + jQuery('#save_project_btn').on('click', function(e) { + e.preventDefault(); + + var projectName = jQuery('#project_name').val(); + var repoUrl = jQuery('#repo_url').val(); + var isPrivate = jQuery('#is_private').is(':checked'); + var accessToken = jQuery('#access_token').val(); + var version = jQuery('#version').val(); + + if (!projectName) { + alert('Bitte geben Sie einen Projektnamen ein.'); + return; + } + + if (!repoUrl || !version) { + alert('Bitte füllen Sie alle erforderlichen Felder aus.'); + return; + } + + jQuery.ajax({ + url: github_installer.ajax_url, + type: 'POST', + data: { + action: 'save_github_project', + nonce: github_installer.nonce, + name: projectName, + repo_url: repoUrl, + is_private: isPrivate, + access_token: accessToken, + version: version + }, + success: function(response) { + if (response.success) { + alert(response.data.message); + location.reload(); + } else { + alert('Fehler: ' + response.data); + } + }, + error: function() { + alert('Ein Fehler ist beim Speichern des Projekts aufgetreten.'); + } + }); + }); + + // Sync project button handler + jQuery(document).on('click', '.github-sync-btn', function() { + var projectId = jQuery(this).data('project-id'); + var button = jQuery(this); + var statusDiv = jQuery('.github-sync-status[data-project-id="' + projectId + '"]'); + + button.prop('disabled', true); + statusDiv.removeClass('success error').addClass('loading').text('Synchronisierung läuft...').show(); + + jQuery.ajax({ + url: github_installer.ajax_url, + type: 'POST', + data: { + action: 'sync_github_project', + nonce: github_installer.nonce, + project_id: projectId + }, + success: function(response) { + button.prop('disabled', false); + if (response.success) { + statusDiv.removeClass('loading error').addClass('success').text(response.data); + setTimeout(function() { + location.reload(); + }, 1500); + } else { + statusDiv.removeClass('loading success').addClass('error').text('Fehler: ' + response.data); + } + }, + error: function() { + button.prop('disabled', false); + statusDiv.removeClass('loading success').addClass('error').text('Ein Fehler ist aufgetreten.'); + } + }); + }); + + // Delete project button handler + jQuery(document).on('click', '.github-delete-btn', function() { + var projectId = jQuery(this).data('project-id'); + + if (!confirm('Möchten Sie dieses Projekt wirklich löschen?')) { + return; + } + + jQuery.ajax({ + url: github_installer.ajax_url, + type: 'POST', + data: { + action: 'delete_github_project', + nonce: github_installer.nonce, + project_id: projectId + }, + success: function(response) { + if (response.success) { + alert(response.data); + location.reload(); + } else { + alert('Fehler: ' + response.data); + } + }, + error: function() { + alert('Ein Fehler ist beim Löschen des Projekts aufgetreten.'); + } + }); + }); }); \ No newline at end of file