Skip to content

Commit

Permalink
Merge pull request #655 from carstingaxion/feature/export-import
Browse files Browse the repository at this point in the history
Export & Import
  • Loading branch information
mauteri authored Jul 14, 2024
2 parents 7957f7b + 6d87867 commit b58db40
Show file tree
Hide file tree
Showing 7 changed files with 516 additions and 3 deletions.
212 changes: 212 additions & 0 deletions includes/core/classes/class-export.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
<?php
/**
* Class responsible for exporting content using WordPress' native export tool.
*
* @package GatherPress\Core
* @since 1.0.0
*/

namespace GatherPress\Core;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore

use GatherPress\Core\Traits\Singleton;
use WP_Post;

/**
* Class Export.
*
* The Export class handles the exporting of content using WordPress' native export tool.
* This class will enhance overall export management, provide effective filtering
* and support validation of the export-objects based on their post type and meta data.
*
* @since 1.0.0
*/
class Export extends Migrate {
/**
* Enforces a single instance of this class.
*/
use Singleton;

/**
* The post_meta name for GatherPress temporary entry,
* to hook into WordPress export.
*
* @since 1.0.0
* @var string $POST_META
*/
const POST_META = 'gatherpress_extend_export';

/**
* Class constructor.
*
* This method initializes the object and sets up necessary hooks.
*
* @since 1.0.0
*/
public function __construct() {
$this->setup_hooks();
}

/**
* Set up hooks for various purposes.
*
* This method adds hooks for different purposes as needed.
*
* @since 1.0.0
*
* @return void
*/
protected function setup_hooks(): void {
/**
* Fires at the beginning of an export, before any headers are sent.
*/
add_action( 'export_wp', array( $this, 'export' ) );
}

/**
* Sets up the necessary hooks for the export process.
*
* @since 1.0.0
*
* @return void
*/
public function export(): void {
add_action( 'the_post', array( $this, 'prepare' ), 10, 2 );
add_filter( 'wxr_export_skip_postmeta', array( $this, 'extend' ), 10, 3 );
}

/**
* Saves a temporary marker as postmeta,
* which allows to hook into the export process per post later on.
*
* Called via setup_postdata() at the beginning of each singular post export.
*
* Fires once the post data has been set up.
*
* @param WP_Post $post The Post object (passed by reference).
* @return void
*/
public function prepare( WP_Post $post ): void {
if ( $this->validate( $post ) ) {
add_post_meta( $post->ID, self::POST_META, true );
}
}

/**
* Extend WordPress' native Export
*
* WordPress' native Export can be extended in hacky way using `wxr_export_skip_postmeta`
* where GatherPress echos out some pseudo-post-meta fields,
* before returning `false` like the default.
*
* @source https://github.com/WordPress/wordpress-develop/blob/6.5/src/wp-admin/includes/export.php#L655-L677
*
* Normally this filters whether to selectively skip post meta used for WXR exports.
* Returning a truthy value from the filter will skip the current meta object from being exported.
*
* @see https://developer.wordpress.org/reference/hooks/wxr_export_skip_postmeta/
*
* But because there is no 'do_action('per-exported-post)',
* GatherPress created a post_meta entry as a temporary marker, to be used as an entry-point into
* WordPress' native export process, which is used now.
*
* @param bool $skip Whether to skip the current post meta. Default false.
* @param string $meta_key Current meta key.
* @param object $meta Current meta object.
* @return bool Whether to skip the current post meta. Default false.
*/
public function extend( bool $skip, string $meta_key, object $meta ): bool {
if ( self::POST_META === $meta_key ) {
// Echos out xml with pseudo-postmeta.
$this->run( get_post( $meta->post_id ) );

// Deletes temporary marker.
delete_post_meta( $meta->post_id, self::POST_META );

// Prevent 'normal' export processing for that particular postmeta field,
// because it doesn't exist in real and will trigger an error.
return true;
}

return $skip;
}

/**
* Checks if the currently exported post is of type 'gatherpress_event'.
*
* @since 1.0.0
*
* @param WP_Post $post Current meta key.
* @return bool True, when the currently exported post is of type 'gatherpress_event', false otherwise.
*/
protected function validate( WP_Post $post ): bool {
return ( Event::POST_TYPE === $post->post_type );
}

/**
* Exports all custom data.
*
* Gets all 'pseudopostmetas' and generates WXR-compatible output for each,
* the generated xml markup is rendered into the WordPress export file directly.
*
* An export file like this can be imported into GatherPress using
* the native 'WordPress importer' and its potential replacement the 'WordPress importer (v2)'.
*
* @since 1.0.0
*
* @param WP_Post $post Current 'gatherpress_event' post being exported.
* @return void
*/
public function run( WP_Post $post ): void {
$pseudopostmetas = $this->get_pseudopostmetas();

array_walk(
$pseudopostmetas,
array( $this, 'render' ),
$post
);
}

/**
* Render custom post_meta data into xml markup to be used while WordÜress' native export.
*
* @param array $callbacks Associative array with (import & export) callback functions for a the non-existent post_meta entry, named by $key.
* @param string $key Name of the custom post_meta, that should be exported.
* @param WP_Post $post The currently exported 'gatherpress_event' post.
* @return void
*/
public function render( array $callbacks, string $key, WP_Post $post ) {
if ( ! isset( $callbacks['export_callback'] ) || ! is_callable( $callbacks['export_callback'] ) ) {
return;
}

$value = call_user_func( $callbacks['export_callback'], $post );

?>
<wp:postmeta>
<wp:meta_key><?php echo wxr_cdata( $key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></wp:meta_key>
<wp:meta_value><?php echo wxr_cdata( $value ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></wp:meta_value>
</wp:postmeta>
<?php
}

/**
* Returns dates, times and timezone from the 'wp_gatherpress_events' DB table
* as serialized string for the current post being exported.
*
* @since 1.0.0
*
* @param WP_Post $post Current 'gatherpress_event' post being exported.
* @return string Serialized JSON string with all date, time & timezone data of the current $post.
*/
public function datetimes_callback( WP_Post $post ): string {
// Make sure to not get any user-related data.
remove_all_filters( 'gatherpress_timezone' );

$event = new Event( $post->ID );

return maybe_serialize( $event->get_datetime() );
}
}
184 changes: 184 additions & 0 deletions includes/core/classes/class-import.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<?php
/**
* Class responsible for importing content using WordPress' native import tool(s).
*
* @package GatherPress\Core
* @since 1.0.0
*/

namespace GatherPress\Core;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore

use GatherPress\Core\Event;
use GatherPress\Core\Migrate;
use GatherPress\Core\Traits\Singleton;
use WP_Post;

/**
* Class Import.
*
* The Import class handles the importing of content using WordPress' native import tool.
* This class will provide effective filtering and support validation of the import-objects
* based on their post type and meta data.
*
* Succesfully identified GatherPress data will be saved into custom DB tables.
*
* @since 1.0.0
*/
class Import extends Migrate {
/**
* Enforces a single instance of this class.
*/
use Singleton;

/**
* Class constructor.
*
* This method initializes the object and sets up necessary hooks.
*
* @since 1.0.0
*/
public function __construct() {
$this->setup_hooks();
}

/**
* Set up hooks for various purposes.
*
* This method adds hooks for different purposes as needed.
*
* @since 1.0.0
*
* @return void
*/
protected function setup_hooks(): void {
if ( class_exists( 'WXR_Importer' ) ) {
/**
* Setup for WordPress Importer (v2).
*
* @see https://github.com/humanmade/Wordpress-Importer
*/
$hook_name = 'wxr_importer.pre_process.post';
} else {
/**
* Setup for default WordPress Importer.
*
* @see https://github.com/WordPress/wordpress-importer/issues/42
*/
$hook_name = 'wp_import_post_data_raw';
}

add_filter( $hook_name, array( $this, 'prepare' ) );
add_action( 'gatherpress_import', array( $this, 'extend' ) );
}

/**
* Extend WordPress' native Import.
*
* @see https://github.com/WordPress/wordpress-importer/blob/71bdd41a2aa2c6a0967995ee48021037b39a1097/src/class-wp-import.php#L631
*
* @param array $post_data_raw The result of 'wp_import_post_data_raw'.
* @return array Returns the unchanged result of 'wp_import_post_data_raw'.
*/
public function prepare( array $post_data_raw ): array {
if ( $this->validate( $post_data_raw ) ) {
/**
* Fires for every GatherPress data to be imported.
*
* @since 1.0.0
*
* @param {array} $post_data_raw Unprocessesd 'gatherpress_event' post being imported.
*/
do_action( 'gatherpress_import', $post_data_raw );
}

return $post_data_raw;
}

/**
* Checks if the currently imported post is of type 'gatherpress_event'.
*
* @param array $post_data_raw The result of 'wp_import_post_data_raw'.
* @return bool True, when the currently imported post is of type 'gatherpress_event', false otherwise.
*/
protected function validate( array $post_data_raw ): bool {
return ( isset( $post_data_raw['post_type'] ) && Event::POST_TYPE === $post_data_raw['post_type'] );
}

/**
* Import all custom data.
*
* @return void
*/
public function extend(): void {
add_filter( 'add_post_metadata', array( $this, 'run' ), 10, 5 );
}

/**
* Import data with custom scheme.
*
* This method is called on every imported post_meta
* and allows to work with the data to be imported.
*
* It checks if the current meta_key is one of GatherPress' pseudopostmetas
* and if an import-callback for that key exists.
* If both is true, the import callback is provided with all available information and called once per meta_key.
*
* The normal saving into the 'wp_postmeta' DB table is disabled in such a case.
*
* @see https://developer.wordpress.org/reference/hooks/add_meta_type_metadata/
* @see https://www.ibenic.com/hook-wordpress-metadata/
*
* @param null|bool $check Whether to allow adding metadata for the given type.
* @param int $object_id ID of the object metadata is for.
* @param string $meta_key Metadata key.
* @param mixed $meta_value Metadata value. Must be serializable if non-scalar.
* @param bool $unique Whether the specified meta key should be unique for the object.
* @return null|bool Returning a non-null value will effectively short-circuit the saving of 'normal' meta data.
*/
public function run( ?bool $check, int $object_id, string $meta_key, $meta_value, bool $unique ): ?bool {
$pseudopostmetas = $this->get_pseudopostmetas();

if ( ! isset( $pseudopostmetas[ $meta_key ] ) ) {
return null;
}

if (
! isset( $pseudopostmetas[ $meta_key ]['import_callback'] ) ||
! is_callable( $pseudopostmetas[ $meta_key ]['import_callback'] )
) {
return null;
}

/*
* Run import callback,
* e.g. Save data into a custom DB table.
*/
call_user_func(
$pseudopostmetas[ $meta_key ]['import_callback'],
$object_id,
$meta_value
);

/*
* Disable saving of 'normal' meta data.
*/
return false;
}


/**
* Save dates, times & timezone for the currently imported 'gatherpress_event' post.
*
* @param int $post_id ID of the object metadata is for.
* @param mixed $data Metadata value. Must be serializable if non-scalar.
* @return void
*/
public function datetimes_callback( int $post_id, $data ): void {
$event = new Event( $post_id );

$event->save_datetimes( maybe_unserialize( $data ) );
}
}
Loading

0 comments on commit b58db40

Please sign in to comment.