PluginProbe
WpStream – Live Streaming, Video on Demand, Pay Per View / trunk
WpStream – Live Streaming, Video on Demand, Pay Per View vtrunk
4.14.1 4.14.0 4.13.2 4.13.1 4.13 4.12.5 4.12.4 4.12.3 4.12.2 4.12.1 4.12 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5 4.5.1 4.5.11 4.5.11.1 4.5.11.2 4.5.11.4 4.5.11.5 4.5.11.6 All 181 releases
wpstream / admin / class-wpstream-admin.php

class-wpstream-admin.php in WpStream – Live Streaming, Video on Demand, Pay Per View trunk, at admin/class-wpstream-admin.php

3,556 lines 171.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * The admin-specific functionality of the plugin.
5 *
6 * @link http://wpstream.net
7 * @since 3.0.1
8 *
9 * @package Wpstream
10 * @subpackage Wpstream/admin
11 */
12
13
14 // Exit if accessed directly.
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 /**
20 * The admin-specific functionality of the plugin.
21 *
22 * This is the admin "god class": it wires up the whole wp-admin surface of the
23 * plugin. Responsibilities include registering admin CSS/JS, building the
24 * top-level WpStream menu and its sub-pages (Credentials, Channels, Recordings,
25 * Settings, Quick Start), rendering channel/settings/onboarding screens and
26 * their modal dialogs, defining the per-event global streaming options, hooking
27 * into WooCommerce to register the custom stream/VOD product types and their
28 * pricing/metabox behaviour, rendering post metaboxes, driving the multipart S3
29 * upload AJAX endpoints, and emitting the various admin notices (plugin update,
30 * cache flush, theme, etc.). Many methods echo HTML directly.
31 *
32 * @package Wpstream
33 * @subpackage Wpstream/admin
34 * @author wpstream <office@wpstream.net>
35 */
36 class Wpstream_Admin {
37
38 /**
39 * Most parts a multipart VOD upload may be split into.
40 *
41 * The storage backend assembles at most this many parts, so a larger
42 * split uploads every byte and still can never complete.
43 *
44 * @var int
45 */
46 const MULTIPART_MAX_PARTS = 1000;
47
48 /**
49 * Store plugin main class to allow public access.
50 *
51 * @since 20180622
52 * @var object The main class.
53 */
54 public $main;
55
56
57 /**
58 * The ID of this plugin.
59 *
60 * @since 3.0.1
61 * @access private
62 * @var string $plugin_name The ID of this plugin.
63 */
64 private $plugin_name;
65
66 /**
67 * The version of this plugin.
68 *
69 * @since 3.0.1
70 * @access private
71 * @var string $version The current version of this plugin.
72 */
73 private $version;
74
75 /**
76 * @var array Definitions of the per-event "global" streaming options (record,
77 * view count, autoplay, encryption, etc.). Each entry holds a
78 * translated label, a help "details" string, and a default value.
79 * Populated lazily on `init` by load_global_event_options().
80 */
81 public $global_event_options ;
82
83
84
85
86
87 /**
88 * Initialize the class and set its properties.
89 *
90 * @since 3.0.1
91 * @param string $plugin_name The name (ID) of this plugin.
92 * @param string $version The version of this plugin.
93 * @param object $plugin_main The main plugin class instance, kept for public access.
94 * @return void
95 */
96 public function __construct( $plugin_name, $version,$plugin_main ) {
97
98 // Remember the plugin identity and the main class for later use.
99 $this->plugin_name = $plugin_name;
100 $this->version = $version;
101 $this->main = $plugin_main;
102
103 // Build the translatable global event options once WP (and its i18n) is ready.
104 add_action('init', array($this, 'load_global_event_options'));
105 }
106
107 /**
108 * Populate $global_event_options with the translatable per-event settings.
109 *
110 * Deferred to the `init` hook so the esc_html__() translations resolve
111 * against a loaded text domain.
112 *
113 * @return void
114 */
115 public function load_global_event_options() {
116 // Each key maps to a label/details/default triple used when rendering the event option toggles.
117 $this->global_event_options = array(
118 'record' => array(
119 'name' => esc_html__('Record Live Stream','wpstream'),
120 'details' => esc_html__('If enabled, live streams will be recorded and saved to your library.','wpstream'),
121 'defaults' => 'no',
122 ),
123 'view_count' => array(
124 'name' => esc_html__('Display Viewer Count','wpstream'),
125 'details' => esc_html__('If enabled, the live viewer count will show up in the player.','wpstream'),
126 'defaults' => 'yes',
127 ),
128 'domain_lock' => array(
129 'name' => esc_html__('Lock To Website','wpstream'),
130 'details' => sprintf ( esc_html__('If enabled, live video will only display on %1$s, otherwise it can show up on any website.','wpstream'),get_bloginfo('wpurl') ),
131 'defaults' => 'no',
132 ),
133 'autoplay' => array(
134 'name' => esc_html__('Autoplay','wpstream'),
135 'details' => esc_html__('If enabled, live video will attempt to start playing automatically. This is only achievable in some browsers.','wpstream'),
136 'defaults' => 'yes',
137 ),
138 'mute' => array(
139 'name' => esc_html__('Start Muted','wpstream'),
140 'details' => esc_html__('If enabled, live video will start muted. This may increase the rate of autoplay in some browsers. ','wpstream'),
141 'defaults' => 'no',
142 ),
143 'low_latency' => array(
144 'name' => esc_html__('Low Latency (beta)','wpstream'),
145 'details' => esc_html__('Shortens the live delay between streamer and viewers. Useful for interactive applications like gaming, auctions, trading etc. Low latency may worsen the viewer experience on some devices.','wpstream'),
146 'defaults' => 'no',
147 ),
148 'adaptive_bitrate' => array(
149 'name' => esc_html__('Adaptive Bitrate (beta)','wpstream'),
150 'details' => esc_html__('Ensures a smooth and uninterrupted viewing experience by adjusting video quality for viewers with reduced network speed or device capabilities.','wpstream'),
151 'defaults' => 'no',
152 ),
153 'encrypt' =>array(
154 'name' => esc_html__('Encrypt Live Stream','wpstream'),
155 'details' => esc_html__('If enabled, video data will be encrypted. Enabling encryption may lead to reduced website performance under certain configurations. Encrypted video may not display in all browsers.','wpstream'),
156 'defaults' => 'no',
157 ),
158 'ses_encrypt'=>array(
159 'name' => esc_html__('Use Sessions with Encryption','wpstream'),
160 'details' => esc_html__('If enabled, encryption key distribution will be checked against valid user sessions. Setting may malfunction or lead to reduced website performance under certain configurations. ','wpstream'),
161 'defaults' => 'no',
162 ),
163 );
164
165 /**
166 * Filter the catalog of per-channel options shown as toggles.
167 *
168 * Each entry is `key => array( name, details, defaults )`. An added key
169 * renders a toggle on the settings and channel screens; it is stored
170 * only if the Channel Settings Module knows the key.
171 *
172 * @since 4.14.0
173 *
174 * @param array $options Option catalog keyed by setting.
175 */
176 $this->global_event_options = (array) apply_filters( 'wpstream_default_event_options', $this->global_event_options );
177 }
178
179 /**
180 * Register and enqueue the plugin's admin-area stylesheets.
181 *
182 * Hooked (via Wpstream_Loader) to admin_enqueue_scripts.
183 *
184 * @since 3.0.1
185 * @return void
186 */
187 public function enqueue_styles() {
188
189 /**
190 * This function is provided for demonstration purposes only.
191 *
192 * An instance of this class should be passed to the run() function
193 * defined in Wpstream_Loader as all of the hooks are defined
194 * in that particular class.
195 *
196 * The Wpstream_Loader will then create the relationship
197 * between the defined hooks and the functions defined in this
198 * class.
199 */
200 // No external webfont: the admin UI falls back to the system font stack
201 // declared in the stylesheets, so no request leaves the site for fonts.
202 // Main admin stylesheet; filemtime() is used as the cache-busting version.
203 wp_enqueue_style(
204 $this->plugin_name,
205 plugin_dir_url( __FILE__ ) . 'css/wpstream-admin.css',
206 array(),
207 filemtime(plugin_dir_path(__FILE__) . 'css/wpstream-admin.css' ),
208 'all'
209 );
210
211 // Ensure the WP media library scripts/styles are available (for logo/image pickers) if not already loaded.
212 if (!did_action('wp_enqueue_media')) {
213 wp_enqueue_media();
214 }
215 }
216
217 /**
218 * Register, enqueue and localize the plugin's admin-area JavaScript.
219 *
220 * Enqueues jQuery UI helpers, the fileupload lib, the admin control/upload
221 * scripts, the start-streaming and settings scripts, and the onboarding
222 * bundles. Each script is fed a localized vars array (translated strings,
223 * URLs, nonces, feature flags). Some bundles are only loaded on specific
224 * admin screens. Hooked (via Wpstream_Loader) to admin_enqueue_scripts.
225 *
226 * @since 3.0.1
227 * @return void
228 */
229 public function enqueue_scripts() {
230
231 // jQuery UI widgets used by the settings/date pickers and sliders.
232 wp_enqueue_script("jquery-ui-slider");
233 wp_enqueue_script("jquery-ui-datepicker");
234 // Blueimp-style file upload library backing the recordings/VOD uploader.
235 wp_enqueue_script('jquery.fileupload', plugin_dir_url( __FILE__ ) .'js/jquery.fileupload.js?v='.time(),array(), WPSTREAM_PLUGIN_VERSION, true);
236
237 // Shared admin utility helpers, then the main admin control script that drives uploads and channel actions.
238 wp_enqueue_script( 'wpstream-admin-utils', plugin_dir_url( __FILE__ ) .'js/utils/admin_utils.js?v='.time(),array(), WPSTREAM_PLUGIN_VERSION, true);
239 wp_enqueue_script('wpstream-admin-control', plugin_dir_url( __FILE__ ) .'js/admin_control.js?v='.time(),array(), WPSTREAM_PLUGIN_VERSION, true);
240 // Hand the control script all of its translated status strings, the admin URL, and the multipart upload nonce.
241 wp_localize_script('wpstream-admin-control', 'wpstream_admin_control_vars',
242 array(
243 'admin_url' => get_admin_url(),
244 'multipart_upload_nonce' => wp_create_nonce( 'wpstream_multipart_upload_nonce' ),
245 'recordings_nonce' => wp_create_nonce( 'wpstream_recordings_nonce' ),
246 'loading_url' => WPSTREAM_PLUGIN_DIR_URL.'/img/loading.gif',
247 'download_mess' => esc_html__('Click to download!','wpstream'),
248 'uploading' => esc_html__('We are uploading your file. Do not close this window!','wpstream'),
249 'upload_complete2' => esc_html__('Upload Complete! You can upload another file!','wpstream'),
250 'not_accepted' => esc_html__('The file is not an accepted video format','wpstream'),
251 'upload_complete' => esc_html__('Upload Complete!','wpstream'),
252 'no_band' => esc_html__('Not enough streaming data.','wpstream'),
253 'no_band_no_store' => esc_html__('Not enough streaming data or storage.','wpstream'),
254 'no_streaming_hours' => esc_html__('Not enough streaming hours.','wpstream'),
255 'exceeding_limit' => esc_html__('File size exceeds 5GB. Initiating multipart upload...','wpstream'),
256 'upload_failed' => esc_html__('Upload Failed!','wpstream'),
257 'upload_failed2' => esc_html__('Upload Failed! Please Try again!','wpstream'),
258 'choose_a_file' => esc_html__('Choose a file&hellip;','wpstream'),
259 'preparing_multipart' => esc_html__('Preparing multipart upload...','wpstream'),
260 'uploading_part' => esc_html__('Uploading part {part} of {total}...','wpstream'),
261 'upload_failed_part' => esc_html__('Failed to upload part {part}. Please try again.','wpstream'),
262 'completing_upload' => esc_html__('Completing upload. Please wait...','wpstream'),
263 'upload_failed_part_retry' => esc_html__('Failed to upload part {part}. Retrying...','wpstream'),
264 'choose_recording' => esc_html__( 'Choose Recording', 'wpstream' ),
265 'select_recording' => esc_html__( 'Please select a recording from the list', 'wpstream' ),
266 'invalid_response' => esc_html__('Invalid response from server. Missing required upload data.', 'wpstream'),
267 'video_processing' => esc_html__( 'The video is still processing', 'wpstream' ),
268 'file_name_text' => esc_html__('File Name:','wpstream'),
269 'channel_create_error' => esc_html__('Something did not work. Please try again.', 'wpstream'),
270 'select_caption_file' => esc_html__('Select .vtt Captions File', 'wpstream'),
271 'select_button' => esc_html__('Select', 'wpstream'),
272 'remove_button' => esc_html__('Remove', 'wpstream'),
273 'use_streaming_hours' => $this->main->quota_manager->get_streaming_ui_flags_from_cache()['use_streaming_hours'],
274 ));
275
276 // Recordings list script + its localized labels and the "create VOD from recording" links (free vs paid depending on WooCommerce).
277 wp_enqueue_script('wpstream-recordings-videos-list', plugin_dir_url( __FILE__ ) .'js/recordings_videos_list.js?v='.time(),array(), WPSTREAM_PLUGIN_VERSION, true);
278 wp_localize_script( 'wpstream-recordings-videos-list', 'wpstream_recordings_videos_list_vars',
279 array(
280 'delete_file' => esc_html__('Delete file', 'wpstream'),
281 'download' => esc_html__( 'Download', 'wpstream'),
282 'download_available' => esc_html__( 'Click to download! The url will work for the next 20 minutes!', 'wpstream'),
283 'add_free_video_url' => esc_url( admin_url( 'post-new.php?post_type=wpstream_product_vod') . '&new_video_name=' ),
284 'create_ftv_vod' => esc_html__( 'Create new Free-To-View VOD from this recording' , 'wpstream' ),
285 'woocommerce_exists' => class_exists( 'WooCommerce' ),
286 'add_paid_video_url' => esc_url( admin_url( 'post-new.php?post_type=product').'&new_video_name=' ),
287 'create_ptv_vod' => esc_html__( 'Create new Pay-Per-View VOD from this recording' , 'wpstream' ),
288 )
289 );
290
291
292 // Settings-page script with its save/logo-picker strings and the broadcaster page URL.
293 wp_enqueue_script('wpstream-settings', plugin_dir_url( __DIR__ ) .'/admin/js/wpstream_settings.js?v='.time(),array(), WPSTREAM_PLUGIN_VERSION, true);
294 wp_localize_script('wpstream-settings', 'wpstream_settings_vars', array(
295 'error_message' => esc_html__( 'Failed to save settings. Please try again.', 'wpstream'),
296 'choose_image_text' => esc_html__( 'Choose Logo Image', 'wpstream'),
297 'select_image_text' => esc_html__( 'Select Image', 'wpstream'),
298 'update_successful' => esc_html__( 'Update Successful.', 'wpstream'),
299 'update_failed' => esc_html__( 'Something went wrong. Try again.', 'wpstream'),
300 'broadcaster_url' => esc_url( esc_url(home_url('/broadcaster-page/') ) ),
301 ));
302
303
304 // Identify the current admin screen so screen-specific bundles load only where needed.
305 $current_screen=get_current_screen();
306
307 // On the credentials/channels/recordings/onboard screens, load the quota widget updater.
308 if ( in_array( $current_screen->base, ['toplevel_page_wpstream_credentials', 'wpstream_page_wpstream_live_channels', 'wpstream_page_wpstream_recordings', 'wpstream_page_wpstream_onboard'] ) ) {
309 wp_enqueue_script( 'wpstream-user-quota-update', plugin_dir_url( __DIR__ ) . 'admin/js/wpstream-user-quota.js', array(), WPSTREAM_PLUGIN_VERSION, true );
310 wp_localize_script( 'wpstream-user-quota-update', 'wpstream_user_quota_vars', array(
311 'admin_url' => get_admin_url()
312 ));
313 }
314
315 // Add localized variables for broadcaster
316 // Provide the broadcaster script (registered elsewhere) with its AJAX URL and nonce.
317 wp_localize_script('wpstream-broadcaster', 'wpstream_broadcaster_vars', array(
318 'ajax_url' => admin_url('admin-ajax.php'),
319 'nonce' => wp_create_nonce('wpstream_broadcaster_nonce'),
320 'plugin_url' => plugin_dir_url(__FILE__),
321 // Gates the broadcaster's diagnostic console logging (see broadcaster.js).
322 'debug' => defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG,
323 ));
324
325 }
326
327
328 /**
329 * Add Plugin Administation menu
330 *
331 * Registers the top-level "WpStream" menu and its sub-pages (Credentials,
332 * All Channels, Recordings, Settings, Quick Start), each mapped to a render
333 * callback on this class and gated to the `administrator` capability.
334 *
335 * @since 3.0.1
336 * @return void
337 */
338 public function wpstream_manage_admin_menu() {
339
340 // Top-level menu; its page defaults to the Credentials screen and uses the WpStream icon at position 20.
341 add_menu_page( __('WpStream','wpstream'), __('WpStream ','wpstream'), 'administrator', 'wpstream_credentials', array($this,'wpstream_set_wpstream_credentials'), WPSTREAM_PLUGIN_DIR_URL.'img/wpstream-icon-menu_2.png',20 );
342 // Credentials sub-page (same slug as the parent so it is the default view).
343 add_submenu_page( 'wpstream_credentials', __('WpStream Credentials','wpstream'), __('Credentials','wpstream'), 'administrator', 'wpstream_credentials', array($this,'wpstream_set_wpstream_credentials') );
344 // All Channels listing.
345 add_submenu_page( 'wpstream_credentials', __('WpStream Live Channels','wpstream'), __('All Channels','wpstream'), 'administrator', 'wpstream_live_channels', array( $this,'wpstream_new_general_set'));
346 // Recordings / media management.
347 add_submenu_page( 'wpstream_credentials', __('WpStream Recordings','wpstream'), __('Recordings','wpstream'), 'administrator', 'wpstream_recordings', array($this,'wpstream_media_management'));
348 // Global plugin settings.
349 add_submenu_page( 'wpstream_credentials', __('WpStream Settings','wpstream'), __('Settings','wpstream'), 'administrator', 'wpstream_settings', array($this,'wpstream_settings'));
350
351 // Quick Start / onboarding entry point.
352 add_submenu_page( 'wpstream_credentials', __('WpStream Quick Start','wpstream'), __('WpStream Quick Start','wpstream'), 'administrator', 'wpstream_onboard', array($this,'wpstream_pre_onboard_display'));
353
354
355
356
357 }
358
359
360
361
362 /**
363 * Shows events wpstream
364 *
365 * Renders the "All Channels" admin screen: the Pay-Per-View (WooCommerce
366 * `product`) channel list, the Free-To-View (`wpstream_product`) list, the
367 * quota/pack summary, and the "no channels" call-to-action. Each channel is
368 * drawn via wpstream_live_stream_unit(). Echoes HTML directly.
369 *
370 * @since 3.0.1
371 * @return void
372 */
373 public function wpstream_new_general_set() {
374 return $this->main->get_live_channel_presentation()->render_channels_page();
375 }
376 public function wpstream_social_share($the_id){
377 return $this->main->get_live_channel_presentation()->render_part( 'social_share', array( $the_id ) );
378 }
379 public function wpstream_live_stream_unit($the_id,$is_front=''){
380 $context = '' === $is_front ? 'admin' : $is_front;
381 return $this->main->get_live_channel_presentation()->render_channel( $the_id, $context );
382 }
383 public function wpstream_live_stream_unit_for_theme($the_id,$is_front=''){
384 return $this->main->get_live_channel_presentation()->render_part( 'theme_channel', array( $the_id, $is_front ) );
385 }
386 public function wpstream_close_modal_button(){
387 return $this->main->get_live_channel_presentation()->render_part( 'close_modal_button' );
388 }
389 public function wpstream_local_event_options_toggle( $is_basic_stream_mode = false ) {
390 return $this->main->get_live_channel_presentation()->render_part( 'local_event_options_toggle', array( $is_basic_stream_mode ) );
391 }
392 public function wpstream_basic_stream_mode_message() {
393 return $this->main->get_live_channel_presentation()->render_part( 'basic_stream_mode_message' );
394 }
395 public function wpstream_display_modal_seetings($the_id){
396 return $this->main->get_live_channel_presentation()->render_part( 'settings_modal', array( $the_id ) );
397 }
398 public function wpstream_display_modal_share($the_id){
399 return $this->main->get_live_channel_presentation()->render_part( 'share_modal', array( $the_id ) );
400 }
401 public function wpstream_display_modal_broadcast($the_id,$external_software_streaming_class,$obs_uri,$obs_stream){
402 return $this->main->get_live_channel_presentation()->render_part(
403 'broadcast_modal',
404 array( $the_id, $external_software_streaming_class, $obs_uri, $obs_stream )
405 );
406 }
407 public function wpstream_encoder_table(){
408 return $this->main->get_live_channel_presentation()->render_part( 'encoder_table' );
409 }
410 public function wpstream_encoder_panel( $encoder, $obs_uri, $obs_stream, $visible = false ){
411 return $this->main->get_live_channel_presentation()->render_part(
412 'encoder_panel',
413 array( $encoder, $obs_uri, $obs_stream, $visible )
414 );
415 }
416 private function wpstream_settings_fields(){
417 // Definition of every settings field, keyed and grouped by 'tab' (general, subscription, messages, default options, VOD defaults, support).
418 return array(
419 1 => array(
420 'tab' => 'general_options',
421 'label' => esc_html__('Slug for free video/channel pages ','wpstream'),
422 'name' => 'free_media_slug',
423 'type' => 'text',
424 'details' => esc_html__('This will replace the default "wpstream" of all your free video/channel urls. Special characters like "&" are not permitted. To have your new slug show up you need to re-save the "Permalinks Settings" under Settings -> Permalinks, even if not making any changes.','wpstream'),
425 ),
426
427 'free_vod_slug' => array(
428 'tab' => 'general_options',
429 'label' => esc_html__('Slug for free VOD pages ','wpstream'),
430 'name' => 'free_media_slug_vod',
431 'type' => 'text',
432 'details' => esc_html__('This will replace the default "wpstream_vod" of all your free VOD urls. Special characters like "&" are not permitted. To have your new slug show up you need to re-save the "Permalinks Settings" under Settings -> Permalinks, even if not making any changes.','wpstream'),
433 ),
434
435 2 => array(
436 'tab' => 'general_options',
437 'label' => esc_html__('Non-Admin User Roles Allowed to Broadcast','wpstream'),
438 'name' => 'stream_role',
439 'type' => 'user_roles',
440 'details' => esc_html__('These types of users can stream via frontend shortcodes / blocks. Single individual channels are automaticlally created for streaming by non-admins.','wpstream'),
441
442 ),
443 3 => array(
444 'tab' => 'general_options',
445 'label' => esc_html__('Non Admin Streamers Channel Type.','wpstream'),
446 'name' => 'user_streaming_channel_type',
447 'type' => 'select',
448 'select_values'=>array(
449 'free' => esc_html__('Free Live Channel','wpstream'),
450 'paid' => esc_html__('Pay-Per-View','wpstream')
451 ),
452 'details' => esc_html__('Choose whether the channels assigned to non-admins are free-for-all or pay-per-view (WooCommerce product).','wpstream'),
453 ),
454
455 4 => array(
456 'tab' => 'general_options',
457 'label' => esc_html__('Default Pay-Per-View Price','wpstream'),
458 'name' => 'user_streaming_default_price',
459 'type' => 'text',
460 'details' => esc_html__('Default price of pay-per-view channels assigned to non-admins.','wpstream'),
461 ),
462
463
464
465 6 => array(
466 'tab' => 'subscription_options',
467 'label' => esc_html__('Use Global Subscription Mode','wpstream'),
468 'name' => 'global_sub',
469 'type' => 'slidertoogle',
470 'details' => esc_html__('If enabled, a client can access all the media products (live and VOD) by purchasing a single subscription. The "WooCommerce Subscriptions" plugin is required.','wpstream'),
471 ),
472
473 7 => array(
474 'tab' => 'subscription_options',
475 'label' => esc_html__('Subscription ID for Global Subscription Mode','wpstream'),
476 'name' => 'global_sub_id',
477 'type' => 'text',
478 'details' => esc_html__('ID of the subscription product to be purchased for global access to media. All non-subscription video products that are not already attached to a subscription will be accessible to users that have purchased it.','wpstream'),
479 ),
480 8 => array(
481 'tab' => 'messages_options',
482 'label' => esc_html__('PPV not logged in message','wpstream'),
483 'name' => 'product_not_login',
484 'type' => 'text',
485 'details' => esc_html__('This message will be displayed on top of the media player for pay-per-view items when user is not logged in.','wpstream'),
486 'default' => esc_html__('You must be logged in to watch this video.','wpstream'),
487 ),
488 9 => array(
489 'tab' => 'messages_options',
490 'label' => esc_html__('PPV not purchased message','wpstream'),
491 'name' => 'product_not_bought',
492 'type' => 'text',
493 'details' => esc_html__('This message will be displayed on top of the media player for common pay-per-view items that have not been purchased.','wpstream'),
494 'default' => esc_html__('You did not yet purchase this item.','wpstream'),
495 ),
496 10 => array(
497 'tab' => 'messages_options',
498 'label' => esc_html__('Subscription not purchased message','wpstream'),
499 'name' => 'product_not_subscribe',
500 'type' => 'text',
501 'details' => esc_html__('This message will be displayed on top of the media player for subscription-type pay-per-view items that have not been purchased.','wpstream'),
502 'default' => esc_html__(' You did not yet subscribe to this item.','wpstream'),
503 ),
504 11 => array(
505 'tab' => 'messages_options',
506 'label' => esc_html__('Thank you message','wpstream'),
507 'name' => 'product_thankyou',
508 'type' => 'text',
509 'details' => esc_html__('This message will be displayed on the thank you page (after purchase) and the confirmation email.','wpstream'),
510 'default' => esc_html__('Thanks for your purchase. You can access your item at any time by visiting the following page: {item_link}','wpstream'),
511 ),
512
513 12 => array(
514 'tab' => 'messages_options',
515 'label' => esc_html__('Subscription Active message','wpstream'),
516 'name' => 'subscription_active',
517 'type' => 'text',
518 'details' => esc_html__('This message will be displayed on subscription product page.','wpstream'),
519 'default' => esc_html__('Your Subscription is Active','wpstream'),
520 ),
521 13 => array(
522 'tab' => 'messages_options',
523 'label' => esc_html__('You are not live message','wpstream'),
524 'name' => 'you_are_not_live',
525 'type' => 'text',
526 'details' => esc_html__('This message will be displayed in player.','wpstream'),
527 'default' => esc_html__('We are not live at this moment','wpstream'),
528 ),
529
530 14 => array(
531 'tab' => 'general_options',
532 'label' => esc_html__('Video player theme','wpstream'),
533 'name' => 'video_player_theme',
534 'type' => 'select',
535 'select_values'=>array(
536 'default' => esc_html__('Default','wpstream'),
537 'city' => esc_html__('City','wpstream'),
538 'forest' => esc_html__('Forest','wpstream'),
539 'fantasy' => esc_html__('Fantasy','wpstream'),
540 'sea' => esc_html__('Sea','wpstream'),
541 ),
542 'details' => esc_html__('Choose the video player theme to have a different look for the player.','wpstream'),
543 ),
544 'wpstream_player_logo' => array(
545 'tab' => 'general_options',
546 'name' => 'player_logo',
547 'label' => esc_html__('Logo for the video player','wpstream'),
548 'type' => 'image',
549 'details' => esc_html__('This logo will be displayed on the the video player.','wpstream'),
550 'default' => '',
551 'image_size' => 'thumbnail',
552 ),
553 // hide the video player logo opacity for now
554 // 'wpstream_player_logo_opacity' => array(
555 // 'tab' => 'general_options',
556 // 'name' => 'player_logo_opacity',
557 // 'label' => esc_html__('Opacity of the video player logo','wpstream'),
558 // 'type' => 'range',
559 // 'details' => esc_html__('Set the opacity of the logo','wpstream'),
560 // 'default' => '',
561 // 'image_size' => 'thumbnail',
562 // ),
563 'wpsteram_player_logo_position' => array(
564 'tab' => 'general_options',
565 'name' => 'player_logo_position',
566 'label' => esc_html__('Position of the video player logo','wpstream'),
567 'type' => 'select',
568 'select_values'=>array(
569 'top-left' => esc_html__('Top Left','wpstream'),
570 'top-right' => esc_html__('Top Right','wpstream'),
571 'bottom-left' => esc_html__('Bottom Left','wpstream'),
572 'bottom-right' => esc_html__('Bottom Right','wpstream'),
573 ),
574 'details' => esc_html__('Choose the position of the logo on the video player.','wpstream'),
575 'default' => '',
576 ),
577
578
579
580 99 => array(
581 'tab' => 'default_options',
582 'label' => esc_html__('Events Options ','wpstream'),
583 'name' => 'user_streaming_global_channel_options',
584 'type' => 'user_streaming_global_channel_options',
585 'details' => esc_html__('Global Options for live events.','wpstream'),
586 ),
587
588 100 => array(
589 'tab' => 'default_options_vod',
590 'label' => esc_html__('Autoplay','wpstream'),
591 'name' => 'vod_autoplay',
592 'type' => 'slidertoogle',
593 'details' => esc_html__('If enabled, video will attempt to start playing automatically. This is only achievable in some browsers.','wpstream'),
594 ),
595
596
597 101 => array(
598 'tab' => 'default_options_vod',
599 'label' => esc_html__('Start Muted','wpstream'),
600 'name' => 'vod_start_muted',
601 'type' => 'slidertoogle',
602 'details' => esc_html__('If enabled, video will start muted. This may increase the rate of autoplay in some browsers.','wpstream'),
603 ),
604
605 102 => array(
606 'tab' => 'default_options_vod',
607 'label' => esc_html__('Lock To Website','wpstream'),
608 'name' => 'vod_domain_lock',
609 'type' => 'slidertoogle',
610 'details' =>sprintf ( esc_html__('If enabled, video will only display on %1$s, otherwise it can show up on any website.','wpstream'),get_bloginfo('wpurl') ),
611 ),
612
613 103 => array(
614 'tab' => 'default_options_vod',
615 'label' => esc_html__('Encrypt Video','wpstream'),
616 'name' => 'vod_encrypt',
617 'type' => 'slidertoogle',
618 'details' => esc_html__('If enabled, video data will be encrypted. Enabling encryption may lead to reduced website performance under certain configurations. Encrypted video may not display in all browsers.','wpstream'),
619 ),
620
621 /* 'vod_domain_lock' =>array(
622 'name' => esc_html__('Video On Demand - Lock To Website','wpstream'),
623 'details' => sprintf ( esc_html__('If enabled, VODS will only display on %1$s, otherwise they can show up on any website.','wpstream'),get_bloginfo('wpurl') ),
624 'defaults' => 'no',
625 ),
626 'vod_encrypt' =>array(
627 'name' => esc_html__('Encrypt Video on Demand','wpstream'),
628 'details' => esc_html__('If enabled, video data will be encrypted. Enabling encryption may lead to reduced website performance under certain configurations. Encrypted video may not display in all browsers.','wpstream'),
629 'defaults' => 'no',
630 ),*/
631
632 104 => array(
633 'tab' => 'support_tab',
634 'label' => esc_html__('Logs','wpstream'),
635 'name' => 'logs',
636 'type' => 'logs_table',
637 'details' => esc_html__('This is the error log of the plugin.','wpstream'),
638 )
639
640 );
641 }
642
643 /**
644 * Stop the request unless the current user may manage plugin settings.
645 *
646 * Shared by the settings and credentials screens so both enforce the
647 * same capability (`manage_options`).
648 *
649 * @return void
650 */
651 private function wpstream_require_settings_capability(){
652 if ( ! current_user_can( 'manage_options' ) ) {
653 wp_die( esc_html__( 'Not enough permissions to make this change', 'wpstream' ) );
654 }
655 }
656
657 /**
658 * Render the WpStream Settings admin screen and persist submitted values.
659 *
660 * On POST it verifies the nonce, then stores only the fields declared for
661 * the active tab as `wpstream_*` options (the streamer role is checked
662 * against the site's roles, the "default options" tab folds its checkboxes
663 * into one map), and flushes rewrite rules. It then renders the tabbed
664 * form from the same field definitions. Echoes HTML.
665 *
666 * @return void
667 */
668 public function wpstream_settings(){
669
670 // Admin-only screen.
671 $this->wpstream_require_settings_capability();
672
673
674 $active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'general_options';
675
676 /**
677 * Filter the settings field definitions.
678 *
679 * Field definitions drive both the save allow-list and the rendered
680 * form, so an added field (`tab`, `label`, `name`, `type`, `details`)
681 * is rendered on its tab and saved as the `wpstream_{name}` option
682 * through sanitize_text_field().
683 *
684 * @since 4.14.0
685 *
686 * @param array $fields Field definitions.
687 * @param string $active_tab Tab being rendered or saved.
688 */
689 $wpstream_settings_array = (array) apply_filters( 'wpstream_settings_fields', $this->wpstream_settings_fields(), $active_tab );
690
691 /**
692 * Filter the settings tabs.
693 *
694 * Added tabs render their fields from `wpstream_settings_fields`
695 * and fire `wpstream_settings_tab_{slug}` for a custom body.
696 *
697 * @since 4.14.0
698 *
699 * @param array $tabs Tab slug => label.
700 * @param string $active_tab Tab being rendered or saved.
701 */
702 $tabs = (array) apply_filters(
703 'wpstream_settings_tabs',
704 array(
705 'general_options' => esc_html__( 'General Options', 'wpstream' ),
706 'default_options' => esc_html__( 'Default Channel Settings', 'wpstream' ),
707 'default_options_vod' => esc_html__( 'VOD Settings', 'wpstream' ),
708 'subscription_options' => esc_html__( 'Subscription Options', 'wpstream' ),
709 'messages_options' => esc_html__( 'Customize Messages', 'wpstream' ),
710 'support_tab' => esc_html__( 'Support', 'wpstream' ),
711 ),
712 $active_tab
713 );
714
715 // Handle a settings submission.
716 if($_SERVER['REQUEST_METHOD'] === 'POST'){
717 // CSRF protection.
718 check_admin_referer( 'wpstream-settings-nonce', 'wpstream-settings-nonce' );
719
720 // Only the fields declared for the active tab may be persisted.
721 $allowed = array();
722 foreach ( $wpstream_settings_array as $field ) {
723 if ( isset( $field['tab'], $field['name'] ) && $field['tab'] === $active_tab ) {
724 $allowed[] = $field['name'];
725 }
726 }
727
728 // Option values as they were before this submission, and as written by it
729 // (both keyed by option name) — announced together once the save is complete.
730 $previous_options = array();
731 $saved_options = array();
732
733 // If the "channel type" section was shown but no streamer role was chosen, clear the stored role.
734 if( isset($_POST['user_streaming_channel_type_hidden']) && intval($_POST['user_streaming_channel_type_hidden'])==1 && !isset($_POST['stream_role']) ){
735 $previous_options['wpstream_stream_role'] = get_option( 'wpstream_stream_role' );
736 $saved_options['wpstream_stream_role'] = '';
737 update_option( sanitize_key('wpstream_stream_role'), '' );
738 }
739
740 // Persist each declared field as a sanitized wpstream_* option; anything else is ignored.
741 foreach ( $allowed as $variable ) {
742 if ( ! isset( $_POST[ $variable ] ) ) {
743 continue;
744 }
745 $value = $_POST[ $variable ];
746
747 // Streamer role is a list of role slugs; keep only the roles the form offers
748 // (editable roles minus administrator, who can always broadcast).
749 if ( 'stream_role' === $variable ) {
750 $known = array_diff( array_keys( get_editable_roles() ), array( 'administrator' ) );
751 $roles = array_values( array_intersect( (array) $value, $known ) );
752 $previous_options['wpstream_stream_role'] = get_option( 'wpstream_stream_role' );
753 $saved_options['wpstream_stream_role'] = $roles;
754 update_option( 'wpstream_stream_role', $roles );
755 continue;
756 }
757
758 $option_name = sanitize_key( 'wpstream_' . $variable );
759 $previous_options[ $option_name ] = get_option( $option_name );
760 $saved_options[ $option_name ] = sanitize_text_field( $value );
761 update_option( $option_name, $saved_options[ $option_name ] );
762 }
763
764
765 // On the "default options" tab, fold the per-option checkboxes into a single 1/0 map.
766 if ( 'default_options' === $active_tab ) {
767 $event_settings=array();
768 foreach($this->global_event_options as $key=>$option){
769 $event_settings[$key]='';
770 if(isset($_POST['wpstream_event_set_'.$key]) && $_POST['wpstream_event_set_'.$key]=='on'){
771 $event_settings[$key]=1;
772 }else{
773 $event_settings[$key]=0;
774 }
775 }
776 $this->main->channel_settings->apply(
777 array(
778 'type' => 'save_defaults',
779 'options' => $event_settings,
780 )
781 );
782 }
783
784
785
786
787 // reset permalinkgs
788 // Slug settings can change permalinks, so drop and rebuild the rewrite rules.
789 global $wp_rewrite;
790
791 update_option( "rewrite_rules", FALSE );
792 $wp_rewrite->flush_rules( true );
793
794 /**
795 * Fires after a WpStream Settings tab was saved and the rewrite
796 * rules rebuilt. Default Channel Settings saved from the
797 * "default options" tab are announced separately by
798 * wpstream_default_channel_settings_saved.
799 *
800 * @since 4.14.0
801 *
802 * @param string $active_tab Settings tab that was submitted.
803 * @param array $saved Option values written, keyed by option name.
804 * @param array $previous The values those options held before.
805 */
806 do_action( 'wpstream_settings_saved', $active_tab, $saved_options, $previous_options );
807
808 }
809
810
811
812
813 // Open the settings panel and the form.
814 print '<div class="theme_options_tab_wpstream" style="display:block;" >
815 <h1>'.__('WpStream Settings','wpstream').'</h1>
816 <form method="post" action="" >';
817
818 // Tab navigation bar; the current tab gets the nav-tab-active class.
819 print '<h2 class="nav-tab-wrapper">';
820 foreach ( $tabs as $tab_slug => $tab_label ) {
821 $tab_slug = sanitize_key( $tab_slug );
822 print '<a href="' . esc_url( add_query_arg( array( 'page' => 'wpstream_settings', 'tab' => $tab_slug ), admin_url( 'admin.php' ) ) ) . '" class="nav-tab ' . ( $active_tab === $tab_slug ? 'nav-tab-active' : '' ) . '">' . esc_html( $tab_label ) . '</a>';
823 }
824 print '</h2>';
825 $help_link='';
826
827 // Basic-stream mode disables/annotates some default-channel controls.
828 $is_basic_stream_mode = $this->wpstream_is_basic_streaming_mode();
829 print '<div class="wpstream_option_wrapper">';
830
831 // Pick the contextual "Video Help" docs link for the active tab.
832 switch ($active_tab) {
833 case 'general_options':
834 $help_link='https://docs.wpstream.net/docs/general-settings/';
835 break;
836 case 'default_options':
837 $help_link='https://docs.wpstream.net/docs/default-channel-settings/';
838 break;
839 case 'default_options_vod':
840 $help_link='https://docs.wpstream.net/docs/vod-settings/';
841 break;
842 case 'subscription_options':
843 $help_link='https://docs.wpstream.net/docs/subscription-options/';
844 break;
845 case 'messages_options':
846 $help_link='https://docs.wpstream.net/docs/customize-messages/';
847 break;
848 }
849
850 print '<div class="options_wrapper">';
851 // Render each field that belongs to the active tab.
852 foreach ($wpstream_settings_array as $key=>$option){
853 // Skip fields from other tabs.
854 if($option['tab']!=$active_tab){
855 continue;
856 }
857
858 // Intro blurb (and basic-mode notice) shown above the global channel options control.
859 if ( $option['type']=='user_streaming_global_channel_options' ) {
860 print '<div class="default-channel-settings-info">';
861 print esc_html__( 'These settings will apply to newly created channels; existing channels will not change settings if you change them here', 'wpstream');
862 if ( $is_basic_stream_mode ) {
863 $this->wpstream_basic_stream_mode_message();
864 }
865 print '</div>';
866
867 }
868 print '<div class="wpstream_option">';
869 // Load this option's stored value.
870 $options_value = get_option('wpstream_'.$option['name']) ;
871
872
873 // Render the appropriate control for this field's type.
874 switch( $option['type'] ) {
875 // Multi-select of WordPress user roles allowed to broadcast.
876 case 'user_roles':
877 print '<label for="'.esc_attr( $option['name'] ).'">'.wp_kses_post( $option['label'] ).'</label>';
878 print $this->wpstream_select_user_roles(esc_attr( $option['name'] ),$options_value);
879 print '<div class="settings_details">'.wp_kses_post( $option['details'] ).'</div>';
880 break;
881 // The grid of default per-event streaming options.
882 case 'user_streaming_global_channel_options':
883 $exclude_array=array();
884 $this->user_streaming_global_channel_options(
885 $option['name'],
886 $options_value,
887 $exclude_array,
888 $is_basic_stream_mode
889 );
890 break;
891 // Plain text input; falls back to the field's default when unset.
892 case 'text':
893 if($options_value==''){
894 $options_value='';
895 if(isset($option['default'])){
896 $options_value=$option['default'];
897 }
898 }
899
900 print '<label for="'.esc_attr( $option['name'] ).'">'.wp_kses_post( $option['label'] ).'</label>';
901 print '<input class="wpstream-text-input-setting" id="'.esc_attr( $option['name'] ).'" type="'.$option['type'].'" size="36" name="'.esc_attr( $option['name'] ).'" value="'.esc_attr($options_value).'" />';
902 print '<div class="settings_details">'.wp_kses_post( $option['details'] ).'</div>';
903 break;
904 // Dropdown built from the field's select_values map; a hidden mirror marks the field as present.
905 case 'select':
906 print '<label for="'.esc_attr( $option['name'] ).'">'.wp_kses_post( $option['label'] ).'</label>';
907 print '<select id="'.esc_attr( $option['name'] ).'" name="'.esc_attr( $option['name'] ).'" >';
908 foreach($option['select_values'] as $key=>$value){
909 print '<option value="'.$key.'" ';
910 // Pre-select the currently stored value.
911 if( $key == esc_html($options_value) ){
912 print ' selected ';
913 }
914 print '>'.$value.'</option>';
915 }
916 print '</select>';
917 print '<input type="hidden" name="'.esc_attr( $option['name'] ).'_hidden" value="1" >';
918 print '<div class="settings_details">'.wp_kses_post( $option['details'] ).'</div>';
919 break;
920 // On/off switch; a hidden 0-valued twin ensures "off" posts a value.
921 case 'slidertoogle':
922 print '<label for="'.esc_attr( $option['name'] ).'">'.wp_kses_post( $option['label'] ).'</label>';
923 print '<div style="display: flex; gap: 25px; justify-content: space-between;">';
924 print '<div class="settings_details">'.wp_kses_post( $option['details'] ).'</div>';
925 print '<label class="wpstream_switch">
926 <input type="hidden" class="wpstream_event_option_itemc" value="0" name="'.esc_attr( $option['name'] ).'" >
927 <input type="checkbox" class="wpstream_event_option_itemc" value="1" name="'.esc_attr( $option['name'] ).'" ';
928 if( intval($options_value) !==0 ){
929 print ' checked ';
930 }
931 print '> <span class="wpstream_slider round"></span>';
932 print '</label>';
933 print '</div>';
934 break;
935 // Media-library image picker with preview + upload/remove buttons.
936 case 'image':
937 $image_url = $options_value ? esc_url($options_value) : '';
938 $has_image = !empty($image_url);
939
940 print '<label for="' . esc_attr( $option['name'] ) . '">' . wp_kses_post( $option['label'] ) . '</label>';
941 print '<div class="wpstream-image-upload-wrapper">';
942 print '<input type="hidden" id="' . esc_attr( $option['name'] ) . '" name="' . esc_attr( $option['name'] ) . '" value="' . $image_url . '" />';
943
944 // Preview area
945 print '<div class="wpstream-image-preview" style="' . (!$has_image ? 'display:none;' : '') . '">';
946 print '<img src="' . $image_url . '" alt="Preview" />';
947 print '</div>';
948
949 // Upload/remove buttons
950 print '<div class="wpstream-image-upload-buttons">';
951 print '<button type="button" class="wpstream-upload-image button">' . esc_html__('Upload Image', 'wpstream') . '</button>';
952 print '<button type="button" class="wpstream-remove-image button" style="' . (!$has_image ? 'display:none;' : '') . '">' . esc_html__('Remove Image', 'wpstream') . '</button>';
953 print '</div>';
954
955 print '</div>';
956 print '<div class="settings_details">' . wp_kses_post( $option['details'] ) . '</div>';
957 break;
958 // 0-100 range slider.
959 case 'range':
960 print '<label for="'.esc_attr( $option['name'] ).'">'.wp_kses_post( $option['label'] ).'</label>';
961 print '<input class="wpstream-range-input" type="range" id="'.esc_attr( $option['name'] ).'" name="'.esc_attr( $option['name'] ).'" min="0" max="100" step="10" value="'.esc_attr($options_value).'" />';
962 print '<div class="settings_details">'.wp_kses_post( $option['details'] ).'</div>';
963 break;
964 // Delegates to the Support tab renderer (plugin error log table).
965 case 'logs_table':
966 $this->wpstream_support_tab();
967 break;
968 }
969 print '</div>';
970 }
971
972 /**
973 * Fires inside the options wrapper of a settings tab, after its fields.
974 *
975 * The dynamic part is the tab slug. Callbacks echo their own markup
976 * and own its escaping; fields they add to the form are saved only
977 * when declared through `wpstream_settings_fields`.
978 *
979 * @since 4.14.0
980 *
981 * @param string $active_tab Tab being rendered.
982 */
983 do_action( 'wpstream_settings_tab_' . $active_tab, $active_tab );
984 print '</div>'; // options wrapper
985 // Contextual help link for tabs that have one.
986 if($help_link!==''){
987 print '<div class="wpstream_options_help"><a href="'.esc_url($help_link).'" target="_blank" >'.esc_html__('Video Help','wpstream').'</a></div>';
988 }
989 print '</div>';
990
991
992 // Save button (hidden on the read-only Support tab).
993 if ( $active_tab != 'support_tab') {
994 print '<div class="wpstream-save-settings">';
995 print '<input type="submit" name="submit" class="wpstream_button wpstream_button_action" value="'.__('Save Changes','wpstream').'" />';
996 print '<div class="spinner"></div>';
997 print '</div>';
998 }
999
1000 // CSRF nonce for the submission handled at the top of this method.
1001 print '<input id="wpstream-settings-nonce" name="wpstream-settings-nonce" type="hidden" value="'.wp_create_nonce('wpstream-settings-nonce').'" /> ';
1002 print '</form>';
1003 print '</div>';
1004
1005 }
1006
1007 /**
1008 * Get system information for support tab
1009 *
1010 * @return array System information
1011 */
1012 private function get_system_info() {
1013 global $wp_version;
1014
1015 // Environment facts: PHP/WP version, debug flag, memory limit and the plugin version.
1016 $php_version = phpversion();
1017 $wp_version_info = $wp_version;
1018 $site_debug_mode = (defined('WP_DEBUG') && WP_DEBUG);
1019 $wp_memory_limit = WP_MEMORY_LIMIT;
1020
1021 $wpstream_version = WPSTREAM_PLUGIN_VERSION;
1022 $wpstream_plugin_outdated = false;
1023
1024 // Check if plugin is outdated
1025 // Mark the plugin outdated if WP's update transient lists an available update for it.
1026 $update_plugins = get_site_transient('update_plugins');
1027 if (isset($update_plugins->response['plugin/wpstream.php'])) {
1028 $wpstream_plugin_outdated = true;
1029 }
1030
1031 // Check API status
1032 // API is "connected" when a WpStream cloud token can be obtained.
1033 $api_status = false;
1034 if (method_exists($this->main->wpstream_live_connection, 'is_connected')) {
1035 $api_status = $this->main->wpstream_live_connection->is_connected();
1036 }
1037
1038 // Return the collected diagnostics for the Support tab to render.
1039 return array(
1040 'php_version' => $php_version,
1041 'wp_version' => $wp_version_info,
1042 'site_debug_mode' => $site_debug_mode,
1043 'wp_memory_limit' => $wp_memory_limit,
1044 'wpstream_version' => $wpstream_version,
1045 'wpstream_plugin_outdated' => $wpstream_plugin_outdated,
1046 'api_status' => $api_status
1047 );
1048 }
1049
1050 /**
1051 * Render system information HTML
1052 *
1053 * Prints the Support-tab diagnostics table (PHP/WP versions, debug mode,
1054 * memory limit, plugin version + update button, API connection) with a
1055 * warning/OK dashicon per row. Echoes HTML.
1056 *
1057 * @return void
1058 */
1059 private function render_system_info() {
1060 // Pull the current environment diagnostics.
1061 $system_info = $this->get_system_info();
1062 // Below: diagnostics table; each row shows a value and a warning/OK indicator based on recommended thresholds.
1063 ?>
1064
1065 <div class="wpstream-system-info">
1066 <h3><?php esc_html_e('System Information', 'wpstream'); ?></h3>
1067 <table class="widefat">
1068 <tbody>
1069 <tr>
1070 <td><strong><?php esc_html_e('PHP Version', 'wpstream'); ?></strong></td>
1071 <td><?php echo esc_html($system_info['php_version']); ?></td>
1072 <td>
1073 <?php if (version_compare($system_info['php_version'], '7.4', '<')): ?>
1074 <span class="dashicons dashicons-warning" style="color: #ffb900;"></span>
1075 <?php esc_html_e('We recommend PHP 7.4 or higher', 'wpstream'); ?>
1076 <?php else: ?>
1077 <span class="dashicons dashicons-yes-alt" style="color: #46b450;"></span>
1078 <?php endif; ?>
1079 </td>
1080 </tr>
1081 <tr>
1082 <td><strong><?php esc_html_e('WordPress Version', 'wpstream'); ?></strong></td>
1083 <td><?php echo esc_html($system_info['wp_version']); ?></td>
1084 <td>
1085 <?php if (version_compare($system_info['wp_version'], '5.6', '<')): ?>
1086 <span class="dashicons dashicons-warning" style="color: #ffb900;"></span>
1087 <?php esc_html_e('We recommend WordPress 5.6 or higher', 'wpstream'); ?>
1088 <?php else: ?>
1089 <span class="dashicons dashicons-yes-alt" style="color: #46b450;"></span>
1090 <?php endif; ?>
1091 </td>
1092 </tr>
1093 <tr>
1094 <td><strong><?php esc_html_e('WP Debug Mode', 'wpstream'); ?></strong></td>
1095 <td><?php echo $system_info['site_debug_mode'] ? esc_html__('Enabled', 'wpstream') : esc_html__('Disabled', 'wpstream'); ?></td>
1096 <td>
1097 <?php if ($system_info['site_debug_mode']): ?>
1098 <span class="dashicons dashicons-info" style="color: #00a0d2;"></span>
1099 <?php esc_html_e('Debug mode should be disabled on production sites', 'wpstream'); ?>
1100 <?php else: ?>
1101 <span class="dashicons dashicons-yes-alt" style="color: #46b450;"></span>
1102 <?php endif; ?>
1103 </td>
1104 </tr>
1105 <tr>
1106 <td><strong><?php esc_html_e('WP Memory Limit', 'wpstream'); ?></strong></td>
1107 <td><?php echo esc_html($system_info['wp_memory_limit']); ?></td>
1108 <td>
1109 <?php
1110 $memory_limit = wp_convert_hr_to_bytes($system_info['wp_memory_limit']);
1111 if ($memory_limit < 64 * 1024 * 1024): // 64MB
1112 ?>
1113 <span class="dashicons dashicons-warning" style="color: #ffb900;"></span>
1114 <?php esc_html_e('We recommend at least 64MB', 'wpstream'); ?>
1115 <?php else: ?>
1116 <span class="dashicons dashicons-yes-alt" style="color: #46b450;"></span>
1117 <?php endif; ?>
1118 </td>
1119 </tr>
1120 <tr>
1121 <td><strong><?php esc_html_e('WpStream Version', 'wpstream'); ?></strong></td>
1122 <td><?php echo esc_html($system_info['wpstream_version']); ?></td>
1123 <td style="display: flex; align-items: center; gap: 5px;">
1124 <?php if ($system_info['wpstream_plugin_outdated']): ?>
1125 <span class="dashicons dashicons-warning" style="color: #ffb900;"></span>
1126 <?php esc_html_e('Update available', 'wpstream'); ?>
1127 <div class="update-button-wrapper">
1128 <button class="wpstream-update-plugin-button button button-primary" data-plugin="wpstream/wpstream.php">
1129 <?php esc_html_e('Update Now', 'wpstream'); ?>
1130 </button>
1131 </div>
1132 <?php else: ?>
1133 <span class="dashicons dashicons-yes-alt" style="color: #46b450;"></span>
1134 <?php endif; ?>
1135 </td>
1136 </tr>
1137 <tr>
1138 <td><strong><?php esc_html_e('API Connection', 'wpstream'); ?></strong></td>
1139 <td><?php echo $system_info['api_status'] ? esc_html__('Connected', 'wpstream') : esc_html__('Disconnected', 'wpstream'); ?></td>
1140 <td>
1141 <?php if (!$system_info['api_status']): ?>
1142 <span class="dashicons dashicons-warning" style="color: #ffb900;"></span>
1143 <?php esc_html_e('API connection issue', 'wpstream'); ?>
1144 <?php else: ?>
1145 <span class="dashicons dashicons-yes-alt" style="color: #46b450;"></span>
1146 <?php endif; ?>
1147 </td>
1148 </tr>
1149 </tbody>
1150 </table>
1151 </div>
1152 <?php
1153 }
1154
1155 /**
1156 * Render support tab content
1157 *
1158 * Prints the Support tab: the system-info table, a table of relevant active
1159 * plugins with update tooltips, and a table of the plugin's recent logs.
1160 * Echoes HTML.
1161 *
1162 * @return void
1163 */
1164 public function wpstream_support_tab() {
1165 // Below: Support tab markup (system info, active plugins table, recent logs table).
1166 ?>
1167 <div class="wrap">
1168 <div class="wpstream-support-tab-root">
1169 <?php $this->render_system_info(); ?>
1170
1171 <div class="wpstream-plugins-table-container">
1172 <h3><?php esc_html_e('Active Plugins', 'wpstream'); ?></h3>
1173 <table class="widefat wpstream-plugins-table">
1174 <thead>
1175 <tr>
1176 <th><?php esc_html_e('Plugin', 'wpstream'); ?></th>
1177 <th><?php esc_html_e('Version', 'wpstream'); ?></th>
1178 </tr>
1179 </thead>
1180 <tbody>
1181 <?php
1182 // Build the relevant-plugins list; show a placeholder row when none are found.
1183 $plugins_data = $this->wpstream_get_plugins_data();
1184 if (empty($plugins_data)) {
1185 echo '<tr><td colspan="3">' . esc_html__('No WPStream plugins found.', 'wpstream') . '</td></tr>';
1186 } else {
1187 // One row per plugin; append an "update available" tooltip when a newer version exists.
1188 foreach ($plugins_data as $plugin) {
1189 echo '<tr>';
1190 echo '<td>' . esc_html($plugin['name']) . '</td>';
1191 echo '<td>';
1192 echo esc_html($plugin['version']);
1193 if ( isset($plugin['new_version']) ) {
1194 echo '<div class="wpstream-tooltip-container">';
1195 echo '<span class="dashicons dashicons-info wpstream-tooltip" title="' . esc_attr($plugin['new_version']) . '">';
1196 echo '</span>';
1197 echo '<div class="wpstream-custom-tooltip">' . sprintf(
1198 esc_html__('A new version is available: %s', 'wpstream'),
1199 esc_html($plugin['new_version'])
1200 ) . '</div>';
1201 echo '</div>';
1202 }
1203 echo '</td>';
1204 echo '</tr>';
1205 }
1206 }
1207 ?>
1208 </tbody>
1209 </table>
1210 </div>
1211
1212 <div class="wpstream-logs-table-container">
1213 <h3><?php esc_html_e('Recent Logs', 'wpstream'); ?></h3>
1214 <table class="widefat wpstream-logs-table">
1215 <thead>
1216 <tr>
1217 <th><?php esc_html_e('Time', 'wpstream'); ?></th>
1218 <th><?php esc_html_e('Type', 'wpstream'); ?></th>
1219 <th><?php esc_html_e('Description', 'wpstream'); ?></th>
1220 </tr>
1221 </thead>
1222 <tbody>
1223 <?php
1224 // Load the stored plugin logs; show a placeholder row when empty.
1225 $logs = get_option('wpstream_logs');
1226 if ( !is_array($logs) || empty($logs) ) {
1227 echo '<tr><td colspan="3">' . esc_html__('No logs found.', 'wpstream') . '</td></tr>';
1228 } else {
1229 // One row per log entry (time, type, description).
1230 foreach ($logs as $log) {
1231 echo '<tr>';
1232 echo '<td>' . esc_html(date('Y-m-d H:i:s', $log['timestamp'] ) ) . '</td>';
1233 echo '<td>' . esc_html($log['type']) . '</td>';
1234 echo '<td>' . esc_html($log['description']) . '</td>';
1235 echo '</tr>';
1236 }
1237 }
1238 ?>
1239 </tbody>
1240 </table>
1241 </div>
1242 </div>
1243 </div>
1244 <?php
1245 }
1246
1247 /**
1248 * Get plugins data.
1249 *
1250 * Collects name/version/active/update info for a fixed allow-list of plugins
1251 * relevant to WpStream (WpStream, WooCommerce, Meta Box, One Click Demo
1252 * Import, Better Messages), used by the Support tab.
1253 *
1254 * @return array List of plugin info arrays (name, version, path, active, needs_update, [new_version]).
1255 */
1256 public function wpstream_get_plugins_data() {
1257 // Ensure get_plugins() is available in non-admin contexts.
1258 if (!function_exists('get_plugins')) {
1259 require_once ABSPATH . 'wp-admin/includes/plugin.php';
1260 }
1261
1262 // Get all installed plugins
1263 $all_plugins = get_plugins();
1264 $update_data = get_site_transient('update_plugins');
1265 $all_plugins_info = [];
1266
1267 // We want to get data only for the WpStream plugins
1268 $wpstream_plugins = array(
1269 'WpStream' => array(
1270 'path' => 'wpstream/wpstream.php',
1271 ),
1272 'WooCommerce' => array(
1273 'path' => 'woocommerce/woocommerce.php',
1274 ),
1275 'Meta Box' => array(
1276 'path' => 'meta-box/meta-box.php',
1277 ),
1278 'One Click Demo Import' => array(
1279 'path' => 'one-click-demo-import/one-click-demo-import.php',
1280 ),
1281 'Better Messages' => array(
1282 'path' => 'bp-better-messages/bp-better-messages.php',
1283 ),
1284
1285 );
1286
1287 // Build an info array for every installed plugin (filtered down afterwards).
1288 foreach ($all_plugins as $plugin_path => $plugin_data) {
1289 $is_active = is_plugin_active( $plugin_path );
1290 $has_update = isset( $update_data->response[$plugin_path] );
1291
1292 $plugin_info = [
1293 'name' => $plugin_data['Name'],
1294 'version' => $plugin_data['Version'],
1295 'path' => $plugin_path,
1296 'active' => $is_active ? 'Yes' : 'No',
1297 'needs_update' => $has_update ? 'Yes' : 'No'
1298 ];
1299
1300 // Record the offered version when an update is pending.
1301 if ($has_update) {
1302 $plugin_info['new_version'] = $update_data->response[$plugin_path]->new_version;
1303 }
1304
1305 $all_plugins_info[] = $plugin_info;
1306 }
1307
1308 // Filter out the elements from $all_plugins_info that are not in $wpstream_plugins
1309 foreach ( $all_plugins_info as $key => $plugin_info ) {
1310 // compare $plugin_info['path'] against the path property on each $wpstream_plugins element item
1311 // if the path is not in $wpstream_plugins, unset the element
1312 if ( !in_array( $plugin_info['path'], array_column( $wpstream_plugins, 'path' ) ) ) {
1313 unset( $all_plugins_info[$key] );
1314 }
1315 }
1316
1317 return $all_plugins_info;
1318 }
1319
1320 /**
1321 * Print a dismissible admin notice when a WpStream plugin update is pending.
1322 *
1323 * @return void
1324 */
1325 public function wpstream_render_outdated_plugin_notice() {
1326 // Only render when WP's update transient lists an update for the plugin.
1327 $has_update = get_site_transient('update_plugins');
1328 if (isset($has_update->response['plugin/wpstream.php'])) {
1329 // Below: warning notice linking to the WP updates page.
1330 ?>
1331 <div class="notice notice-warning is-dismissible">
1332 <p>
1333 <?php
1334 printf(
1335 /* translators: 1: Link to update page */
1336 esc_html__('A new version of WpStream is available. Please update to the latest version for the best experience. Go to the %1$s to update now.', 'wpstream'),
1337 '<a href="' . esc_url(admin_url('update-core.php')) . '">' . esc_html__('updates page', 'wpstream') . '</a>'
1338 );
1339 ?>
1340 </p>
1341 </div>
1342 <?php
1343 }
1344 }
1345
1346
1347 /**
1348 * Set user roles
1349 *
1350 * Render the grid of per-event streaming option switches (from
1351 * $global_event_options). Reused for both the site-wide default settings
1352 * and the per-channel settings modal. Echoes HTML.
1353 *
1354 * @param string $name Field name prefix/context (unused in body; kept for callers).
1355 * @param array|string $value Stored 1/0 map of option states (empty falls back to defaults).
1356 * @param array|string $local_array Option keys to EXCLUDE from this render (per-channel context).
1357 * @param bool $disabled True to render the switches disabled.
1358 * @param bool $is_basic_stream_mode True to disable switches for basic-streaming accounts.
1359 * @return void
1360 */
1361 public function user_streaming_global_channel_options(
1362 $name,
1363 $value,
1364 $local_array='',
1365 $disabled = false,
1366 $is_basic_stream_mode = false
1367 ) {
1368
1369 // Render one switch per defined global event option.
1370 foreach($this->global_event_options as $key=>$option){
1371
1372 // Skip options listed in the exclude array (used by the per-channel modal).
1373 if( is_array($local_array) && !in_array($key,$local_array)){
1374 print '<div class="wpstream_setting_event_unit_wrapper wpstream-setting-'.esc_attr($key).' ">';
1375
1376 print '<label for="'.$option['name'].'">'.$option['name'].'</label>';
1377
1378 print '<div style="display: flex; gap: 25px; justify-content: space-between;">';
1379 print '<div class="settings_details">'.$option['details'].'</div>';
1380 print '
1381 <label class="wpstream_switch">
1382 <input type="checkbox" class="wpstream_event_option_item" data-attr-ajaxname="'.esc_attr($key).'" name="wpstream_event_set_'.esc_attr($key).'" ';
1383 // Check state: only a stored value of 1 renders the switch on.
1384 // An absent key resolves to off everywhere the settings are
1385 // consumed, so the screen must not promise otherwise.
1386 if( isset($value[$key]) && intval($value[$key]) !== 0 ){
1387 print ' checked ';
1388 }
1389 // Disable the switch when requested or in basic-streaming mode.
1390 if ( $disabled || $is_basic_stream_mode ) {
1391 print ' disabled ';
1392 }
1393
1394
1395 print '> <span class="wpstream_slider round"></span>';
1396 print '</label>';
1397 print '</div>';
1398
1399
1400 print '</div>';
1401 }
1402 }
1403
1404
1405 }
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415 /*
1416 * Set user roles
1417 *
1418 * @since 3.0.1
1419 */
1420 /**
1421 * Build a multi-select of editable user roles (administrator excluded)
1422 * for the "roles allowed to broadcast" setting.
1423 *
1424 * @param string $name Field name (rendered as name="{name}[]").
1425 * @param array|string $value Currently selected role keys ('' becomes an empty array).
1426 * @return string HTML <select> markup.
1427 */
1428 public function wpstream_select_user_roles($name,$value){
1429 // Normalize an empty stored value to an array for in_array() below.
1430 if($value==''){
1431 $value=array();
1432 }
1433
1434 // All editable roles minus administrator (admins always can broadcast).
1435 $roles = get_editable_roles();
1436 $return = '<select id="wpstream_user_roles" name="'.esc_html($name).'[]" multiple>';
1437 unset( $roles['administrator'] );
1438
1439 // One option per role, pre-selecting those already chosen.
1440 foreach ($roles as $key=>$role){
1441 $return .= '<option value="'.$key.'" ';
1442 if( in_array($key, $value) ){
1443 $return .= ' selected ';
1444 }
1445 $return .= '>'.$role['name'].'</option>';
1446 }
1447 $return .= '</select>';
1448
1449
1450 return $return;
1451 }
1452
1453
1454 /*
1455 * Set credential admin function
1456 *
1457 * @since 3.0.1
1458 */
1459 /**
1460 * Render the Credentials admin screen and save submitted API credentials.
1461 *
1462 * On POST it stores the WpStream username/password options and clears the
1463 * cached token/quota transients so the next request re-authenticates. It
1464 * then shows the connection status and the credentials form. Echoes HTML.
1465 *
1466 * @return void
1467 */
1468 public function wpstream_set_wpstream_credentials(){
1469 $credentials_from_constants = wpstream_api_credentials_from_constants();
1470
1471 // Handle a credentials submission.
1472 if($_SERVER['REQUEST_METHOD'] === 'POST'){
1473 // CSRF/permission gate: only an admin submitting the credentials
1474 // form (with its nonce) may overwrite the stored API credentials.
1475 $this->wpstream_require_settings_capability();
1476 check_admin_referer( 'wpstream_save_credentials', 'wpstream_credentials_nonce' );
1477
1478 // wp-config owns the complete pair. A forged/manual POST must be the
1479 // same no-op as the disabled form presented to the administrator.
1480 if ( ! $credentials_from_constants ) {
1481
1482 $allowed_html = array();
1483 $exclude_array = array();
1484 $allowed_html = array();
1485
1486 // Username before this submission: tells a cleared form apart from a new account.
1487 $previous_username = (string) get_option( 'wpstream_api_username', '' );
1488
1489 // Persist only the recognised credential fields.
1490 foreach($_POST as $variable=>$value){
1491 if ($variable!='submit'){
1492 if (!in_array($variable, $exclude_array) ){
1493 switch ( $variable ) {
1494 case 'api_username':
1495 // Unslashed first: WordPress slashes every $_POST value,
1496 // so a username containing a quote would otherwise be
1497 // stored with a stray backslash and never authenticate.
1498 // Then sanitized as text.
1499 update_option( sanitize_key('wpstream_api_username'), sanitize_text_field( wp_unslash( $value ) ) );
1500 break;
1501 case 'api_password':
1502 // Only unslashed, never sanitized: the password is a secret
1503 // sent verbatim to the API, and sanitizing would silently
1504 // change it. Unslashing undoes the escaping WordPress adds
1505 // to every $_POST value, without which a password holding a
1506 // quote or a backslash is stored — and sent — wrong, so the
1507 // customer is told their correct password is incorrect.
1508 // Not autoloaded: it is only needed when authenticating.
1509 // An empty submission keeps the current password, since the
1510 // form never pre-fills the field with the stored secret.
1511 $password = wp_unslash( $value );
1512 if ( $password !== '' ) {
1513 update_option( 'wpstream_api_password', $password, false );
1514 } elseif (
1515 array_key_exists( 'api_username', $_POST )
1516 && '' === sanitize_text_field( wp_unslash( $_POST['api_username'] ) )
1517 ) {
1518 // Clearing the account is an explicit disconnect, not a
1519 // password-only edit. Do not retain an orphaned secret.
1520 delete_option( 'wpstream_api_password' );
1521 }
1522 break;
1523 }
1524 }
1525 }
1526 }
1527
1528
1529
1530 // Invalidate any cached token/quota so the new credentials are used immediately.
1531 update_option('wpstream_token_expire',0);
1532 update_option('wpstream_current_token',' ');
1533 delete_transient( 'wpstream_token_api');
1534 delete_transient('wpstream_token_request_30');
1535 $this->main->quota_manager->invalidate();
1536
1537 // Announce the account change. Only the username travels: the
1538 // password and any token stay server-side.
1539 $username = (string) get_option( 'wpstream_api_username', '' );
1540 if ( '' !== $username ) {
1541 /**
1542 * Fires after WpStream account credentials were saved and the
1543 * cached token/quota dropped; the connection itself is verified
1544 * on the next API call.
1545 *
1546 * @since 4.14.0
1547 *
1548 * @param string $username WpStream.net username or e-mail.
1549 */
1550 do_action( 'wpstream_account_connected', $username );
1551 } elseif ( '' !== $previous_username ) {
1552 /**
1553 * Fires after the WpStream account username was cleared.
1554 *
1555 * @since 4.14.0
1556 *
1557 * @param string $username The username that was removed.
1558 */
1559 do_action( 'wpstream_account_disconnected', $previous_username );
1560 }
1561 }
1562 }
1563
1564
1565 $allowed_html = array();
1566
1567
1568 // Field definitions for the credentials form (username + password).
1569 $wpstream_options_array =array(
1570 2 => array(
1571 'label' => 'WpStream.net Username or Email',
1572 'name' => 'api_username',
1573 'type' => 'text',
1574 ),
1575 3 => array(
1576 'label' => 'WpStream.net Password',
1577 'name' => 'api_password',
1578 'type' => 'password',
1579 ),
1580
1581 );
1582
1583
1584 // Existing installs saved the password as an autoloaded option; flip it
1585 // to non-autoloaded so the secret is not loaded on every request.
1586 if ( function_exists( 'wp_set_option_autoload' ) ) {
1587 wp_set_option_autoload( 'wpstream_api_password', false );
1588 }
1589
1590 // Connectivity and quota data drive the connection-status banner below.
1591 $connected = $this->main->wpstream_live_connection->is_connected();
1592 $pack_details = $this->main->quota_manager->get_live_quota_data( 'wpstream_set_wpstream_credentials' );
1593
1594 $this->main->show_user_data($pack_details);
1595
1596 print '<form method="post" action="" >';
1597 // CSRF nonce for the credentials save handled at the top of this method.
1598 wp_nonce_field( 'wpstream_save_credentials', 'wpstream_credentials_nonce' );
1599 print '<div class="theme_options_tab_wpstream" style="display:block;" >
1600 <h1>'.__('WpStream Credentials','wpstream').'</h1>';
1601
1602 // Credentials supplied via wp-config.php constants take
1603 // precedence over anything saved here — tell the admin.
1604 if( $credentials_from_constants ){
1605 echo '<div class="api_conected">'.esc_html__('Your WpStream credentials are defined in wp-config.php; values saved on this screen are ignored.','wpstream').'</div>';
1606 }
1607
1608 // Connection status banner: no credentials, bad credentials, connected, or CURL failure.
1609 if( wpstream_get_api_username()=='' || wpstream_get_api_password()== '' ){
1610 echo '<div class="api_not_conected wpstream_orange">';
1611 $admin_url_onboard=get_admin_url().'admin.php?page=wpstream_onboard';
1612 printf ( __('To connect your plugin, enter your WpStream credentials below or go <a href="%s" target="_blank">here</a> to create an account.','wpstream'),$admin_url_onboard);
1613 echo '</div>';
1614
1615 }else if(!$connected){
1616 // Translate the literals at definition — gettext cannot extract variable strings.
1617 $text = get_option('wpstream_curl_failed') === "0" ?
1618 __( ' Incorrect username or password. Please check your credentials or go <a href="https://wpstream.net/my-account/edit-account/" target="_blank">here</a> to reset your password.', 'wpstream' ) :
1619 __( 'Not connected to WpStream. Please note the errors above and contact support.', 'wpstream' );
1620 echo '<div class="api_not_conected">'.wp_kses_post($text).'</div>';
1621 }else{
1622 // Credentials present and a token was obtained: connected.
1623 echo '<div class="api_conected">'.__('Connected to WpStream.net!','wpstream').'</div>';
1624 }
1625 // Render the credential inputs. The username is pre-filled;
1626 // the password field is always left blank so the stored
1627 // secret is never echoed into the page source (an empty
1628 // submission keeps the saved password). Inputs are disabled
1629 // when the wp-config constants are in charge.
1630 $inputs_disabled = $credentials_from_constants ? ' disabled' : '';
1631 print '<div class="wpstream_option_wrapper">';
1632 foreach ($wpstream_options_array as $key=>$option){
1633 print '<div class="wpstream_option">';
1634
1635 print '<label for="'.$option['name'].'">'.$option['label'].'</label>';
1636 if( $option['type']==='password' ){
1637 $placeholder = get_option('wpstream_'.$option['name'],'') !== '' ? '*****' : '';
1638 print '<input id="'.$option['name'].'" type="password" size="36" name="'.$option['name'].'" value="" autocomplete="new-password" placeholder="'.$placeholder.'"'.$inputs_disabled.' />';
1639 }else{
1640 $options_value = esc_html( get_option('wpstream_'.$option['name'],'') );
1641 print '<input id="'.$option['name'].'" type="'.$option['type'].'" size="36" name="'.$option['name'].'" value="'.esc_html($options_value).'"'.$inputs_disabled.' />';
1642 }
1643
1644 print '</div>';
1645 }
1646 print '</div>';
1647
1648
1649 print '<input type="submit"'.$inputs_disabled.' name="submit" class="wpstream_button wpstream_button_action" value="'.__('Save Changes','wpstream').'" />';
1650
1651 print '<h3>Video Tutorials</h3>';
1652
1653 print '<a class="how_to_videos" target="_blank" href="https://youtu.be/9DQrxsKcpmQ">How to Live Stream to WordPress with OBS</a>';
1654 print '<a class="how_to_videos" target="_blank" href="https://youtu.be/qMSjJCskAfM">How to Live Stream to WordPress in less than 3 Minutes</a>';
1655 print '<a class="how_to_videos" target="_blank" href="https://youtu.be/h6myD_vhKcg">How to Live-Stream to WordPress using your iPhone</a>';
1656
1657 print '<a style="margin-top:10px;" href="https://www.youtube.com/channel/UCIjItiJc4Z7aJApj3W6ArJA" target="_blank" class="how_to_videos">More Tutorials On Our YouTube Channel</a>';
1658
1659
1660 print '</div>';
1661 print '</form>';
1662
1663 // Quick-action links: create free / paid channel, or jump to the channels list.
1664 print '<div class="theme_options_tab_wpstream" style="display:block;" >';
1665 $link_new = admin_url('admin.php?page=wpstream_live_channels');
1666 $link_new_paid = admin_url('post-new.php?post_type=product').'&new_stream='. rawurlencode('new');
1667 $link_new_free = admin_url('post-new.php?post_type=wpstream_product');
1668
1669
1670 print '<a href="'.esc_url($link_new_free).'" class="wpstream_no_chanel_add_channel">'.esc_html__('Create new Free-To-View channel','wpstream').'</a>';
1671 print '<a href="'.esc_url($link_new_paid).'" class="wpstream_no_chanel_add_channel">'.esc_html__('Create Pay-Per-View channel','wpstream').'</a>';
1672 print '<a href="'.esc_url($link_new).'" class="wpstream_no_chanel_add_channel">'.esc_html__('My Channels','wpstream').'</a>';
1673 print '</div>';
1674
1675
1676 }
1677
1678
1679
1680 /**
1681 * Media Management
1682 *
1683 * Render the Recordings admin screen: the quota summary, the upload widget,
1684 * and the list of existing recordings. Echoes HTML.
1685 *
1686 * @since 3.0.1
1687 * @return void
1688 */
1689 public function wpstream_media_management(){
1690 // Storage/streaming quota data for the summary header.
1691 $pack_details = $this->main->quota_manager->get_live_quota_data( 'wpstream_media_management' );
1692
1693 $this->main->show_user_data($pack_details);
1694
1695
1696 // Upload widget section.
1697 print '<div id="wpstream_media_upload"><h3>'.__('Upload New Recording','wpstream').'</h3>'.$this->wpstream_present_media_upload().'</div>';
1698
1699 // Existing recordings list section.
1700 print '<div id="wpstream_file_management"><h3 id="video_management_title">'.__('Your Recordings','wpstream').'</h3>'.$this->wpstream_present_file_management().'</div>';
1701
1702
1703 }
1704
1705
1706
1707
1708 /**
1709 *
1710 *
1711 * WpStream Pagination
1712 *
1713 * @since 3.0.1
1714 *
1715 *
1716 */
1717
1718 /**
1719 * Build a numeric pager (first/prev/window/next/last) for list screens.
1720 *
1721 * @param int $pages Total number of pages.
1722 * @param int $range How many page links to show on each side of the current page.
1723 * @return string Pager HTML, or '' when there is a single page / no pages.
1724 */
1725 public function wpstream_pagination($pages , $range = 2) {
1726 $return='';
1727 // Total visible window width around the current page.
1728 $showitems = ($range * 2) + 1;
1729 // Current page from the query string (defaults to 1).
1730 $paged = ( isset( $_GET['paged'] ) ) ? intval($_GET['paged']) : 1;
1731
1732
1733 // Only render a pager when there is more than one page.
1734 if (1 != $pages && $pages != 0) {
1735 $return.= '<ul class="pagination wpstream_pagination">';
1736 // "Previous" arrow.
1737 $return.= "<li class=\"roundleft\"><a href='" . get_pagenum_link($paged - 1) . "'><</a></li>";
1738
1739 $last_page = get_pagenum_link($pages);
1740 // Emit page-number links, but only those within the visible window (or all if they fit).
1741 for ($i = 1; $i <= $pages; $i++) {
1742 if (1 != $pages && (!($i >= $paged + $range + 1 || $i <= $paged - $range - 1) || $pages <= $showitems )) {
1743 if ($paged == $i) {
1744 // Current page marker.
1745 $return.= '<li class="active"><a href="' . esc_url(get_pagenum_link($i)) . '" >' . $i . '</a><li>';
1746 } else {
1747 $return.= '<li><a href="' . esc_url(get_pagenum_link($i)) . '" >' . $i . '</a><li>';
1748 }
1749 }
1750 }
1751
1752 // "Next" target, clamped to the last page.
1753 $prev_page = get_pagenum_link($paged + 1);
1754 if (($paged + 1) > $pages) {
1755 $prev_page = get_pagenum_link($paged);
1756 } else {
1757 $prev_page = get_pagenum_link($paged + 1);
1758 }
1759
1760
1761 // "Next" and "Last" arrows, then close the list.
1762 $return.= "<li class=\"roundright\"><a href='" . $prev_page . "'>></a><li>";
1763 $return.= "<li class=\"roundright\"><a href='" . $last_page . "'>>><li>";
1764 $return.= "</ul>";
1765 }
1766 return $return;
1767 }
1768
1769
1770
1771
1772
1773
1774 /**
1775 * Media upload
1776 *
1777 * Build the S3 direct-upload widget for new recordings: checks storage
1778 * quota and API connectivity, then renders a multipart form pre-populated
1779 * with the signed S3 upload fields.
1780 *
1781 * @since 3.0.1
1782 * @return string Upload widget HTML, or an alert/notice when unavailable.
1783 */
1784 public function wpstream_present_media_upload(){
1785 $to_return='';
1786
1787 // Refuse when the account is out of storage/data quota.
1788 if ( ! $this->main->quota_manager->has_storage_quota( null, 'recordings_screen' ) ) {
1789 return '<div class="wpstream_upload_alert">'.esc_html__('You don\'t have enough cloud storage or data to upload a new item. Please delete some videos or upgrade your plan.','wpstream').'</div>';
1790 }
1791
1792 // Request the signed S3 POST fields from the cloud API.
1793 $formInputs=$this->main->wpstream_live_connection->wpstream_get_signed_form_upload_data();
1794
1795 // On failure, show either a "not connected" or an "out of quota" message.
1796 if( !$formInputs['success'] ){
1797 if ($formInputs['error'] == 'not_connected'){
1798 $to_return.='<div class="wpstream_upload_container">'.esc_html__('Not connected. Please connect to WpStream to upload videos.','wpstream').'</div>';
1799 }
1800 else {
1801 $to_return.='<div class="wpstream_upload_alert">'.esc_html__('You don\'t have enough cloud storage and data to upload a new item. Please delete some videos or upgrade your plan.','wpstream').'</div>';
1802 }
1803 return $to_return;
1804 }
1805
1806 // Signed data obtained: render the direct-to-S3 upload form.
1807 if($formInputs['success'] ===true){
1808
1809
1810
1811 $to_return.='<div class="wpstream_upload_container">';
1812 $to_return.='<div id="wpstream_uploaded_mes">'.esc_html__('Please select or drop a video file. Do not close this window during the upload!','wpstream').'</div>';
1813 $to_return.='<form action="https://wpstream-video.s3.amazonaws.com/"
1814 method="POST"
1815 enctype="multipart/form-data"
1816 data-singleFileUploads="true"
1817 data-limitMultiFileUploads="1"
1818 data-limitConcurrentUploads="1"
1819 class="direct-upload">';
1820
1821 $to_return.='<input id="wpstream_upload" type="file" class="inputfile inputfile-1" value="Pick a video file" name="file" multiple>';
1822 $to_return.='<label for="wpstream_upload"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" viewBox="0 0 20 17"><path d="M10 0l-5.2 4.9h3.3v5.1h3.8v-5.1h3.3l-5.2-4.9zm9.3 11.5l-3.2-2.1h-2l3.4 2.6h-3.5c-.1 0-.2.1-.2.1l-.8 2.3h-6l-.8-2.2c-.1-.1-.1-.2-.2-.2h-3.6l3.4-2.6h-2l-3.2 2.1c-.4.3-.7 1-.6 1.5l.6 3.1c.1.5.7.9 1.2.9h16.3c.6 0 1.1-.4 1.3-.9l.6-3.1c.1-.5-.2-1.2-.7-1.5z"/></svg> <span id="wpstream_label_action">' . esc_html__('Choose a file&hellip;','wpstream') . '</span></label>';
1823
1824
1825 $to_return.='<div class="wpstream_file_drop_color">';
1826 $to_return.='<div class="wpstream_form_ex">'.esc_html__('Drop a video file here!','wpstream').'</div>';
1827 $to_return.='<div class="wpstream_form_ex_details">'.__('The Video File must be encoded with the following settings:<br>
1828
1829 Container: <strong>MP4</strong>,<br>
1830 Video codec: <strong>H264</strong>,<br>
1831 Audio codec: <strong>AAC</strong>.<br>
1832 Media will fail to play if it does not follow the above settings.
1833 You may use a tool like MediaInfo to verify your file. Also you may convert it with specialized software like HandBrake.','wpstream').'<strong> '.esc_html__('Accepted file extensions: .mp4, .mov','wpstream').'</strong></div>';
1834 // Inject each signed S3 field as a hidden input so the browser POST is authorized.
1835 if(is_array($formInputs)){
1836 foreach ($formInputs['ref'] as $name => $value) {
1837 $to_return.='<input type="hidden" name="'. $name.'" value="'.$value.'">';
1838 }
1839 }
1840
1841 $to_return.='
1842 <div class="progress-bar-area"></div></div>
1843 </form>';
1844
1845 $to_return.='</div>';
1846 }
1847
1848 return $to_return;
1849
1850 }
1851
1852
1853
1854
1855
1856 /**
1857 * Display movie list
1858 *
1859 * Fetch the account's recordings from the cloud API and render them:
1860 * still-processing ("pending") items first, then completed items with
1861 * delete/download links and "create VOD from this recording" actions.
1862 *
1863 * @since 3.0.1
1864 * @return string Recordings list HTML, or a notice when not connected / empty.
1865 */
1866 public function wpstream_present_file_management(){
1867 // Pull the raw recordings payload from the API.
1868 $video_list_raw = $this->main->wpstream_live_connection->wpstream_get_videos_from_api();
1869
1870 // false means the API call failed / not connected.
1871 if ( $video_list_raw === false ) {
1872 return '<div class="wpstream_upload_container">'.esc_html__('Not connected. Please connect to WpStream to upload videos.','wpstream').'</div>';
1873 }
1874
1875 // Normalise the completed-items list.
1876 $video_list_raw_array = [];
1877 if( isset( $video_list_raw['items'] ) ){
1878 $video_list_raw_array = $video_list_raw['items'];
1879 }
1880
1881 // Sort completed items newest-first by their 'time' field.
1882 $keys = array_column( $video_list_raw_array, 'time' );
1883 array_multisort($keys, SORT_DESC , $video_list_raw_array);
1884
1885 $to_return='';
1886
1887 // show pending items
1888 // Render still-processing uploads with a "processing" badge.
1889 if ( key_exists( 'pending', $video_list_raw ) && is_array( $video_list_raw['pending'] ) ) {
1890 foreach ( $video_list_raw['pending'] as $key => $video ) {
1891 $video_size = intval($video['size']/1048576);
1892 $video_name = esc_html($video['name']);
1893 if($video_name!=''):
1894 $to_return.='<div class="wpstream_video_wrapper">';
1895 $to_return.='<div class="wpstream_video_title">';
1896 $to_return.='<div class="wpstream_video_notice"></div></div>';
1897 $to_return.='<div class="wpstream_video_title"><strong class="storage_file_name">'.esc_html__('File Name :','wpstream').'</strong>'.'<span class="storage_file_name_real">'.$video_name.'</span><span class="storage_file_size">'.$video_size.' MB </span></div>';
1898 $to_return.='<div class="wpstream_video_pending">' . esc_html__( 'The video is still processing', 'wpstream') . '</div>';
1899 $to_return.='</div>';
1900 endif;
1901
1902 }
1903 }
1904
1905 // show uploaded items
1906 // Render each completed recording with its size, delete/download controls and VOD-creation links.
1907 if( is_array($video_list_raw['items'] ) ) {
1908 foreach ($video_list_raw_array as $key =>$video){
1909 // Keep the service-provided Source Recording identity
1910 // separate from its context-specific HTML rendering.
1911 $video_size = intval($video['size']/1048576);
1912 $video_source_name = (string) $video['name'];
1913 $video_name = esc_html($video_source_name);
1914 if($video_name!=''):
1915 $to_return.='<div class="wpstream_video_wrapper">';
1916
1917 $to_return.='<div class="wpstream_video_title">';
1918 $to_return.='<div class="wpstream_video_notice"></div></div>';
1919 $to_return.='<div class="wpstream_video_title"><strong class="storage_file_name">'.esc_html__('File Name:','wpstream').'</strong>'.'<span class="storage_file_name_real">'.$video_name.'</span><span class="storage_file_size">'.$video_size.' MB </span></div>';
1920 // Delete control. The shared JS utility owns the
1921 // confirmation; the renderer supplies inert data
1922 // only so any valid recording name remains safe.
1923 $to_return.='<div class="wpstream_delete_media" data-filename="'.esc_attr($video_source_name).'">'.esc_html__('delete file','wpstream').'</div>';
1924 // Download trigger (JS fetches a time-limited signed URL) and the resulting link.
1925 $to_return.='<div class="wpstream_get_download_link" data-filename="'.$video_name.'">'.esc_html__('download','wpstream').'</div>';
1926 $to_return.='<a href="" class="wpstream_download_link">'.esc_html__('Click to download! The url will work for the next 20 minutes!','wpstream').'</a>';
1927
1928 // Pre-fill new-VOD links (free and, if WooCommerce present, paid) with this recording's name.
1929 $add_free_video_url=admin_url('post-new.php?post_type=wpstream_product_vod').'&new_video_name='. rawurlencode($video_source_name);
1930 $add_paid_video_url=admin_url('post-new.php?post_type=product').'&new_video_name='. rawurlencode($video_source_name);
1931
1932
1933
1934 $to_return .='<a class="create_new_free_video" href="'.esc_url($add_free_video_url).'">'.esc_html__('Create new Free-To-View VOD from this recording').'</a>';
1935 if (class_exists('WooCommerce')) {
1936 $to_return .='<a class="create_new_ppv_video" href="'.esc_url($add_paid_video_url).'">'.esc_html__('Create new Pay-Per-View VOD from this recording').'</a>';
1937 }
1938
1939 $to_return.='</div>';
1940 endif;
1941
1942 }
1943 $current_page= get_current_screen();
1944 }
1945
1946 // no items to show
1947 // Empty-state message when there are neither completed nor pending items.
1948 if ( !is_array( $video_list_raw['items'] ) || ( key_exists( 'pending', $video_list_raw ) && !is_array( $video_list_raw['pending'] ) ) ) {
1949 $to_return.= '<div class="wpstream_video_wrapper">'.esc_html__('You don\'t have any videos.','wpstream').'</div>';
1950 }
1951 return $to_return;
1952 }
1953
1954
1955 /**
1956 * Preserve the legacy free-Channel publish marker.
1957 *
1958 * Channel Settings initialization is coordinated by the Streaming Content
1959 * Creation Module; this compatibility hook only maintains historical meta.
1960 *
1961 * @since 3.0.1
1962 * @param int $post_id Post ID being published.
1963 * @param WP_Post $post Post object being published.
1964 * @return void
1965 */
1966 public function wpstream_publish_wpstream_product($post_id,$post){
1967 // Only applies to free channel posts.
1968 if( $post->post_type == 'wpstream_product' ){
1969 // Preserve the legacy marker used by existing installations.
1970 update_post_meta ($post_id,'local_event_options_test','working_on_'.$post_id);
1971 }
1972 }
1973
1974 /**
1975 * Initialize existing Streaming Content when it is published or saved.
1976 *
1977 * The shared Module performs safe legacy repair and retries cloud provisioning
1978 * for Live Channels. VODs receive only their applicable local initialization.
1979 *
1980 * @param int $post_id Post ID being published.
1981 * @param WP_Post $post Post object being published.
1982 * @return void
1983 */
1984 public function wpstream_create_remote_channel_on_publish( $post_id, $post ) {
1985 // Skip autosaves and revisions (they are not real publishes).
1986 if ( defined( 'DOING_AUTOSAVE') && DOING_AUTOSAVE ) {
1987 return;
1988 }
1989 if ( wp_is_post_revision( $post_id ) ) {
1990 return;
1991 }
1992 $this->main->streaming_content_creation->initialize_existing( $post_id );
1993 }
1994
1995 /**
1996 * Initialize a manually saved paid streaming product after WooCommerce
1997 * has persisted its product-type taxonomy. The earlier publish hook may
1998 * run while a new product still looks like `simple`; this later adapter
1999 * enters the same idempotent creation policy once `live_stream` or
2000 * `video_on_demand` is observable.
2001 *
2002 * @param int $post_id WooCommerce product ID.
2003 * @return void
2004 */
2005 public function wpstream_initialize_paid_streaming_product( $post_id ) {
2006 if ( 'product' !== get_post_type( $post_id ) ) {
2007 return;
2008 }
2009 if ( ! has_term( array( 'live_stream', 'video_on_demand' ), 'product_type', $post_id ) ) {
2010 return;
2011 }
2012 $this->main->streaming_content_creation->initialize_existing( $post_id );
2013 }
2014
2015 /**
2016 * Delete the remote channel when its Live Channel post is deleted for good.
2017 *
2018 * Hooked to before_delete_post: fires only on permanent deletion, never on
2019 * trash, so trashing stays reversible and a restored channel keeps its
2020 * cloud identity. Only a Live Channel that was actually Provisioned
2021 * (`channelId` meta present) has anything at the service to remove.
2022 *
2023 * The local deletion proceeds whatever the service answers — this hook
2024 * cannot cancel it, and a channel the customer removed must leave the
2025 * site either way.
2026 *
2027 * @param int $post_id Post ID being permanently deleted.
2028 * @param WP_Post $post Post object being permanently deleted.
2029 * @return void
2030 */
2031 public function wpstream_delete_remote_channel_on_delete( $post_id, $post ) {
2032 // Only Live Channel posts: the free post type, or a WooCommerce
2033 // product of the live_stream type (paid channels).
2034 $is_live_channel = $post instanceof WP_Post
2035 && ( 'wpstream_product' === $post->post_type
2036 || ( 'product' === $post->post_type && has_term( 'live_stream', 'product_type', $post_id ) ) );
2037 if ( ! $is_live_channel ) {
2038 return;
2039 }
2040
2041 // A channel that never obtained a cloud identity has nothing to delete.
2042 if ( '' === (string) get_post_meta( $post_id, 'channelId', true ) ) {
2043 return;
2044 }
2045
2046 $this->main->wpstream_live_connection->wpstream_delete_channel( $post_id );
2047 }
2048
2049 /**
2050 * Restore a paid Live Channel as a draft instead of immediately putting
2051 * it back on sale. WooCommerce restores ordinary products to their
2052 * previous status; a channel requires the seller to republish it.
2053 *
2054 * @param string $new_status Status selected by earlier filters.
2055 * @param int $post_id Restored post ID.
2056 * @param string $previous_status Status before trashing.
2057 * @return string
2058 */
2059 public function wpstream_paid_live_untrash_status( $new_status, $post_id, $previous_status ) {
2060 if ( 'product' === get_post_type( $post_id ) && has_term( 'live_stream', 'product_type', $post_id ) ) {
2061 return 'draft';
2062 }
2063 return $new_status;
2064 }
2065
2066 /** Invalidate VOD DRM lookup state whenever publication visibility changes. */
2067 public function wpstream_invalidate_vod_drm_on_status_change( $new_status, $old_status, $post ) {
2068 if ( $post instanceof WP_Post && 'wpstream_product_vod' === $post->post_type && $new_status !== $old_status ) {
2069 $this->main->drm_key_delivery->invalidate_vod_lookup( $post->ID );
2070 }
2071 }
2072
2073 /** Invalidate VOD DRM lookup state before its key-index metadata is deleted. */
2074 public function wpstream_invalidate_vod_drm_on_delete( $post_id, $post ) {
2075 if ( $post instanceof WP_Post && 'wpstream_product_vod' === $post->post_type ) {
2076 $this->main->drm_key_delivery->invalidate_vod_lookup( $post_id );
2077 }
2078 }
2079
2080 /**
2081 * save meta options
2082 *
2083 * Persist the allow-listed free channel / VOD meta fields on save for the
2084 * `wpstream_product` and `wpstream_product_vod` post types. Hooked to the
2085 * post save action.
2086 *
2087 * @since 3.0.1
2088 * @param int $post_id Post ID being saved.
2089 * @param WP_Post $post Post object being saved.
2090 * @return void
2091 */
2092 public function wpstream_free_product_update_post($post_id,$post){
2093
2094 // Guard against non-post callers.
2095 if(!is_object($post) || !isset($post->post_type)) {
2096 return;
2097 }
2098
2099
2100
2101
2102
2103 // Only handle the free channel and VOD post types.
2104 if( $post->post_type == 'wpstream_product' ||
2105 $post->post_type == 'wpstream_product_vod' ):
2106 $previous_vod_type = 'wpstream_product_vod' === $post->post_type
2107 ? intval( get_post_meta( $post_id, 'wpstream_product_type', true ) )
2108 : 0;
2109 $previous_recording = 'wpstream_product_vod' === $post->post_type
2110 ? (string) get_post_meta( $post_id, 'wpstream_free_video', true )
2111 : '';
2112
2113 // Meta keys this handler is allowed to write.
2114 $allowed_keys=array(
2115 'wpstream_product_type',
2116 'wpstream_free_video',
2117 'wpstream_free_video_external',
2118 'wpstream_closed_captions_file'
2119 );
2120
2121
2122 // Save each allow-listed, scalar POST field as sanitized post meta.
2123 foreach ($_POST as $key => $value) {
2124 if( !is_array ($value) ){
2125 if (in_array ($key, $allowed_keys)) {
2126 $postmeta = sanitize_text_field ( $value );
2127 update_post_meta($post_id, sanitize_key($key), $postmeta );
2128 }
2129 }
2130 }
2131
2132 if ( 'wpstream_product_vod' === $post->post_type && isset( $_POST['wpstream_product_type'] ) ) {
2133 $new_vod_type = intval( $_POST['wpstream_product_type'] );
2134 if ( 3 === $new_vod_type ) {
2135 $this->wpstream_retire_vod_recording_identity( $post_id );
2136 delete_post_meta( $post_id, 'wpstream_free_video' );
2137 } elseif ( 2 === $new_vod_type ) {
2138 $new_recording = isset( $_POST['wpstream_free_video'] ) && is_scalar( $_POST['wpstream_free_video'] )
2139 ? sanitize_text_field( wp_unslash( $_POST['wpstream_free_video'] ) )
2140 : $previous_recording;
2141 if ( 2 !== $previous_vod_type || $new_recording !== $previous_recording ) {
2142 $this->wpstream_retire_vod_recording_identity( $post_id );
2143 }
2144 delete_post_meta( $post_id, 'wpstream_free_video_external' );
2145 }
2146 }
2147
2148 endif;
2149
2150 }
2151
2152 /** Retire key and hosted-player metadata owned by a VOD's old recording. */
2153 private function wpstream_retire_vod_recording_identity( $post_id ) {
2154 $this->main->drm_key_delivery->invalidate_vod_lookup( $post_id );
2155 foreach ( array( 'hlsDecryptionKey', 'hlsDecryptionKeyIndex', 'wpstream_vod_video_data', 'wpstream_vod_embed_key', 'wpstream_vod_embed_url' ) as $meta_key ) {
2156 delete_post_meta( $post_id, $meta_key );
2157 }
2158 }
2159
2160
2161 /**
2162 * save meta options
2163 *
2164 * Register the plugin's post metaboxes: VOD settings on `wpstream_product_vod`,
2165 * the video collection box on `wpstream_bundles`, and (for WooCommerce bundle
2166 * products) the video-collection options box on `product`. Hooked to add_meta_boxes.
2167 *
2168 * @since 3.0.1
2169 * @return void
2170 */
2171 public function add_wpstream_product_metaboxes() {
2172 global $post;
2173 $post_id = $post->ID;
2174
2175
2176 // VOD settings metabox and the bundle video-collection metabox.
2177 add_meta_box( 'add_wpstream_product_metaboxes-sectionid', esc_html__( 'Video On Demand Settings', 'wpstream' ),array($this,'display_meta_options'),'wpstream_product_vod' ,'normal','default');
2178 add_meta_box( 'custom_metabox_video_collection', esc_html__( 'Video Collection', 'wpstream' ), 'wpstream_bundle_custom_metabox_callback', 'wpstream_bundles', 'normal', 'high' );
2179
2180 // For WooCommerce bundle products, add the video-collection options box on the product screen.
2181 if(function_exists('wc_get_product')):
2182 $product = wc_get_product( $post_id );
2183 if ( $product ) {
2184
2185 if ( $product->get_type() === 'wpstream_bundle' ) {
2186 add_meta_box(
2187 'wpstream_woo_custom_metabox',
2188 esc_html__( 'Video Collection Options', 'wpstream' ),
2189 'wpstream_bundle_custom_metabox_callback',
2190 'product',
2191 'normal',
2192 'default'
2193 );
2194 }
2195 }
2196 endif;
2197
2198 /**
2199 * Fires after the plugin registered its metaboxes for the edit screen.
2200 *
2201 * @since 4.14.0
2202 *
2203 * @param int $post_id Post being edited.
2204 * @param WP_Post $post The post.
2205 */
2206 do_action( 'wpstream_registered_metaboxes', intval( $post_id ), $post );
2207 }
2208
2209
2210 /**
2211 * make woocomerce virtual products
2212 *
2213 * Force WpStream WooCommerce product types (live_stream, video_on_demand,
2214 * wpstream_bundle) to be virtual so they need no shipping. Hooked to save.
2215 *
2216 * @since 3.0.1
2217 * @param int $post_id Post ID being saved (unused; uses global $post).
2218 * @param WP_Post $post Post object (overwritten by global $post).
2219 * @return void
2220 */
2221 public function wpstream_make_product_virtual($post_id,$post){
2222 global $post;
2223 if(isset($post->ID)){
2224 // Only WooCommerce products are relevant.
2225 if ( $post->post_type !== 'product' ) return;
2226 // Mark WpStream product types as virtual.
2227 $term_list = wp_get_post_terms($post->ID, 'product_type');
2228 if( !empty($term_list) &&
2229 isset($term_list[0]->slug) &&
2230 in_array($term_list[0]->slug, ['live_stream', 'video_on_demand', 'wpstream_bundle'])
2231 ){
2232 update_post_meta( $post->ID, '_virtual', 'yes' );
2233 }
2234 }
2235 }
2236
2237
2238
2239 /**
2240 * render meta options
2241 *
2242 * Render the "Video On Demand Settings" metabox: media-type selector,
2243 * recording chooser, optional captions (.vtt) picker, and the self-hosted/
2244 * external video URL field. Echoes HTML.
2245 *
2246 * @since 3.0.1
2247 * @param WP_Post $post Post being edited (overwritten by global $post).
2248 * @return void
2249 */
2250 public function display_meta_options( $post ) {
2251 // Nonce for the metabox save + use the global post object.
2252 wp_nonce_field( plugin_basename( __FILE__ ), 'estate_agent_noncename' );
2253 global $post;
2254
2255 // Determine the pre-selected media type / video.
2256 $is_live = '';
2257 $is_video = '';
2258 $is_video_external = '';
2259 // When arriving from "create VOD from recording", preselect the recording.
2260 if( isset( $_GET['new_video_name']) && $_GET['new_video_name']!='' ){
2261 $is_video = ' selected ';
2262 $wpstream_free_video = esc_html( $_GET['new_video_name']);
2263 }else{
2264 // Otherwise read the saved type/video from post meta and select accordingly.
2265 $wpstream_product_type = esc_html(get_post_meta($post->ID, 'wpstream_product_type', true));
2266 $wpstream_free_video = esc_html(get_post_meta($post->ID, 'wpstream_free_video', true));
2267
2268 if($wpstream_product_type==1){
2269 $is_live = ' selected ';
2270 }
2271
2272 if($wpstream_product_type==2){
2273 $is_video = ' selected ';
2274 }
2275
2276 if($wpstream_product_type==3){
2277 $is_video_external = ' selected ';
2278 }
2279 }
2280
2281 // Media-type dropdown (recording vs self-hosted/external).
2282 print'
2283 <p class="meta-options">
2284 <label for="wpstream_product_type">'.__('Media Type:','wpstream').' </label><br />
2285 <select id="wpstream_product_type" name="wpstream_product_type">
2286 <option value="2" '.$is_video.'>'.__('Recording','wpstream').'</option>
2287 <option value="3" '.$is_video_external.'>'.__('Self Hosted or External Video','wpstream').'</option>
2288 </select>
2289 </p>
2290 ';
2291
2292
2293 // Fetch the account's recordings to populate the chooser.
2294 $video_list = $this->main->wpstream_live_connection->wpstream_get_videos();
2295
2296
2297 // Recording chooser dropdown, pre-selecting the saved recording.
2298 print '<div class="meta-options video_free">';
2299 print '<p class="meta-option wpstream_free_video">';
2300 print '<label for="wpstream_free_video">'.__('Choose video:','wpstream').' </label><br />
2301 <select id="wpstream_free_video" name="wpstream_free_video">';
2302
2303 if( is_array( $video_list ) ) {
2304 foreach ($video_list as $key=>$value){
2305 print '<option value="'.$key.'"';
2306 if($wpstream_free_video === $key){
2307 print ' selected ';
2308 }
2309 print '>'.$value.'</option>';
2310 }
2311 }
2312 print'</select>';
2313 print '</p> ';
2314 print '</div>';
2315
2316 // Optional captions (.vtt) picker; hide the select button when a file is already set.
2317 $wpstream_closed_captions_file = get_post_meta($post->ID, 'wpstream_closed_captions_file', true);
2318
2319 $button_style = $wpstream_closed_captions_file ? 'style="display:none;"' : '';
2320
2321 print '<p class="meta-option wpstream_vod_captions_url">';
2322 print '<label for="wpstream_vod_captions_url_button">'.__('Captions file (optional):','wpstream').' </label><br />
2323 <input type="hidden" id="wpstream_closed_captions_file" name="wpstream_closed_captions_file" value="'.esc_attr($wpstream_closed_captions_file).'" />
2324 <input id="wpstream_vod_captions_url_button" type="button" class="upload_button button" value="'.esc_html__('Select .vtt Captions File','wpstream').'" '.$button_style.' />
2325 <span class="wpstream_caption_file_display">'.( $wpstream_closed_captions_file ? esc_html( basename( $wpstream_closed_captions_file ) ) : '' ).'</span>';
2326 if ( $wpstream_closed_captions_file ) {
2327 print '<input type="button" class="button wpstream_remove_caption" value="'.esc_html__('Remove','wpstream').'" style="margin-left: 5px;" />';
2328 }
2329 print '</p> ';
2330
2331 // Self-hosted / external video URL field with a media-library select button.
2332 $wpstream_free_video_external= esc_html(get_post_meta($post->ID, 'wpstream_free_video_external', true));
2333 print '<div class="meta-options1 video_free_external">
2334 <label for="wpstream_free_video_external">'.__('Video:','wpstream').' </label><br />
2335
2336 <input id="wpstream_free_video_external" type="text" size="36" name="wpstream_free_video_external" value="'.$wpstream_free_video_external.'" />
2337 <input id="wpstream_free_video_external_button" type="button" size="40" class="upload_button button" value="'.esc_html__('Select Video','wpstream').'" />';
2338 // Show the recording hint vs the external hint depending on the saved media type.
2339 if($wpstream_product_type==2){
2340 $show_recording='';
2341 $show_external='style="display:none"';
2342 }else{
2343 $show_recording='style="display:none"';
2344 $show_external='';
2345 }
2346 print '<p '.$show_recording.' class="wpstream_option_vod_source wpstream_show_recording">'.esc_html__('Choose one of your existing recordings.','wpstream').'</p>';
2347 print '<p '. $show_external.' class="wpstream_option_vod_source wpstream_show_external">'.esc_html__('Upload a video from your computer or paste the URL of a YouTube/external video.','wpstream').'</p>';
2348
2349 print '</div> ';
2350
2351 /**
2352 * Fires at the end of the Video On Demand Settings metabox.
2353 *
2354 * Callbacks echo their own fields and own their escaping; saving
2355 * them is the callback's job (hook `save_post`).
2356 *
2357 * @since 4.14.0
2358 *
2359 * @param int $post_id VOD post ID.
2360 * @param WP_Post $post The post.
2361 */
2362 do_action( 'wpstream_vod_meta_options_after', intval( $post->ID ), $post );
2363 }
2364
2365
2366
2367
2368
2369
2370
2371 /**
2372 * Add new product types to Woocommerce select product type
2373 *
2374 * Register the "Live Channel" and "Video On Demand" WooCommerce product
2375 * types in the product-type dropdown. Filter callback.
2376 *
2377 * @since 3.0.1
2378 * @param array $types Existing product-type => label map.
2379 * @return array The map with the WpStream types added.
2380 */
2381 public function wpstream_add_products( $types ){
2382 $types[ 'live_stream' ] = __( 'Live Channel','wpstream' );
2383 $types[ 'video_on_demand' ] = __( 'Video On Demand','wpstream' );
2384
2385 return $types;
2386 }
2387
2388 /**
2389 * Map the WpStream product-type slugs to their WC_Product subclasses.
2390 * Filter callback for woocommerce_product_class.
2391 *
2392 * @param string $classname Default product class name.
2393 * @param string $product_type The product type slug being instantiated.
2394 * @return string The resolved product class name.
2395 */
2396 public function wpstream_add_products_class( $classname, $product_type ) {
2397 /**
2398 * Filter the product-type slug => WC_Product subclass map.
2399 *
2400 * A mapped class must exist; unknown classes leave WooCommerce's own
2401 * resolution in place.
2402 *
2403 * @since 4.14.0
2404 *
2405 * @param array $map Slug => class name.
2406 */
2407 $map = (array) apply_filters(
2408 'wpstream_product_class_map',
2409 array(
2410 'live_stream' => 'WC_Product_Live_Stream',
2411 'video_on_demand' => 'WC_Product_Video_On_Demand',
2412 )
2413 );
2414 if ( isset( $map[ $product_type ] ) && class_exists( $map[ $product_type ] ) ) {
2415 $classname = $map[ $product_type ];
2416 }
2417 return $classname;
2418 }
2419
2420 /**
2421 * Load the custom WC_Product subclass files when WooCommerce is active.
2422 *
2423 * @return void
2424 */
2425 public function wpstream_add_custom_wc_products() {
2426 // Require the live-stream and VOD product classes (WooCommerce only).
2427 if( class_exists( 'WooCommerce' ) ){
2428 require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-wc-product-live-stream.php';
2429 require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-wc-product-video-on-demand.php';
2430 }
2431 }
2432
2433 /**
2434 * Js action to do when user pick live stream or video on demand
2435 *
2436 * @since 3.0.1
2437 */
2438
2439 /**
2440 * Whether WpStream WooCommerce product UI should load for this product.
2441 *
2442 * Skips simple, course, and other non-WpStream types so LearnDash and
2443 * other integrations can use the General product data tab without conflict.
2444 *
2445 * @param int $post_id Product post ID. Uses global $post when 0.
2446 * @return bool
2447 */
2448 public function wpstream_is_wpstream_wc_product_context( $post_id = 0 ) {
2449 // Creating a new stream/VOD from a query-string action always counts.
2450 if ( isset( $_GET['new_stream'] ) || isset( $_GET['new_video_name'] ) ) {
2451 return true;
2452 }
2453
2454 // On the first product save WooCommerce has not necessarily made
2455 // the selected taxonomy term observable through wc_get_product()
2456 // yet. Trust only its allow-listed product-type form field here;
2457 // the save handler remains scoped to WooCommerce product metadata.
2458 if ( isset( $_POST['product-type'] ) && is_scalar( $_POST['product-type'] ) ) {
2459 $submitted_type = sanitize_key( wp_unslash( $_POST['product-type'] ) );
2460 if ( in_array( $submitted_type, array( 'live_stream', 'video_on_demand', 'wpstream_bundle', 'subscription' ), true ) ) {
2461 return true;
2462 }
2463 }
2464
2465 // Fall back to the current global post when no ID was passed.
2466 if ( ! $post_id ) {
2467 global $post;
2468 $post_id = ( $post && isset( $post->ID ) ) ? (int) $post->ID : 0;
2469 }
2470
2471 // Need a product ID and WooCommerce to resolve the type.
2472 if ( ! $post_id || ! function_exists( 'wc_get_product' ) ) {
2473 return false;
2474 }
2475
2476 $product = wc_get_product( $post_id );
2477 if ( ! $product ) {
2478 return false;
2479 }
2480
2481 // True only for WpStream (and subscription) product types.
2482 return in_array(
2483 $product->get_type(),
2484 array( 'live_stream', 'video_on_demand', 'wpstream_bundle', 'subscription' ),
2485 true
2486 );
2487 }
2488
2489 /**
2490 * WooCommerce product types that use the core Regular price field.
2491 *
2492 * @return string[]
2493 */
2494 public function wpstream_get_wc_product_types_with_pricing() {
2495 return array( 'live_stream', 'video_on_demand', 'wpstream_bundle' );
2496 }
2497
2498 /**
2499 * Add show_if_* classes to the pricing panel for WpStream product types.
2500 *
2501 * WooCommerce renders .options_group.pricing with show_if_simple only. Custom
2502 * types rely on show_if_{type} classes so meta-boxes-product.js can toggle them
2503 * when the product type dropdown changes, including on unsaved new products.
2504 *
2505 * @param string $hook Admin screen hook suffix.
2506 */
2507 public function wpstream_enqueue_wc_product_pricing_visibility( $hook ) {
2508 // Only on the add/edit-post admin screens.
2509 if ( ! in_array( $hook, array( 'post.php', 'post-new.php' ), true ) ) {
2510 return;
2511 }
2512
2513 // Only on the WooCommerce product post type.
2514 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
2515 if ( ! $screen || 'product' !== $screen->post_type ) {
2516 return;
2517 }
2518
2519 // Need WooCommerce's product meta-boxes script to attach to.
2520 if ( ! wp_script_is( 'wc-admin-product-meta-boxes', 'registered' ) ) {
2521 return;
2522 }
2523
2524 // Pass the WpStream pricing types to JS and add show_if_{type} classes so the pricing panel toggles correctly.
2525 $types_json = wp_json_encode( array_values( $this->wpstream_get_wc_product_types_with_pricing() ) );
2526
2527 wp_add_inline_script(
2528 'wc-admin-product-meta-boxes',
2529 "jQuery( function( $ ) {
2530 var wpstreamPricingProductTypes = {$types_json};
2531 var \$pricing = $( '.options_group.pricing' );
2532 var \$soldIndividually = $( '._sold_individually_field' ).parent();
2533
2534 wpstreamPricingProductTypes.forEach( function( type ) {
2535 \$pricing.addClass( 'show_if_' + type );
2536 \$soldIndividually.addClass( 'show_if_' + type );
2537 } );
2538
2539 if ( wpstreamPricingProductTypes.indexOf( $( '#product-type' ).val() ) !== -1 ) {
2540 \$pricing.show();
2541 \$soldIndividually.show();
2542 }
2543 } );"
2544 );
2545 }
2546
2547
2548 /**
2549 * Add custom classes to the product types
2550 *
2551 * Adjust the WooCommerce product-data tab visibility classes so WpStream
2552 * types hide the Shipping tab and show the Inventory tab. Filter callback.
2553 *
2554 * @since 3.0.1
2555 * @param array $tabs WooCommerce product data tabs config.
2556 * @return array The modified tabs config.
2557 */
2558 public function wpstream_hide_attributes_data_panel( $tabs) {
2559
2560 // Hide Shipping and show Inventory for WpStream product types.
2561 $tabs['shipping']['class'][] = 'hide_if_live_stream hide_if_video_on_demand hide_if_wpstream_bundle';
2562 $tabs['inventory']['class'][] = 'show_if_live_stream show_if_video_on_demand show_if_wpstream_bundle';
2563
2564 return $tabs;
2565 }
2566
2567
2568
2569 /**
2570 * Hide buy now on products if Netflix mode
2571 *
2572 * When global subscription ("Netflix") mode is on, mark WpStream media
2573 * products as not individually purchasable (access comes via the
2574 * subscription instead). Filter callback for is_purchasable.
2575 *
2576 * @since 3.12
2577 * @param bool $purchaseable_product_wpblog Current purchasable flag.
2578 * @param WC_Product $product The product being checked.
2579 * @return bool False for WpStream media in global-sub mode, else the original flag.
2580 */
2581 public function wpstream_hide_buy_now_subscription_mode( $purchaseable_product_wpblog,$product){
2582 $product_id=$product->get_id();
2583
2584 // Stable type slug is '' when the product has no term (never a crash).
2585 $product_type_slug = wpstream_get_product_type_slug($product_id);
2586
2587 // Global subscription ("Netflix") mode flag.
2588 $subscription_model = intval( get_option('wpstream_global_sub','')) ;
2589
2590 if($subscription_model==1){ // if we have Neflix mode
2591 // WpStream media types are not directly purchasable in this mode.
2592 if( $product_type_slug=='live_stream' || $product_type_slug=='video_on_demand' || $product_type_slug=='wpstream_bundle' ){
2593 return false;
2594 }
2595 }
2596
2597 return $purchaseable_product_wpblog;
2598 }
2599
2600
2601
2602
2603
2604
2605 /**
2606 * Add custom fields to custom product types
2607 *
2608 * Render the extra WooCommerce "General" product fields for WpStream types:
2609 * the subscription-live flag, the recording/video chooser, and the
2610 * "attach to subscription" multi-select. Echoes HTML.
2611 *
2612 * @since 3.0.1
2613 * @return void
2614 */
2615 public function wpstream_add_custom_general_fields() {
2616 global $woocommerce, $post;
2617
2618 // A new WooCommerce product is still an auto-draft `simple` product
2619 // when this hook renders. Emit the type-scoped controls there so
2620 // WooCommerce can reveal them after the administrator selects a
2621 // WpStream type; the show_if_* classes keep them hidden otherwise.
2622 $post_id = isset( $post->ID ) ? (int) $post->ID : 0;
2623 $is_new_product = $post_id && 'auto-draft' === get_post_status( $post_id );
2624 if ( ! $is_new_product && ! $this->wpstream_is_wpstream_wc_product_context( $post_id ) ) {
2625 return;
2626 }
2627 // Subscription-based live channel toggle (only when WC Subscriptions is active).
2628 if(function_exists('wcs_user_has_subscription')){
2629 echo '<div class="options_group show_if_subscription">';
2630 woocommerce_wp_select(
2631 array(
2632 'id' => '_subscript_live_event',
2633 'label' => __( 'Is a subscription based live channel ?', 'woocommerce' ),
2634 'options' => array("yes"=>"yes","no"=>"no", "none" => "none")
2635 )
2636 );
2637 echo '</div>';
2638 }
2639
2640 echo '<div class="options_group show_if_live_stream" style="border:none;"></div>';
2641 // VOD video chooser section.
2642 echo '<div class="options_group show_if_video_on_demand">';
2643
2644
2645 // Pre-selected video: from the new-video query arg, else the saved _movie_url meta.
2646 $selected='';
2647 if( isset( $_GET['new_video_name']) && $_GET['new_video_name']!='' ){
2648 $selected=esc_html($_GET['new_video_name']);
2649 }
2650 if($selected==''){
2651 $selected= get_post_meta($post->ID,'_movie_url',true);
2652 }
2653 // Only administrators may assign videos.
2654 if( !current_user_can('administrator') ){
2655 print '<div style="margin:10px;">'.esc_html__('You need to be an administrator in order to assign videos','wpstream').'</div>';
2656 }else{
2657 woocommerce_wp_select(
2658 array(
2659 'id' => '_movie_url',
2660 'label' => __( 'Choose video', 'woocommerce' ),
2661 'options' => $this->main->wpstream_live_connection->wpstream_get_videos(),
2662 'selected'=> true,
2663 'value' => $selected
2664 )
2665 );
2666 }
2667
2668
2669
2670 echo '</div>';
2671
2672 // "Attach to subscription" multi-select (only when WC Subscriptions is active).
2673 if(function_exists('wcs_user_has_subscription')){
2674 $selected_sub='';
2675 echo '<div class="options_group show_if_video_on_demand show_if_live_stream">';
2676 if( isset( $_GET['wpstream_parent_sub']) && $_GET['wpstream_parent_sub']!='' ){
2677 $selected_sub=esc_html($_GET['wpstream_parent_sub']);
2678 }
2679 if($selected_sub==''){
2680 $selected_sub= get_post_meta($post->ID,'_wpstream_parent_sub',true);
2681 }
2682 woocommerce_wp_select(
2683 array(
2684 'id' => '_wpstream_parent_sub',
2685 'name' => '_wpstream_parent_sub[]',
2686 'label' => __( 'Attach to subscription', 'woocommerce' ),
2687 'options' => $this->wpstream_return_subscriptions_created(),
2688 'selected'=> true,
2689 'value' => $selected_sub,
2690 'custom_attributes' => array('multiple' => 'multiple')
2691 )
2692 );
2693
2694 echo '</div>';
2695
2696 }
2697 }
2698
2699
2700 /**
2701 * Build an id => title map of all WooCommerce subscription products,
2702 * for the "attach to subscription" selector. Includes a "none" option.
2703 *
2704 * @return array Map of subscription product ID => title (0 => 'none').
2705 */
2706 public function wpstream_return_subscriptions_created(){
2707 // Seed with the "none" choice.
2708 $return=array('0'=>'none');
2709
2710 // Query all subscription-type products, title-ordered.
2711 $args = array(
2712 'post_type' => 'product',
2713 'posts_per_page' => -1,
2714 'orderby' => 'title',
2715 'order' => 'ASC',
2716 'tax_query' => array(
2717 'relation' => 'AND',
2718 array(
2719 'taxonomy' => 'product_type',
2720 'field' => 'slug',
2721 'terms' => array( 'subscription'),
2722 )
2723 )
2724 );
2725
2726 // Collect each subscription's id => title.
2727 $subscriptions = new WP_Query($args);
2728 if($subscriptions->have_posts()):
2729 while ($subscriptions->have_posts()): $subscriptions->the_post();
2730 $return[ get_the_ID() ] = get_the_title();
2731 endwhile;
2732 endif;
2733
2734 // Restore the main query and return the map.
2735 wp_reset_postdata();
2736 return $return;
2737
2738 }
2739
2740
2741
2742
2743
2744
2745 /**
2746 * Save custom fields
2747 *
2748 * Persist the WpStream WooCommerce product fields (_movie_url,
2749 * _subscript_live_event, _wpstream_parent_sub) on save and reset the
2750 * event_passed flag. Hooked to the product save action.
2751 *
2752 * @since 3.0.1
2753 * @param int $post_id Product ID being saved.
2754 * @return void
2755 */
2756 public function wpstream_add_custom_general_fields_save( $post_id ){
2757 // Only handle WpStream product contexts.
2758 if ( ! $this->wpstream_is_wpstream_wc_product_context( (int) $post_id ) ) {
2759 return;
2760 }
2761
2762 // A Source Recording owns the cached DRM and hosted-player identity.
2763 // Retire that identity before replacing the source so its old key index
2764 // is still available to invalidate the lookup caches.
2765 if (
2766 has_term( 'video_on_demand', 'product_type', $post_id )
2767 && ! empty( $_POST['_movie_url'] )
2768 && is_scalar( $_POST['_movie_url'] )
2769 ) {
2770 $previous_recording = (string) get_post_meta( $post_id, '_movie_url', true );
2771 $new_recording = sanitize_text_field( wp_unslash( $_POST['_movie_url'] ) );
2772 if ( $new_recording !== $previous_recording ) {
2773 $this->wpstream_retire_vod_recording_identity( $post_id );
2774 }
2775 }
2776
2777 // Meta keys this handler may write.
2778 $permited_values = array(
2779 '_movie_url',
2780 '_subscript_live_event',
2781 '_wpstream_parent_sub',
2782
2783 );
2784
2785
2786
2787 // Reset event_passed and save each allow-listed field.
2788 foreach($_POST as $key=>$value){
2789 update_post_meta( $post_id, 'event_passed', 0 );
2790 if( in_array($key, $permited_values) ){
2791 if( !empty( $_POST[$key] ) ){
2792 $key = sanitize_key($key);
2793 $value = sanitize_text_field($_POST[$key]);
2794
2795 // The parent-subscription field is an array; sanitize each element.
2796 if($key=='_wpstream_parent_sub'){
2797 $value= $_POST[$key];
2798 $value = array_map("sanitize_text_field", $value);
2799
2800 }
2801 update_post_meta( $post_id, $key, $value );
2802 }
2803 }
2804 }
2805 //die();
2806
2807 }
2808
2809 /**
2810 * Add to cart redirect
2811 *
2812 * Render the simple add-to-cart template for WpStream product types.
2813 *
2814 * @since 3.0.1
2815 * @return void
2816 */
2817 public function wpstream_add_to_cart() {
2818 wc_get_template( 'single-product/add-to-cart/simple.php' );
2819 }
2820
2821
2822 /**
2823 * Replace add to cart button
2824 *
2825 * For live_stream / video_on_demand products, swap the loop add-to-cart
2826 * button for a direct shop add-to-cart link. Filter callback.
2827 *
2828 * @since 3.0.1
2829 * @param string $button Original button HTML.
2830 * @param WC_Product $product The product (overwritten by global $product).
2831 * @return string The (possibly replaced) button HTML.
2832 */
2833 public function replacing_add_to_cart_button( $button, $product ) {
2834 global $product;
2835 $product_type = $product->get_type();
2836
2837 // Only WpStream media types get the custom link; others keep the default button.
2838 if($product_type==='live_stream' || $product_type=='video_on_demand'){
2839 return $button = '<a class="button" href="'.get_site_url().'/shop/?add-to-cart=' .$product->get_id(). '&quantity=1">' . __( 'Add to Cart', 'woocommerce' ) . '</a>';
2840 }else{
2841 return $button;
2842 }
2843 }
2844
2845
2846 /**
2847 * Admin notices
2848 *
2849 * Print global admin notices: (commented-out) WooCommerce-missing notice
2850 * and an error if the PHP cURL extension is unavailable, plus the dismiss
2851 * nonce. Echoes HTML.
2852 *
2853 * @since 3.0.1
2854 * @return void
2855 */
2856 public function wpstream_admin_notice() {
2857 global $pagenow;
2858 global $typenow;
2859
2860 // Stored dismissed-notice flags.
2861 $wpstream_notices = get_option('wpstream_notices');
2862
2863 /*
2864 if ( !in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) {
2865 if( !is_array($wpstream_notices) ||
2866 !isset($wpstream_notices['wpstream_woo_notice']) ||
2867 ( isset($wpstream_notices['wpstream_woo_notice']) && $wpstream_notices['wpstream_woo_notice']!='yes') ){
2868
2869
2870 print '<div class="notice wpstream_notices notice-error is-dismissible" data-notice-type="wpstream_woo_notice" >
2871 <p>'.__( 'WpStream Pay-Per-View Live Streaming and VOD only works with WooCommerce - Please enable and activate the WooCommerce plugin if you want to monetize your Live Events or Recorded Videos', 'wpstream' ).'</p>
2872 </div>';
2873 }
2874 }
2875 */
2876
2877 // Hard requirement: warn when the PHP cURL extension is missing.
2878 if( !in_array ('curl', get_loaded_extensions())) {
2879 print '<div class="notice notice-error is-dismissible">
2880 <p>'.__( 'The php CURL library is not enabled on your server. WpStream plugin needs this library in order to work. Please address this issue with your hosting provider.', 'wpstream' ).'</p>
2881 </div>';
2882 }
2883
2884
2885 // Nonce used by the JS that records notice dismissals.
2886 $ajax_nonce = wp_create_nonce( "wpstream_notice_nonce" );
2887 print '<input type="hidden" id="wpstream_notice_nonce" value="'.esc_html($ajax_nonce).'"/>';
2888
2889 }
2890
2891 /**
2892 * Get plugin latest update release date from WordPress.org
2893 * @param $plugin_slug
2894 * @param $version
2895 *
2896 * @return bool
2897 */
2898 public function get_plugin_release_date( $plugin_slug, $version = null ) {
2899 // This runs on every admin page while an update is pending, so the
2900 // answer is cached for 12h — including a negative one, otherwise a
2901 // failed lookup would retry per page load. Keyed by version so a
2902 // newer release triggers a fresh lookup.
2903 $cache_key = 'wpstream_release_date_' . md5( $plugin_slug . '|' . (string) $version );
2904 $cached = get_transient( $cache_key );
2905 if ( false !== $cached ) {
2906 // 'none' is the stored form of a negative result.
2907 return 'none' === $cached ? false : $cached;
2908 }
2909
2910 // Query the WordPress.org plugins info API (short timeout: this is
2911 // cosmetic notice logic and must never stall the admin).
2912 $api_url = 'https://api.wordpress.org/plugins/info/1.0/' . $plugin_slug . '.json';
2913 $response = wp_remote_get( $api_url, array( 'timeout' => 3 ) );
2914
2915 // Resolve the answer (false = no usable date), then cache it once.
2916 $release_date = false;
2917
2918 if ( ! is_wp_error( $response ) ) {
2919 // Decode the JSON body; a date needs a versions list to validate against.
2920 $data = json_decode( wp_remote_retrieve_body( $response ), true );
2921
2922 if ( $data && isset( $data['versions'] ) ) {
2923 // no version provided, get the latest version
2924 if ( ! $version ) {
2925 $version = $data['version'];
2926 }
2927
2928 // The version must exist in the versions array.
2929 // (note: last_updated is the plugin's overall date, not per-version)
2930 if ( isset( $data['versions'][ $version ] ) && isset( $data['last_updated'] ) ) {
2931 $release_date = date( 'Y-m-d', strtotime( $data['last_updated'] ) );
2932 }
2933 }
2934 }
2935
2936 set_transient( $cache_key, false === $release_date ? 'none' : $release_date, 12 * HOUR_IN_SECONDS );
2937 return $release_date;
2938 }
2939
2940 /**
2941 * Adds notice for the WpStream update availability
2942 * when the update is not older than 30 days
2943 */
2944 public function wpstream_plugin_update_available_notice() {
2945 // Only show to users who can update plugins.
2946 if (!current_user_can('update_plugins')) {
2947 return;
2948 }
2949
2950 // Look up a pending update for this plugin in WP's transient.
2951 $plugin_slug = 'wpstream/wpstream.php';
2952 $update_data = get_site_transient('update_plugins');
2953
2954 if ( is_object( $update_data ) &&
2955 property_exists( $update_data, 'response' ) &&
2956 is_array($update_data->response) &&
2957 key_exists($plugin_slug, $update_data->response)
2958 ) {
2959 $new_version = $update_data->response[$plugin_slug]->new_version;
2960
2961 // Suppress the notice for very fresh releases (grace period).
2962 $release_date = $this->get_plugin_release_date( 'wpstream', $new_version );
2963
2964 if ( $release_date ) {
2965 $days_since_release = ( time() - strtotime( $release_date ) ) / DAY_IN_SECONDS;
2966
2967 // if there's an update newer than 7 days, do not show the notice
2968 if ( $days_since_release < 7 ) {
2969 return;
2970 }
2971 }
2972 // Build the one-click update URL (nonce-protected) and print the notice.
2973 $update_url = wp_nonce_url(
2974 self_admin_url('update.php?action=upgrade-plugin&plugin=' . urlencode($plugin_slug)),
2975 'upgrade-plugin_' . $plugin_slug
2976 );
2977
2978 echo '<div class="notice notice-warning is-dismissible">';
2979 echo '<p><strong>' . __('WpStream Plugin Update Available', 'wpstream') . '</strong></p>';
2980 echo '<p>' . sprintf(
2981 __('Version %s is available. Please update to the latest version for new features and security improvements.', 'wpstream'),
2982 '<strong>' . esc_html($new_version) . '</strong>'
2983 ) . '</p>';
2984 echo '<p><a href="' . esc_url($update_url) . '" class="button button-primary">' .
2985 __('Update Now', 'wpstream') . '</a></p>';
2986 echo '</div>';
2987 }
2988 }
2989
2990 /**
2991 * Admin notices
2992 *
2993 * AJAX handler that records a dismissed notice: marks the posted notice
2994 * type as 'yes' in the wpstream_notices option so it stops showing.
2995 *
2996 * @since 3.0.1
2997 * @return void Ends the request with die().
2998 */
2999 public function wpstream_update_cache_notice(){
3000
3001 // CSRF gate: the dismiss JS sends the nonce printed next to the notice.
3002 check_ajax_referer( 'wpstream_notice_nonce', 'security' );
3003 // Dismissals are stored in a site-wide option, so require admin rights.
3004 if ( ! current_user_can( 'manage_options' ) ) {
3005 die();
3006 }
3007
3008 // Which notice was dismissed.
3009 $notice_type = esc_html($_POST['notice_type']);
3010 $notices = get_option('wp_stream_notices');
3011
3012 // Normalise to an array.
3013 if(! is_array($notices) ){
3014 $notices=array();
3015 }
3016
3017 // Flag this notice as dismissed and persist.
3018 $notices[$notice_type]='yes';
3019
3020 update_option('wpstream_notices',$notices);
3021 die();
3022 }
3023
3024
3025
3026
3027 /**
3028 * Activate metaboxes for Streaming controls on sidebar
3029 *
3030 * Register the "Live Streaming" sidebar metabox on free channel posts, and
3031 * also on WooCommerce products that are live_stream (or a subscription
3032 * flagged as a live event). Hooked to add_meta_boxes.
3033 *
3034 * @since 3.0.1
3035 * @return void
3036 */
3037 public function wpstream_startstreaming_sidebar_meta() {
3038 return $this->main->get_live_channel_presentation()->register_metaboxes();
3039 }
3040 public function wpstream_start_stream_meta(){
3041 return $this->main->get_live_channel_presentation()->render_sidebar();
3042 }
3043 public function wpstream_is_basic_streaming_mode(){
3044 return $this->main->quota_manager->is_basic_streaming_mode( null, 'wpstream_is_basic_streaming_mode' );
3045 }
3046
3047 /**
3048 * Whether the account's plan meters streaming by hours (vs. data).
3049 *
3050 * @return bool
3051 */
3052 public function wpstream_is_use_streaming_hours() {
3053 $pack_details = $this->main->quota_manager->get_live_quota_data( 'wpstream_start_channel' );
3054 return $this->main->quota_manager->uses_streaming_hours( $pack_details );
3055 }
3056
3057 /**
3058 * Cached-only flags for start_streaming.js localization (no API on cold cache).
3059 *
3060 * @return array{is_basic_streaming: bool, use_streaming_hours: bool}
3061 */
3062 public function wpstream_get_start_streaming_localization_flags() {
3063 return $this->main->get_live_channel_presentation()->get_localization_flags();
3064 }
3065
3066
3067
3068 /**
3069 * Register a hidden dashboard page used as the onboarding wizard endpoint.
3070 *
3071 * @return void
3072 */
3073 public function add_dashboard_page() {
3074 $this->main->get_onboarding()->render_part( 'dashboard_page' );
3075 }
3076
3077
3078
3079
3080 /**
3081 * Bootstrap the full-screen onboarding wizard screen (non-AJAX requests).
3082 *
3083 * @return void
3084 */
3085 public function wpstream_load_onboarding_wizard() {
3086 $this->main->get_onboarding()->render_part( 'standalone' );
3087 }
3088
3089
3090 /*
3091 * Add on boarding to footer
3092 */
3093 /**
3094 * On the onboarding admin page, print the onboarding modal into the footer.
3095 *
3096 * @return void
3097 */
3098 public function wpstream_admin_footer_onboarding(){
3099 $this->main->get_onboarding()->render_part( 'page_footer' );
3100 }
3101
3102
3103 /*
3104 * On Board Display
3105 *
3106 */
3107 /**
3108 * Render the Quick Start landing screen (logo, intro, "Start the Guide"
3109 * button and the script that opens the onboarding wizard). Echoes HTML.
3110 *
3111 * @return void
3112 */
3113 public function wpstream_pre_onboard_display(){
3114 echo $this->main->get_onboarding()->render();
3115 }
3116
3117
3118
3119
3120
3121
3122
3123
3124 /*
3125 *
3126 * On Boarding Content
3127 *
3128 */
3129 /**
3130 * Emit the full onboarding wizard markup by rendering each step in order
3131 * (header, account, path choice, live/VOD branches, footer). Echoes HTML.
3132 *
3133 * @return void
3134 */
3135 public function wpstream_onboard_display() {
3136 $this->main->get_onboarding()->render_part( 'wizard' );
3137 }
3138
3139
3140 /*
3141 *
3142 * On Boarding Step 1 - the login/register
3143 *
3144 */
3145 /**
3146 * Onboarding step 1: WpStream account login/registration UI, including the
3147 * ALTCHA captcha widget (on HTTPS) and a hidden generated registration
3148 * password. Emits a marker div when a token already exists. Echoes HTML.
3149 *
3150 * @return void
3151 */
3152 public function wpstream_onboarding_step1(){
3153 $this->main->get_onboarding()->render_part( 'account' );
3154 }
3155
3156 public function randomPassword() {
3157 return $this->main->get_onboarding()->render_part( 'password' );
3158 }
3159
3160 public function wpstream_onboarding_step2(){
3161 $this->main->get_onboarding()->render_part( 'path' );
3162 }
3163
3164 public function wpstream_onboarding_step3_live_streaming(){
3165 $this->main->get_onboarding()->render_part( 'live' );
3166 }
3167
3168 public function wpstream_onboarding_step_3_A_live_streaming_free_view(){
3169 $this->main->get_onboarding()->render_part( 'live_free' );
3170 }
3171
3172 public function wpstream_onboarding_step_3_B_live_streaming_pay_per_view(){
3173 $this->main->get_onboarding()->render_part( 'live_ppv' );
3174 }
3175
3176 public function wpstream_onboarding_woo_warning(){
3177 $this->main->get_onboarding()->render_part( 'woo_warning' );
3178 }
3179
3180 public function wpstream_onboarding_step4_vod(){
3181 $this->main->get_onboarding()->render_part( 'vod' );
3182 }
3183
3184 public function wpstream_onboarding_step_4_free_vod(){
3185 $this->main->get_onboarding()->render_part( 'vod_free' );
3186 }
3187
3188 public function wpstream_obboarding_file_warning(){
3189 $this->main->get_onboarding()->render_part( 'file_warning' );
3190 }
3191
3192 public function wpstream_onboarding_step_4_ppv_vod(){
3193 $this->main->get_onboarding()->render_part( 'vod_ppv' );
3194 }
3195
3196 public function onboarding_wizard_header() {
3197 $this->main->get_onboarding()->render_part( 'wizard_header' );
3198 }
3199
3200
3201
3202
3203
3204 /*
3205 *
3206 * On Boarding Footer
3207 *
3208 */
3209 /**
3210 * Close the onboarding wizard wrapper and print its modal background. Echoes HTML.
3211 *
3212 * @return void
3213 */
3214 public function onboarding_wizard_footer() {
3215 $this->main->get_onboarding()->render_part( 'wizard_footer' );
3216 }
3217
3218
3219
3220 /*
3221 *
3222 * On Boarding create PPV channel
3223 *
3224 */
3225
3226
3227 /**
3228 * AJAX: create a Pay-Per-View live channel during onboarding.
3229 *
3230 * Inserts a WooCommerce `product`, sets its price, marks it live_stream,
3231 * and returns the edit link (with onboarding query args) as JSON.
3232 * Nonce- and administrator-gated.
3233 *
3234 * @return void Ends with die().
3235 */
3236 public function wpstream_on_board_create_channel_ppv(){
3237 return $this->main->get_onboarding()->handle_ajax( 'wpstream_on_board_create_channel_ppv' );
3238 }
3239
3240 /*
3241 *
3242 * On Boarding create channel
3243 *
3244 */
3245
3246
3247 /**
3248 * AJAX: create a Free-To-View live channel during onboarding.
3249 *
3250 * Inserts a `wpstream_product` post and returns its edit link (with
3251 * onboarding query args) as JSON. Nonce- and administrator-gated.
3252 *
3253 * @return void Ends with die().
3254 */
3255 public function wpstream_on_board_create_channel(){
3256 return $this->main->get_onboarding()->handle_ajax( 'wpstream_on_board_create_channel' );
3257 }
3258
3259 /*
3260 *
3261 * On Boarding create free vod
3262 *
3263 */
3264 /**
3265 * AJAX: create a Free-To-View VOD during onboarding.
3266 *
3267 * Inserts a `wpstream_product_vod` post, links the chosen recording, and
3268 * returns the edit link (with onboarding query args). Nonce/admin gated.
3269 *
3270 * @return void Ends with die().
3271 */
3272 public function wpstream_on_board_create_free_vod(){
3273 return $this->main->get_onboarding()->handle_ajax( 'wpstream_on_board_create_free_vod' );
3274 }
3275
3276
3277 /*
3278 *
3279 * On Boarding create ppv vod
3280 *
3281 */
3282
3283
3284 /**
3285 * AJAX: create a Pay-Per-View VOD during onboarding.
3286 *
3287 * Inserts a WooCommerce `product`, sets its price, links the recording,
3288 * marks it video_on_demand, and returns the edit link (with onboarding
3289 * query args). Nonce- and administrator-gated.
3290 *
3291 * @return void Ends with die().
3292 */
3293 public function wpstream_on_board_create_ppv_vod(){
3294 return $this->main->get_onboarding()->handle_ajax( 'wpstream_on_board_create_ppv_vod' );
3295 }
3296
3297
3298 /*
3299 *
3300 * On Boarding login
3301 *
3302 */
3303 /**
3304 * AJAX: log in to WpStream during onboarding.
3305 *
3306 * Stores the submitted credentials, clears the token cache, then attempts
3307 * to fetch a token; returns success/failure JSON without ever exposing the
3308 * token to the client. Nonce- and administrator-gated.
3309 *
3310 * @return void Ends with die().
3311 */
3312 public function wpstream_on_board_login(){
3313 return $this->main->get_onboarding()->handle_ajax( 'wpstream_on_board_login' );
3314 }
3315
3316 /**
3317 * AJAX proxy: fetch a captcha challenge from the baker API and return it.
3318 * Used on HTTP sites where the Altcha widget cannot run (requires Web Crypto / HTTPS).
3319 * The JS side solves the PoW locally and sends back the full base64 Altcha payload.
3320 * Logged-in administrators only: every call proxies an outbound request, so the
3321 * endpoint is gated like the rest of the onboarding wizard.
3322 */
3323 public function wpstream_get_captcha_challenge() {
3324 return $this->main->get_onboarding()->handle_ajax( 'wpstream_get_captcha_challenge' );
3325 }
3326
3327 /*
3328 *
3329 * On Boarding login
3330 *
3331 */
3332 /**
3333 * AJAX: register a new WpStream account during onboarding.
3334 *
3335 * Validates the email/password, requires an ALTCHA solution, calls the
3336 * account-create API, and on success stores the credentials and fetches a
3337 * token. Returns JSON. Nonce- and administrator-gated.
3338 *
3339 * @return void Ends with die().
3340 */
3341 public function wpstream_on_board_register(){
3342 return $this->main->get_onboarding()->handle_ajax( 'wpstream_on_board_register' );
3343 }
3344
3345
3346 /*
3347 *
3348 * Validate for register
3349 *
3350 */
3351 /**
3352 * Validate the onboarding registration email + password.
3353 *
3354 * Checks for a non-empty, well-formed email whose domain has DNS records,
3355 * and a password of at least 5 characters.
3356 *
3357 * @param string $wpstream_register_email Submitted email.
3358 * @param string $wpstream_register_password Submitted password.
3359 * @return array{success: bool, message?: string} Validation result.
3360 */
3361 public function wpstream_validate_onboard_register($wpstream_register_email,$wpstream_register_password){
3362 return $this->main->get_onboarding()->validate_registration( $wpstream_register_email, $wpstream_register_password );
3363 }
3364
3365 /**
3366 * Handle the AJAX request to initiate a multipart upload
3367 *
3368 * Validates the request, builds a clean filename, and asks the cloud API to
3369 * open a multipart S3 upload; returns the upload id and per-part pre-signed
3370 * URLs as JSON. Nonce- and administrator-gated.
3371 *
3372 * @since 3.0.1
3373 * @return void Responds via wp_send_json_*.
3374 */
3375 public function handle_initiate_multipart_upload() {
3376 check_ajax_referer( 'wpstream_multipart_upload_nonce', 'security' );
3377
3378 // Security check - only admins can do this
3379 if (!current_user_can('administrator')) {
3380 wp_send_json_error('Unauthorized access');
3381 return;
3382 }
3383
3384 // Get file details from request
3385 $file_name = sanitize_text_field($_POST['file_name']);
3386 $file_size = intval($_POST['file_size']);
3387 $content_type = sanitize_text_field($_POST['content_type']);
3388 $num_parts = intval($_POST['parts']);
3389
3390 // Reject incomplete/invalid file metadata.
3391 if (empty($file_name) || $file_size <= 0 || $num_parts <= 0) {
3392 wp_send_json_error('Invalid file information');
3393 return;
3394 }
3395
3396 // The part count is caller-supplied: refuse a split the backend can
3397 // never assemble before spending a cloud call on it.
3398 if ($num_parts > self::MULTIPART_MAX_PARTS) {
3399 wp_send_json_error(
3400 sprintf(
3401 /* translators: 1: requested part count, 2: maximum part count */
3402 __('The file would be split into %1$d parts but at most %2$d are allowed. Reload the page to get the current uploader and try again.', 'wpstream'),
3403 $num_parts,
3404 self::MULTIPART_MAX_PARTS
3405 )
3406 );
3407 return;
3408 }
3409
3410 // Prepare a clean filename (similar to the standard upload process)
3411 // Keep the extension, slugify the base name (spaces -> _, strip non-word chars).
3412 $file_name_array = explode(".", $file_name);
3413 $file_extension = $file_name_array[count($file_name_array) - 1];
3414 $temp_file_name = $file_name_array[0];
3415 $temp_file_name = str_replace(' ', '_', $temp_file_name);
3416 $temp_file_name = preg_replace('/\W/', '', $temp_file_name);
3417 $clean_file_name = $temp_file_name . '.' . $file_extension;
3418
3419 // Make API call to initiate multipart upload; the auth token is
3420 // injected by the transport (not-connected surfaces as a WP_Error
3421 // whose message is the legacy "Not connected to WPStream service").
3422 $api_params = array(
3423 'size' => $file_size,
3424 'name' => $clean_file_name,
3425 'content_type' => $content_type,
3426 'parts' => $num_parts
3427 );
3428
3429 $response_data = $this->main->wpstream_live_connection->authorized_request('video/upload', $api_params);
3430
3431 // Bubble up any API error.
3432 if (is_wp_error($response_data)) {
3433 wp_send_json_error($response_data->get_error_message());
3434 return;
3435 }
3436 if (!isset($response_data['success']) || $response_data['success'] !== true) {
3437 $error_message = isset($response_data['error']) ? $response_data['error'] : 'Failed to initiate multipart upload';
3438 wp_send_json_error($error_message);
3439 return;
3440 }
3441
3442 // Return the upload ID and pre-signed URLs for each part
3443 wp_send_json_success($response_data);
3444 }
3445
3446 /**
3447 * Handle the AJAX request to complete a multipart upload
3448 *
3449 * Validates the request and tells the cloud API to finalize the multipart S3
3450 * upload for the given parts/handle. Nonce- and administrator-gated.
3451 *
3452 * @since 3.0.1
3453 * @return void Responds via wp_send_json_*.
3454 */
3455 public function handle_complete_multipart_upload() {
3456 check_ajax_referer( 'wpstream_multipart_upload_nonce', 'security' );
3457
3458 // Security check - only admins can do this
3459 if (!current_user_can('administrator')) {
3460 wp_send_json_error('Unauthorized access');
3461 return;
3462 }
3463
3464 // Get completion details. The client sends 'parts' as the plain part
3465 // COUNT (not a part list), so parse it as a positive integer.
3466 $parts = isset($_POST['parts']) ? absint(wp_unslash($_POST['parts'])) : 0;
3467 $file_name = sanitize_text_field($_POST['file_name']);
3468 $handle = sanitize_text_field($_POST['handle']);
3469
3470 // Reject incomplete completion metadata: no parts uploaded or no file name.
3471 if ($parts < 1 || empty($file_name)) {
3472 wp_send_json_error('Invalid completion information');
3473 return;
3474 }
3475
3476 // Send the finalize ("complete") request with the uploaded part list;
3477 // the auth token is injected by the transport (not-connected surfaces
3478 // as a WP_Error whose message is the legacy "Not connected to WPStream
3479 // service").
3480 $api_params = array(
3481 'parts' => $parts,
3482 'name' => $file_name,
3483 'handle' => $handle,
3484 'action' => 'complete'
3485 );
3486
3487 $response_data = $this->main->wpstream_live_connection->authorized_request('video/upload', $api_params);
3488
3489 // Bubble up any API error.
3490 if (is_wp_error($response_data)) {
3491 wp_send_json_error($response_data->get_error_message());
3492 return;
3493 }
3494 if (!isset($response_data['success']) || $response_data['success'] !== true) {
3495 $error_message = isset($response_data['error']) ? $response_data['error'] : 'Failed to complete multipart upload';
3496 wp_send_json_error($error_message);
3497 return;
3498 }
3499
3500 /**
3501 * Fires after the cloud accepted a completed multipart VOD upload.
3502 *
3503 * @since 4.14.0
3504 *
3505 * @param string $file_name Uploaded file name.
3506 * @param string $handle Upload handle issued at initiation.
3507 * @param int $parts Number of parts uploaded.
3508 */
3509 do_action( 'wpstream_multipart_upload_completed', $file_name, $handle, $parts );
3510
3511 // Return success
3512 wp_send_json_success();
3513 }
3514
3515 /**
3516 * AJAX: update the WpStream plugin in place from the Settings/Support tab.
3517 *
3518 * Loads the upgrader APIs, initializes WP_Filesystem, runs the plugin
3519 * upgrade, re-activates the plugin, and returns success/error JSON.
3520 * Requires the update_plugins capability.
3521 *
3522 * @return void Responds via wp_send_json_*.
3523 */
3524 public function wpstream_settings_tab_update_plugin() {
3525 // CSRF gate: the update button JS sends the settings-page nonce as 'security'.
3526 check_ajax_referer( 'wpstream-settings-nonce', 'security' );
3527 // Capability gate.
3528 if ( !current_user_can( 'update_plugins' ) ) {
3529 wp_send_json_error( __( 'Not enough permissions to make this change', 'wpstream' ) );
3530 }
3531
3532 // Load the core upgrade/filesystem APIs.
3533 include_once ABSPATH . 'wp-admin/includes/plugin-install.php';
3534 include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
3535 include_once ABSPATH . 'wp-admin/includes/file.php';
3536
3537 // Initialize the filesystem abstraction (needed to write plugin files).
3538 $credentials = request_filesystem_credentials('');
3539 if ( !WP_Filesystem( $credentials ) ) {
3540 wp_send_json_error( __( 'Failed to connect to the filesystem', 'wpstream' ) );
3541 }
3542
3543 // Run the in-place upgrade for this plugin, then re-activate it.
3544 $upgrader = new Plugin_Upgrader(new Automatic_Upgrader_Skin());
3545 $plugin_path = plugin_basename( WPSTREAM_PLUGIN_PATH . 'wpstream.php' );
3546 $result = $upgrader->upgrade( $plugin_path );
3547 activate_plugin( $plugin_path );
3548
3549 if ( is_wp_error( $result ) ) {
3550 wp_send_json_error( __( 'Update failed due to', 'wpstream' ) . $result->get_error_message() );
3551 }
3552
3553 wp_send_json_success();
3554 }
3555 }
3556