id on this site of the shared tabs imported, by site of origin (md5 of its address). */
const OPTION_MAP = 'prodtabs_import_map';
/** Transient: number of shared tabs left out of imported products, for the notice. */
const NOTICE = 'prodtabs_import_missing';
/**
* Shared tabs of the current WordPress import: former id => id on this site.
*
* @var int[]
*/
private static $map = array();
/**
* Products of the current WordPress import that received tabs, as keys.
*
* @var true[]
*/
private static $products = array();
/** Shared tabs left out during this request. */
private static $missing = 0;
/**
* Address of the site the current WordPress import comes from, noted at its start; null outside an import.
*
* @var string|null
*/
private static $base = null;
/** Hooks. */
public static function init(): void {
add_action( 'import_start', array( __CLASS__, 'import_start' ) );
add_action( 'wp_import_insert_post', array( __CLASS__, 'imported_post' ), 10, 2 );
add_filter( 'wp_import_existing_post', array( __CLASS__, 'existing_post' ), 10, 2 );
add_action( 'import_post_meta', array( __CLASS__, 'imported_meta' ), 10, 2 );
add_action( 'import_end', array( __CLASS__, 'import_end' ) );
add_action( 'shutdown', array( __CLASS__, 'remember_missing' ) );
if ( is_admin() ) {
add_action( 'admin_notices', array( __CLASS__, 'notice' ) );
}
}
/**
* Reference of a shared tab that holds on another site: its GUID and its slug.
*
* @param int $id Shared tab.
* @return string[] ref and slug; empty when the post is not a shared tab.
*/
public static function reference( int $id ): array {
$post = $id ? get_post( $id ) : null;
if ( ! $post || Library::POST_TYPE !== $post->post_type ) {
return array();
}
return array(
'ref' => self::guid( (string) $post->guid ),
'slug' => (string) $post->post_name,
);
}
/**
* GUID with its ampersands as such: WordPress stores it with & or & depending on how the post was created
* (by hand, by the importer, from an escaped export).
*
* @param string $guid GUID.
*/
public static function guid( string $guid ): string {
return str_replace( '&', '&', str_replace( '&', '&', $guid ) );
}
/**
* Shared tab of this site for a reference: the same GUID, or else the same slug.
*
* @param string $guid GUID on the site of origin.
* @param string $slug Slug on the site of origin.
* @return int 0 when none.
*/
public static function find( string $guid, string $slug ): int {
global $wpdb;
// A published tab first, then the oldest: the same file imported twice gives two posts with the same GUID. The
// GUID is compared with its ampersands as such on both sides (see guid()). Posts of every language.
if ( '' !== $guid ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- lookup during an import.
$id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND REPLACE(REPLACE(guid, '&', '&'), '&', '&') = %s AND post_status NOT IN ('trash', 'auto-draft') ORDER BY post_status = 'publish' DESC, ID ASC LIMIT 1", Library::POST_TYPE, self::guid( $guid ) ) );
if ( $id ) {
return $id;
}
}
if ( '' !== $slug ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- lookup during an import.
$id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_name = %s AND post_status NOT IN ('trash', 'auto-draft') ORDER BY post_status = 'publish' DESC, ID ASC LIMIT 1", Library::POST_TYPE, $slug ) );
if ( $id ) {
return $id;
}
}
return 0;
}
/**
* Stored structure of a product, with the reference of each shared tab it uses, for an export.
*
* @param array $layout Stored structure.
* @return array
*/
public static function export_layout( array $layout ): array {
foreach ( $layout as $i => $entry ) {
if ( ! is_array( $entry ) ) {
continue;
}
if ( 'library' === ( $entry['type'] ?? '' ) ) {
$ref = self::reference( absint( $entry['library_id'] ?? 0 ) );
if ( $ref ) {
$layout[ $i ]['ref'] = $ref['ref'];
$layout[ $i ]['ref_slug'] = $ref['slug'];
}
} elseif ( 'local' === ( $entry['type'] ?? '' ) && ! empty( $entry['origin'] ) ) {
$ref = self::reference( absint( $entry['origin'] ) );
if ( $ref ) {
$layout[ $i ]['origin_ref'] = $ref['ref'];
$layout[ $i ]['origin_slug'] = $ref['slug'];
}
}
}
return $layout;
}
/**
* Imported structure pointed to the shared tabs of this site, by their reference. Entries without a reference (a
* file made before ProdTabs 0.4) keep their ids.
*
* @param array $layout Imported structure.
* @return array
*/
public static function import_layout( array $layout ): array {
$out = array();
foreach ( $layout as $entry ) {
if ( ! is_array( $entry ) ) {
continue;
}
$type = $entry['type'] ?? '';
if ( 'library' === $type && ( isset( $entry['ref'] ) || isset( $entry['ref_slug'] ) ) ) {
$id = self::find( (string) ( $entry['ref'] ?? '' ), (string) ( $entry['ref_slug'] ?? '' ) );
if ( ! $id ) {
++self::$missing;
continue;
}
$entry['library_id'] = $id;
$entry['key'] = 'lib_' . $id;
} elseif ( 'local' === $type && ( isset( $entry['origin_ref'] ) || isset( $entry['origin_slug'] ) ) ) {
// Customized from a shared tab: the text is the product's own, only the link to the original is lost.
$entry['origin'] = self::find( (string) ( $entry['origin_ref'] ?? '' ), (string) ( $entry['origin_slug'] ?? '' ) );
}
unset( $entry['ref'], $entry['ref_slug'], $entry['origin_ref'], $entry['origin_slug'] );
$out[] = $entry;
}
return $out;
}
/** WordPress Importer: the file is read. Its site is noted now: Tools > Import deletes the upload before the end. */
public static function import_start(): void {
self::$base = self::import_base();
}
/**
* WordPress Importer: a shared tab was created.
*
* @param int $post_id New id.
* @param int $original_id Id on the site of origin.
*/
public static function imported_post( $post_id, $original_id ): void {
if ( Library::POST_TYPE === get_post_type( (int) $post_id ) ) {
self::$map[ (int) $original_id ] = (int) $post_id;
}
}
/**
* WordPress Importer: a shared tab of the file already exists here (the file imported again).
*
* @param int $exists Id of the existing post, 0 when none.
* @param array $post Post of the file.
* @return int
*/
public static function existing_post( $exists, $post ) {
if ( $exists && is_array( $post ) && Library::POST_TYPE === ( $post['post_type'] ?? '' ) && ! empty( $post['post_id'] ) ) {
self::$map[ (int) $post['post_id'] ] = (int) $exists;
}
return $exists;
}
/**
* WordPress Importer: a meta was written; the products whose tabs came with the file are linked at the end.
*
* @param int $post_id Post id.
* @param string $key Meta key.
*/
public static function imported_meta( $post_id, $key ): void {
if ( Tabs::META_LAYOUT === $key ) {
self::$products[ (int) $post_id ] = true;
}
}
/** WordPress Importer: end of the import. Remembers the shared tabs, then links the products. */
public static function import_end(): void {
$base = null !== self::$base ? self::$base : self::import_base();
self::$base = null;
$maps = get_option( self::OPTION_MAP, array() );
$maps = is_array( $maps ) ? $maps : array();
$site = md5( $base );
$map = array_replace( isset( $maps[ $site ] ) && is_array( $maps[ $site ] ) ? $maps[ $site ] : array(), self::$map );
// An unknown origin is never this site: its ids would point to other tabs of this site.
$same = '' !== $base && in_array( untrailingslashit( $base ), array( untrailingslashit( home_url() ), untrailingslashit( site_url() ) ), true );
$products = array_keys( self::$products );
if ( self::$map && '' !== $base ) {
$maps[ $site ] = $map;
update_option( self::OPTION_MAP, $maps, false );
}
self::$map = array();
self::$products = array();
foreach ( $products as $product_id ) {
$layout = get_post_meta( $product_id, Tabs::META_LAYOUT, true );
if ( ! is_array( $layout ) ) {
continue;
}
$linked = self::link_ids( $layout, $map, $same );
if ( $linked !== $layout ) {
update_post_meta( $product_id, Tabs::META_LAYOUT, wp_slash( $linked ) );
clean_post_cache( $product_id );
}
}
}
/**
* A structure imported by the WordPress Importer pointed to the shared tabs of this site, by their former id.
*
* @param array $layout Stored structure.
* @param int[] $map Former id => id on this site.
* @param bool $same Whether the file comes from this very site: ids not in the map are then kept.
* @return array
*/
public static function link_ids( array $layout, array $map, bool $same ): array {
$out = array();
foreach ( $layout as $entry ) {
if ( ! is_array( $entry ) ) {
continue;
}
$type = $entry['type'] ?? '';
if ( 'library' === $type ) {
$old = absint( $entry['library_id'] ?? 0 );
$new = $map[ $old ] ?? ( $same ? $old : 0 );
if ( ! $new ) {
++self::$missing;
continue;
}
$entry['library_id'] = $new;
$entry['key'] = 'lib_' . $new;
} elseif ( 'local' === $type && ! empty( $entry['origin'] ) ) {
$old = absint( $entry['origin'] );
$entry['origin'] = $map[ $old ] ?? ( $same ? $old : 0 );
}
$out[] = $entry;
}
return $out;
}
/**
* Address of the site the WordPress Importer reads the file of, empty when unknown: the site itself (wp:base_blog_url
* of the file), which on a multisite differs from the address of its network (wp:base_site_url, the only one the
* importer keeps, used when the file cannot be read).
*/
private static function import_base(): string {
$importer = self::importer();
if ( ! $importer ) {
return '';
}
$file = ! empty( $importer->id ) ? (string) get_attached_file( (int) $importer->id ) : '';
if ( '' !== $file && is_readable( $file ) ) {
// The header of the file comes before its first item.
$head = (string) file_get_contents( $file, false, null, 0, 65536 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local file of the import.
if ( preg_match( '#
' . esc_html( $text ) . '