Files
wp-fedistream/includes/WooCommerce/Integration.php

747 lines
23 KiB
PHP
Raw Permalink Normal View History

<?php
/**
* WooCommerce Integration.
*
* @package WP_FediStream
*/
namespace WP_FediStream\WooCommerce;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Main WooCommerce integration class.
*/
class Integration {
/**
* Whether WooCommerce is active.
*
* @var bool
*/
private bool $woocommerce_active = false;
/**
* Constructor.
*/
public function __construct() {
// Check WooCommerce immediately since we're instantiated during plugins_loaded.
$this->check_woocommerce();
// If plugins_loaded hasn't fully completed, hook init at priority 20.
// Otherwise, run init directly.
if ( ! did_action( 'plugins_loaded' ) || doing_action( 'plugins_loaded' ) ) {
add_action( 'plugins_loaded', array( $this, 'init' ), 20 );
} else {
$this->init();
}
}
/**
* Check if WooCommerce is active.
*
* @return void
*/
public function check_woocommerce(): void {
$this->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',
)
);
?>
<div id="fedistream_product_data" class="panel woocommerce_options_panel">
<div class="options_group show_if_fedistream_album">
<p class="form-field">
<label for="_fedistream_linked_album"><?php esc_html_e( 'Linked Album', 'wp-fedistream' ); ?></label>
<select id="_fedistream_linked_album" name="_fedistream_linked_album" class="wc-enhanced-select" style="width: 50%;">
<option value=""><?php esc_html_e( 'Select an album...', 'wp-fedistream' ); ?></option>
<?php foreach ( $albums as $album ) : ?>
<option value="<?php echo esc_attr( $album->ID ); ?>" <?php selected( $linked_album, $album->ID ); ?>>
<?php echo esc_html( $album->post_title ); ?>
</option>
<?php endforeach; ?>
</select>
<?php echo wc_help_tip( __( 'Select the FediStream album this product represents.', 'wp-fedistream' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</p>
</div>
<div class="options_group show_if_fedistream_track">
<p class="form-field">
<label for="_fedistream_linked_track"><?php esc_html_e( 'Linked Track', 'wp-fedistream' ); ?></label>
<select id="_fedistream_linked_track" name="_fedistream_linked_track" class="wc-enhanced-select" style="width: 50%;">
<option value=""><?php esc_html_e( 'Select a track...', 'wp-fedistream' ); ?></option>
<?php foreach ( $tracks as $track ) : ?>
<option value="<?php echo esc_attr( $track->ID ); ?>" <?php selected( $linked_track, $track->ID ); ?>>
<?php echo esc_html( $track->post_title ); ?>
</option>
<?php endforeach; ?>
</select>
<?php echo wc_help_tip( __( 'Select the FediStream track this product represents.', 'wp-fedistream' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</p>
</div>
<div class="options_group">
<p class="form-field">
<label for="_fedistream_pricing_type"><?php esc_html_e( 'Pricing Type', 'wp-fedistream' ); ?></label>
<select id="_fedistream_pricing_type" name="_fedistream_pricing_type" class="wc-enhanced-select" style="width: 50%;">
<option value="fixed" <?php selected( $pricing_type, 'fixed' ); ?>><?php esc_html_e( 'Fixed Price', 'wp-fedistream' ); ?></option>
<option value="pwyw" <?php selected( $pricing_type, 'pwyw' ); ?>><?php esc_html_e( 'Pay What You Want', 'wp-fedistream' ); ?></option>
<option value="nyp" <?php selected( $pricing_type, 'nyp' ); ?>><?php esc_html_e( 'Name Your Price (Free+)', 'wp-fedistream' ); ?></option>
</select>
</p>
</div>
<div class="options_group fedistream-pwyw-options">
<?php
woocommerce_wp_text_input(
array(
'id' => '_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,
)
);
?>
</div>
<div class="options_group">
<?php
woocommerce_wp_checkbox(
array(
'id' => '_fedistream_include_streaming',
'label' => __( 'Include Streaming', 'wp-fedistream' ),
'description' => __( 'Purchase unlocks full-quality streaming access.', 'wp-fedistream' ),
'value' => $include_streaming,
)
);
?>
</div>
</div>
<div id="fedistream_formats_data" class="panel woocommerce_options_panel">
<div class="options_group">
<p class="form-field">
<label><?php esc_html_e( 'Available Formats', 'wp-fedistream' ); ?></label>
<span class="fedistream-format-checkboxes">
<?php
$formats = array(
'mp3' => 'MP3 (320kbps)',
'flac' => 'FLAC (Lossless)',
'wav' => 'WAV (Uncompressed)',
'aac' => 'AAC (256kbps)',
'ogg' => 'OGG Vorbis',
);
foreach ( $formats as $format => $label ) :
$checked = is_array( $available_formats ) && in_array( $format, $available_formats, true );
?>
<label style="display: block; margin-bottom: 5px;">
<input type="checkbox" name="_fedistream_available_formats[]" value="<?php echo esc_attr( $format ); ?>" <?php checked( $checked ); ?>>
<?php echo esc_html( $label ); ?>
</label>
<?php endforeach; ?>
</span>
</p>
<p class="description" style="margin-left: 150px;">
<?php esc_html_e( 'Select which audio formats customers can download after purchase.', 'wp-fedistream' ); ?>
</p>
</div>
</div>
<script type="text/javascript">
jQuery(function($) {
function togglePricingOptions() {
var type = $('#_fedistream_pricing_type').val();
if (type === 'pwyw' || type === 'nyp') {
$('.fedistream-pwyw-options').show();
} else {
$('.fedistream-pwyw-options').hide();
}
}
$('#_fedistream_pricing_type').on('change', togglePricingOptions);
togglePricingOptions();
// Show/hide tabs based on product type.
$('input#_virtual, input#_downloadable').on('change', function() {
var type = $('select#product-type').val();
if (type === 'fedistream_album' || type === 'fedistream_track') {
$('input#_virtual').prop('checked', true);
$('input#_downloadable').prop('checked', true);
}
});
$('select#product-type').on('change', function() {
var type = $(this).val();
if (type === 'fedistream_album' || type === 'fedistream_track') {
$('input#_virtual').prop('checked', true).trigger('change');
$('input#_downloadable').prop('checked', true).trigger('change');
}
}).trigger('change');
});
</script>
<?php
}
/**
* Save product meta.
*
* @param int $product_id Product ID.
* @return void
*/
public function save_product_meta( int $product_id ): void {
// Linked content.
if ( isset( $_POST['_fedistream_linked_album'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
update_post_meta( $product_id, '_fedistream_linked_album', absint( $_POST['_fedistream_linked_album'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
}
if ( isset( $_POST['_fedistream_linked_track'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
update_post_meta( $product_id, '_fedistream_linked_track', absint( $_POST['_fedistream_linked_track'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
}
// Pricing options.
if ( isset( $_POST['_fedistream_pricing_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
update_post_meta( $product_id, '_fedistream_pricing_type', sanitize_text_field( wp_unslash( $_POST['_fedistream_pricing_type'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
}
if ( isset( $_POST['_fedistream_min_price'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
update_post_meta( $product_id, '_fedistream_min_price', wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['_fedistream_min_price'] ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
}
if ( isset( $_POST['_fedistream_suggested_price'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
update_post_meta( $product_id, '_fedistream_suggested_price', wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['_fedistream_suggested_price'] ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
}
// Streaming access.
$include_streaming = isset( $_POST['_fedistream_include_streaming'] ) ? 'yes' : 'no'; // phpcs:ignore WordPress.Security.NonceVerification.Missing
update_post_meta( $product_id, '_fedistream_include_streaming', $include_streaming );
// Available formats.
if ( isset( $_POST['_fedistream_available_formats'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
$formats = array_map( 'sanitize_text_field', wp_unslash( $_POST['_fedistream_available_formats'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
update_post_meta( $product_id, '_fedistream_available_formats', $formats );
} else {
update_post_meta( $product_id, '_fedistream_available_formats', array() );
}
}
/**
* Display track preview on product page.
*
* @return void
*/
public function display_track_preview(): void {
global $product;
if ( ! $product ) {
return;
}
$product_type = $product->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 );
?>
<div class="fedistream-product-preview">
<h4><?php esc_html_e( 'Preview', 'wp-fedistream' ); ?></h4>
<div class="fedistream-mini-player" data-track-id="<?php echo esc_attr( $track_id ); ?>">
<button class="fedistream-preview-play" type="button" aria-label="<?php esc_attr_e( 'Play preview', 'wp-fedistream' ); ?>">
<span class="dashicons dashicons-controls-play"></span>
</button>
<div class="fedistream-preview-progress">
<div class="fedistream-preview-progress-bar"></div>
</div>
<?php if ( $duration ) : ?>
<span class="fedistream-preview-duration"><?php echo esc_html( gmdate( 'i:s', $duration ) ); ?></span>
<?php endif; ?>
</div>
</div>
<?php
}
/**
* Render album preview tracklist.
*
* @param int $album_id Album ID.
* @return void
*/
private function render_album_preview( int $album_id ): void {
$tracks = get_posts(
array(
'post_type' => '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;
}
?>
<div class="fedistream-product-tracklist">
<h4><?php esc_html_e( 'Tracklist', 'wp-fedistream' ); ?></h4>
<ol class="fedistream-album-tracks">
<?php foreach ( $tracks as $track ) : ?>
<?php
$duration = get_post_meta( $track->ID, '_fedistream_duration', true );
?>
<li class="fedistream-album-track" data-track-id="<?php echo esc_attr( $track->ID ); ?>">
<span class="fedistream-track-title"><?php echo esc_html( $track->post_title ); ?></span>
<?php if ( $duration ) : ?>
<span class="fedistream-track-duration"><?php echo esc_html( gmdate( 'i:s', $duration ) ); ?></span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ol>
</div>
<?php
}
/**
* Grant streaming/download access on purchase completion.
*
* @param int $order_id Order ID.
* @return void
*/
public function grant_access_on_purchase( int $order_id ): void {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
$customer_id = $order->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 '<a href="' . esc_url( get_edit_post_link( $album_id ) ) . '">' . esc_html( $album->post_title ) . '</a>';
}
} else {
echo '<span class="na">&ndash;</span>';
}
} 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 '<a href="' . esc_url( get_edit_post_link( $track_id ) ) . '">' . esc_html( $track->post_title ) . '</a>';
}
} else {
echo '<span class="na">&ndash;</span>';
}
} else {
echo '<span class="na">&ndash;</span>';
}
}
}