<?php
/**
 * Export Post Controller
 *
 * This file enables advanced post exports inside the GREYD.SUITE.
 * Posts of all supported post types can be exported via the WordPress
 * backend (edit.php) and later be imported to any GREYD.SUITE site.
 *
 * The export contains a JSON file that consists of one or multiple
 * arrays of all exported posts. Each of the array contains the following:
 *  - post_elements     array of WP_Post properties
 *  - meta              all post_meta information
 *  - taxonomies        all taxonomy terms
 *  - media             all nested media elements (thumbnail & other)
 *
 * @since 0.8.4
 */
namespace Greyd;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

new Post_Export();
class Post_Export {

	/**
	 * Holds all WP_Post objects for either ex- or import.
	 *
	 * @var array
	 */
	public static $posts = array();

	/**
	 * Holds all WP_Media objects for either ex- or import.
	 *
	 * @var array
	 */
	public static $media = array();

	/**
	 * Constructor
	 */
	public function __construct() {

		// Export
		add_action( 'greyd_ajax_mode_post_export', array( $this, 'handle_export' ) );
		add_filter( 'greyd_export_post_meta-dynamic_meta', array( $this, 'prepare_dynamic_meta' ), 10, 3 );

		// add bulk action callbacks
		add_action( 'admin_init', array( $this, 'add_bulk_action_callbacks' ) );
	}

	/**
	 * Handle the ajax export action
	 *
	 * @action 'greyd_ajax_mode_post_export'
	 *
	 * @param array $data   holds the $_POST['data']
	 */
	public function handle_export( $data ) {

		Post_Export_Helper::enable_logs();

		do_action( 'greyd_post_export_log', "\r\n\r\n" . 'HANDLE EXPORT' . "\r\n", $data );

		$post_id = isset( $data['post_id'] ) ? $data['post_id'] : '';
		$args    = array(
			'append_nested'  => isset( $data['nested'] ) ? true : false,
			'whole_posttype' => isset( $data['whole_posttype'] ) ? true : false,
			'resolve_menus'  => isset( $data['resolve_menus'] ) ? true : false,
			'translations'   => isset( $data['translations'] ) ? true : false,
		);
		if ( ! empty( $post_id ) ) {

			// export post
			$post = self::export_post( $post_id, $args );

			/**
			 * Create the export ZIP-archive.
			 * If posts & media are null, write_export_file() uses the class vars.
			 */
			$posts    = $media = $args['append_nested'] ? null : array();
			$filename = self::write_filename( $post, $args );
			$filepath = self::write_export_file( $filename, $posts, $media );

			if ( ! $filepath ) {
				Post_Export_Helper::error( __( "The export file could not be written.", 'greyd_hub' ) );
			}

			Post_Export_Helper::success( Post_Export_Helper::convert_content_dir_to_path( $filepath ) );
		}
		Post_Export_Helper::error( __( "No valid Post ID could found.", 'greyd_hub' ) );
	}

	/**
	 * Get post with all its meta, taxonomies, media etc.
	 *
	 * @param int   $post_id
	 * @param array $args       Arguments.
	 *
	 * @return WP_Post Advanced WP_Post object preparred for export.
	 */
	public static function export_post( $post_id, $args = array() ) {

		$args = wp_parse_args( $args, array(
			'append_nested'  => true,
			'whole_posttype' => false,
			'resolve_menus'  => true,
			'translations'   => false,
		) );

		// reset the class vars
		self::$posts = array();
		self::$media = array();

		// prepare the export based on this post
		$post = self::prepare_post( $post_id, $args );

		// if this is a posttype, enable bulk export
		if (
			class_exists( '\Greyd\Posttypes\Posttype_Helper' )
			&& $args['whole_posttype']
			&& $post->post_type === 'tp_posttypes'
		) {

			$post_type = \Greyd\Posttypes\Posttype_Helper::get_posttype_slug_from_title( $post->post_title );

			do_action( 'greyd_post_export_log', "\r\n" . "EXPORT MULTIPLE POSTS from posttype '" . $post_type . "'\r\n" );

			$post_ids = get_posts(
				array(
					'numberposts' => -1,
					'post_type'   => $post_type,
					'fields'      => 'ids',
				)
			);

			if ( $post_ids && is_array( $post_ids ) ) {
				foreach ( $post_ids as $_post_id ) {
					self::prepare_post( $_post_id, $args );
				}
			}
		}

		return $post;
	}

	/**
	 * Export posts with all its meta, taxonomies, media etc.
	 *
	 * @param int[] $post_ids   Array of post IDs.
	 * @param array $args       Arguments.
	 *
	 * @return WP_Post[]        Advanced WP_Post objects preparred for export.
	 */
	public static function export_posts( $post_ids, $args = array() ) {

		$args = wp_parse_args( $args, array(
			'append_nested'  => true,
			'whole_posttype' => false,
			'translations'   => false,
		) );

		// reset the class vars
		self::$posts = array();
		self::$media = array();

		if ( $post_ids && is_array( $post_ids ) ) {
			foreach ( $post_ids as $_post_id ) {
				self::prepare_post( $_post_id, $args );
			}
		}

		return self::$posts;
	}

	/**
	 * Prepare post for export.
	 *
	 * This function attaches all post meta options, taxonomy terms
	 * and other post properties to the WP_Post object. It also gets
	 * all nested templates, forms, images etc. inside the post content,
	 * dynamic meta or set as thumbnail and prepares them for later
	 * replacement.
	 *
	 * @param int   $post_id    WP_Post ID.
	 * @param array $args       Arguments.
	 *
	 * @return object|bool Preparred WP_Post object on success. False on failure.
	 *
	 * This function automatically sets the following class vars.
	 * Use them to export all nested posts at once.
	 *
	 * @var array class::$posts     Array of all preparred post objects.
	 * @var array class::$media     Array of all media files.
	 */
	public static function prepare_post( $post_id, $args = array() ) {

		// return if we're already processed this post
		if ( isset( self::$posts[ $post_id ] ) ) {
			do_action( 'greyd_post_export_log', "\r\n" . "Post '$post_id' already processed" );
			return self::$posts[ $post_id ];
		}

		// arguments
		$default_args = array(
			'append_nested'  => true,
			'whole_posttype' => false,
			'resolve_menus'  => true,
			'translations'   => false,
		);
		$args         = array_merge( $default_args, $args );

		// get WP_Post
		if ( ! $post = get_post( $post_id ) ) {
			return false;
		}

		do_action( 'greyd_post_export_log', "\r\n\r\n|\r\n|  PREPARE POST {$post_id} '{$post->post_title}'\r\n|" );

		// do_action( "greyd_post_export_log", esc_attr($post->post_content) );

		/**
		 * First we append the post object to the class var. We do this to
		 * kind of 'reserve' the position of the post inside the array.
		 */
		self::$posts[ $post_id ] = $post;

		/**
		 * Prepare the $post object.
		 *
		 * After that, it has some additional properties:
		 *
		 * @property array nested   Nested post arrays keyed by post_id:
		 *      {{post_id}} => array(
		 *          @property int    ID
		 *          @property string post_name
		 *          @property string post_type
		 *          @property string front_url
		 *          @property string file_path (only for attachments)
		 *      )
		 * @property WP_Term[] nested_terms   Nested terms keyed by term_id.
		 * @property array meta     All post meta options as arrays.
		 * @property array terms    All post terms sorted by the taxonomy.
		 * @property array media    If this is an attachment:
		 *      @property string name
		 *      @property string path
		 *      @property string url
		 * @property array language Translation information:
		 *      @property string code
		 *      @property string tool
		 *      @property array  post_ids
		 *      @property array  args
		 */
		$post               = self::prepare_nested_posts( $post );
		$post               = self::prepare_nested_terms( $post );
		$post->post_content = self::prepare_strings( $post->post_content, $post_id );
		$post->meta         = self::prepare_meta( $post_id, $args );
		$post->terms        = self::prepare_taxonomy_terms( $post );
		$post->media        = self::prepare_media( $post_id );
		$post->language     = self::prepare_language( $post, $args );

		/**
		 * Resolve menus
		 */
		if ( $args['resolve_menus'] ) {
			$post->post_content = self::resolve_menus( $post->post_content, $post_id, $post );
		}

		/**
		 * Now we update the post in the class var.
		 */
		self::$posts[ $post_id ] = $post;

		/**
		 * Let's save the media to the class var.
		 *
		 * We need this, so write_export_file() can access all the files at once.
		 */
		if ( ! empty( $post->media ) ) {
			self::$media[ $post_id ] = $post->media;
		}

		/**
		 * The post thumbnail always has to be included in the export,
		 * because WP references it with an ID, therefore it needs to
		 * be accessable as a post.
		 */
		if ( $thumbnail_id = get_post_thumbnail_id( $post ) ) {
			self::prepare_post( $thumbnail_id, $args );
		}

		/**
		 * Now we loop through all the nested posts (if the option is set).
		 */
		if ( $args['append_nested'] ) {
			foreach ( $post->nested as $nested_id => $nested_name ) {
				self::prepare_post( $nested_id, $args );
			}
		}

		/**
		 * Now we loop through all translations of this post (if the option is set).
		 */
		if ( $args['translations'] && count( $post->language['post_ids'] ) ) {
			foreach ( $post->language['post_ids'] as $lang => $translated_post_id ) {
				self::prepare_post( $translated_post_id, $args );
			}
		}

		return $post;
	}

	/**
	 * Prepare nested posts in content for export
	 *
	 * @param WP_Post $post
	 *
	 * @return object Preparred with new property: 'nested'
	 */
	public static function prepare_nested_posts( $post ) {
		do_action( 'greyd_post_export_log', "\r\n" . 'Prepare nested posts.' );

		$post->nested = array();
		$content      = is_object( $post ) ? $post->post_content : '';

		if ( empty( $content ) ) {
			do_action( 'greyd_post_export_log', '=> post content is empty' );
			return $post;
		}

		// get regex patterns
		$replace_id_patterns = (array) get_nested_post_patterns( $post->ID, $post );

		foreach ( $replace_id_patterns as $key => $pattern ) {

			// doing it wrong
			if (
				! isset( $pattern['search'] ) ||
				! is_array( $pattern['search'] ) ||
				count( $pattern['search'] ) < 2 ||
				! isset( $pattern['replace'] ) ||
				! is_array( $pattern['replace'] )
			) {
				continue;
			}

			$match_regex = '/' . implode( '([\da-z\-\_]+?)', $pattern['search'] ) . '/';
			$regex_group = isset( $pattern['group'] ) ? (int) $pattern['group'] : 2;
			// do_action( "greyd_post_export_log", "  - test regex: ".esc_attr($match_regex) );

			// search for all occurrences
			preg_match_all( $match_regex, $content, $matches );
			$found_posts = isset( $matches[ $regex_group ] ) ? $matches[ $regex_group ] : null;
			if ( ! empty( $found_posts ) ) {

				do_action( 'greyd_post_export_log', "\r\n" . "  Replace '" . $key . "':" );
				foreach ( $found_posts as $name_or_id ) {

					$nested_post = null;
					$nested_id = $name_or_id;

					// WP_Post->ID
					if ( is_numeric( $name_or_id ) ) {
						$nested_post = get_post( $name_or_id );
					}
					// WP_Post->post_name
					else {
						if ( isset( $pattern['post_type'] ) ) {
							$args = (object) array(
								'post_name' => $name_or_id,
								'post_type' => $pattern['post_type'],
							);
						} else {
							$args = $name_or_id;
						}
						// get post
						$nested_post = Post_Export_Helper::get_post_by_name_and_type( $args );
						if ( $nested_post ) {
							$nested_id = $nested_post->ID;
						}
					}

					if ( ! $nested_post ) {
						do_action( 'greyd_post_export_log', "  - post with id or name '$name_or_id' could not be found." );
						continue;
					}

					$search_regex   = '/' . implode( $nested_id, $pattern['search'] ) . '/';
					$replace_string = implode( '{{' . $nested_id . '}}', $pattern['replace'] );

					// replace in $content
					$content = preg_replace( $search_regex, $replace_string, $content );

					// collect in $post->nested
					if ( ! isset( $post->nested[ $nested_id ] ) ) {
						if ( $nested_post ) {

							$post->nested[ $nested_id ] = array(
								'ID'        => $nested_id,
								'post_name' => $nested_post->post_name,
								'post_type' => $nested_post->post_type,
								'front_url' => $nested_post->post_type === 'attachment' ? wp_get_attachment_url( $nested_post->ID ) : get_permalink( $nested_id ),
							);
							if ( $nested_post->post_type === 'attachment' ) {
								// $post->nested[ $nested_id ]['file_path'] = get_attached_file( $nested_post->ID );
								$post->nested[ $nested_id ] = array(
									'ID'        => $nested_id,
									'post_name' => $nested_post->post_name,
									'post_type' => $nested_post->post_type,
									'front_url' => wp_get_attachment_url( $nested_id ),
									'file_path' => get_attached_file( $nested_id ),
								);

							} else {
								$post->nested[ $nested_id ] = array(
									'ID'        => $nested_id,
									'post_name' => $nested_post->post_name,
									'post_type' => $nested_post->post_type,
									'front_url' => get_permalink( $nested_id ),
								);
							}
							
							// also replace the front url inside the post content
							$content = str_replace( $post->nested[ $nested_id ]['front_url'], '{{' . $nested_id . '-front-url}}', $content );

							do_action(
								'greyd_post_export_log',
								sprintf(
									"  - nested post '%s' attached for export.\r\n     * ID: %s\r\n     * TYPE: %s\r\n     * URL: %s",
									$nested_post->post_name,
									$nested_id,
									$nested_post->post_type,
									$post->nested[ $nested_id ]['front_url']
								)
							);
						} else {
							$post->nested[ $nested_id ] = null;
						}
					}
				}
			}
		}
		$post->post_content = $content;

		do_action( 'greyd_post_export_log', '=> nested elements were preparred' );

		return $post;
	}

	/**
	 * Prepare nested terms inside the post content
	 *
	 * @since 1.7
	 *
	 * @return object Preparred with new property: 'nested_terms'
	 */
	public static function prepare_nested_terms( $post ) {
		do_action( 'greyd_post_export_log', "\r\n" . 'Prepare nested terms.' );

		$post->nested_terms = array();
		$content            = is_object( $post ) ? $post->post_content : '';

		if ( empty( $content ) ) {
			do_action( 'greyd_post_export_log', '=> post content is empty' );
			return $post;
		}

		// register temporary taxonomies to prevent errors
		Post_Export_Helper::get_dynamic_taxonomies();

		// get patterns
		$replace_id_patterns = (array) get_nested_term_patterns( $post->ID, $post );

		foreach ( $replace_id_patterns as $key => $pattern ) {

			// doing it wrong
			if (
				! isset( $pattern['search'] ) ||
				! is_array( $pattern['search'] ) ||
				count( $pattern['search'] ) < 2 ||
				! isset( $pattern['replace'] ) ||
				! is_array( $pattern['replace'] )
			) {
				continue;
			}

			$match_regex = '/' . implode( '([\da-z\-\_]+?)', $pattern['search'] ) . '/';
			$regex_group = isset( $pattern['group'] ) ? (int) $pattern['group'] : 2;
			// do_action( "greyd_post_export_log", "  - test regex: ".esc_attr($match_regex) );

			// search for all occurrences
			preg_match_all( $match_regex, $content, $matches );
			$found_terms = isset( $matches[ $regex_group ] ) ? $matches[ $regex_group ] : null;
			if ( ! empty( $found_terms ) ) {

				do_action( 'greyd_post_export_log', '  - replace ' . $key . ':' );
				foreach ( $found_terms as $term_id ) {

					// default value for term_ids
					if ( $term_id == 0 || $term_id == -1 ) {
						continue;
					}

					$search_regex   = '/' . implode( $term_id, $pattern['search'] ) . '/';
					$replace_string = implode( '{{' . $term_id . '}}', $pattern['replace'] );

					// replace in $content
					$content = preg_replace( $search_regex, $replace_string, $content );

					// collect in $post->nested_terms
					if ( ! isset( $post->nested_terms[ $term_id ] ) ) {
						$term_object = get_term( $term_id );
						if ( ! $term_object || is_wp_error( $term_object ) ) {
							do_action( 'greyd_post_export_log', "  - term with id '$term_id' could not be found." );
							$post->nested_terms[ $term_id ] = null;
						} else {
							do_action( 'greyd_post_export_log', "  - term with id '$term_id' found.", $term_object );
							$post->nested_terms[ $term_id ] = $term_object;
						}
					}
				}
			}
		}
		$post->post_content = $content;

		do_action( 'greyd_post_export_log', '=> nested terms were preparred' );

		return $post;
	}

	/**
	 * Replace strings for export
	 *
	 * @param string $subject
	 * @param int    $post_id
	 *
	 * @return string $subject
	 */
	public static function prepare_strings( $subject, $post_id, $log = true ) {

		if ( empty( $subject ) ) {
			return $subject;
		}

		if ( $log ) {
			do_action( 'greyd_post_export_log', "\r\n" . 'Prepare strings.' );
		}

		// get patterns
		$replace_strings = (array) get_nested_string_patterns( $subject, $post_id );
		foreach ( $replace_strings as $name => $string ) {
			$subject = str_replace( $string, '{{' . $name . '}}', $subject );
			// if ($log) do_action( "greyd_post_export_log", sprintf("  - '%s' was preparred for export.", $name ) );
		}
		if ( $log ) {
			do_action( 'greyd_post_export_log', '=> strings were preparred' );
		}
		return $subject;
	}

	/**
	 * Prepare meta for consumption
	 *
	 * @param int   $post_id Post ID.
	 * @param array $args Export arguments.
	 *
	 * @return array
	 */
	public static function prepare_meta( $post_id, $args = array() ) {
		do_action( 'greyd_post_export_log', "\r\n" . 'Prepare post meta.' );

		$meta          = get_post_meta( $post_id );
		$prepared_meta = array();

		// Transfer all meta
		foreach ( $meta as $meta_key => $meta_array ) {
			foreach ( $meta_array as $meta_value ) {

				// don't prepare blacklisted meta
				if ( in_array( $meta_key, Post_Export_Helper::blacklisted_meta(), true ) ) {
					continue;
				}
				// skip certain meta keys
				elseif ( Post_Export_Helper::maybe_skip_meta_option( $meta_key, $meta_value ) ) {
					continue;
				}

				$meta_value = maybe_unserialize( $meta_value );

				/**
				 * Add filter to modify the post meta value before export
				 *
				 * @filter 'greyd_export_post_meta-{{meta_key}}'
				 */
				$meta_value = apply_filters( 'greyd_export_post_meta-' . $meta_key, $meta_value, $post_id, $args );

				$prepared_meta[ $meta_key ][] = $meta_value;
			}
		}

		do_action( 'greyd_post_export_log', '=> post meta prepared' );
		return $prepared_meta;
	}

	/**
	 * Modify dynamic meta value before export
	 *
	 * @filter 'greyd_export_post_meta-dynamic_meta'
	 *
	 * @param mixed $meta_value
	 * @param int   $post_id
	 * @param array $args       Export arguments
	 *
	 * @return mixed $meta_value
	 */
	public function prepare_dynamic_meta( $meta_value, $post_id, $args = array() ) {
		do_action( 'greyd_post_export_log', '  - prepare dynamic meta for export' );

		if ( ! class_exists( '\Greyd\Posttypes\Posttype_Helper' ) ) {
			do_action( 'greyd_post_export_log', '  - Posttype class does not exist.' );
			return $meta_value;
		}

		$post     = get_post( $post_id );
		$posttype = \Greyd\Posttypes\Posttype_Helper::get_dynamic_posttype_by_slug( $post->post_type );
		if ( $posttype && isset( $posttype['fields'] ) ) {
			foreach ( (array) $posttype['fields'] as $field ) {
				$current_value = null;
				if ( is_object( $field ) ) {
					$field = (array) $field;
				}

				// media files
				if ( $field['type'] === 'file' ) {
					$current_value = isset( $meta_value[ $field['name'] ] ) ? $meta_value[ $field['name'] ] : null;
					if ( is_numeric( $current_value ) ) {
						do_action( 'greyd_post_export_log', sprintf( "  - file value for '%s' is an ID (%s) and was prepared for export.", $field['name'], $current_value ) );
						if ( isset( $args['append_nested'] ) && $args['append_nested'] ) {
							do_action( 'greyd_post_export_log', "\r\n---------------------" );
							self::prepare_post( $current_value, $args );
							do_action( 'greyd_post_export_log', "\r\n---------------------\r\n" );
						}
						$current_value = '{{' . $current_value . '}}';
					}
				}
				// url or tinymce editor
				elseif ( $field['type'] === 'url' || $field['type'] === 'text_html' ) {
					$current_value = isset( $meta_value[ $field['name'] ] ) ? $meta_value[ $field['name'] ] : '';
					$current_value = self::prepare_strings( $current_value, $post_id, false );
					do_action( 'greyd_post_export_log', sprintf( "  - content of field '%s' was preparred for export", $field['name'] ) );
				}

				if ( $current_value ) {
					$meta_value[ $field['name'] ] = $current_value;
				}
			}
		}
		return $meta_value;
	}

	/**
	 * Format taxonomy terms for consumption
	 *
	 * @param  int $post_id Post ID.
	 * @return array
	 */
	public static function prepare_taxonomy_terms( $post ) {
		do_action( 'greyd_post_export_log', "\r\n" . 'Prepare taxonomy terms.' );

		$taxonomy_terms = array();
		$taxonomies     = get_object_taxonomies( $post );

		// if we haven't found any taxonomies we create them temporarily.
		if ( empty( $taxonomies ) ) {
			do_action( 'greyd_post_export_log', '  - object taxonomies are empty' );
			$taxonomies = Post_Export_Helper::get_dynamic_taxonomies( $post->post_type );
		}

		if ( empty( $taxonomies ) ) {
			do_action( 'greyd_post_export_log', '=> no taxonomy terms found' );
			return array();
		}

		foreach ( $taxonomies as $taxonomy ) {

			/**
			 * Retrieve post terms directly from the database.
			 *
			 * @since 1.2.7
			 *
			 * WPML attaches a lot of filters to the function wp_get_object_terms(). This results
			 * in terms of the wrong language beeing attached to a post export. This function performs
			 * way more consistent in all tests. Therefore it completely replaced it in this class.
			 *
			 * @deprecated since 1.2.7: $terms = wp_get_object_terms( $post->ID, $taxonomy );
			 */
			$terms = \Greyd\Helper::get_post_taxonomy_terms( $post->ID, $taxonomy );

			if ( empty( $terms ) ) {
				$taxonomy_terms[ $taxonomy ] = array();
				do_action( 'greyd_post_export_log', "  - no terms found for taxonomy '$taxonomy'." );
			} else {
				$taxonomy_terms[ $taxonomy ] = $terms;
				$count                       = count( $terms );
				do_action(
					'greyd_post_export_log',
					"  - {$count} " . ( $count > 1 ? 'terms' : 'term' ) . " of taxonomy '$taxonomy' prepared:\r\n    - " . implode(
						"\r\n    - ",
						array_map(
							function( $term ) {
								return "{$term->name} (#{$term->term_id})";
							},
							$terms
						)
					)
				);
			}
		}

		do_action( 'greyd_post_export_log', '=> all taxonomy terms prepared' );
		return $taxonomy_terms;
	}

	/**
	 * Format media items for export
	 *
	 * @param  int $post_id Post ID.
	 * @return array        Array with filename as key and url & path as values.
	 */
	public static function prepare_media( $post_id ) {
		$return = array();

		if ( $file_path = get_attached_file( $post_id ) ) {
			$file_url  = wp_get_attachment_url( $post_id );
			$file_name = wp_basename( $file_path );
			$return    = array(
				'name' => $file_name,
				'path' => $file_path,
				'url'  => $file_url,
			);
			do_action( 'greyd_post_export_log', "\r\n" . sprintf( "The file '%s' was added to the post.", $file_name ) );
		}
		return $return;
	}

	/**
	 * Get all necessary language information
	 *
	 * @since 1.6
	 *
	 * @param WP_Post $post     The post object.
	 * @param array   $args       Export arguments.
	 *
	 * @return array $language
	 *      @property string code       The post’s language code (eg. 'en')
	 *      @property string tool       The plugin used to setup the translation.
	 *      @property array  post_ids   All translated post IDs keyed by language code.
	 *      @property array  args       Additional arguments (depends on the tool)
	 */
	public static function prepare_language( $post, $args = array() ) {

		do_action( 'greyd_post_export_log', "\r\n" . 'Prepare post language info.' );

		$language = array(
			'code'     => null,
			'tool'     => null,
			'post_ids' => array(),
			'args'     => array(),
		);

		$tool = Post_Export_Helper::get_translation_tool();

		switch ( $tool ) {
			case 'wpml':
				$language['tool'] = 'wpml';

				$language_details = Post_Export_Helper::get_post_language_info( $post );
				if ( $language_details && isset( $language_details['language_code'] ) ) {
					$language['code'] = $language_details['language_code'];
					$language['args'] = $language_details;
					do_action( 'greyd_post_export_log', "  - post has language '{$language['code']}'" );
				}

				// prepare post id's if translations are included
				if ( $args['translations'] ) {
					$language['post_ids'] = Post_Export_Helper::get_translated_post_ids( $post );
					if ( ! empty( $language['post_ids'] ) ) {
						do_action( 'greyd_post_export_log', '  - translations of this post prepared: ' . implode( ', ', $language['post_ids'] ) );
					}
				}
				break;

			default:
				// $language['code'] = Post_Export_Helper::get_wp_lang(); // this leads to unwanted errors...
				break;
		}

		do_action( 'greyd_post_export_log', '=> post language info prepared' );
		return $language;
	}

	/**
	 * Convert navigation links into custom links:
	 * 
	 * * From:
	 * <!-- wp:navigation-link {"label":"Sticky Navbar","type":"page","id":548,"url":"{{site_url}}/sticky-navbar/","kind":"post-type"} /-->
	 * 
	 * * To:
	 * <!-- wp:navigation-link {"label":"Sticky Navbar","url":"{{site_url}}/sticky-navbar/","kind":"custom"} /-->
	 *
	 * @param string $subject
	 * @param  int    $post_id Post ID.
	 *
	 * @return string $subject
	 */
	public static function resolve_menus( $subject, $post_id, $post ) {
		do_action( 'greyd_post_export_log', "\r\n" . 'Resolve nested menus.' );

		// return if subject doesn't contain any menus
		if ( strpos( $subject, 'wp:navigation-link' ) === false ) {
			do_action( 'greyd_post_export_log', '=> no menus found' );
			return $subject;
		}
		
		// loop through all navigation links
		$subject = preg_replace_callback( '/<!-- wp:navigation-link (.*?) \/-->/', function( $matches ) use ( $post_id, $post ) {

			// get the navigation link attributes
			$attributes = json_decode( $matches[1], true );

			// if already a custom link, return the original string
			if ( ! isset( $attributes['kind'] ) || $attributes['kind'] === 'custom' ) {
				return $matches[0];
			}

			// change kind into custom
			$attributes['kind'] = 'custom';

			// remove type & id
			unset( $attributes['type'] );
			unset( $attributes['id'] );

			// return the new navigation link
			return '<!-- wp:navigation-link ' . json_encode( $attributes ) . ' /-->';

		}, $subject );

		do_action( 'greyd_post_export_log', '=> nested menus were resolved' );

		/**
		 * Add filter to modify the post content after resolving menus
		 */
		return apply_filters( 'greyd_post_export_resolve_menus', $subject, $post_id, $post );
	}

	/**
	 * Add export bulk action callbacks.
	 */
	public function add_bulk_action_callbacks() {
		// usual posttypes
		foreach ( Post_Export_Helper::get_supported_post_types() as $posttype ) {
			add_filter( 'bulk_actions-edit-' . $posttype, array( $this, 'add_export_bulk_action' ) );
			add_filter( 'handle_bulk_actions-edit-' . $posttype, array( $this, 'handle_export_bulk_action' ), 10, 3 );
		}
		// media library
		add_filter( 'bulk_actions-upload', array( $this, 'add_export_bulk_action' ) );
		add_filter( 'handle_bulk_actions-upload', array( $this, 'handle_export_bulk_action' ), 10, 3 );
	}

	/**
	 * Add export to the bulk action dropdown
	 */
	public function add_export_bulk_action( $bulk_actions ) {
		$bulk_actions['greyd_export'] = __( 'Export', 'greyd_hub' );

		if ( !empty( Post_Export_Helper::get_translation_tool() ) ) {
			$bulk_actions['greyd_export_multilanguage'] = __( 'Export incl. translations', 'greyd_hub' );
		}
		return $bulk_actions;
	}

	/**
	 * Handle export bulk action
	 *
	 * set via 'add_bulk_action_callbacks'
	 *
	 * @param string $sendback The redirect URL.
	 * @param string $doaction The action being taken.
	 * @param array  $items    Array of IDs of posts.
	 */
	public static function handle_export_bulk_action( $sendback, $doaction, $items ) {
		if ( count( $items ) === 0 ) {
			return $sendback;
		}
		if ( $doaction !== 'greyd_export' && $doaction !== 'greyd_export_multilanguage' ) {
			return $sendback;
		}

		$args = array(
			'append_nested'  => true,
			'translations'   => $doaction === 'greyd_export_multilanguage'
		);

		self::export_posts( $items, $args );

		$filename = self::write_filename( $items );
		$filepath = self::write_export_file( $filename );

		if ( $filepath ) {
			$href = Post_Export_Helper::convert_content_dir_to_path( $filepath );
			// $sendback = add_query_arg( 'download', $href, $sendback );
			$sendback = $href;
		} else {
			// set transient to display admin notice
			set_transient( 'greyd_transient_notice', 'error::' .  __( "The export file could not be written.", 'greyd_hub' ) );
		}

		return $sendback;
	}

	/**
	 * Write export as a .zip archive
	 *
	 * @param string $filename  Name of the final archive.
	 * @param array  $posts     Content of posts.json inside archive.
	 *                          Defaults to class var $posts.
	 * @param array  $media     Media files.
	 *                          Defaults to class var $media.
	 *
	 * @return mixed $path      Path to the archive. False on failure.
	 */
	public static function write_export_file( $filename, $posts = null, $media = null ) {

		do_action( 'greyd_post_export_log', "\r\n" . "Write export .zip archive '$filename'" );

		$posts_data = $posts ? $posts : self::$posts;
		$media_data = $media ? $media : self::$media;

		// set monthly folder
		$folder = date( 'y-m' );
		$path   = Post_Export_Helper::get_file_path( $folder );

		// write the temporary posts.json file
		$json_name = 'posts.json';
		$json_path = $path . $json_name;
		$json_file = fopen( $json_path, 'w' );

		if ( ! $json_file ) {
			return false;
		}

		fwrite( $json_file, json_encode( $posts_data, JSON_PRETTY_PRINT ) );
		fclose( $json_file );

		// create a zip archive
		$zip      = new \ZipArchive();
		$zip_name = str_replace( '.zip', '', $filename ) . '.zip';
		$zip_path = $path . $zip_name;

		// delete previous zip archive
		if ( file_exists( $zip_path ) ) {
			unlink( $zip_path );
		}

		// add files to the zip archive
		if ( $zip->open( $zip_path, \ZipArchive::CREATE ) ) {

			// copy the json to the archive
			$zip->addFile( $json_path, $json_name );

			// add media
			$zip->addEmptyDir( 'media' );
			if ( is_array( $media_data ) && count( $media_data ) > 0 ) {
				foreach ( $media_data as $post_id => $_media ) {
					if ( isset( $_media['path'] ) && isset( $_media['name'] ) ) {
						$zip->addFile( $_media['path'], 'media/' . $_media['name'] );
					}
				}
			}

			$zip->close();
		} else {
			return false;
		}

		// delete temporary json file
		unlink( $json_path );

		// return path to file
		return $zip_path;
	}


	/**
	 * =================================================================
	 *                          Helper functions
	 * =================================================================
	 */

	/**
	 * Get all posts from class var
	 */
	public static function get_all_posts() {
		return self::$posts;
	}

	/**
	 * Get all posts from class var
	 */
	public static function get_all_media() {
		return self::$media;
	}

	/**
	 * Create filename from export attributes
	 *
	 * @param WP_Post|array $posts
	 * @param array         $args
	 *
	 * @return string $filename
	 */
	public static function write_filename( $posts, $args = array() ) {

		// vars
		$filename     = array();
		$default_args = array(
			'whole_posttype' => false,
			// we don't need other arguments to create the filename
		);
		$args = array_merge( $default_args, (array) $args );

		// bulk export
		if ( is_array( $posts ) && isset( $posts[0] ) ) {
			$bulk = true;
			$post = $posts[0];
			$post = ! is_object( $post ) ? get_post( $post ) : $post;
		}
		// single export
		elseif ( is_object( $posts ) ) {
			$bulk = false;
			$post = $posts;
			// add post name to filename
			$filename[] = $post->post_name;
		}
		// unknown export
		if ( ! isset( $post ) || ! $post || ! isset( $post->post_type ) ) {
			return 'post-export';
		}

		$post_type     = $post->post_type;
		$default_types = array(
			'post'             => 'post',
			'page'             => 'page',
			'attachment'       => 'media_file',
			'tp_forms'         => 'form',
			'dynamic_template' => 'template',
			'tp_posttypes'     => 'posttype',
			'greyd_popup'      => 'popup',
		);

		// handle default posttypes
		if ( isset( $default_types[ $post_type ] ) ) {

			if ( $args['whole_posttype'] && $post_type == 'tp_posttypes' ) {
				$post_type = 'posts_and_posttype';
			} else {
				$post_type = $default_types[ $post_type ] . ( $bulk ? 's' : '' );
			}
		}
		// handle other posttypes
		elseif ( $bulk ) {
			$post_type_obj = get_post_type_object( $post_type );
			if ( $post_type_obj && isset( $post_type_obj->labels ) ) {
				$post_type = $post_type_obj->labels->name;
			}
		}

		// add post type to filename
		$filename[] = $post_type;

		// add site title to filename
		$filename[] = get_bloginfo( 'name' );

		// cleanup strings
		foreach ( $filename as $k => $string ) {
			$filename[ $k ] = preg_replace( '/[^a-z_]/', '', strtolower( preg_replace( '/-+/', '_', $string ) ) );
		}

		return implode( '-', $filename );
	}


	/**
	 * =================================================================
	 *                          Compatiblity functions
	 * =================================================================
	 * 
	 * @since 2.0
	 * 
	 * These functions are used by external plugins:
	 * * get_supported_post_types
	 * * get_translation_tool
	 * * get_languages_codes
	 * * enable_logs
	 * * import_posts
	 * * import_get_conflict_posts_for_backend_form
	 * 
	 * They are used by the old export class and are therefore
	 * still needed for backwards compatibility.
	 */

	public static $logs = false;

	public static function get_supported_post_types() {
		return Post_Export_Helper::get_supported_post_types();
	}

	public static function get_translation_tool() {
		return Post_Export_Helper::get_translation_tool();
	}

	public static function get_languages_codes() {
		return Post_Export_Helper::get_language_codes();
	}

	public static function enable_logs() {
		return Post_Export_Helper::enable_logs();
	}

	public static function import_posts( $posts, $conflict_actions = array(), $zip_file = '' ) {
		return Post_Import::import_posts( $posts, $conflict_actions, $zip_file );
	}
}
