diff --git a/update-info.json b/update-info.json
new file mode 100644
index 0000000..f0fe430
--- /dev/null
+++ b/update-info.json
@@ -0,0 +1,11 @@
+{
+ "name": "WP Security Pack",
+ "version": "1.0",
+ "download_url": "https://codeberg.org/ArkHost/WP-Security-Pack/archive/v1.0.zip",
+ "homepage": "https://codeberg.org/ArkHost/WP-Security-Pack",
+ "description": "WordPress security without the nonsense. No upsells, no premium tier, no fake threat counters.",
+ "changelog": "
1.0
",
+ "requires": "5.0",
+ "tested": "6.9",
+ "requires_php": "7.4"
+}
diff --git a/wp-security-pack/includes/class-wpsp-updater.php b/wp-security-pack/includes/class-wpsp-updater.php
new file mode 100644
index 0000000..38c9446
--- /dev/null
+++ b/wp-security-pack/includes/class-wpsp-updater.php
@@ -0,0 +1,178 @@
+checked ) ) {
+ return $transient;
+ }
+
+ $remote = $this->get_remote_info();
+ if ( ! $remote ) {
+ return $transient;
+ }
+
+ $current_version = WPSP_VERSION;
+ if ( version_compare( $current_version, $remote->version, '<' ) ) {
+ $transient->response[ WPSP_PLUGIN_BASENAME ] = (object) array(
+ 'slug' => self::SLUG,
+ 'plugin' => WPSP_PLUGIN_BASENAME,
+ 'new_version' => $remote->version,
+ 'url' => $remote->homepage ?? 'https://codeberg.org/ArkHost/WP-Security-Pack',
+ 'package' => $remote->download_url,
+ 'icons' => array(),
+ 'banners' => array(),
+ );
+ }
+
+ return $transient;
+ }
+
+ /**
+ * Plugin info for the update screen.
+ *
+ * @param false|object|array $result Result.
+ * @param string $action API action.
+ * @param object $args Arguments.
+ * @return false|object
+ */
+ public function plugin_info( $result, $action, $args ) {
+ if ( 'plugin_information' !== $action || self::SLUG !== ( $args->slug ?? '' ) ) {
+ return $result;
+ }
+
+ $remote = $this->get_remote_info();
+ if ( ! $remote ) {
+ return $result;
+ }
+
+ return (object) array(
+ 'name' => $remote->name ?? 'WP Security Pack',
+ 'slug' => self::SLUG,
+ 'version' => $remote->version,
+ 'author' => 'ArkHost',
+ 'homepage' => $remote->homepage ?? 'https://codeberg.org/ArkHost/WP-Security-Pack',
+ 'download_link' => $remote->download_url,
+ 'sections' => array(
+ 'description' => $remote->description ?? 'WordPress security without the nonsense.',
+ 'changelog' => $remote->changelog ?? '',
+ ),
+ 'requires' => $remote->requires ?? '5.0',
+ 'tested' => $remote->tested ?? '6.9',
+ 'requires_php' => $remote->requires_php ?? '7.4',
+ );
+ }
+
+ /**
+ * Fix folder name after download.
+ * Codeberg archives use "WP-Security-Pack" but we need "wp-security-pack".
+ *
+ * @param string $source Source path.
+ * @param string $remote_source Remote source path.
+ * @param WP_Upgrader $upgrader Upgrader instance.
+ * @param array $args Extra arguments.
+ * @return string|WP_Error
+ */
+ public function fix_folder_name( $source, $remote_source, $upgrader, $args ) {
+ global $wp_filesystem;
+
+ // Only process our plugin.
+ if ( ! isset( $args['plugin'] ) || WPSP_PLUGIN_BASENAME !== $args['plugin'] ) {
+ return $source;
+ }
+
+ $correct_name = self::SLUG;
+ $new_source = trailingslashit( $remote_source ) . $correct_name . '/';
+
+ // If source already correct, return as is.
+ if ( trailingslashit( $source ) === $new_source ) {
+ return $source;
+ }
+
+ // Rename folder.
+ if ( $wp_filesystem->move( $source, $new_source ) ) {
+ return $new_source;
+ }
+
+ return new WP_Error( 'rename_failed', 'Could not rename plugin folder.' );
+ }
+
+ /**
+ * Get remote update info.
+ *
+ * @return object|false
+ */
+ private function get_remote_info() {
+ $cached = get_transient( self::CACHE_KEY );
+ if ( false !== $cached ) {
+ return $cached;
+ }
+
+ $response = wp_remote_get( self::UPDATE_URL, array(
+ 'timeout' => 10,
+ 'headers' => array( 'Accept' => 'application/json' ),
+ ) );
+
+ if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
+ // Cache failure for 1 hour to avoid hammering.
+ set_transient( self::CACHE_KEY, false, HOUR_IN_SECONDS );
+ return false;
+ }
+
+ $body = wp_remote_retrieve_body( $response );
+ $data = json_decode( $body );
+
+ if ( ! $data || empty( $data->version ) ) {
+ set_transient( self::CACHE_KEY, false, HOUR_IN_SECONDS );
+ return false;
+ }
+
+ // Cache for 12 hours.
+ set_transient( self::CACHE_KEY, $data, 12 * HOUR_IN_SECONDS );
+
+ return $data;
+ }
+}
diff --git a/wp-security-pack/wp-security-pack.php b/wp-security-pack/wp-security-pack.php
index 058e708..d01cef4 100644
--- a/wp-security-pack/wp-security-pack.php
+++ b/wp-security-pack/wp-security-pack.php
@@ -83,6 +83,7 @@ final class WP_Security_Pack {
if ( is_admin() ) {
require_once WPSP_PLUGIN_DIR . 'includes/class-wpsp-admin.php';
+ require_once WPSP_PLUGIN_DIR . 'includes/class-wpsp-updater.php';
}
}
@@ -136,7 +137,8 @@ final class WP_Security_Pack {
$this->components['login_protection'] = new WPSP_Login_Protection();
if ( is_admin() ) {
- $this->components['admin'] = new WPSP_Admin();
+ $this->components['admin'] = new WPSP_Admin();
+ $this->components['updater'] = new WPSP_Updater();
}
}