woocommerce_active = class_exists( 'WooCommerce' );
}
/**
* Initialize WooCommerce integration.
*
* @return void
*/
public function init(): void {
if ( ! $this->woocommerce_active ) {
return;
}
// Register custom product types.
add_filter( 'product_type_selector', array( $this, 'add_product_types' ) );
add_filter( 'woocommerce_product_class', array( $this, 'product_class' ), 10, 2 );
// Initialize product type classes.
add_action( 'init', array( $this, 'register_product_types' ), 5 );
// Add product data tabs.
add_filter( 'woocommerce_product_data_tabs', array( $this, 'add_product_data_tabs' ) );
add_action( 'woocommerce_product_data_panels', array( $this, 'add_product_data_panels' ) );
// Save product meta.
add_action( 'woocommerce_process_product_meta', array( $this, 'save_product_meta' ) );
// Frontend hooks.
add_action( 'woocommerce_single_product_summary', array( $this, 'display_track_preview' ), 25 );
// Purchase access hooks.
add_action( 'woocommerce_order_status_completed', array( $this, 'grant_access_on_purchase' ) );
// Download hooks.
add_filter( 'woocommerce_downloadable_file_allowed_mime_types', array( $this, 'allowed_audio_mimes' ) );
// Admin columns.
add_filter( 'manage_edit-product_columns', array( $this, 'add_product_columns' ) );
add_action( 'manage_product_posts_custom_column', array( $this, 'render_product_columns' ), 10, 2 );
}
/**
* Check if WooCommerce is active.
*
* @return bool
*/
public function is_active(): bool {
return $this->woocommerce_active;
}
/**
* Register custom product types.
*
* @return void
*/
public function register_product_types(): void {
// Product types are registered via class loading.
}
/**
* Add custom product types to the selector.
*
* @param array $types Product types.
* @return array Modified product types.
*/
public function add_product_types( array $types ): array {
$types['fedistream_album'] = __( 'FediStream Album', 'wp-fedistream' );
$types['fedistream_track'] = __( 'FediStream Track', 'wp-fedistream' );
return $types;
}
/**
* Get product class for custom types.
*
* @param string $classname Product class name.
* @param string $product_type Product type.
* @return string Modified class name.
*/
public function product_class( string $classname, string $product_type ): string {
if ( 'fedistream_album' === $product_type ) {
return AlbumProduct::class;
}
if ( 'fedistream_track' === $product_type ) {
return TrackProduct::class;
}
return $classname;
}
/**
* Add product data tabs.
*
* @param array $tabs Product data tabs.
* @return array Modified tabs.
*/
public function add_product_data_tabs( array $tabs ): array {
$tabs['fedistream'] = array(
'label' => __( 'FediStream', 'wp-fedistream' ),
'target' => 'fedistream_product_data',
'class' => array( 'show_if_fedistream_album', 'show_if_fedistream_track' ),
'priority' => 21,
);
$tabs['fedistream_formats'] = array(
'label' => __( 'Audio Formats', 'wp-fedistream' ),
'target' => 'fedistream_formats_data',
'class' => array( 'show_if_fedistream_album', 'show_if_fedistream_track' ),
'priority' => 22,
);
return $tabs;
}
/**
* Add product data panels.
*
* @return void
*/
public function add_product_data_panels(): void {
global $post;
$product_id = $post->ID;
// Get linked content.
$linked_album = get_post_meta( $product_id, '_fedistream_linked_album', true );
$linked_track = get_post_meta( $product_id, '_fedistream_linked_track', true );
// Get pricing options.
$pricing_type = get_post_meta( $product_id, '_fedistream_pricing_type', true ) ?: 'fixed';
$min_price = get_post_meta( $product_id, '_fedistream_min_price', true );
$suggested_price = get_post_meta( $product_id, '_fedistream_suggested_price', true );
// Get format options.
$available_formats = get_post_meta( $product_id, '_fedistream_available_formats', true ) ?: array( 'mp3' );
$include_streaming = get_post_meta( $product_id, '_fedistream_include_streaming', true );
// Get albums for dropdown.
$albums = get_posts(
array(
'post_type' => 'fedistream_album',
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => 'title',
'order' => 'ASC',
)
);
// Get tracks for dropdown.
$tracks = get_posts(
array(
'post_type' => 'fedistream_track',
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => 'title',
'order' => 'ASC',
)
);
?>
'_fedistream_min_price',
'label' => __( 'Minimum Price', 'wp-fedistream' ) . ' (' . get_woocommerce_currency_symbol() . ')',
'desc_tip' => true,
'description' => __( 'Minimum price for Pay What You Want. Leave empty for no minimum.', 'wp-fedistream' ),
'type' => 'text',
'data_type' => 'price',
'value' => $min_price,
)
);
woocommerce_wp_text_input(
array(
'id' => '_fedistream_suggested_price',
'label' => __( 'Suggested Price', 'wp-fedistream' ) . ' (' . get_woocommerce_currency_symbol() . ')',
'desc_tip' => true,
'description' => __( 'Suggested price shown to customers.', 'wp-fedistream' ),
'type' => 'text',
'data_type' => 'price',
'value' => $suggested_price,
)
);
?>
'_fedistream_include_streaming',
'label' => __( 'Include Streaming', 'wp-fedistream' ),
'description' => __( 'Purchase unlocks full-quality streaming access.', 'wp-fedistream' ),
'value' => $include_streaming,
)
);
?>
get_type();
if ( 'fedistream_track' === $product_type ) {
$track_id = get_post_meta( $product->get_id(), '_fedistream_linked_track', true );
if ( $track_id ) {
$this->render_track_preview( $track_id );
}
} elseif ( 'fedistream_album' === $product_type ) {
$album_id = get_post_meta( $product->get_id(), '_fedistream_linked_album', true );
if ( $album_id ) {
$this->render_album_preview( $album_id );
}
}
}
/**
* Render track preview player.
*
* @param int $track_id Track ID.
* @return void
*/
private function render_track_preview( int $track_id ): void {
$audio_id = get_post_meta( $track_id, '_fedistream_audio_file', true );
$audio_url = $audio_id ? wp_get_attachment_url( $audio_id ) : '';
if ( ! $audio_url ) {
return;
}
$duration = get_post_meta( $track_id, '_fedistream_duration', true );
?>
'fedistream_track',
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_key' => '_fedistream_track_number', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'orderby' => 'meta_value_num',
'order' => 'ASC',
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
array(
'key' => '_fedistream_album_id',
'value' => $album_id,
),
),
)
);
if ( empty( $tracks ) ) {
return;
}
?>
ID, '_fedistream_duration', true );
?>
-
post_title ); ?>
get_customer_id();
if ( ! $customer_id ) {
return;
}
foreach ( $order->get_items() as $item ) {
$product_id = $item->get_product_id();
$product_type = \WC_Product_Factory::get_product_type( $product_id );
if ( 'fedistream_album' === $product_type ) {
$album_id = get_post_meta( $product_id, '_fedistream_linked_album', true );
if ( $album_id ) {
$this->grant_album_access( $customer_id, $album_id, $order_id );
}
} elseif ( 'fedistream_track' === $product_type ) {
$track_id = get_post_meta( $product_id, '_fedistream_linked_track', true );
if ( $track_id ) {
$this->grant_track_access( $customer_id, $track_id, $order_id );
}
}
}
}
/**
* Grant album access to a customer.
*
* @param int $customer_id Customer ID.
* @param int $album_id Album ID.
* @param int $order_id Order ID.
* @return void
*/
private function grant_album_access( int $customer_id, int $album_id, int $order_id ): void {
global $wpdb;
$table = $wpdb->prefix . 'fedistream_purchases';
// Check if access already exists.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$exists = $wpdb->get_var(
$wpdb->prepare(
"SELECT id FROM {$table} WHERE user_id = %d AND content_type = 'album' AND content_id = %d",
$customer_id,
$album_id
)
);
if ( $exists ) {
return;
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->insert(
$table,
array(
'user_id' => $customer_id,
'content_type' => 'album',
'content_id' => $album_id,
'order_id' => $order_id,
'purchased_at' => current_time( 'mysql' ),
),
array( '%d', '%s', '%d', '%d', '%s' )
);
// Also grant access to all tracks in the album.
$tracks = get_posts(
array(
'post_type' => 'fedistream_track',
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
array(
'key' => '_fedistream_album_id',
'value' => $album_id,
),
),
)
);
foreach ( $tracks as $track ) {
$this->grant_track_access( $customer_id, $track->ID, $order_id );
}
}
/**
* Grant track access to a customer.
*
* @param int $customer_id Customer ID.
* @param int $track_id Track ID.
* @param int $order_id Order ID.
* @return void
*/
private function grant_track_access( int $customer_id, int $track_id, int $order_id ): void {
global $wpdb;
$table = $wpdb->prefix . 'fedistream_purchases';
// Check if access already exists.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$exists = $wpdb->get_var(
$wpdb->prepare(
"SELECT id FROM {$table} WHERE user_id = %d AND content_type = 'track' AND content_id = %d",
$customer_id,
$track_id
)
);
if ( $exists ) {
return;
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->insert(
$table,
array(
'user_id' => $customer_id,
'content_type' => 'track',
'content_id' => $track_id,
'order_id' => $order_id,
'purchased_at' => current_time( 'mysql' ),
),
array( '%d', '%s', '%d', '%d', '%s' )
);
}
/**
* Check if user has purchased content.
*
* @param int $user_id User ID.
* @param string $content_type Content type (album or track).
* @param int $content_id Content ID.
* @return bool
*/
public static function user_has_purchased( int $user_id, string $content_type, int $content_id ): bool {
global $wpdb;
$table = $wpdb->prefix . 'fedistream_purchases';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$exists = $wpdb->get_var(
$wpdb->prepare(
"SELECT id FROM {$table} WHERE user_id = %d AND content_type = %s AND content_id = %d",
$user_id,
$content_type,
$content_id
)
);
return (bool) $exists;
}
/**
* Add allowed audio MIME types.
*
* @param array $types Allowed MIME types.
* @return array Modified MIME types.
*/
public function allowed_audio_mimes( array $types ): array {
$types['flac'] = 'audio/flac';
$types['wav'] = 'audio/wav';
$types['ogg'] = 'audio/ogg';
$types['aac'] = 'audio/aac';
return $types;
}
/**
* Add product columns.
*
* @param array $columns Columns.
* @return array Modified columns.
*/
public function add_product_columns( array $columns ): array {
$new_columns = array();
foreach ( $columns as $key => $value ) {
$new_columns[ $key ] = $value;
if ( 'product_type' === $key ) {
$new_columns['fedistream_linked'] = __( 'FediStream', 'wp-fedistream' );
}
}
return $new_columns;
}
/**
* Render product columns.
*
* @param string $column Column name.
* @param int $post_id Post ID.
* @return void
*/
public function render_product_columns( string $column, int $post_id ): void {
if ( 'fedistream_linked' !== $column ) {
return;
}
$product_type = \WC_Product_Factory::get_product_type( $post_id );
if ( 'fedistream_album' === $product_type ) {
$album_id = get_post_meta( $post_id, '_fedistream_linked_album', true );
if ( $album_id ) {
$album = get_post( $album_id );
if ( $album ) {
echo '' . esc_html( $album->post_title ) . '';
}
} else {
echo '–';
}
} elseif ( 'fedistream_track' === $product_type ) {
$track_id = get_post_meta( $post_id, '_fedistream_linked_track', true );
if ( $track_id ) {
$track = get_post( $track_id );
if ( $track ) {
echo '' . esc_html( $track->post_title ) . '';
}
} else {
echo '–';
}
} else {
echo '–';
}
}
}