From 51c25553ee814975f6cb9c658ef55bbc978b4e06 Mon Sep 17 00:00:00 2001 From: siluthfi Date: Sat, 16 Aug 2025 16:40:14 +0800 Subject: [PATCH 01/12] feat: Implement findPluginByClass function and update uninstallPlugin method --- OjtPlugin.inc.php | 2 +- helpers/OJTHelper.inc.php | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/OjtPlugin.inc.php b/OjtPlugin.inc.php index 2279a15..b944c86 100644 --- a/OjtPlugin.inc.php +++ b/OjtPlugin.inc.php @@ -712,7 +712,7 @@ public function getActions($request, $actionArgs) */ public function uninstallPlugin($plugin) { - $path = $this->getModulesPath($plugin->product); + $path = findPluginByClass($plugin)->pluginPath; try { if (!is_dir($path)) { throw new \Exception("$plugin->name not Found"); diff --git a/helpers/OJTHelper.inc.php b/helpers/OJTHelper.inc.php index 05de2d2..828096e 100644 --- a/helpers/OJTHelper.inc.php +++ b/helpers/OJTHelper.inc.php @@ -84,4 +84,19 @@ function vd($value) var_dump($value); echo ''; } +} + +if (!function_exists('findPluginByClass')) { + function findPluginByClass($name) + { + $allPlugin = PluginRegistry::getPlugins(); + foreach ($allPlugin as $key => $value) { + foreach($value as $plugin) { + if ($name->className == $plugin->getName()) { + return $plugin; + } + } + } + return null; + } } \ No newline at end of file From c537672b330c90f01121fd6cce29b00fd5b4d147 Mon Sep 17 00:00:00 2001 From: siluthfi Date: Sat, 16 Aug 2025 16:41:23 +0800 Subject: [PATCH 02/12] feat: Add ApiUpdatePlugin and ApiPostValidate traits for plugin update handling --- OjtPluginApiHandler.inc.php | 379 +------------------------------- src/Actions/ApiUpdatePlugin.php | 372 +++++++++++++++++++++++++++++++ src/Traits/ApiPostValidate.php | 63 ++++++ 3 files changed, 437 insertions(+), 377 deletions(-) create mode 100644 src/Actions/ApiUpdatePlugin.php create mode 100644 src/Traits/ApiPostValidate.php diff --git a/OjtPluginApiHandler.inc.php b/OjtPluginApiHandler.inc.php index 692031f..41dbafc 100644 --- a/OjtPluginApiHandler.inc.php +++ b/OjtPluginApiHandler.inc.php @@ -1,6 +1,7 @@ [ // check update plugin - 'check-update-plugin/{pluginClass}' => [$this, 'checkUpdatePlugin'], + 'check-update-plugin/{pluginClass}' => [ApiUpdatePlugin::class, 'handle'], ], ]; } - public function checkUpdatePlugin($args, $request) - { - if(!$request->isPost()) { - http_response_code(405); // Method Not Allowed - return new JSONMessage(false, 'This endpoint only accepts POST requests.'); - } - - $getBearerToken = $this->getAuthorizationHeader(); - - if (empty($getBearerToken)) { - http_response_code(401); // Unauthorized - return new JSONMessage(false, 'Authorization header is missing or empty.'); - } - - if (!str_contains($getBearerToken, 'Bearer ')) { - http_response_code(401); // Unauthorized - return new JSONMessage(false, 'Invalid authorization format. Expected "Bearer {token}".'); - } - - $getBearerToken = str_replace('Bearer ', '', $getBearerToken); - - $pluginClass = $args['pluginClass'] ?? null; - $getAllPlugins = PluginRegistry::getAllPlugins(); - if(!isset($getAllPlugins[$pluginClass])) { - http_response_code(404); // Not Found - return new JSONMessage(false, 'Plugin class not found: ' . $pluginClass); - } - $plugin = $getAllPlugins[$pluginClass]; - - // check token - $getServicePanelData = $plugin->getSetting(CONTEXT_SITE, 'service_panel_data'); - if($getServicePanelData['token'] == null) { - http_response_code(403); // Forbidden - return new JSONMessage(false, 'Service panel token is not set for this plugin.'); - } - - if ($getServicePanelData['token'] !== $getBearerToken) { - http_response_code(403); // Forbidden - return new JSONMessage(false, 'Invalid or expired token.'); - } - - $data = null; - if (!empty($request->getUserVars())) { - $data = $request->getUserVars(); - } else { - $data = json_decode(file_get_contents('php://input'), true); - } - - if (empty($data)) { - http_response_code(400); // Bad Request - return new JSONMessage(false, 'Request body is empty or invalid.'); - } - - $requiredFields = ['link_download', 'latest_version', 'ojs_version']; - - foreach ($requiredFields as $field) { - if (empty($data[$field])) { - http_response_code(400); - return new JSONMessage(false, "Missing required parameter: $field"); - } - } - - $latestVersion = $data['latest_version']; - $linkDownload = $data['link_download']; - $ojsVersion = $data['ojs_version']; - - // validate OJS version - if ($ojsVersion != $this->ojtPlugin->getJournalVersion()) { - http_response_code(400); // Bad Request - return new JSONMessage(false, 'OJS version mismatch. Expected: ' . $this->ojtPlugin->getJournalVersion() . ', Received: ' . $ojsVersion); - } - - // validate link download if https - if (stripos($linkDownload, 'https://') !== 0) { - http_response_code(400); // Bad Request - return new JSONMessage(false, 'Download link must start with "https://".'); - } - - // validate plugin version - import('lib.pkp.classes.site.VersionCheck'); - $version = VersionCheck::parseVersionXML($plugin->getPluginPath() . '/version.xml'); - - // Check if latest version is lower than current version - if (version_compare($latestVersion, $version['release'], '<')) { - http_response_code(409); // Conflict - return new JSONMessage(false, 'Latest version is lower than current version. Current: ' . $version['release'] . ', Latest: ' . $latestVersion); - } - - // Check if latest version is equal to current version - if (version_compare($latestVersion, $version['release'], '=')) { - http_response_code(409); // Conflict - return new JSONMessage(false, 'Plugin is already up to date. Current version: ' . $version['release']); - } - - $dataPlugin = [ - 'plugin_class' => $plugin, - 'class' => $pluginClass, - 'category' => $plugin->getCategory(), - 'path' => $plugin->getPluginPath(), - ]; - - try { - $updateResult = $this->downloadAndExtractPlugin($linkDownload, $dataPlugin); - - if (!$updateResult['success']) { - http_response_code(500); - return new JSONMessage(false, $updateResult['message']); - } - - $response = [ - 'ojs_version' => $ojsVersion, - 'product_version' => $latestVersion, - 'update_success' => true, - 'message' => 'Plugin updated successfully' - ]; - - header('Content-Type: application/json'); - http_response_code(200); - return json_encode($response); - - } catch (Exception $e) { - error_log('Plugin update failed: ' . $e->getMessage()); - http_response_code(500); - return new JSONMessage(false, 'Plugin update failed: ' . $e->getMessage()); - } - } - protected function handleRoute($handler, $routeParams, $request) { if (is_array($handler) && count($handler) === 2) { @@ -223,255 +97,6 @@ protected function handleRoute($handler, $routeParams, $request) return new JSONMessage(false, "Invalid route handler configuration"); } - - private function getAuthorizationHeader() - { - $headers = null; - if (isset($_SERVER['Authorization'])) { - $headers = trim($_SERVER["Authorization"]); - } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { //Nginx or fast CGI - $headers = trim($_SERVER["HTTP_AUTHORIZATION"]); - } elseif (function_exists('apache_request_headers')) { - $requestHeaders = apache_request_headers(); - // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization) - $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); - //print_r($requestHeaders); - if (isset($requestHeaders['Authorization'])) { - $headers = trim($requestHeaders['Authorization']); - } - } - return $headers; - } - - /** - * Download and extract plugin from URL - * - * @param string $url Download URL for the plugin ZIP file - * @param array $dataPlugin Plugin information array containing class, category, and path - * @return array Result array with success status and message - */ - private function downloadAndExtractPlugin($url, $dataPlugin) - { - // Check if ZipArchive extension is available - if (!class_exists('ZipArchive')) { - return [ - 'success' => false, - 'message' => 'PHP ZipArchive extension is required but not installed' - ]; - } - - // Validate URL format - if (!filter_var($url, FILTER_VALIDATE_URL)) { - return [ - 'success' => false, - 'message' => 'Invalid download URL provided' - ]; - } - - $pluginClass = $dataPlugin['class']; - $pluginCategory = $dataPlugin['category']; - $pluginPath = $dataPlugin['path']; - - // put in files directory - $filesDir = Config::getVar('files', 'files_dir'); - if (!$filesDir || !is_writable($filesDir)) { - return [ - 'success' => false, - 'message' => 'Files directory is not configured or not writable' - ]; - } - - $tempFileName = $filesDir . DIRECTORY_SEPARATOR . 'plugin-' . $pluginClass . '.zip'; - - try { - // Download the plugin ZIP file - $downloadResult = $this->downloadFile($url, $tempFileName); - if (!$downloadResult['success']) { - return $downloadResult; - } - - // Extract the plugin - $extractResult = $this->extractPlugin($tempFileName, $pluginPath); - - // Clean up temporary file - if (file_exists($tempFileName)) { - unlink($tempFileName); - } - - return $extractResult; - - } catch (Exception $e) { - // Clean up on error - if (file_exists($tempFileName)) { - unlink($tempFileName); - } - - return [ - 'success' => false, - 'message' => 'Download failed: ' . $e->getMessage() - ]; - } - } - - /** - * Download file from URL using Guzzle HTTP client - * - * @param string $url Source URL - * @param string $destination Local file path - * @return array Result array with success status and message - */ - private function downloadFile($url, $destination) - { - try { - $client = new \GuzzleHttp\Client([ - 'timeout' => 60, // 60 seconds timeout - 'verify' => true, // Verify SSL certificates - ]); - - $resource = fopen($destination, 'w'); - if (!$resource) { - return [ - 'success' => false, - 'message' => 'Cannot create temporary file for download' - ]; - } - - $stream = \GuzzleHttp\Psr7\Utils::streamFor($resource); - $response = $client->request('GET', $url, [ - 'sink' => $stream, - 'headers' => [ - 'User-Agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'OJS-Plugin-Updater/1.0', - ] - ]); - - fclose($resource); - - if ($response->getStatusCode() !== 200) { - unlink($destination); - return [ - 'success' => false, - 'message' => 'Download failed with HTTP status: ' . $response->getStatusCode() - ]; - } - - // Verify file was downloaded and has content - if (!file_exists($destination) || filesize($destination) === 0) { - if (file_exists($destination)) { - unlink($destination); - } - return [ - 'success' => false, - 'message' => 'Downloaded file is empty or corrupt' - ]; - } - - return ['success' => true, 'message' => 'File downloaded successfully']; - - } catch (Exception $e) { - if (file_exists($destination)) { - unlink($destination); - } - return [ - 'success' => false, - 'message' => 'Download error: ' . $e->getMessage() - ]; - } - } - - /** - * Extract plugin ZIP file to appropriate directory - * - * @param string $zipFile Path to ZIP file - * @param string $pluginPath Path to plugin directory - * @return array Result array with success status and message - */ - private function extractPlugin($zipFile, $pluginPath) - { - try { - $zip = new ZipArchive(); - $result = $zip->open($zipFile); - - if ($result !== true) { - $errorMessages = [ - ZipArchive::ER_OK => 'No error', - ZipArchive::ER_MULTIDISK => 'Multi-disk zip archives not supported', - ZipArchive::ER_RENAME => 'Renaming temporary file failed', - ZipArchive::ER_CLOSE => 'Closing zip archive failed', - ZipArchive::ER_SEEK => 'Seek error', - ZipArchive::ER_READ => 'Read error', - ZipArchive::ER_WRITE => 'Write error', - ZipArchive::ER_CRC => 'CRC error', - ZipArchive::ER_ZIPCLOSED => 'Containing zip archive was closed', - ZipArchive::ER_NOENT => 'No such file', - ZipArchive::ER_EXISTS => 'File already exists', - ZipArchive::ER_OPEN => 'Can\'t open file', - ZipArchive::ER_TMPOPEN => 'Failure to create temporary file', - ZipArchive::ER_ZLIB => 'Zlib error', - ZipArchive::ER_MEMORY => 'Memory allocation failure', - ZipArchive::ER_CHANGED => 'Entry has been changed', - ZipArchive::ER_COMPNOTSUPP => 'Compression method not supported', - ZipArchive::ER_EOF => 'Premature EOF', - ZipArchive::ER_INVAL => 'Invalid argument', - ZipArchive::ER_NOZIP => 'Not a zip archive', - ZipArchive::ER_INTERNAL => 'Internal error', - ZipArchive::ER_INCONS => 'Zip archive inconsistent', - ZipArchive::ER_REMOVE => 'Can\'t remove file', - ZipArchive::ER_DELETED => 'Entry has been deleted' - ]; - - $errorMessage = $errorMessages[$result] ?? 'Unknown error'; - return [ - 'success' => false, - 'message' => "Failed to open ZIP file: $errorMessage (Code: $result)" - ]; - } - - $excludePluginName = explode('/', $pluginPath); - array_pop($excludePluginName); - $pluginFolder = implode('/', $excludePluginName); - - // Ensure extraction directory exists and is writable - if (!is_dir($pluginFolder)) { - if (!mkdir($pluginFolder, 0755, true)) { - $zip->close(); - return [ - 'success' => false, - 'message' => 'Cannot create plugin directory: ' . $pluginFolder - ]; - } - } - - if (!is_writable($pluginFolder)) { - $zip->close(); - return [ - 'success' => false, - 'message' => 'Plugin directory is not writable: ' . $pluginFolder - ]; - } - - // Extract the plugin - if (!$zip->extractTo($pluginFolder)) { - $zip->close(); - return [ - 'success' => false, - 'message' => 'Failed to extract plugin files. Check directory permissions.' - ]; - } - - $zip->close(); - - return [ - 'success' => true, - 'message' => 'Plugin extracted successfully to ' . $pluginFolder - ]; - - } catch (Exception $e) { - return [ - 'success' => false, - 'message' => 'Extraction error: ' . $e->getMessage() - ]; - } - } } ?> \ No newline at end of file diff --git a/src/Actions/ApiUpdatePlugin.php b/src/Actions/ApiUpdatePlugin.php new file mode 100644 index 0000000..a5de814 --- /dev/null +++ b/src/Actions/ApiUpdatePlugin.php @@ -0,0 +1,372 @@ +ojtPlugin = new OjtPlugin(); + } + + public function handle($args, $request) + { + return $this->checkUpdatePlugin($args, $request); + } + + public function checkUpdatePlugin($args, $request) + { + $this->validatePostRequest($request); + + $getBearerToken = $this->validateBearerToken(); + + $pluginClass = $args['pluginClass'] ?? null; + $getAllPlugins = PluginRegistry::getAllPlugins(); + if(!isset($getAllPlugins[$pluginClass])) { + http_response_code(404); // Not Found + return new JSONMessage(false, 'Plugin class not found: ' . $pluginClass); + } + $plugin = $getAllPlugins[$pluginClass]; + + // check token + $getServicePanelData = $plugin->getSetting(CONTEXT_SITE, 'service_panel_data'); + if($getServicePanelData['token'] == null) { + http_response_code(403); // Forbidden + return new JSONMessage(false, 'Service panel token is not set for this plugin.'); + } + + if ($getServicePanelData['token'] !== $getBearerToken) { + http_response_code(403); // Forbidden + return new JSONMessage(false, 'Invalid or expired token.'); + } + + $data = null; + if (!empty($request->getUserVars())) { + $data = $request->getUserVars(); + } else { + $data = json_decode(file_get_contents('php://input'), true); + } + + if (empty($data)) { + http_response_code(400); // Bad Request + return new JSONMessage(false, 'Request body is empty or invalid.'); + } + + $requiredFields = ['link_download', 'latest_version', 'ojs_version']; + + foreach ($requiredFields as $field) { + if (empty($data[$field])) { + http_response_code(400); + return new JSONMessage(false, "Missing required parameter: $field"); + } + } + + $latestVersion = $data['latest_version']; + $linkDownload = $data['link_download']; + $ojsVersion = $data['ojs_version']; + + // validate OJS version + if ($ojsVersion != $this->ojtPlugin->getJournalVersion()) { + http_response_code(400); // Bad Request + return new JSONMessage(false, 'OJS version mismatch. Expected: ' . $this->ojtPlugin->getJournalVersion() . ', Received: ' . $ojsVersion); + } + + // validate link download if https + if (stripos($linkDownload, 'https://') !== 0) { + http_response_code(400); // Bad Request + return new JSONMessage(false, 'Download link must start with "https://".'); + } + + // validate plugin version + import('lib.pkp.classes.site.VersionCheck'); + $version = VersionCheck::parseVersionXML($plugin->getPluginPath() . '/version.xml'); + + // Check if latest version is lower than current version + if (version_compare($latestVersion, $version['release'], '<')) { + http_response_code(409); // Conflict + return new JSONMessage(false, 'Latest version is lower than current version. Current: ' . $version['release'] . ', Latest: ' . $latestVersion); + } + + // Check if latest version is equal to current version + if (version_compare($latestVersion, $version['release'], '=')) { + http_response_code(409); // Conflict + return new JSONMessage(false, 'Plugin is already up to date. Current version: ' . $version['release']); + } + + $dataPlugin = [ + 'plugin_class' => $plugin, + 'class' => $pluginClass, + 'category' => $plugin->getCategory(), + 'path' => $plugin->getPluginPath(), + ]; + + try { + $updateResult = $this->downloadAndExtractPlugin($linkDownload, $dataPlugin); + + if (!$updateResult['success']) { + http_response_code(500); + return new JSONMessage(false, $updateResult['message']); + } + + $response = [ + 'ojs_version' => $ojsVersion, + 'product_version' => $latestVersion, + 'update_success' => true, + 'message' => 'Plugin updated successfully' + ]; + + header('Content-Type: application/json'); + http_response_code(200); + return json_encode($response); + + } catch (\Exception $e) { + error_log('Plugin update failed: ' . $e->getMessage()); + http_response_code(500); + return new JSONMessage(false, 'Plugin update failed: ' . $e->getMessage()); + } + } + + /** + * Download and extract plugin from URL + * + * @param string $url Download URL for the plugin ZIP file + * @param array $dataPlugin Plugin information array containing class, category, and path + * @return array Result array with success status and message + */ + private function downloadAndExtractPlugin($url, $dataPlugin) + { + // Check if ZipArchive extension is available + if (!class_exists('ZipArchive')) { + return [ + 'success' => false, + 'message' => 'PHP ZipArchive extension is required but not installed' + ]; + } + + // Validate URL format + if (!filter_var($url, FILTER_VALIDATE_URL)) { + return [ + 'success' => false, + 'message' => 'Invalid download URL provided' + ]; + } + + $pluginClass = $dataPlugin['class']; + $pluginCategory = $dataPlugin['category']; + $pluginPath = $dataPlugin['path']; + + // put in files directory + $filesDir = \Config::getVar('files', 'files_dir'); + if (!$filesDir || !is_writable($filesDir)) { + return [ + 'success' => false, + 'message' => 'Files directory is not configured or not writable' + ]; + } + + $tempFileName = $filesDir . DIRECTORY_SEPARATOR . 'plugin-' . $pluginClass . '.zip'; + + try { + // Download the plugin ZIP file + $downloadResult = $this->downloadFile($url, $tempFileName); + if (!$downloadResult['success']) { + return $downloadResult; + } + + // Extract the plugin + $extractResult = $this->extractPlugin($tempFileName, $pluginPath); + + // Clean up temporary file + if (file_exists($tempFileName)) { + unlink($tempFileName); + } + + return $extractResult; + + } catch (\Exception $e) { + // Clean up on error + if (file_exists($tempFileName)) { + unlink($tempFileName); + } + + return [ + 'success' => false, + 'message' => 'Download failed: ' . $e->getMessage() + ]; + } + } + + /** + * Download file from URL using Guzzle HTTP client + * + * @param string $url Source URL + * @param string $destination Local file path + * @return array Result array with success status and message + */ + private function downloadFile($url, $destination) + { + try { + $client = new \GuzzleHttp\Client([ + 'timeout' => 60, // 60 seconds timeout + 'verify' => true, // Verify SSL certificates + ]); + + $resource = fopen($destination, 'w'); + if (!$resource) { + return [ + 'success' => false, + 'message' => 'Cannot create temporary file for download' + ]; + } + + $stream = \GuzzleHttp\Psr7\Utils::streamFor($resource); + $response = $client->request('GET', $url, [ + 'sink' => $stream, + 'headers' => [ + 'User-Agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'OJS-Plugin-Updater/1.0', + ] + ]); + + fclose($resource); + + if ($response->getStatusCode() !== 200) { + unlink($destination); + return [ + 'success' => false, + 'message' => 'Download failed with HTTP status: ' . $response->getStatusCode() + ]; + } + + // Verify file was downloaded and has content + if (!file_exists($destination) || filesize($destination) === 0) { + if (file_exists($destination)) { + unlink($destination); + } + return [ + 'success' => false, + 'message' => 'Downloaded file is empty or corrupt' + ]; + } + + return ['success' => true, 'message' => 'File downloaded successfully']; + + } catch (\Exception $e) { + if (file_exists($destination)) { + unlink($destination); + } + return [ + 'success' => false, + 'message' => 'Download error: ' . $e->getMessage() + ]; + } + } + + /** + * Extract plugin ZIP file to appropriate directory + * + * @param string $zipFile Path to ZIP file + * @param string $pluginPath Path to plugin directory + * @return array Result array with success status and message + */ + private function extractPlugin($zipFile, $pluginPath) + { + try { + $zip = new ZipArchive(); + $result = $zip->open($zipFile); + + if ($result !== true) { + $errorMessages = [ + ZipArchive::ER_OK => 'No error', + ZipArchive::ER_MULTIDISK => 'Multi-disk zip archives not supported', + ZipArchive::ER_RENAME => 'Renaming temporary file failed', + ZipArchive::ER_CLOSE => 'Closing zip archive failed', + ZipArchive::ER_SEEK => 'Seek error', + ZipArchive::ER_READ => 'Read error', + ZipArchive::ER_WRITE => 'Write error', + ZipArchive::ER_CRC => 'CRC error', + ZipArchive::ER_ZIPCLOSED => 'Containing zip archive was closed', + ZipArchive::ER_NOENT => 'No such file', + ZipArchive::ER_EXISTS => 'File already exists', + ZipArchive::ER_OPEN => 'Can\'t open file', + ZipArchive::ER_TMPOPEN => 'Failure to create temporary file', + ZipArchive::ER_ZLIB => 'Zlib error', + ZipArchive::ER_MEMORY => 'Memory allocation failure', + ZipArchive::ER_CHANGED => 'Entry has been changed', + ZipArchive::ER_COMPNOTSUPP => 'Compression method not supported', + ZipArchive::ER_EOF => 'Premature EOF', + ZipArchive::ER_INVAL => 'Invalid argument', + ZipArchive::ER_NOZIP => 'Not a zip archive', + ZipArchive::ER_INTERNAL => 'Internal error', + ZipArchive::ER_INCONS => 'Zip archive inconsistent', + ZipArchive::ER_REMOVE => 'Can\'t remove file', + ZipArchive::ER_DELETED => 'Entry has been deleted' + ]; + + $errorMessage = $errorMessages[$result] ?? 'Unknown error'; + return [ + 'success' => false, + 'message' => "Failed to open ZIP file: $errorMessage (Code: $result)" + ]; + } + + $excludePluginName = explode('/', $pluginPath); + array_pop($excludePluginName); + $pluginFolder = implode('/', $excludePluginName); + + // Ensure extraction directory exists and is writable + if (!is_dir($pluginFolder)) { + if (!mkdir($pluginFolder, 0755, true)) { + $zip->close(); + return [ + 'success' => false, + 'message' => 'Cannot create plugin directory: ' . $pluginFolder + ]; + } + } + + if (!is_writable($pluginFolder)) { + $zip->close(); + return [ + 'success' => false, + 'message' => 'Plugin directory is not writable: ' . $pluginFolder + ]; + } + + // Extract the plugin + if (!$zip->extractTo($pluginFolder)) { + $zip->close(); + return [ + 'success' => false, + 'message' => 'Failed to extract plugin files. Check directory permissions.' + ]; + } + + $zip->close(); + + return [ + 'success' => true, + 'message' => 'Plugin extracted successfully to ' . $pluginFolder + ]; + + } catch (Exception $e) { + return [ + 'success' => false, + 'message' => 'Extraction error: ' . $e->getMessage() + ]; + } + } +} \ No newline at end of file diff --git a/src/Traits/ApiPostValidate.php b/src/Traits/ApiPostValidate.php new file mode 100644 index 0000000..5327331 --- /dev/null +++ b/src/Traits/ApiPostValidate.php @@ -0,0 +1,63 @@ +isPost()) { + header('Content-Type: application/json'); + http_response_code(405); // Method Not Allowed + echo json_encode(['success' => false, 'message' => 'This endpoint only accepts POST requests.']); + exit; + } + } + + private function getAuthorizationHeader() + { + $headers = null; + if (isset($_SERVER['Authorization'])) { + $headers = trim($_SERVER["Authorization"]); + } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { //Nginx or fast CGI + $headers = trim($_SERVER["HTTP_AUTHORIZATION"]); + } elseif (function_exists('apache_request_headers')) { + $requestHeaders = apache_request_headers(); + // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization) + $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); + //print_r($requestHeaders); + if (isset($requestHeaders['Authorization'])) { + $headers = trim($requestHeaders['Authorization']); + } + } + return $headers; + } + + public function validateBearerToken() + { + $getBearerToken = $this->getAuthorizationHeader(); + + if (empty($getBearerToken)) { + header('Content-Type: application/json'); + http_response_code(401); // Unauthorized + echo json_encode(['success' => false, 'message' => 'Authorization header is missing or empty.']); + exit; + } + + if (!str_contains($getBearerToken, 'Bearer ')) { + header('Content-Type: application/json'); + http_response_code(401); // Unauthorized + echo json_encode(['success' => false, 'message' => 'Invalid authorization format. Expected "Bearer {token}".']); + exit; + } + + $getBearerToken = str_replace('Bearer ', '', $getBearerToken); + + return $getBearerToken; + } +} \ No newline at end of file From f0637494ab8b518445db438de2e9b6c51494512e Mon Sep 17 00:00:00 2001 From: siluthfi Date: Tue, 19 Aug 2025 09:27:30 +0800 Subject: [PATCH 03/12] fix: Update uninstallPlugin method to use plugin object directly and adjust plugin removal logic --- OjtPageHandler.inc.php | 4 +++- OjtPlugin.inc.php | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/OjtPageHandler.inc.php b/OjtPageHandler.inc.php index 1552017..b99c7d4 100644 --- a/OjtPageHandler.inc.php +++ b/OjtPageHandler.inc.php @@ -553,9 +553,11 @@ public function uninstallPlugin($args, $request) $this->resetSetting($removePlugin->class, false); } + $dataPlugin = findPluginByClass($removePlugin->className); + // trying to remove plugin try { - $plugin->uninstallPlugin($removePlugin); + $plugin->uninstallPlugin($dataPlugin); } catch (Exception $e) { $json['error'] = 1; $json['msg'] = $e->getMessage(); diff --git a/OjtPlugin.inc.php b/OjtPlugin.inc.php index b944c86..b913432 100644 --- a/OjtPlugin.inc.php +++ b/OjtPlugin.inc.php @@ -712,7 +712,7 @@ public function getActions($request, $actionArgs) */ public function uninstallPlugin($plugin) { - $path = findPluginByClass($plugin)->pluginPath; + $path = $plugin->pluginPath; try { if (!is_dir($path)) { throw new \Exception("$plugin->name not Found"); From 9ad13c833a0bb84faa183afbd0b3cae3acf4619d Mon Sep 17 00:00:00 2001 From: siluthfi Date: Tue, 19 Aug 2025 09:27:48 +0800 Subject: [PATCH 04/12] feat: Add remove-plugin route and update findPluginByClass function for improved plugin management --- OjtPluginApiHandler.inc.php | 2 ++ helpers/OJTHelper.inc.php | 14 ++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/OjtPluginApiHandler.inc.php b/OjtPluginApiHandler.inc.php index 41dbafc..d09799c 100644 --- a/OjtPluginApiHandler.inc.php +++ b/OjtPluginApiHandler.inc.php @@ -1,5 +1,6 @@ [ // check update plugin 'check-update-plugin/{pluginClass}' => [ApiUpdatePlugin::class, 'handle'], + 'remove-plugin/{pluginClass}' => [ApiRemovePlugin::class, 'handle'] ], ]; } diff --git a/helpers/OJTHelper.inc.php b/helpers/OJTHelper.inc.php index 828096e..6b11c96 100644 --- a/helpers/OJTHelper.inc.php +++ b/helpers/OJTHelper.inc.php @@ -89,14 +89,12 @@ function vd($value) if (!function_exists('findPluginByClass')) { function findPluginByClass($name) { - $allPlugin = PluginRegistry::getPlugins(); - foreach ($allPlugin as $key => $value) { - foreach($value as $plugin) { - if ($name->className == $plugin->getName()) { - return $plugin; - } - } + $getAllPlugins = PluginRegistry::getAllPlugins(); + if(!isset($getAllPlugins[$name])) { + return null; } - return null; + $plugin = $getAllPlugins[$name]; + + return $plugin; } } \ No newline at end of file From cdde469ff71f1efc667337ca9e62b1c35af7cb12 Mon Sep 17 00:00:00 2001 From: siluthfi Date: Tue, 19 Aug 2025 09:29:38 +0800 Subject: [PATCH 05/12] refactor: Consolidate token validation and data retrieval in ApiPostValidate trait --- src/Actions/ApiUpdatePlugin.php | 41 +++++++-------------------------- src/Traits/ApiPostValidate.php | 35 ++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/Actions/ApiUpdatePlugin.php b/src/Actions/ApiUpdatePlugin.php index a5de814..d1175a2 100644 --- a/src/Actions/ApiUpdatePlugin.php +++ b/src/Actions/ApiUpdatePlugin.php @@ -32,34 +32,9 @@ public function checkUpdatePlugin($args, $request) { $this->validatePostRequest($request); - $getBearerToken = $this->validateBearerToken(); + $pluginData = $this->validateDataAndToken($args, $request); - $pluginClass = $args['pluginClass'] ?? null; - $getAllPlugins = PluginRegistry::getAllPlugins(); - if(!isset($getAllPlugins[$pluginClass])) { - http_response_code(404); // Not Found - return new JSONMessage(false, 'Plugin class not found: ' . $pluginClass); - } - $plugin = $getAllPlugins[$pluginClass]; - - // check token - $getServicePanelData = $plugin->getSetting(CONTEXT_SITE, 'service_panel_data'); - if($getServicePanelData['token'] == null) { - http_response_code(403); // Forbidden - return new JSONMessage(false, 'Service panel token is not set for this plugin.'); - } - - if ($getServicePanelData['token'] !== $getBearerToken) { - http_response_code(403); // Forbidden - return new JSONMessage(false, 'Invalid or expired token.'); - } - - $data = null; - if (!empty($request->getUserVars())) { - $data = $request->getUserVars(); - } else { - $data = json_decode(file_get_contents('php://input'), true); - } + $data = $this->getBodyData($request); if (empty($data)) { http_response_code(400); // Bad Request @@ -93,7 +68,7 @@ public function checkUpdatePlugin($args, $request) // validate plugin version import('lib.pkp.classes.site.VersionCheck'); - $version = VersionCheck::parseVersionXML($plugin->getPluginPath() . '/version.xml'); + $version = VersionCheck::parseVersionXML($pluginData['plugin']->getPluginPath() . '/version.xml'); // Check if latest version is lower than current version if (version_compare($latestVersion, $version['release'], '<')) { @@ -108,10 +83,10 @@ public function checkUpdatePlugin($args, $request) } $dataPlugin = [ - 'plugin_class' => $plugin, - 'class' => $pluginClass, - 'category' => $plugin->getCategory(), - 'path' => $plugin->getPluginPath(), + 'plugin_class' => $pluginData['plugin'], + 'class' => $pluginData['pluginClass'], + 'category' => $pluginData['plugin']->getCategory(), + 'path' => $pluginData['plugin']->getPluginPath(), ]; try { @@ -362,7 +337,7 @@ private function extractPlugin($zipFile, $pluginPath) 'message' => 'Plugin extracted successfully to ' . $pluginFolder ]; - } catch (Exception $e) { + } catch (\Exception $e) { return [ 'success' => false, 'message' => 'Extraction error: ' . $e->getMessage() diff --git a/src/Traits/ApiPostValidate.php b/src/Traits/ApiPostValidate.php index 5327331..398a1bc 100644 --- a/src/Traits/ApiPostValidate.php +++ b/src/Traits/ApiPostValidate.php @@ -3,6 +3,7 @@ namespace Openjournalteam\OjtPlugin\Traits; use JSONMessage; +use PluginRegistry; trait ApiPostValidate { @@ -38,7 +39,7 @@ private function getAuthorizationHeader() return $headers; } - public function validateBearerToken() + public function validateDataAndToken($args, $request) { $getBearerToken = $this->getAuthorizationHeader(); @@ -58,6 +59,36 @@ public function validateBearerToken() $getBearerToken = str_replace('Bearer ', '', $getBearerToken); - return $getBearerToken; + $pluginClass = $args['pluginClass'] ?? null; + $plugin = findPluginByClass($pluginClass); + + // check token + $getServicePanelData = $plugin->getSetting(CONTEXT_SITE, 'service_panel_data'); + if($getServicePanelData['token'] == null) { + http_response_code(403); // Forbidden + return new JSONMessage(false, 'Service panel token is not set for this plugin.'); + } + + if ($getServicePanelData['token'] !== $getBearerToken) { + http_response_code(403); // Forbidden + return new JSONMessage(false, 'Invalid or expired token.'); + } + + return [ + "plugin" => $plugin, + "pluginClass" => $pluginClass + ]; + } + + public function getBodyData($request) + { + $data = null; + if (!empty($request->getUserVars())) { + $data = $request->getUserVars(); + } else { + $data = json_decode(file_get_contents('php://input'), true); + } + + return $data; } } \ No newline at end of file From 02d172bf313c03d0567aef266227993580a7a0fe Mon Sep 17 00:00:00 2001 From: siluthfi Date: Tue, 19 Aug 2025 09:29:51 +0800 Subject: [PATCH 06/12] feat: Implement ApiRemovePlugin class for plugin removal functionality --- src/Actions/ApiRemovePlugin.php | 49 +++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/Actions/ApiRemovePlugin.php diff --git a/src/Actions/ApiRemovePlugin.php b/src/Actions/ApiRemovePlugin.php new file mode 100644 index 0000000..0279c34 --- /dev/null +++ b/src/Actions/ApiRemovePlugin.php @@ -0,0 +1,49 @@ +ojtPlugin = new OjtPlugin(); + } + + public function handle($args, $request) + { + return $this->removePlugin($args, $request); + } + + public function removePlugin($args, $request) + { + $this->validatePostRequest($request); + + $pluginData = $this->validateDataAndToken($args, $request); + + try { + $this->ojtPlugin->uninstallPlugin($pluginData['plugin']); + + header('Content-Type: application/json'); + http_response_code(200); + return new JSONMessage(true, [ + 'remove_success' => true, + "plugin" => $pluginData['plugin'], + 'message' => 'Plugin removed successfully.' + ]); + } catch (\Throwable $th) { + header('Content-Type: application/json'); + http_response_code(500); + return new JSONMessage(false, [ + 'remove_success' => false, + 'message' => 'Plugin remove failed: ' . $th->getMessage() + ]); + } + } +} \ No newline at end of file From a8eedfea9197ce437fc64124735d97b3ab104cb7 Mon Sep 17 00:00:00 2001 From: siluthfi Date: Wed, 20 Aug 2025 11:37:41 +0800 Subject: [PATCH 07/12] refactor: Enhance error handling and response formatting in ApiRemovePlugin and ApiPostValidate traits --- src/Actions/ApiRemovePlugin.php | 19 +++++++++++-------- src/Traits/ApiPostValidate.php | 9 ++++++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Actions/ApiRemovePlugin.php b/src/Actions/ApiRemovePlugin.php index 0279c34..276cbc0 100644 --- a/src/Actions/ApiRemovePlugin.php +++ b/src/Actions/ApiRemovePlugin.php @@ -2,7 +2,6 @@ namespace Openjournalteam\OjtPlugin\Actions; -use JSONMessage; use OjtPlugin; use Openjournalteam\OjtPlugin\Traits\ApiPostValidate; @@ -28,22 +27,26 @@ public function removePlugin($args, $request) $pluginData = $this->validateDataAndToken($args, $request); try { - $this->ojtPlugin->uninstallPlugin($pluginData['plugin']); - + $uninstallPlugin = $this->ojtPlugin->uninstallPlugin($pluginData['plugin']); + if (!$uninstallPlugin) { + throw new \Exception('Plugin uninstall failed.'); + } + header('Content-Type: application/json'); http_response_code(200); - return new JSONMessage(true, [ + echo json_encode([ 'remove_success' => true, - "plugin" => $pluginData['plugin'], - 'message' => 'Plugin removed successfully.' + 'message' => 'Plugin removed successfully.', ]); + exit; } catch (\Throwable $th) { header('Content-Type: application/json'); http_response_code(500); - return new JSONMessage(false, [ + echo json_encode([ 'remove_success' => false, - 'message' => 'Plugin remove failed: ' . $th->getMessage() + 'message' => 'Plugin remove failed: ' . $th->getMessage() . ' on line ' . $th->getLine() . ' in ' . $th->getFile() ]); + exit; } } } \ No newline at end of file diff --git a/src/Traits/ApiPostValidate.php b/src/Traits/ApiPostValidate.php index 398a1bc..a8ed711 100644 --- a/src/Traits/ApiPostValidate.php +++ b/src/Traits/ApiPostValidate.php @@ -2,7 +2,6 @@ namespace Openjournalteam\OjtPlugin\Traits; -use JSONMessage; use PluginRegistry; trait ApiPostValidate @@ -65,13 +64,17 @@ public function validateDataAndToken($args, $request) // check token $getServicePanelData = $plugin->getSetting(CONTEXT_SITE, 'service_panel_data'); if($getServicePanelData['token'] == null) { + header('Content-Type: application/json'); http_response_code(403); // Forbidden - return new JSONMessage(false, 'Service panel token is not set for this plugin.'); + echo json_encode(['success' => false, 'message' => 'Service panel token is not set for this plugin.']); + exit; } if ($getServicePanelData['token'] !== $getBearerToken) { + header('Content-Type: application/json'); http_response_code(403); // Forbidden - return new JSONMessage(false, 'Invalid or expired token.'); + echo json_encode(['success' => false, 'message' => 'Invalid or expired token.']); + exit; } return [ From e95d3fa7ba17d94e149b17b72dc72afaca48814a Mon Sep 17 00:00:00 2001 From: siluthfi Date: Sat, 23 Aug 2025 00:31:46 +0800 Subject: [PATCH 08/12] feat: Add checkPluginExisted method to ApiPostValidate trait for plugin existence validation --- src/Traits/ApiPostValidate.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Traits/ApiPostValidate.php b/src/Traits/ApiPostValidate.php index a8ed711..d3ba65e 100644 --- a/src/Traits/ApiPostValidate.php +++ b/src/Traits/ApiPostValidate.php @@ -94,4 +94,9 @@ public function getBodyData($request) return $data; } + + public function checkPluginExisted($args, $request) + { + + } } \ No newline at end of file From 185074ed050cc512536fcf51ef66971cf33788e3 Mon Sep 17 00:00:00 2001 From: Dede Nugroho <49790011+thisnugroho@users.noreply.github.com> Date: Thu, 4 Sep 2025 16:21:10 +0800 Subject: [PATCH 09/12] fix update failed for new validation --- OjtPageHandler.inc.php | 14 +++++++------- version.xml | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/OjtPageHandler.inc.php b/OjtPageHandler.inc.php index b99c7d4..cea1d5d 100644 --- a/OjtPageHandler.inc.php +++ b/OjtPageHandler.inc.php @@ -345,7 +345,7 @@ public function getPluginGalleryList($args, $request) public function getExclusivePlugins($args, $request) { - + // Reset array keys if needed return showJson([]); @@ -437,7 +437,10 @@ public function installPlugin($args, $request) if ($update && $fileManager->fileExists($indexFile)) { $pluginInstance = include($indexFile); - $license = $pluginInstance->getSetting($this->contextId, 'licenseMain'); + + // licenseMain is old version validation + // the updated one is license + $license = $pluginInstance->getSetting($this->contextId, 'license') || $pluginInstance->getSetting($this->contextId, 'licenseMain'); } $downloadLink = $ojtPlugin->getPluginDownloadLink($pluginToInstall->token, $license, $this->baseUrl); @@ -505,7 +508,7 @@ protected function simulateRegisterModules($pluginToInstall) } catch (\Throwable $deleteError) { // Log the error error_log("Error in recursiveDelete: " . $deleteError->getMessage()); - + // Use the plugin's method to send Discord notification $ojtPlugin->sendDiscordNotificationForDeleteError($pluginToInstall->folder, $deleteError); } @@ -594,8 +597,5 @@ public function checkPluginInstalled($args, $request) } // TODO: this function purposes to delete certain plugins inside the modules - public function deteleModules() - { - - } + public function deteleModules() {} } diff --git a/version.xml b/version.xml index b811bbe..5a0cc2d 100644 --- a/version.xml +++ b/version.xml @@ -5,8 +5,8 @@ ojtPlugin plugins.generic - 2.1.1.0 - 2025-07-24 + 2.1.1.1 + 2025-09-04 0 OjtPlugin From 42bf9dfb40ab08da445488a04782edaedb2270c4 Mon Sep 17 00:00:00 2001 From: Dede Nugroho <49790011+thisnugroho@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:25:41 +0800 Subject: [PATCH 10/12] fix: license issue --- OjtPageHandler.inc.php | 6 +++++- version.xml | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/OjtPageHandler.inc.php b/OjtPageHandler.inc.php index cea1d5d..2c3946c 100644 --- a/OjtPageHandler.inc.php +++ b/OjtPageHandler.inc.php @@ -440,7 +440,11 @@ public function installPlugin($args, $request) // licenseMain is old version validation // the updated one is license - $license = $pluginInstance->getSetting($this->contextId, 'license') || $pluginInstance->getSetting($this->contextId, 'licenseMain'); + $license = $pluginInstance->getSetting($this->contextId, 'license'); + + if(!$license) { + $license = $pluginInstance->getSetting($this->contextId, 'licenseMain'); + } } $downloadLink = $ojtPlugin->getPluginDownloadLink($pluginToInstall->token, $license, $this->baseUrl); diff --git a/version.xml b/version.xml index 5a0cc2d..633d77f 100644 --- a/version.xml +++ b/version.xml @@ -5,8 +5,8 @@ ojtPlugin plugins.generic - 2.1.1.1 - 2025-09-04 + 2.1.1.2 + 2025-09-15 0 OjtPlugin From 2060a24d4b1c8eb3fef360d416a9b1e47314d822 Mon Sep 17 00:00:00 2001 From: siluthfi Date: Sat, 20 Sep 2025 15:13:41 +0800 Subject: [PATCH 11/12] fix: encode journal data as JSON before updating service panel settings --- OjtPlugin.inc.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OjtPlugin.inc.php b/OjtPlugin.inc.php index b913432..f48ba7e 100644 --- a/OjtPlugin.inc.php +++ b/OjtPlugin.inc.php @@ -479,8 +479,8 @@ public static function reportToServicePanel($plugin, $isGlobalPlugin = false, $p try { $response = $apiService->registerClient($params, $headers); - - $plugin->updateSetting(CONTEXT_SITE, 'service_panel_data', $response['journal_data']); + + $plugin->updateSetting(CONTEXT_SITE, 'service_panel_data', json_encode($response['journal_data'])); return true; } catch (\Throwable $th) { From fa7d7e2a4e345362e5f1c34d22d554614975f812 Mon Sep 17 00:00:00 2001 From: siluthfi Date: Sat, 20 Sep 2025 15:24:57 +0800 Subject: [PATCH 12/12] fix: update service panel data without JSON encoding --- OjtPlugin.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OjtPlugin.inc.php b/OjtPlugin.inc.php index f48ba7e..8503544 100644 --- a/OjtPlugin.inc.php +++ b/OjtPlugin.inc.php @@ -480,7 +480,7 @@ public static function reportToServicePanel($plugin, $isGlobalPlugin = false, $p try { $response = $apiService->registerClient($params, $headers); - $plugin->updateSetting(CONTEXT_SITE, 'service_panel_data', json_encode($response['journal_data'])); + $plugin->updateSetting(CONTEXT_SITE, 'service_panel_data', $response['journal_data']); return true; } catch (\Throwable $th) {