diff --git a/src/Admin/Updater.php b/src/Admin/Updater.php
new file mode 100644
index 0000000..c179b53
--- /dev/null
+++ b/src/Admin/Updater.php
@@ -0,0 +1,476 @@
+plugin_config = [
+ 'plugin_file' => self::PLUGIN_SLUG . '/' . self::PLUGIN_SLUG . '.php',
+ 'slug' => self::PLUGIN_SLUG,
+ 'proper_folder_name' => self::PLUGIN_SLUG,
+ 'api_url' => 'https://api.wordpress.org/plugins/info/1.0/' . self::PLUGIN_SLUG . '.json',
+ 'repo_url' => 'https://wordpress.org/plugins/' . self::PLUGIN_SLUG,
+ ];
+ }
+
+ /**
+ * Sets up the hooks.
+ */
+ public function init() : void {
+ add_filter( 'auto_update_plugin', [ $this, 'disable_autoupdate' ], 10, 2 );
+ add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'api_check' ] );
+ add_filter( 'plugins_api_result', [ $this, 'api_result' ], 10, 3 );
+ add_filter( 'upgrader_source_selection', [ $this, 'upgrader_source_selection' ], 10, 2 );
+ add_action( 'in_plugin_update_message-' . $this->plugin_config['plugin_file'], [ $this, 'update_message' ] );
+ }
+
+ /**
+ * Disable auto updates for major releases of this plugin.
+ *
+ * @param bool|null $update Whether to update.
+ * @param object $item The plugin object.
+ *
+ * @return bool|null
+ */
+ public function disable_autoupdate( $update, $item ) {
+ // Return early if this is not our plugin.
+ if ( $item->slug !== $this->plugin_config['slug'] && $item->plugin !== $this->plugin_config['plugin_file'] ) {
+ return $update;
+ }
+
+ // Bail if there's no new version.
+ if ( empty( $item->new_version ) ) {
+ return $update;
+ }
+
+ // Get the update type.
+ $update_type = self::get_semver_update_type( $item->new_version, WPGRAPHQL_ACF_VERSION );
+
+ // Non-'major' updates are allowed.
+ if ( 'major' !== $update_type ) {
+ return $update;
+ }
+
+ // Major updates should never happen automatically.
+ return false;
+ }
+
+ /**
+ * Hooks into the plugin upgrader to get the correct plugin version we want to install.
+ *
+ * @param object $transient The plugin upgrader transient.
+ *
+ * @return object
+ */
+ public function api_check( $transient ) {
+ // Clear the transient.
+ delete_site_transient( self::VERSION_TRANSIENT );
+
+ // Get the latest version we allow to be installed from this version.
+ // In the new codebase, we'll just do this on semver-autoupdates.
+ $plugin_data = $this->get_plugin_data();
+ $version = $plugin_data['Version'];
+ $new_version = $this->get_latest_version();
+
+ // Check if this is a version update.
+ $is_update = version_compare( $new_version, $version, '>' );
+
+ if ( ! $is_update ) {
+ return $transient;
+ }
+
+ // Get the download URL for reuse.
+ $download_url = $this->get_download_url( $new_version );
+
+ // Populate the transient data.
+ if ( ! isset( $transient->response[ $this->plugin_config['plugin_file'] ] ) ) {
+ $transient->response[ $this->plugin_config['plugin_file'] ] = (object) $this->plugin_config;
+ }
+
+ $transient->response[ $this->plugin_config['plugin_file'] ]->new_version = $new_version;
+ $transient->response[ $this->plugin_config['plugin_file'] ]->package = $download_url;
+ $transient->response[ $this->plugin_config['plugin_file'] ]->zip_url = $download_url;
+
+ $update_type = self::get_semver_update_type( $new_version, WPGRAPHQL_ACF_VERSION );
+ if ( 'major' === $update_type ) {
+ $message = sprintf(
+ /* translators: %s: breaking change message. */
+ __( '⚠ Warning: %s', 'wp-graphql-acf' ),
+ self::get_breaking_change_message( $new_version )
+ );
+ $transient->response[ $this->plugin_config['plugin_file'] ]->upgrade_notice = $message;
+ }
+
+
+ return $transient;
+ }
+
+ /**
+ * Filters the Installation API response result
+ *
+ * @param object|\WP_Error $response The API response object.
+ * @param string $action The type of information being requested from the Plugin Installation API.
+ * @param object $args Plugin API arguments.
+ *
+ * @return object|\WP_Error
+ */
+ public function api_result( $response, $action, $args ) {
+ /**
+ * Filters the slug used to check the API response.
+ * This is useful for testing.
+ *
+ * @param ?string $custom_slug The custom slug to use. If null, the default slug is used.
+ * @param object $args The API request arguments.
+ */
+ $custom_slug = apply_filters( 'wpgraphql_acf_wporg_api_result_slug', null, $args );
+ if ( null !== $custom_slug ) {
+ $args->slug = $custom_slug;
+ }
+
+ // Bail if this is not checking our plugin.
+ if ( ! isset( $args->slug ) || ( $args->slug !== $this->plugin_config['slug'] && $args->slug !== $custom_slug ) ) {
+ return $response;
+ }
+
+ // Get the latest version.
+ $new_version = $this->get_latest_version();
+
+ // Bail if the version is not newer.
+ if ( version_compare( $new_version, WPGRAPHQL_ACF_VERSION, '<=' ) ) {
+ return $response;
+ }
+
+ // If we're returning a different version than the latest from WP.org, override the response.
+ $response->version = $new_version;
+ $response->download_link = $this->get_download_url( $new_version );
+
+ // If this is a major update, add a warning.
+ $update_type = self::get_semver_update_type( $new_version, WPGRAPHQL_ACF_VERSION );
+ $warning = '';
+
+ if ( 'major' === $update_type ) {
+ $message = sprintf(
+ /* translators: %s: version number. */
+ __( '⚠ Warning
%s
', 'wp-graphql-acf' ),
+ self::get_breaking_change_message( $new_version )
+ );
+ $warning = $this->get_inline_notice( 'major', $message );
+ }
+
+ // If there is a warning, append it to each section.
+ if ( '' !== $warning ) {
+ foreach ( $response->sections as $key => $section ) {
+ $response->sections[ $key ] = $warning . $section;
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * Rename the downloaded zip
+ *
+ * @param string $source File source location.
+ * @param string $remote_source Remote file source location.
+ *
+ * @return string|\WP_Error
+ */
+ public function upgrader_source_selection( $source, $remote_source ) {
+ global $wp_filesystem;
+
+ if ( strstr( $source, '/wp-graphql-acf' ) ) {
+ $corrected_source = trailingslashit( $remote_source ) . trailingslashit( $this->plugin_config['proper_folder_name'] );
+
+ if ( $wp_filesystem->move( $source, $corrected_source, true ) ) {
+ return $corrected_source;
+ } else {
+ return new \WP_Error();
+ }
+ }
+
+ return $source;
+ }
+
+ /**
+ * Outputs a warning message about a major update.
+ *
+ * @param array $args The update message arguments.
+ */
+ public function update_message( $args ) : void {
+ if ( ! isset( $args['slug'] ) || $args['slug'] !== $this->plugin_config['slug'] ) {
+ return;
+ }
+
+ // Get the latest version.
+ $new_version = $args['new_version'];
+
+ // If this is a major update, add a warning.
+ $update_type = self::get_semver_update_type( $new_version, WPGRAPHQL_ACF_VERSION );
+
+ if ( 'major' !== $update_type ) {
+ return;
+ }
+
+ $message = sprintf(
+ /* translators: %s: version number. */
+ __( '⚠ Warning%s
', 'wp-graphql-acf' ),
+ self::get_breaking_change_message( $new_version )
+ );
+
+ echo '' . wp_kses_post( $this->get_inline_notice( 'major', $message ) );
+ }
+
+ /**
+ * Returns the notice to display inline on the plugins page.
+ *
+ * @param string $upgrade_type The type of upgrade.
+ * @param string $message The message to display.
+ */
+ public function get_inline_notice( string $upgrade_type, string $message, ) : string {
+ ob_start();
+ ?>
+
+
+
+
+
+
+ get_wporg_data();
+
+ /** @var string $latest_version */
+ $latest_version = isset( $data->version ) ? $data->version : '';
+
+ /** @var array $versions */
+ $versions = isset( $data->versions ) ? (array) $data->versions : [];
+
+ // Sort the versions by descending order.
+ uksort(
+ $versions,
+ static function ( $a, $b ) {
+ return version_compare( $b, $a );
+ }
+ );
+
+ foreach ( $versions as $version => $download_url ) {
+ // Skip trunk.
+ if ( 'trunk' === $version ) {
+ continue;
+ }
+
+ // If the current version is < 1.0.0, but this version is >= 2, skip it.
+ // @phpstan-ignore-next-line
+ if ( version_compare( WPGRAPHQL_ACF_VERSION, '1.0.0', '<' ) && version_compare( $version, '2.0.0', '>=' ) ) {
+ continue;
+ }
+
+ // Return the first matching version.
+ $latest_version = $version;
+ break;
+ }
+
+ if ( ! empty( $latest_version ) ) {
+ set_site_transient( self::VERSION_TRANSIENT, $latest_version, 6 * HOUR_IN_SECONDS );
+ }
+ }
+
+ return $latest_version;
+ }
+
+ /**
+ * Gets the data from the WordPress plugin directory.
+ *
+ * @return ?object
+ */
+ protected function get_wporg_data() {
+ // Return cached data if available.
+ if ( ! empty( $this->wporg_data ) ) {
+ return $this->wporg_data;
+ }
+
+ // Get data from transient.
+ $data = get_site_transient( self::WPORG_DATA_TRANSIENT );
+
+ if ( empty( $data ) || ! is_object( $data ) ) {
+ /**
+ * Filters the API URL to use for fetching the plugin data.
+ * Useful for testing.
+ *
+ * @param string $api_url The API URL to use.
+ */
+ $api_url = apply_filters( 'wpgraphql_acf_wporg_api_url', $this->plugin_config['api_url'] );
+
+ $req = wp_remote_get( $api_url ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get
+
+ $body = wp_remote_retrieve_body( $req );
+
+ if ( empty( $body ) ) {
+ return null;
+ }
+
+ $data = json_decode( $body );
+
+ // Bail because we couldn't decode the body.
+ if ( empty( $data ) || ! is_object( $data ) ) {
+ return null;
+ }
+
+ // Refresh every 6 hours.
+ set_site_transient( self::WPORG_DATA_TRANSIENT, $data, 6 * HOUR_IN_SECONDS );
+ }
+
+ // Stash for reuse.
+ $this->wporg_data = $data;
+
+ return $data;
+ }
+
+ /**
+ * Gets the download URL for a specific version.
+ *
+ * @param string $version Version number.
+ *
+ * @return string|false
+ */
+ protected function get_download_url( string $version ) {
+ $data = $this->get_wporg_data();
+
+ return ! empty( $data->versions->$version ) ? $data->versions->$version : false;
+ }
+
+ /**
+ * Returns the SemVer type of update based on the new version.
+ *
+ * @param string $new_version The SemVer-compliant version number (x.y.z)
+ * @param string $current_version The SemVer-compliant version number (x.y.z)
+ *
+ * @return string{major|minor|patch} The type of update (major, minor, patch).
+ */
+ protected static function get_semver_update_type( string $new_version, string $current_version ) : string {
+ $current = explode( '.', $current_version );
+ $new = explode( '.', $new_version );
+
+ // If the first digit is 0, we need to compare the next digit.
+ if ( '0' === $new[0] && '0' !== $current[0] ) {
+ return self::get_semver_update_type(
+ implode( '.', array_slice( $new, 1 ) ),
+ implode( '.', array_slice( $current, 1 ) )
+ );
+ }
+
+ // If the major version is different, this is a major update.
+ if ( $current[0] !== $new[0] ) {
+ return 'major';
+ }
+
+ // If the minor version is different, this is a minor update.
+ if ( $current[1] !== $new[1] ) {
+ return 'minor';
+ }
+
+ return 'patch';
+ }
+
+ /**
+ * Gets the message to display for a breaking change.
+ *
+ * @param string $version The version number.
+ */
+ protected static function get_breaking_change_message( string $version ) : string {
+ return sprintf(
+ /* translators: %s: version number. */
+ __( 'Version %s of WPGraphQL for ACF is a major update and may contain breaking changes. Please review the changelog and test before updating on a production site.', 'wp-graphql-acf' ),
+ $version
+ );
+ }
+
+}
diff --git a/src/class-acf.php b/src/class-acf.php
index e7f8e21..1566641 100644
--- a/src/class-acf.php
+++ b/src/class-acf.php
@@ -8,6 +8,7 @@
namespace WPGraphQL\ACF;
use GraphQL\Type\Definition\ResolveInfo;
+use WPGraphQL\ACF\Admin\Updater;
/**
* Final class ACF
@@ -17,7 +18,7 @@ final class ACF {
/**
* Stores the instance of the WPGraphQL\ACF class
*
- * @var ACF The one true WPGraphQL\Extensions\ACF
+ * @var \WPGraphQL\ACF\ACF The one true WPGraphQL\Extensions\ACF
* @access private
*/
private static $instance;
@@ -25,12 +26,11 @@ final class ACF {
/**
* Get the singleton.
*
- * @return ACF
+ * @return \WPGraphQL\ACF\ACF
*/
public static function instance() {
-
- if ( ! isset( self::$instance ) && ! ( self::$instance instanceof ACF ) ) {
- self::$instance = new ACF();
+ if ( ! isset( self::$instance ) && ! ( self::$instance instanceof self ) ) {
+ self::$instance = new self();
self::$instance->setup_constants();
self::$instance->includes();
self::$instance->actions();
@@ -41,7 +41,7 @@ public static function instance() {
/**
* Fire off init action
*
- * @param ACF $instance The instance of the WPGraphQL\ACF class
+ * @param \WPGraphQL\ACF\ACF $instance The instance of the WPGraphQL\ACF class
*/
do_action( 'graphql_acf_init', self::$instance );
@@ -63,7 +63,6 @@ public function __clone() {
// Cloning instances of the class is forbidden.
_doing_it_wrong( __FUNCTION__, esc_html__( 'The \WPGraphQL\ACF class should not be cloned.', 'wp-graphql-acf' ), '0.0.1' );
-
}
/**
@@ -76,7 +75,6 @@ public function __wakeup() {
// De-serializing instances of the class is forbidden.
_doing_it_wrong( __FUNCTION__, esc_html__( 'De-serializing instances of the \WPGraphQL\ACF class is not allowed', 'wp-graphql-acf' ), '0.0.1' );
-
}
/**
@@ -86,22 +84,23 @@ public function __wakeup() {
* @return void
*/
private function setup_constants() {
+ $main_file_path = dirname( __DIR__ ) . '/wp-graphql-acf.php';
+
// Plugin Folder Path.
if ( ! defined( 'WPGRAPHQL_ACF_PLUGIN_DIR' ) ) {
- define( 'WPGRAPHQL_ACF_PLUGIN_DIR', plugin_dir_path( __FILE__ . '/..' ) );
+ define( 'WPGRAPHQL_ACF_PLUGIN_DIR', plugin_dir_path( $main_file_path ) );
}
// Plugin Folder URL.
if ( ! defined( 'WPGRAPHQL_ACF_PLUGIN_URL' ) ) {
- define( 'WPGRAPHQL_ACF_PLUGIN_URL', plugin_dir_url( __FILE__ . '/..' ) );
+ define( 'WPGRAPHQL_ACF_PLUGIN_URL', plugin_dir_url( $main_file_path ) );
}
// Plugin Root File.
if ( ! defined( 'WPGRAPHQL_ACF_PLUGIN_FILE' ) ) {
- define( 'WPGRAPHQL_ACF_PLUGIN_FILE', __FILE__ . '/..' );
+ define( 'WPGRAPHQL_ACF_PLUGIN_FILE', $main_file_path );
}
-
}
/**
@@ -121,7 +120,6 @@ private function includes() {
* cycle
*/
private function actions() {
-
}
/**
@@ -133,28 +131,33 @@ private function filters() {
* This filters any field that returns the `ContentTemplate` type
* to pass the source node down to the template for added context
*/
- add_filter( 'graphql_resolve_field', function( $result, $source, $args, $context, ResolveInfo $info, $type_name, $field_key, $field, $field_resolver ) {
- if ( isset( $info->returnType ) && strtolower( 'ContentTemplate' ) === strtolower( $info->returnType ) ) {
- if ( is_array( $result ) && ! isset( $result['node'] ) && ! empty( $source ) ) {
- $result['node'] = $source;
+ add_filter(
+ 'graphql_resolve_field',
+ static function ( $result, $source, $args, $context, ResolveInfo $info, $type_name, $field_key, $field, $field_resolver ) {
+ if ( isset( $info->returnType ) && strtolower( 'ContentTemplate' ) === strtolower( $info->returnType ) ) {
+ if ( is_array( $result ) && ! isset( $result['node'] ) && ! empty( $source ) ) {
+ $result['node'] = $source;
+ }
}
- }
- return $result;
- }, 10, 9 );
-
+ return $result;
+ },
+ 10,
+ 9
+ );
}
/**
* Initialize
*/
private function init() {
-
$config = new Config();
add_action( 'graphql_register_types', [ $config, 'init' ], 10, 1 );
$acf_settings = new ACF_Settings();
$acf_settings->init();
+ $updater = new Updater();
+ $updater->init();
}
}