init_components();
$this->init_hooks();
$this->load_textdomain();
}
/**
* Prevent cloning.
*
* @return void
*/
private function __clone() {}
/**
* Prevent unserialization.
*
* @throws \Exception Always throws to prevent unserialization.
* @return void
*/
public function __wakeup(): void {
throw new \Exception( 'Cannot unserialize singleton' );
}
/**
* Initialize plugin components.
*
* @return void
*/
private function init_components(): void {
// Initialize post types.
$this->post_types['artist'] = new Artist();
$this->post_types['album'] = new Album();
$this->post_types['track'] = new Track();
$this->post_types['playlist'] = new Playlist();
// Initialize taxonomies.
$this->taxonomies['genre'] = new Genre();
$this->taxonomies['mood'] = new Mood();
$this->taxonomies['license'] = new LicenseTaxonomy();
// Initialize admin components.
if ( is_admin() ) {
new ListColumns();
}
// Initialize frontend components (only if licensed).
if ( ! is_admin() && LicenseManager::is_license_valid() ) {
new TemplateLoader();
new Shortcodes();
} elseif ( ! is_admin() ) {
// Register shortcodes that show license message.
new Shortcodes( true ); // Unlicensed mode.
}
// Initialize widgets (always needed for admin widget management).
new Widgets();
// Initialize AJAX handlers.
new Ajax();
// Initialize ActivityPub integration (only if licensed and enabled).
if ( get_option( 'wp_fedistream_enable_activitypub', 1 ) && LicenseManager::is_license_valid() ) {
new ActivityPubIntegration();
new ActivityPubRestApi();
}
// Initialize WooCommerce integration.
if ( get_option( 'wp_fedistream_enable_woocommerce', 0 ) && $this->is_woocommerce_active() ) {
new WooCommerceIntegration();
new DigitalDelivery();
new StreamingAccess();
}
// Initialize Prometheus integration.
if ( get_option( 'wp_fedistream_enable_prometheus', 0 ) && $this->is_prometheus_active() ) {
new PrometheusIntegration();
}
// Initialize user library and notifications.
new UserLibrary();
new LibraryPage();
new Notifications();
// Initialize license manager.
LicenseManager::get_instance();
}
/**
* Initialize WordPress hooks.
*
* @return void
*/
private function init_hooks(): void {
add_action( 'init', array( $this, 'maybe_install_defaults' ), 20 );
add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) );
// Add settings link to plugins page.
add_filter( 'plugin_action_links_' . WP_FEDISTREAM_BASENAME, array( $this, 'add_plugin_action_links' ) );
}
/**
* Add action links to the plugins page.
*
* @param array $links Existing action links.
* @return array Modified action links.
*/
public function add_plugin_action_links( array $links ): array {
$settings_link = sprintf(
'%s',
esc_url( admin_url( 'admin.php?page=fedistream-settings' ) ),
esc_html__( 'Settings', 'wp-fedistream' )
);
$dashboard_link = sprintf(
'%s',
esc_url( admin_url( 'admin.php?page=fedistream' ) ),
esc_html__( 'Dashboard', 'wp-fedistream' )
);
// Add our links at the beginning.
array_unshift( $links, $dashboard_link, $settings_link );
return $links;
}
/**
* Maybe install default taxonomy terms.
*
* @return void
*/
public function maybe_install_defaults(): void {
Installer::install_defaults();
}
/**
* Load plugin textdomain.
*
* @return void
*/
private function load_textdomain(): void {
load_plugin_textdomain(
'wp-fedistream',
false,
dirname( WP_FEDISTREAM_BASENAME ) . '/languages'
);
}
/**
* Add admin menu.
*
* @return void
*/
public function add_admin_menu(): void {
// Main menu.
add_menu_page(
__( 'FediStream', 'wp-fedistream' ),
__( 'FediStream', 'wp-fedistream' ),
'edit_fedistream_tracks',
'fedistream',
array( $this, 'render_dashboard_page' ),
'dashicons-format-audio',
30
);
// Dashboard submenu.
add_submenu_page(
'fedistream',
__( 'Dashboard', 'wp-fedistream' ),
__( 'Dashboard', 'wp-fedistream' ),
'edit_fedistream_tracks',
'fedistream',
array( $this, 'render_dashboard_page' )
);
// Artists submenu.
add_submenu_page(
'fedistream',
__( 'Artists', 'wp-fedistream' ),
__( 'Artists', 'wp-fedistream' ),
'edit_fedistream_artists',
'edit.php?post_type=fedistream_artist'
);
// Albums submenu.
add_submenu_page(
'fedistream',
__( 'Albums', 'wp-fedistream' ),
__( 'Albums', 'wp-fedistream' ),
'edit_fedistream_albums',
'edit.php?post_type=fedistream_album'
);
// Tracks submenu.
add_submenu_page(
'fedistream',
__( 'Tracks', 'wp-fedistream' ),
__( 'Tracks', 'wp-fedistream' ),
'edit_fedistream_tracks',
'edit.php?post_type=fedistream_track'
);
// Playlists submenu.
add_submenu_page(
'fedistream',
__( 'Playlists', 'wp-fedistream' ),
__( 'Playlists', 'wp-fedistream' ),
'edit_fedistream_playlists',
'edit.php?post_type=fedistream_playlist'
);
// Genres submenu.
add_submenu_page(
'fedistream',
__( 'Genres', 'wp-fedistream' ),
__( 'Genres', 'wp-fedistream' ),
'manage_fedistream_genres',
'edit-tags.php?taxonomy=fedistream_genre'
);
// Settings submenu.
add_submenu_page(
'fedistream',
__( 'Settings', 'wp-fedistream' ),
__( 'Settings', 'wp-fedistream' ),
'manage_fedistream_settings',
'fedistream-settings',
array( $this, 'render_settings_page' )
);
}
/**
* Render dashboard page.
*
* @return void
*/
public function render_dashboard_page(): void {
// Get stats.
$artist_count = wp_count_posts( 'fedistream_artist' )->publish ?? 0;
$album_count = wp_count_posts( 'fedistream_album' )->publish ?? 0;
$track_count = wp_count_posts( 'fedistream_track' )->publish ?? 0;
$playlist_count = wp_count_posts( 'fedistream_playlist' )->publish ?? 0;
?>
handle_settings_save( $current_tab );
// Get current settings.
$enable_activitypub = get_option( 'wp_fedistream_enable_activitypub', 1 );
$enable_woocommerce = get_option( 'wp_fedistream_enable_woocommerce', 0 );
$enable_prometheus = get_option( 'wp_fedistream_enable_prometheus', 0 );
$max_upload_size = get_option( 'wp_fedistream_max_upload_size', 50 );
$default_license = get_option( 'wp_fedistream_default_license', 'all-rights-reserved' );
// License settings.
$license_key = LicenseManager::get_license_key();
$license_server_url = LicenseManager::get_server_url();
$license_status = LicenseManager::get_cached_status();
$license_data = LicenseManager::get_cached_data();
$last_check = LicenseManager::get_last_check();
$tabs = array(
'license' => __( 'License', 'wp-fedistream' ),
'settings' => __( 'Default Settings', 'wp-fedistream' ),
'integrations' => __( 'Integrations', 'wp-fedistream' ),
);
?>
render_license_tab( $license_key, $license_server_url, $license_status, $license_data, $last_check );
break;
case 'settings':
$this->render_settings_tab( $max_upload_size, $default_license );
break;
case 'integrations':
$this->render_integrations_tab( $enable_activitypub, $enable_woocommerce, $enable_prometheus );
break;
}
?>
isset( $_POST['license_key'] ) ? sanitize_text_field( wp_unslash( $_POST['license_key'] ) ) : '',
'server_url' => isset( $_POST['license_server_url'] ) ? esc_url_raw( wp_unslash( $_POST['license_server_url'] ) ) : '',
'server_secret' => isset( $_POST['license_server_secret'] ) ? sanitize_text_field( wp_unslash( $_POST['license_server_secret'] ) ) : '',
) );
echo '' . esc_html__( 'License settings saved.', 'wp-fedistream' ) . '
';
break;
case 'settings':
update_option( 'wp_fedistream_max_upload_size', absint( $_POST['max_upload_size'] ?? 50 ) );
update_option( 'wp_fedistream_default_license', sanitize_text_field( wp_unslash( $_POST['default_license'] ?? 'all-rights-reserved' ) ) );
echo '' . esc_html__( 'Settings saved.', 'wp-fedistream' ) . '
';
break;
case 'integrations':
update_option( 'wp_fedistream_enable_activitypub', isset( $_POST['enable_activitypub'] ) ? 1 : 0 );
update_option( 'wp_fedistream_enable_woocommerce', isset( $_POST['enable_woocommerce'] ) ? 1 : 0 );
update_option( 'wp_fedistream_enable_prometheus', isset( $_POST['enable_prometheus'] ) ? 1 : 0 );
echo '' . esc_html__( 'Integration settings saved.', 'wp-fedistream' ) . '
';
break;
}
}
/**
* Render the License tab.
*
* @param string $license_key License key.
* @param string $server_url Server URL.
* @param string $status License status.
* @param array $license_data License data.
* @param int $last_check Last check timestamp.
* @return void
*/
private function render_license_tab( string $license_key, string $server_url, string $status, array $license_data, int $last_check ): void {
$status_classes = array(
'valid' => 'notice-success',
'invalid' => 'notice-error',
'expired' => 'notice-warning',
'revoked' => 'notice-error',
'inactive' => 'notice-warning',
'unchecked' => 'notice-info',
'unconfigured' => 'notice-info',
);
$status_messages = array(
'valid' => __( 'License is active and valid.', 'wp-fedistream' ),
'invalid' => __( 'License is invalid.', 'wp-fedistream' ),
'expired' => __( 'License has expired.', 'wp-fedistream' ),
'revoked' => __( 'License has been revoked.', 'wp-fedistream' ),
'inactive' => __( 'License is inactive. Please activate it.', 'wp-fedistream' ),
'unchecked' => __( 'License has not been validated yet.', 'wp-fedistream' ),
'unconfigured' => __( 'License server is not configured.', 'wp-fedistream' ),
);
$status_icons = array(
'valid' => 'dashicons-yes-alt',
'invalid' => 'dashicons-dismiss',
'expired' => 'dashicons-warning',
'revoked' => 'dashicons-dismiss',
'inactive' => 'dashicons-marker',
'unchecked' => 'dashicons-info-outline',
'unconfigured' => 'dashicons-admin-generic',
);
$status_class = $status_classes[ $status ] ?? 'notice-info';
$status_message = $status_messages[ $status ] ?? __( 'Unknown status.', 'wp-fedistream' );
$status_icon = $status_icons[ $status ] ?? 'dashicons-info-outline';
?>
id, $fedistream_screens, true ) ) {
return;
}
wp_enqueue_style(
'wp-fedistream-admin',
WP_FEDISTREAM_URL . 'assets/css/admin.css',
array(),
WP_FEDISTREAM_VERSION
);
wp_enqueue_script(
'wp-fedistream-admin',
WP_FEDISTREAM_URL . 'assets/js/admin.js',
array( 'jquery', 'jquery-ui-sortable' ),
WP_FEDISTREAM_VERSION,
true
);
}
/**
* Enqueue frontend assets.
*
* @return void
*/
public function enqueue_frontend_assets(): void {
// Always enqueue as shortcodes/widgets can be used anywhere.
// Assets are lightweight and properly cached.
wp_enqueue_style(
'wp-fedistream',
WP_FEDISTREAM_URL . 'assets/css/frontend.css',
array(),
WP_FEDISTREAM_VERSION
);
wp_enqueue_script(
'wp-fedistream',
WP_FEDISTREAM_URL . 'assets/js/frontend.js',
array(),
WP_FEDISTREAM_VERSION,
true
);
wp_localize_script(
'wp-fedistream',
'wpFediStream',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'wp-fedistream-nonce' ),
)
);
}
/**
* Get the path to a template file.
*
* Checks theme for override first, then falls back to plugin template.
*
* @param string $template Template name (without extension).
* @return string Full path to template file.
*/
public function get_template_path( string $template ): string {
// Check theme for override.
$theme_template = locate_template( "fedistream/{$template}.php" );
if ( $theme_template ) {
return $theme_template;
}
// Use plugin template.
return WP_FEDISTREAM_PATH . "templates/{$template}.php";
}
/**
* Render a PHP template.
*
* @param string $template Template name (without extension).
* @param array $context Template context variables.
* @param bool $is_main_template Whether this is the main page template.
* @return string Rendered template.
*/
public function render( string $template, array $context = array(), bool $is_main_template = false ): string {
// If we're already rendering the main template, block any other renders.
if ( self::$rendering_main_template && ! $is_main_template ) {
return '';
}
// Prevent infinite recursion in rendering.
if ( self::$render_depth >= self::MAX_RENDER_DEPTH ) {
return '';
}
// Set main template lock if this is the main template.
$was_main = self::$rendering_main_template;
if ( $is_main_template ) {
self::$rendering_main_template = true;
}
++self::$render_depth;
try {
$template_path = $this->get_template_path( $template );
if ( ! file_exists( $template_path ) ) {
throw new \Exception( "Template not found: {$template}" );
}
// Extract context variables for use in template.
// phpcs:ignore WordPress.PHP.DontExtract.extract_extract
extract( $context, EXTR_SKIP );
// Start output buffering.
ob_start();
// Include the template.
include $template_path;
// Get the rendered content.
$result = ob_get_clean();
} finally {
--self::$render_depth;
if ( $is_main_template ) {
self::$rendering_main_template = $was_main;
}
}
return $result;
}
/**
* Render a partial template (helper for use within templates).
*
* @param string $partial Partial template name (without extension).
* @param array $context Template context variables.
* @return string Rendered partial.
*/
public function render_partial( string $partial, array $context = array() ): string {
return $this->render( $partial, $context, false );
}
/**
* Get a post type instance.
*
* @param string $name Post type name.
* @return object|null Post type instance or null.
*/
public function get_post_type( string $name ): ?object {
return $this->post_types[ $name ] ?? null;
}
/**
* Get a taxonomy instance.
*
* @param string $name Taxonomy name.
* @return object|null Taxonomy instance or null.
*/
public function get_taxonomy( string $name ): ?object {
return $this->taxonomies[ $name ] ?? null;
}
/**
* Check if WooCommerce is active.
*
* @return bool
*/
public function is_woocommerce_active(): bool {
return class_exists( 'WooCommerce' );
}
/**
* Check if ActivityPub plugin is active.
*
* @return bool
*/
public function is_activitypub_active(): bool {
return class_exists( 'Activitypub\Activitypub' );
}
/**
* Check if WP Prometheus plugin is active.
*
* @return bool
*/
public function is_prometheus_active(): bool {
return defined( 'WP_PROMETHEUS_VERSION' ) || class_exists( 'WP_Prometheus\Plugin' );
}
}