PluginProbe
WpStream – Live Streaming, Video on Demand, Pay Per View / 4.14.1
WpStream – Live Streaming, Video on Demand, Pay Per View v4.14.1
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 / includes / class-wpstream-live-api-connection.php

class-wpstream-live-api-connection.php in WpStream – Live Streaming, Video on Demand, Pay Per View 4.14.1, at includes/class-wpstream-live-api-connection.php

2,171 lines 83.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * HTTP client for the WPStream cloud backend (baker.wpstream.net / rest-baker.wpstream.net).
4 *
5 * This class is the single funnel through which the plugin talks to the remote
6 * streaming API. It:
7 * - authenticates against the API (`access_token` grant) and caches the bearer
8 * token in a transient so every call does not re-login (see wpstream_get_token()).
9 * - funnels every backend call through a single WP-HTTP transport (request())
10 * that centralises timeout, error handling, HTTP-status handling, JSON decoding
11 * and failure logging, returning a decoded array or WP_Error (never output).
12 * - owns authentication end to end: authorized_request() fetches the token and
13 * injects it into the body itself, and is_connected() answers "is this site
14 * linked to a WpStream account?" — production code outside this class never
15 * handles the token.
16 * - drives the channel lifecycle: start (channel/start), stop (channel/stop),
17 * update settings (channel/update), poll status (channel/info) and list
18 * active channels (channel/list).
19 * - manages recorded video assets stored in the cloud (video/list, video/upload,
20 * video/download, video/delete).
21 * - exposes a large surface of `wp_ajax_*` handlers that the admin JS calls to
22 * turn channels on/off, save per-event and global settings, check DNS/quota/whip,
23 * and enumerate pending recordings.
24 * - composes Wpstream_User_Quota_Service (consumed directly by the quota
25 * manager) and Wpstream_Channel_Service in the constructor.
26 *
27 * NOTE: several AJAX handlers below intentionally left as-is have weak or missing
28 * nonce/ownership checks; this is a comments-only pass, so nothing here is changed.
29 *
30 * @package Wpstream
31 * @subpackage Wpstream/includes
32 */
33
34
35 // Exit if accessed directly.
36 if ( ! defined( 'ABSPATH' ) ) {
37 exit;
38 }
39
40 /**
41 * Remote streaming-API client plus the admin AJAX surface that drives it.
42 */
43 class Wpstream_Live_Api_Connection {
44
45 /** @var Wpstream_User_Quota_Service Handles pack/quota lookups against the API. */
46 private $user_quota_service;
47
48 /** @var Wpstream_Channel_Service Handles remote channel creation. */
49 private $channel_service;
50
51 /** @var Wpstream_Channel_Settings Deep owner used by compatibility handlers. */
52 private $channel_settings;
53
54 /** @var Wpstream_Streaming_Content_Creation|null Local-first creation lifecycle owner. */
55 private $streaming_content_creation;
56
57
58 /**
59 * Compose the collaborator services and register every admin AJAX endpoint
60 * (plus the admin-notice hook) this class serves.
61 *
62 * All handlers are `wp_ajax_*` only (logged-in admin surface); none are
63 * registered for `wp_ajax_nopriv_*`.
64 */
65 public function __construct() {
66 // Collaborators receive $this so they can reuse the token + cURL helpers.
67 $this->user_quota_service = new Wpstream_User_Quota_Service( $this );
68 $this->channel_service = new Wpstream_Channel_Service( $this );
69 if ( class_exists( 'Wpstream_Channel_Settings' ) ) {
70 $this->channel_settings = new Wpstream_Channel_Settings(
71 new Wpstream_WordPress_Channel_Settings_Storage()
72 );
73 }
74
75 // Channel on/off: request a live URI (start) and stop a running channel.
76 add_action( 'wp_ajax_wpstream_give_me_live_uri', array($this,'wpstream_give_me_live_uri') );
77 add_action( 'wp_ajax_wpstream_turn_of_channel', array($this,'wpstream_turn_of_channel') );
78 // Per-event and global streaming settings persistence.
79 add_action( 'wp_ajax_wpstream_update_local_event_settings',array($this,'wpstream_update_local_event_settings'));
80 add_action( 'wp_ajax_wpstream_update_use_global_event_options',array($this,'wpstream_update_use_global_event_options'));
81 add_action( 'wp_ajax_wpstream_update_default_channel_settings', array( $this, 'wpstream_update_default_channel_settings' ) );
82 add_action( 'wp_ajax_wpstream_update_settings', array( $this, 'wpstream_update_settings' ) );
83
84 // Status/connectivity polling endpoints used while a broadcast is starting.
85 add_action( 'wp_ajax_wpstream_check_event_status', array($this,'wpstream_check_event_status') );
86 add_action( 'wp_ajax_wpstream_check_whipurl', array($this, 'wpstream_check_whipurl') );
87 add_action( 'wp_ajax_wpstream_check_user_quota', array($this, 'wpstream_check_user_quota') );
88
89 // Recorded-file management (download link, delete).
90 add_action( 'wp_ajax_wpstream_get_download_link', array($this,'wpstream_get_download_link') );
91 add_action( 'wp_ajax_wpstream_get_delete_file', array($this,'wpstream_get_delete_file') );
92
93 // Connection/credential warnings printed at the top of WPStream admin pages.
94 add_action( 'admin_notices',array($this, 'wpstream_admin_notices') );
95
96 // Poll for recordings that are still being processed in the cloud.
97 add_action( 'wp_ajax_wpstream_check_pending_videos', array($this,'wpstream_check_pending_videos') );
98
99
100 }
101
102 /** Inject the Channel Settings Module after the connection Adapter is composed. */
103 public function set_channel_settings( Wpstream_Channel_Settings $channel_settings ) {
104 $this->channel_settings = $channel_settings;
105 }
106
107 /** Inject the Streaming Content Creation Module after all Adapters are composed. */
108 public function set_streaming_content_creation( Wpstream_Streaming_Content_Creation $streaming_content_creation ) {
109 $this->streaming_content_creation = $streaming_content_creation;
110 }
111
112
113 /*
114 * Admin Notices
115 *
116 *
117 *
118 * */
119 /**
120 * Print connectivity warnings at the top of WPStream admin pages.
121 *
122 * Warns when the PHP cURL extension is missing, and when no auth token can
123 * be obtained (i.e. the site is not connected to WpStream.net). Hooked on
124 * `admin_notices`.
125 *
126 * @return void Echoes notice markup; returns early on non-WPStream screens.
127 */
128 function wpstream_admin_notices(){
129 // Current admin page slug, used to limit where these notices appear.
130 global $pagenow;
131
132
133
134
135 // Only run on admin.php-hosted pages (the plugin's menu screens).
136 if($pagenow!='admin.php'){
137 return;
138 }
139
140 // Whitelist of WPStream admin screens allowed to show the notice.
141 $permited_pages=array('wpstream_plugin_options','wpstream_live_channels','wpstream_recordings','wpstream_settings');
142 // '' when no ?page= is present, so the variable is always defined below.
143 $page = !empty($_GET['page']) ? esc_html($_GET['page']) : '';
144 if ($page !== '') {
145 // Bail if the requested page is not one of ours.
146 if( !in_array($page, $permited_pages)){
147 return;
148 }
149 }
150
151 // Verify the cURL extension is loaded; without it no API call can succeed.
152 if(in_array('curl', get_loaded_extensions())){
153 //cURL module has been loaded
154 } else{
155 print '<div class="api_not_conected wpstream_notice_top">We could not connect to WpStream.net. Make sure you have the php Curl library enabled and your hosting allows outgoing HTTP Connection. </div>';
156 }
157
158 // Connectivity probe: no obtainable token means not connected.
159 if( !$this->is_connected() and $page!='wpstream_plugin_options'){
160 // echo 'wpstream_curl_failed: ' . get_option('wpstream_curl_failed');
161 // wpstream_curl_failed === "0" => credentials wrong (no cURL error);
162 // otherwise a transport/HTTP error occurred and was surfaced above.
163 // Translate the literals at definition — gettext cannot extract variable strings.
164 $text = get_option('wpstream_curl_failed') === "0" ?
165 __( 'Not connected to WpStream. Please check your credentials <a href="/wp-admin/admin.php?page=wpstream_credentials">here</a>.', 'wpstream' ) :
166 __( 'Not connected to WpStream. Please note the errors above and contact support.', 'wpstream' );
167
168 // Render the appropriate "not connected" message (contains a link, so kses instead of esc_html).
169 echo '<div class="api_not_conected wpstream_notice_top">'.wp_kses_post($text).'</div>';
170 }
171
172 }
173
174
175 /*
176 * Curl request
177 *
178 *
179 *
180 * */
181
182 /**
183 * POST to the WPStream backend API over the WP HTTP layer.
184 *
185 * The single transport every backend call funnels through. Never produces
186 * output; user-facing error rendering belongs to callers/admin notices.
187 *
188 * @param string $endpoint API path appended to WPSTREAM_API (e.g. 'channel/start').
189 * @param array $body POST body fields.
190 * @param int $timeout Request timeout in seconds.
191 * @return array|WP_Error Decoded response array, or WP_Error on transport/HTTP/JSON failure.
192 */
193 public function request( $endpoint, $body, $timeout = 10 ) {
194 // Test host wins over production when defined (same rule as before).
195 $base_api_url = defined( 'WPSTREAM_TEST_API' ) ? WPSTREAM_TEST_API : WPSTREAM_API;
196
197 $args = array(
198 'timeout' => $timeout,
199 'body' => $body,
200 );
201
202 /**
203 * Filters the WP HTTP args of an outgoing WpStream API request.
204 *
205 * Lets trusted code adjust `timeout`, `headers` or `body` before the
206 * call. The destination URL/host is not modifiable here; `$endpoint`
207 * is read-only context. `$args['body']` may carry credentials — the
208 * account access token on most endpoints, and the WpStream username
209 * and plaintext password on the `access_token` endpoint — so this
210 * filter is for trusted site code only: never log, forward or expose
211 * `$args` wholesale.
212 *
213 * @since 4.14.0
214 *
215 * @param array $args Args passed to wp_remote_post()/wp_remote_get().
216 * @param string $endpoint API path (request()) or full URL (get()).
217 */
218 $args = apply_filters( 'wpstream_api_request_args', $args, $endpoint );
219
220 $response = wp_remote_post( $base_api_url . '/' . $endpoint, $args );
221
222 return $this->handle_response( $response, $endpoint );
223 }
224
225 /**
226 * GET a full URL over the WP HTTP layer with the same response contract
227 * as request(). Used for the legacy REST host and other WpStream GET
228 * endpoints that do not live under the WPSTREAM_API base path.
229 *
230 * @param string $url Full URL to fetch.
231 * @param int $timeout Request timeout in seconds.
232 * @return array|WP_Error Decoded response array, or WP_Error on transport/HTTP/JSON failure.
233 */
234 public function get( $url, $timeout = 10 ) {
235 /** This filter is documented in request() above. */
236 $args = apply_filters( 'wpstream_api_request_args', array( 'timeout' => $timeout ), $url );
237
238 $response = wp_remote_get( $url, $args );
239
240 return $this->handle_response( $response, $url );
241 }
242
243 /**
244 * Shared response handling for request()/get(): validate, decode, record.
245 *
246 * @param array|WP_Error $response Raw wp_remote_* return value.
247 * @param string $endpoint Endpoint/URL used for logging context.
248 * @return array|WP_Error Decoded response array, or WP_Error on failure.
249 */
250 private function handle_response( $response, $endpoint ) {
251 // Transport-level failure (DNS, refused connection...): hand the
252 // WP_Error straight to the caller.
253 if ( is_wp_error( $response ) ) {
254 return $this->record_failure( $response->get_error_message(), $endpoint, $response );
255 }
256
257 // Non-200 status: error out with the HTTP code as error data so
258 // callers can react to specific statuses without string parsing.
259 $http_code = wp_remote_retrieve_response_code( $response );
260 if ( 200 !== $http_code ) {
261 return $this->record_failure( $http_code, $endpoint, new WP_Error(
262 'wpstream_http_error',
263 'API returned HTTP ' . $http_code . ' on endpoint ' . $endpoint,
264 $http_code
265 ) );
266 }
267
268 // Decode the JSON body into an associative array for callers; a body
269 // that does not parse (proxy/maintenance HTML, truncation) is an error.
270 $decoded = json_decode( wp_remote_retrieve_body( $response ), true );
271 if ( JSON_ERROR_NONE !== json_last_error() ) {
272 return $this->record_failure( json_last_error(), $endpoint, new WP_Error(
273 'wpstream_bad_json',
274 'Malformed API response: ' . json_last_error_msg() . ' on endpoint ' . $endpoint,
275 json_last_error()
276 ) );
277 }
278
279 // Healthy call: mark the connection as OK for the admin notice.
280 $this->record_call_outcome( 0 );
281
282 return $decoded;
283 }
284
285 /**
286 * Log an API failure, record its marker, and pass the WP_Error through.
287 *
288 * @param string|int $marker Value stored in wpstream_curl_failed (non-zero = failing).
289 * @param string $endpoint Endpoint the failing call targeted (for the log).
290 * @param WP_Error $error The error to return to the caller.
291 * @return WP_Error The $error given, unchanged.
292 */
293 private function record_failure( $marker, $endpoint, $error ) {
294 $logger = new WpStream_Logger();
295 $logger->add( new WpStream_Log_Entry( array(
296 'type' => 'error',
297 'description' => $error->get_error_message() . ' on endpoint ' . $endpoint,
298 ) ) );
299
300 $this->record_call_outcome( $marker );
301
302 /**
303 * Fires when a call to the WpStream API failed (transport, HTTP status
304 * or malformed body).
305 *
306 * The error carries no credentials: the request body is not attached.
307 *
308 * @since 4.14.0
309 *
310 * @param WP_Error $error The failure (`wpstream_http_error`, `wpstream_bad_json`, transport codes).
311 * @param string $endpoint API path the call targeted.
312 */
313 do_action( 'wpstream_api_request_failed', $error, (string) $endpoint );
314
315 return $error;
316 }
317
318 /**
319 * Persist the last API-call outcome (0 = ok) for the admin-notice logic,
320 * writing the option only when the value actually changed.
321 *
322 * @param string|int $marker 0 on success, error/HTTP/JSON code otherwise.
323 * @return void
324 */
325 private function record_call_outcome( $marker ) {
326 // String-compare so a missing option (false) still differs from 0,
327 // while "0" (DB round-trip) and 0 count as unchanged.
328 $previous = get_option( 'wpstream_curl_failed' );
329 if ( (string) $previous !== (string) $marker ) {
330 update_option( 'wpstream_curl_failed', $marker, false );
331
332 /**
333 * Fires when the API connection outcome changes (healthy ↔ failing).
334 *
335 * Fires once per transition, not per call, so it suits alerting.
336 *
337 * @since 4.14.0
338 *
339 * @param string|int $marker New outcome: 0 healthy, otherwise the HTTP/JSON/transport code.
340 * @param string|int $previous The outcome it replaced (false when never recorded).
341 */
342 do_action( 'wpstream_api_connection_state_changed', $marker, $previous );
343 }
344 }
345
346 /**
347 * POST an authenticated call to the backend API.
348 *
349 * The connection fetches the account token itself and injects it as the
350 * `access_token` body field, so callers never handle credentials. When no
351 * token is available no request is made and a `wpstream_not_connected`
352 * WP_Error is returned (its message is caller-safe UI text). An HTTP 401
353 * invalidates the cached token and is retried once with fresh credentials;
354 * a second 401 is returned and its rejected replacement is not cached.
355 *
356 * @param string $endpoint API path appended to WPSTREAM_API (e.g. 'channel/list').
357 * @param array $fields POST body fields, without access_token.
358 * @param int $timeout Request timeout in seconds.
359 * @return array|WP_Error Decoded response array, or WP_Error on failure —
360 * code `wpstream_not_connected` when the site holds no token.
361 */
362 public function authorized_request( $endpoint, $fields = array(), $timeout = 10 ) {
363 $access_token = $this->wpstream_get_token();
364
365 // No token: the site is not linked to a WpStream account. No call is
366 // made, and the connection-outcome option is left untouched (nothing
367 // about the transport failed).
368 if ( empty( $access_token ) ) {
369 return new WP_Error( 'wpstream_not_connected', 'Not connected to WPStream service' );
370 }
371
372 $fields['access_token'] = $access_token;
373 $response = $this->request( $endpoint, $fields, $timeout );
374
375 // Baker can invalidate a cached token before its local transient expires.
376 // Refresh and replay once so the authenticated operation self-heals in
377 // the same public call; the direct second request prevents retry loops.
378 if ( is_wp_error( $response ) && 401 === (int) $response->get_error_data() ) {
379 delete_transient( 'wpstream_token_api' );
380 $access_token = $this->wpstream_get_token();
381 if ( empty( $access_token ) ) {
382 return new WP_Error( 'wpstream_not_connected', 'Not connected to WPStream service' );
383 }
384
385 $fields['access_token'] = $access_token;
386 $response = $this->request( $endpoint, $fields, $timeout );
387 if ( is_wp_error( $response ) && 401 === (int) $response->get_error_data() ) {
388 delete_transient( 'wpstream_token_api' );
389 }
390
391 return $response;
392 }
393
394 return $response;
395 }
396
397 /**
398 * Whether this site is Connected: a WpStream API token exists (cached or
399 * freshly obtained). Says nothing about any channel being live — use it
400 * only for "is the account linked?" checks.
401 *
402 * @return bool
403 */
404 public function is_connected() {
405 $token = $this->wpstream_get_token();
406 return ! empty( $token );
407 }
408
409 /**
410 * Return the API token, or send the standard "not connected" JSON and die.
411 *
412 * Shared gate for the AJAX handlers that cannot proceed without a linked
413 * WpStream account; keeps the legacy response keys the dashboard JS reads.
414 *
415 * @return string The access token (never returns on failure).
416 */
417 private function require_token_or_die() {
418 $access_token = $this->wpstream_get_token();
419 if ( empty( $access_token ) ) {
420 wp_send_json( array(
421 'is_record' => '',
422 'connected' => false,
423 'event_data' => '',
424 'error' => esc_html__( 'You are not connected to wpstream.net! Please check your WpStream credentials!', 'wpstream' ),
425 ) );
426 }
427 return $access_token;
428 }
429
430 /**
431 * Resolve which streaming server hosts a given show, cached in a transient.
432 *
433 * Calls the legacy REST endpoint `rest-baker.wpstream.net?apiFunctionName=server_id_by_show_id`.
434 *
435 * @param int|string $show_id Local show/channel post ID.
436 * @return string Server id on success, or '' when unavailable/failed.
437 * @since 3.0.1
438 */
439
440 function wpstream_retrieve_server_id_based_on_show_id($show_id){
441
442 // Per-show cache key so repeat lookups avoid a network round-trip.
443 $transient_name = 'server_id_to_return_' . $show_id;
444 $server_id_to_return = get_transient( $transient_name );
445
446 // Cache hit: return the stored server id.
447 if ( false !== $server_id_to_return ) {
448 return $server_id_to_return;
449 }
450
451 // Cache miss: query the legacy REST endpoint (it only accepts its
452 // parameters, token included, as GET query args).
453 $token = $this->wpstream_get_token();
454 $url = WPSTREAM_REST_API . '/?apiFunctionName=server_id_by_show_id&show_id=' . intval( $show_id ) . '&access_token=' . $token;
455
456 $response = $this->get( $url, 45 );
457
458 // Accept only a success flag with a non-empty result.
459 if ( ! is_wp_error( $response ) && isset( $response['success'], $response['result'] ) && $response['success'] === true && $response['result'] != '' ) {
460 $server_id_to_return = $response['result'];
461 /**
462 * Filter how long a show's server id is cached, in seconds.
463 *
464 * Clamped to [1, 3600]. The transient name and value are frozen.
465 *
466 * @since 4.14.0
467 *
468 * @param int $seconds Lifetime; default 60.
469 */
470 $ttl = wpstream_tunable( 'wpstream_server_id_cache_ttl', 60, HOUR_IN_SECONDS );
471 // Cache the result briefly and return it.
472 set_transient( $transient_name, $server_id_to_return, $ttl );
473 return $server_id_to_return;
474 }
475
476 // Any failure yields an empty string (not cached).
477 return '';
478 }
479
480 /**
481 * Deprecated misspelled alias of wpstream_retrieve_server_id_based_on_show_id().
482 *
483 * Kept because the method is public and may be called by external code.
484 *
485 * @deprecated 4.13.4 Use wpstream_retrieve_server_id_based_on_show_id() instead.
486 */
487 function retrive_server_id_based_on_show_id($show_id){
488 return $this->wpstream_retrieve_server_id_based_on_show_id( $show_id );
489 }
490
491
492
493 /**
494 * AJAX: poll a channel's status while a broadcast is starting up.
495 *
496 * Calls channel/info (via wpstream_check_event_status_api_call), persists the
497 * returned event data, fires `wpstream_channel_became_active` on the active
498 * transition, derives the OBS ingest URI/stream key from broadcast_url, and
499 * echoes the response as JSON. Endpoint: wp_ajax_wpstream_check_event_status.
500 *
501 * @return void Prints JSON then dies.
502 * @since 3.0.1
503 */
504
505 public function wpstream_check_event_status(){
506 // Verify the start-event nonce before doing anything.
507 check_ajax_referer( 'wpstream_start_event_nonce', 'nonce' );
508 // Sanitize inputs: the channel id and an optional caller-context note.
509 $channel_id = intval($_POST['channel_id']);
510
511 // Ownership gate: only the channel's author (or an admin) may query
512 // status and have ingest credentials written back to its meta.
513 if ( ! wpstream_can_manage_channel( get_current_user_id(), $channel_id ) ) {
514 print json_encode( array(
515 'success' => false,
516 'error' => esc_html__( 'You are not allowed to control this channel.', 'wpstream' ),
517 ) );
518 die();
519 }
520
521 $notes = 'wpstream_check_event_status_note';
522 if(isset($_POST['notes'])){
523 $notes = sanitize_text_field($_POST['notes']);
524 }
525
526
527 // Ask the API for the channel's current status/details.
528 $response = $this->wpstream_check_event_status_api_call($channel_id,$notes);
529
530 // Remember the prior status so we can detect a transition to active.
531 $previous_status = get_post_meta( $channel_id, 'status', true );
532
533 // Only process a successful API response.
534 if( isset($response['success']) && $response['success']){
535 // Persist all returned fields to post meta + a short transient.
536 $this->api20_wpstream_update_event($response,$channel_id);
537
538 if (
539 isset( $response['status'] )
540 && $response['status'] === 'active'
541 && $previous_status !== 'active'
542 ) {
543 /**
544 * Fires when the Channel status transitions to `active`
545 * (the broadcaster has connected). This is NOT "Live": a
546 * channel is Live only when it is active AND a playback URL
547 * exists, and the playback URL may not exist yet at this
548 * moment — do not embed a player from this hook. Fires once
549 * per live session (guarded by the previously stored status).
550 *
551 * @param int $channel_id Channel post ID.
552 * @param array $response API response data.
553 * @param string $notes Caller context from JS (e.g. wpstream_check_live_connections_on_start).
554 */
555 do_action( 'wpstream_channel_became_active', $channel_id, $response, $notes );
556 }
557
558 if (
559 isset( $response['status'] )
560 && in_array( $response['status'], array( 'stopped', 'stopping', 'ended' ), true )
561 && $previous_status === 'active'
562 ) {
563 /**
564 * Fires when a live channel stops streaming (counterpart of
565 * wpstream_channel_became_active). Guarded by the previously
566 * stored status, so it fires once per live session — repeat
567 * polls after the stop see a non-active previous status and
568 * do not re-fire it.
569 *
570 * @param int $channel_id Channel post ID.
571 * @param array $response API response data.
572 * @param string $notes Caller context from JS.
573 */
574 do_action( 'wpstream_channel_became_inactive', $channel_id, $response, $notes );
575 }
576
577 // When active with a broadcast URL, expose ingest details to the client.
578 if( isset($response['broadcast_url']) && isset($response['status']) && $response['status']==='active' ){
579
580
581 // Surface the QoS/live-data URL under the expected key.
582 $response['live_data_url'] = $response['qos_url'];
583
584 /* obsolote due to new url format
585 $local_event_options = get_post_meta ($channel_id,'local_event_options',true);
586 if( is_array( $local_event_options ) && intval( $local_event_options['autostart']) ==1 ){
587 $to_split=explode('/',$response['broadcast_url']);
588 $obs_stream = array_pop($to_split);;
589 $obs_uri = str_replace($obs_stream,'',$response['broadcast_url']);
590 }else{
591 $to_split=explode('wpstream/',$response['broadcast_url']);
592 $obs_uri = $to_split[0].'wpstream/';
593 $obs_stream = $to_split[1];
594 }
595 */
596
597
598 // Split broadcast_url into the RTMP server URI and stream key:
599 // the last path segment is the stream key, the rest is the URI.
600 $to_split = explode('/',$response['broadcast_url']);
601 $obs_stream = array_pop($to_split);;
602 $obs_uri = str_replace($obs_stream,'',$response['broadcast_url']);
603
604 // Add the derived ingest fields to the response payload.
605 $response['obs_uri'] = $obs_uri;
606 $response['obs_stream'] = $obs_stream;
607
608 // Persist ingest URI, stream key and full broadcast URL to meta.
609 update_post_meta($channel_id,'obs_uri',$obs_uri);
610 update_post_meta($channel_id,'obs_stream',$obs_stream);
611 update_post_meta($channel_id,'broadcast_url',$response['broadcast_url']);
612 // Store the embed key when the API supplied one.
613 if ( isset( $response['embedKey'] ) && $response['embedKey'] != '' ) {
614 update_post_meta( $channel_id,'embedKey',$response['embedKey'] );
615 }
616
617 }
618
619
620 }
621
622 // Return the (possibly augmented) status payload to the JS caller.
623 print json_encode($response);
624 die();
625
626 }
627
628 /**
629 * AJAX: return the stored WHIP publish URL for a channel (WebRTC ingest).
630 *
631 * Reads the `whipUrl` post meta (no remote call). Endpoint:
632 * wp_ajax_wpstream_check_whipurl.
633 *
634 * @return void Prints JSON then dies.
635 */
636 public function wpstream_check_whipurl() {
637 // Verify the start-event nonce and read the target channel id.
638 check_ajax_referer( 'wpstream_start_event_nonce', 'nonce' );
639 $channel_id = intval($_POST['channel_id']);
640
641 // Ownership gate: never return another broadcaster's WHIP credential.
642 if ( ! wpstream_can_manage_channel( get_current_user_id(), $channel_id ) ) {
643 print json_encode( array(
644 'success' => false,
645 'error' => esc_html__( 'You are not allowed to control this channel.', 'wpstream' ),
646 ) );
647 die();
648 }
649
650 // The WHIP URL is cached on the channel post meta once known.
651 $whip_url = get_post_meta($channel_id, 'whipUrl', true);
652
653 if ( $whip_url ) {
654 // Found: return it to the browser broadcaster.
655 print json_encode(
656 array(
657 'success' => true,
658 'whip_url' => $whip_url,
659 )
660 );
661 } else {
662 // Not yet available for this channel.
663 print json_encode(
664 array(
665 'success' => false,
666 'error' => esc_html__('WHIP URL not found for this channel.', 'wpstream'),
667 )
668 );
669 }
670 die();
671 }
672
673 /**
674 * Query the remote API for a channel's current status/details.
675 *
676 * Endpoint: channel/info. Returns the decoded response (embed data included).
677 *
678 * @param int $channel_id Channel post ID.
679 * @param string $notes Caller context passed through to the API.
680 * @return array|false Decoded response array, or false when no token is available.
681 * @since 3.0.1
682 */
683
684
685 public function wpstream_check_event_status_api_call($channel_id,$notes){
686
687 // This site's host scopes the request; the token is injected by the transport.
688 $domain = parse_url ( get_site_url() );
689 $url = 'channel/info';
690
691 // Build the POST body; 'embed' asks the API to include embed data.
692 $curl_post_fields=array(
693 'channel_id' => $channel_id,
694 'domain' => $domain['host'],
695 'embed' => true,
696 'notes' => $notes
697 );
698
699
700
701
702 // POST to channel/info; callers (and the JS poller) expect an array
703 // with success/error keys, so map a WP_Error onto that shape — except
704 // not-connected, which keeps the legacy `false` so Channel_State never
705 // caches a payload for an unlinked site.
706 $response = $this->authorized_request( $url, $curl_post_fields, WPSTREAM_TIMEOUT_CONST );
707 if ( is_wp_error( $response ) ) {
708 if ( 'wpstream_not_connected' === $response->get_error_code() ) {
709 return false;
710 }
711 return array( 'success' => false, 'error' => $response->get_error_message() );
712 }
713
714 return $response;
715
716
717 }
718
719
720
721
722
723
724
725
726
727
728
729
730 /**
731 * Clear cached per-event meta (stats/HLS/server) for a channel.
732 *
733 * @param int $event_id Channel/event post ID.
734 * @return void
735 */
736 public function wpstream_reset_event_data($event_id){
737 // Blank out the three volatile fields tied to a live session.
738 update_post_meta($event_id,'stats_url','');
739 update_post_meta($event_id,'hls_playback_url','');
740 update_post_meta($event_id,'server_id', '' );
741 }
742
743 /**
744 * Persist every field of an API event response to post meta and cache it.
745 *
746 * @param array $response Decoded API response (key/value event fields).
747 * @param int $channel_id Channel post ID to store the meta against.
748 * @return array|false The stored data on success, false if $response is not an array.
749 * @since 3.0.1
750 */
751
752
753
754 function api20_wpstream_update_event($response,$channel_id){
755
756 // Allowlist filtering and the status-cache write are owned by
757 // Wpstream_Channel_State (the cache's single writer); it returns the
758 // filtered payload, or false for a non-array response.
759 $event_data_for_transient = Wpstream_Channel_State::store( $channel_id, $response );
760 if ( false === $event_data_for_transient ) {
761 // Non-array response: nothing to store.
762 return false;
763 }
764
765 // Mirror each stored field into post meta — same allowlisted set as
766 // the cache, so a response can never plant arbitrary meta keys.
767 foreach ( $event_data_for_transient as $key => $value ) {
768 update_post_meta( $channel_id, $key, $value );
769 }
770 return $event_data_for_transient;
771
772
773 }
774
775 /**
776 * AJAX: save a single channel's per-event streaming options.
777 *
778 * Sanitizes the posted option map, enforces the encrypt/low-latency/ABR
779 * mutual-exclusion rules, stores it as `local_event_options` post meta, then
780 * pushes the config to the API via channel/update.
781 * Endpoint: wp_ajax_wpstream_update_local_event_settings.
782 *
783 * @return void
784 * @since 3.0.1
785 */
786
787
788 public function wpstream_update_local_event_settings(){
789
790 // Nonce + auth gate: must be a logged-in administrator.
791 check_ajax_referer( 'wpstream_start_event_nonce', 'security' );
792 if(!is_user_logged_in()){
793 exit('not logged in');
794 }
795 if( !current_user_can('administrator') ){
796 exit('not admin');
797 }
798
799 $channel_id = intval( $_POST['show_id'] ?? 0 );
800 if ( ! $this->is_published_live_channel( $channel_id ) ) {
801 wp_send_json_error( array( 'success' => false, 'error' => 'invalid_channel' ), 400 );
802 }
803
804 $result = $this->channel_settings->apply(
805 array(
806 'type' => 'save_customized',
807 'channel_id' => $channel_id,
808 'options' => ( isset( $_POST['option'] ) && is_array( $_POST['option'] ) ) ? $_POST['option'] : array(),
809 )
810 );
811
812 if ( empty( $result['success'] ) ) {
813 wp_send_json_error( array( 'success' => false, 'error' => $result['error'] ?? 'invalid_value' ), 400 );
814 }
815
816 $this->send_channel_settings_baker_result( $result );
817 wp_send_json( array( 'success' => true === ( $result['baker_success'] ?? false ) ) );
818
819 }
820
821 /**
822 * Push the site's global streaming options onto one channel via the API.
823 *
824 * Not wired to an AJAX action in this class; callable helper. Reads the
825 * `wpstream_user_streaming_global_channel_options` option and syncs it up.
826 *
827 * @return void Emits JSON error on auth/param failure.
828 */
829 public function wpstream_update_local_event_settings_with_global() {
830 // Nonce check (note: check_ajax_referer is called twice here).
831 check_ajax_referer( 'wpstream_start_event_nonce', 'security' );
832
833 check_ajax_referer( 'wpstream_start_event_nonce', 'security' );
834 // Auth gate: logged-in administrator required.
835 if(!is_user_logged_in()){
836 wp_send_json_error(['success' => false, 'message' => __('Not logged in', 'wpstream')]);
837 }
838 if( !current_user_can('administrator') ){
839 wp_send_json_error(['success' => false, 'message' => __('Not admin', 'wpstream')]);
840 }
841
842 // Require both expected POST params.
843 if ( !isset($_POST['show_id']) || !isset($_POST['use_global']) ) {
844 wp_send_json_error(['success' => false, 'message' => __('Missing parameters', 'wpstream')]);
845 }
846
847 // Apply the Channel's resolved saved snapshot.
848 $show_id = intval($_POST['show_id']);
849 $this->wpstream_update_channel_on_baker( $show_id, array() );
850 }
851
852 /**
853 * AJAX: toggle whether a channel uses global settings or its own local ones.
854 *
855 * Stores the `use_global_event_options` flag, ensures local options exist
856 * when switching to local, then syncs global options to the API either way.
857 * Endpoint: wp_ajax_wpstream_update_use_global_event_options.
858 *
859 * @return void Emits JSON error on failure.
860 */
861 public function wpstream_update_use_global_event_options() {
862 // Nonce + administrator auth gate.
863 check_ajax_referer( 'wpstream_start_event_nonce', 'security' );
864 if(!is_user_logged_in()){
865 wp_send_json_error(['success' => false, 'message' => __('Not logged in', 'wpstream')]);
866 }
867 if( !current_user_can('administrator') ){
868 wp_send_json_error(['success' => false, 'message' => __('Not admin', 'wpstream')]);
869 }
870
871 // Both parameters are mandatory.
872 if ( !isset($_POST['show_id']) || !isset($_POST['use_global']) ) {
873 wp_send_json_error(['success' => false, 'message' => __('Missing parameters', 'wpstream')]);
874 }
875
876 $channel_id = intval( $_POST['show_id'] );
877 if ( ! $this->is_published_live_channel( $channel_id ) ) {
878 wp_send_json_error( array( 'success' => false, 'error' => 'invalid_channel' ), 400 );
879 }
880
881 $result = $this->channel_settings->apply(
882 array(
883 'type' => 'select_mode',
884 'channel_id' => $channel_id,
885 'mode' => intval( $_POST['use_global'] ) ? 'defaults' : 'customized',
886 )
887 );
888
889 if ( empty( $result['success'] ) ) {
890 wp_send_json_error( array( 'success' => false, 'message' => __( 'Failed to update event', 'wpstream' ) ) );
891 }
892
893 $this->send_channel_settings_baker_result( $result );
894 // The channel now resolves to a different settings map (its own, or its
895 // creation snapshot) than the one the modal on screen was rendered with.
896 // Return that map so the browser can repaint the switches: a bare
897 // {success} reply leaves stale switches, and the next option click would
898 // save those stale values over the channel's real settings.
899 wp_send_json(
900 array(
901 'success' => true === ( $result['baker_success'] ?? false ),
902 'mode' => $result['state']['mode'] ?? '',
903 'options' => $result['state']['options'] ?? array(),
904 )
905 );
906 }
907
908 /**
909 * Whether Channel Settings may mutate the requested post.
910 *
911 * @param int $channel_id Candidate post ID.
912 * @return bool
913 */
914 private function is_published_live_channel( $channel_id ) {
915 $post = get_post( $channel_id );
916 if ( ! $post instanceof WP_Post || 'publish' !== $post->post_status ) {
917 return false;
918 }
919
920 return 'wpstream_product' === $post->post_type
921 || ( 'product' === $post->post_type && has_term( 'live_stream', 'product_type', $post ) );
922 }
923
924 /** Preserve the legacy disconnected response emitted by settings AJAX Adapters. */
925 private function send_channel_settings_baker_result( $result ) {
926 $baker_result = $result['baker_result'] ?? null;
927 if ( is_wp_error( $baker_result ) && 'wpstream_not_connected' === $baker_result->get_error_code() ) {
928 wp_send_json(
929 array(
930 'is_record' => '',
931 'connected' => false,
932 'event_data' => '',
933 'error' => esc_html__( 'You are not connected to wpstream.net! Please check your WpStream credentials!', 'wpstream' ),
934 )
935 );
936 }
937 }
938
939 /**
940 * AJAX: save the site-wide default streaming options.
941 *
942 * Sanitizes the posted option map, applies the encrypt/low-latency/ABR
943 * mutual-exclusion rules, and stores it in the
944 * `wpstream_user_streaming_global_channel_options` option.
945 * Endpoint: wp_ajax_wpstream_update_default_channel_settings.
946 *
947 * @return void Emits JSON success/error then dies.
948 */
949 public function wpstream_update_default_channel_settings() {
950 // CSRF gate, then authorization: writing site-wide defaults requires manage_options.
951 // The nonce alone is not authorization — anyone holding a valid settings nonce must
952 // still be an administrator to change these options.
953 check_ajax_referer( 'wpstream-settings-nonce', 'security' );
954 if ( ! current_user_can( 'manage_options' ) ) {
955 wp_send_json_error( array( 'success' => false ), 403 );
956 }
957
958 // Sanitize each posted option key/value, keeping only keys the plugin defines.
959 // This is a fixed catalog (the global event options), so an arbitrary key set
960 // cannot inject unknown entries into the stored option.
961 $result = $this->channel_settings->apply(
962 array(
963 'type' => 'save_defaults',
964 'options' => ( isset( $_POST['option'] ) && is_array( $_POST['option'] ) ) ? $_POST['option'] : array(),
965 )
966 );
967
968 if ( empty( $result['success'] ) ) {
969 wp_send_json_error( array( 'success' => false, 'error' => $result['error'] ?? 'invalid_value' ), 400 );
970 }
971
972 wp_send_json( array( 'success' => true ) );
973
974 // Low-latency/ABR disable encryption... (keys may be absent after allowlist filtering).
975
976 // ...and encryption disables low-latency/ABR (mutually exclusive).
977
978 // Merge the posted keys onto the stored option rather than replacing it. The
979 // settings form only posts the channel toggles, so a plain update_option() would
980 // drop sibling keys the form never sends — notably the site-wide ses_encrypt flag.
981
982 // Persist the merged global defaults option.
983 }
984
985 /**
986 * AJAX: generic single-option saver, typed by the option's field kind.
987 *
988 * Sanitizes the value according to `option_type` (checkbox/text/select/
989 * multiple-select) and stores it under `wpstream_{option_name}`.
990 * Endpoint: wp_ajax_wpstream_update_settings.
991 *
992 * @return void Emits JSON success/error then dies.
993 */
994 public function wpstream_update_settings() {
995 // CSRF gate, then authorization: this generic option writer requires manage_options.
996 // A valid settings nonce is not authorization on its own.
997 check_ajax_referer( 'wpstream-settings-nonce', 'security' );
998 if ( ! current_user_can( 'manage_options' ) ) {
999 wp_send_json_error( array( 'success' => false ), 403 );
1000 }
1001
1002 // Option identity and its field type drive the sanitization below.
1003 $option_name = sanitize_key( $_POST['option_name'] ?? '' );
1004 $option_type = sanitize_key( $_POST['option_type'] ?? '' );
1005
1006 // Only the settings-screen fields that auto-save through this endpoint may be written.
1007 // This is an explicit allowlist rather than "any wpstream_ key": it deliberately excludes
1008 // the credential options (api_username / api_password), which are never editable here.
1009 // Deliberately not filterable: a filter would let any plugin re-open those
1010 // credential options through admin-ajax.
1011 $allowed_options = array(
1012 'free_media_slug',
1013 'free_media_slug_vod',
1014 'stream_role',
1015 'user_streaming_channel_type',
1016 'user_streaming_default_price',
1017 'global_sub',
1018 'global_sub_id',
1019 'product_not_login',
1020 'product_not_bought',
1021 'product_not_subscribe',
1022 'product_thankyou',
1023 'subscription_active',
1024 'you_are_not_live',
1025 'video_player_theme',
1026 'player_logo_position',
1027 'vod_autoplay',
1028 'vod_start_muted',
1029 'vod_domain_lock',
1030 'vod_encrypt',
1031 );
1032 if ( ! in_array( $option_name, $allowed_options, true ) ) {
1033 wp_send_json_error( array( 'success' => false ), 400 );
1034 }
1035
1036 // Read the raw value once (may be a string or, for multi-selects, an array).
1037 $raw_option_value = $_POST['option_value'] ?? '';
1038
1039 // Sanitize the incoming value per field type.
1040 switch( $option_type ) {
1041 case 'checkbox':
1042 // Coerce to an int (0/1).
1043 $option_value = filter_var( $raw_option_value, FILTER_VALIDATE_INT );
1044 break;
1045 case 'text':
1046 case 'select':
1047 // Single scalar text value.
1048 $option_value = sanitize_text_field( $raw_option_value );
1049 break;
1050 case 'multiple-select':
1051 // Expect an array; empty when not an array.
1052 if ( !is_array( $raw_option_value ) ) {
1053 $option_value = array();
1054 break;
1055 }
1056 $option_value = array_map( 'sanitize_text_field', $raw_option_value );
1057 break;
1058 default:
1059 // Fallback: treat as plain text.
1060 $option_value = sanitize_text_field( $raw_option_value );
1061 }
1062
1063 // Persist under a namespaced option name.
1064 $successful_update = update_option( 'wpstream_' . $option_name, $option_value );
1065
1066 if( $successful_update ) {
1067 // Saved (value changed).
1068 wp_send_json(
1069 array(
1070 'success' => true,
1071 )
1072 );
1073 } else {
1074 // update_option returned false (unchanged value or failure).
1075 wp_send_json_error(
1076 array(
1077 'success' => false,
1078 )
1079 );
1080 }
1081
1082 wp_die();
1083 }
1084
1085 /**
1086 * Push a channel's resolved streaming config to the API (channel/update).
1087 *
1088 * Builds CORS origin, key-access IP, record/encrypt/low-latency/ABR flags
1089 * from the saved options and POSTs them. Prints a minimal {success} JSON.
1090 *
1091 * @param int $channel_id Channel post ID.
1092 * @param array $to_save_option Retained for compatibility; the Module resolves the saved snapshot.
1093 * @return void Prints a {success} JSON then exits.
1094 * @since 4.2
1095 */
1096
1097
1098 public function wpstream_update_channel_on_baker( $channel_id, $to_save_option = array() ) {
1099
1100 $configuration = $this->channel_settings->prepare_baker( (int) $channel_id );
1101 $response = $this->synchronize_channel_settings( (int) $channel_id, $configuration );
1102 wp_send_json(
1103 array(
1104 'success' => ! is_wp_error( $response ) && ! empty( $response['success'] ),
1105 )
1106 );
1107 }
1108
1109 /**
1110 * Deprecated misspelled alias of wpstream_update_channel_on_baker().
1111 *
1112 * Kept because the method is public and may be called by external code.
1113 *
1114 * @deprecated 4.13.4 Use wpstream_update_channel_on_baker() instead.
1115 */
1116 public function wpstream_update_chanel_on_baker( $channel_id, $to_save_option = array() ) {
1117 return $this->wpstream_update_channel_on_baker( $channel_id, $to_save_option );
1118 }
1119
1120 /**
1121 * Address this site fetches HLS keys from, as sent to Baker in `allow_key_access_from`.
1122 *
1123 * Prefers the web server's own SERVER_ADDR. When that is absent (CLI, cron,
1124 * proxies, containers) the site's host name is resolved once and cached for
1125 * an hour. Only when resolution fails too does the value fall open to
1126 * 0.0.0.0/0, and a warning is written to the plugin log so the operator
1127 * can see that key delivery is not pinned to this server.
1128 *
1129 * @return string IPv4 address or '0.0.0.0/0'.
1130 */
1131 public function wpstream_key_access_address() {
1132 // The request's own server address is the best answer when present.
1133 $server_addr = isset( $_SERVER['SERVER_ADDR'] ) ? (string) $_SERVER['SERVER_ADDR'] : '';
1134 if ( false !== filter_var( $server_addr, FILTER_VALIDATE_IP ) ) {
1135 return $server_addr;
1136 }
1137
1138 // Otherwise use the resolved site address cached from an earlier request.
1139 $cached = get_transient( 'wpstream_key_access_address' );
1140 if ( false !== $cached ) {
1141 return $cached;
1142 }
1143
1144 // Resolve the site's host name; gethostbyname() echoes the name back on failure.
1145 $host = (string) wp_parse_url( home_url(), PHP_URL_HOST );
1146 $resolved = '' !== $host ? gethostbyname( $host ) : '';
1147 if ( false !== filter_var( $resolved, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
1148 set_transient( 'wpstream_key_access_address', $resolved, HOUR_IN_SECONDS );
1149 return $resolved;
1150 }
1151
1152 // Last resort: leave key access open and say so in the log.
1153 $logger = new WpStream_Logger();
1154 $logger->add( new WpStream_Log_Entry( array(
1155 'type' => 'error',
1156 'description' => 'HLS key access left open (0.0.0.0/0): SERVER_ADDR is missing and "' . $host . '" could not be resolved.',
1157 ) ) );
1158
1159 return '0.0.0.0/0';
1160 }
1161
1162 /**
1163 * The address or CIDR range allowed to fetch a channel's HLS keys.
1164 *
1165 * A filtered value is accepted only when it is an IP address or an
1166 * IP/prefix CIDR range; anything else keeps the detected address so a
1167 * malformed callback cannot open or break key delivery.
1168 *
1169 * @param int $channel_id Live Channel post ID.
1170 * @return string IP address or CIDR range.
1171 */
1172 private function allow_key_access_from( $channel_id ) {
1173 $detected = $this->wpstream_key_access_address();
1174
1175 /**
1176 * Filter the address allowed to fetch the channel's HLS keys.
1177 *
1178 * Use when keys are served from a different host than this site
1179 * (CDN, reverse proxy). Must be an IP or CIDR range.
1180 *
1181 * @since 4.14.0
1182 *
1183 * @param string $address Detected server address (or `0.0.0.0/0` when unknown).
1184 * @param int $channel_id Live Channel post ID.
1185 */
1186 $filtered = trim( (string) apply_filters( 'wpstream_allow_key_access_from', $detected, (int) $channel_id ) );
1187
1188 // Accept "ip" or "ip/prefix" only; the prefix is bounded by the address family.
1189 $parts = explode( '/', $filtered, 2 );
1190 $is_ip = false !== filter_var( $parts[0], FILTER_VALIDATE_IP );
1191 $max_prefix = false !== filter_var( $parts[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ? 32 : 128;
1192 $is_ok = $is_ip && ( ! isset( $parts[1] ) || ( ctype_digit( $parts[1] ) && (int) $parts[1] <= $max_prefix ) );
1193
1194 return $is_ok ? $filtered : $detected;
1195 }
1196
1197 /**
1198 * The URL prefix Baker's players fetch HLS keys from for a channel.
1199 *
1200 * @param int $channel_id Live Channel post ID.
1201 * @return string URL prefix the key name is appended to.
1202 */
1203 private function hls_keys_url_prefix( $channel_id ) {
1204 /**
1205 * Filter the HLS key delivery URL prefix.
1206 *
1207 * The key name is appended verbatim; the endpoint must answer with the
1208 * plugin's DRM key delivery (see Wpstream_Drm_Key_Delivery).
1209 *
1210 * @since 4.14.0
1211 *
1212 * @param string $prefix Default `{site_url}?wpstream_livedrm=`.
1213 * @param int $channel_id Live Channel post ID.
1214 */
1215 return esc_url_raw( (string) apply_filters( 'wpstream_hls_keys_url_prefix', get_site_url() . '?wpstream_livedrm=', (int) $channel_id ) );
1216 }
1217
1218 /** Production Baker Adapter operation; authentication and HTTP remain connection-owned. */
1219 public function synchronize_channel_settings( $channel_id, $configuration ) {
1220 $domain = wp_parse_url( get_site_url() );
1221
1222 return $this->authorized_request(
1223 'channel/update',
1224 array(
1225 'channel_id' => (int) $channel_id,
1226 'domain' => $domain['host'] ?? '',
1227 'allow_access_from' => $configuration['allow_access_from'],
1228 'record' => $configuration['record'],
1229 'encrypt' => $configuration['encrypt'],
1230 'autostart' => $configuration['autostart'],
1231 'low_latency' => $configuration['low_latency'],
1232 'abr' => $configuration['abr'],
1233 'hls_keys_url_prefix' => $this->hls_keys_url_prefix( $channel_id ),
1234 'allow_key_access_from' => $this->allow_key_access_from( $channel_id ),
1235 'to_save_option' => $configuration['options'],
1236 )
1237 );
1238 }
1239
1240
1241
1242
1243
1244 /**
1245 * AJAX: stop a running channel (channel/stop).
1246 *
1247 * Requires a token and stream permission, then POSTs the stop request and
1248 * echoes a minimal connected/error JSON payload.
1249 * Endpoint: wp_ajax_wpstream_turn_of_channel.
1250 *
1251 * @return void Prints JSON then dies.
1252 * @since 5.0
1253 */
1254 public function wpstream_turn_of_channel(){
1255
1256 // Target channel id from the request.
1257 $channel_id = intval($_POST['show_id']);
1258
1259 // Ownership gate: only the channel's author (or an admin) may stop it.
1260 // Runs BEFORE the token check so an unauthorized caller is denied
1261 // regardless of whether this site is connected to wpstream.net.
1262 // NOTE: intentionally NOT nonce-gated — see wpstream_give_me_live_uri()
1263 // for the caching rationale (stale cached start-event nonce breaks real
1264 // users). No wp_ajax_nopriv_ registration, so guests cannot reach it;
1265 // the ownership check is the real CSRF/IDOR guard for logged-in callers.
1266 if( !wpstream_can_manage_channel( get_current_user_id(), $channel_id ) ){
1267 exit('You are not allowed to control this channel. Code 408');
1268 }
1269
1270 // Require a valid token; otherwise emit a "not connected" JSON and stop.
1271 $this->require_token_or_die();
1272 // Current user id (captured; not used further here).
1273 $current_user = wp_get_current_user();
1274 $userID = $current_user->ID;
1275
1276
1277 // Enforce the site's streaming-permission rule.
1278 global $wpstream_plugin;
1279 if( !$wpstream_plugin->main->wpstream_check_user_can_stream() ){
1280 exit('You are not allowed to stream.Code 407');
1281 }
1282
1283 /**
1284 * Fires when an authorised stop request is about to be sent to Baker.
1285 *
1286 * @since 4.14.0
1287 *
1288 * @param int $channel_id Live Channel post ID.
1289 * @param int $user_id User who asked to stop the channel.
1290 */
1291 do_action( 'wpstream_channel_stop_requested', intval( $channel_id ), intval( $userID ) );
1292
1293 // Build and send the channel/stop request.
1294 $url = 'channel/stop';
1295 $domain = parse_url ( get_site_url() );
1296 $curl_post_fields=array(
1297 'channel_id' => $channel_id,
1298 'domain' => $domain['host'],
1299
1300 );
1301
1302
1303
1304 // POST to channel/stop.
1305 $response = $this->authorized_request( $url, $curl_post_fields );
1306
1307 // Success => report connected; otherwise report already off / not found.
1308 // Only the outcome is returned — the decoded upstream response stays
1309 // server-side (no JS consumer reads it, and it can carry API internals).
1310 if ( ! is_wp_error( $response ) && isset( $response['success'] ) && $response['success'] === true ) {
1311
1312 // End-of-stream hook: fire once per live session. The stored
1313 // status is the guard — only a channel still marked active
1314 // transitions here, and persisting 'stopped' immediately keeps
1315 // both this path and the status poll from firing again for the
1316 // same session.
1317 if ( get_post_meta( $channel_id, 'status', true ) === 'active' ) {
1318 update_post_meta( $channel_id, 'status', 'stopped' );
1319
1320 /** This action is documented in wpstream_check_event_status() above. */
1321 do_action( 'wpstream_channel_became_inactive', $channel_id, $response, 'wpstream_turn_of_channel' );
1322 }
1323
1324 wp_send_json( array(
1325 'connected' => true,
1326 ) );
1327 } else {
1328 /**
1329 * Fires when Baker refused (or failed) a stop request.
1330 *
1331 * @since 4.14.0
1332 *
1333 * @param int $channel_id Live Channel post ID.
1334 * @param array $response Decoded upstream response; a transport
1335 * failure is reported as array( 'error' => message ).
1336 */
1337 do_action( 'wpstream_channel_stop_failed', intval( $channel_id ), is_wp_error( $response ) ? array( 'error' => $response->get_error_message() ) : (array) $response );
1338
1339 wp_send_json( array(
1340 'connected' => false,
1341 'error' => esc_html__( 'Channel is already turned off or does not exist!', 'wpstream' ),
1342 ) );
1343 }
1344
1345
1346
1347
1348 }
1349
1350
1351 /**
1352 * AJAX: turn a channel ON and return its live ingest data.
1353 *
1354 * Resolves the effective options (local overrides vs global defaults),
1355 * normalises the encrypt/low-latency/ABR/record flags, then delegates to
1356 * wpstream_request_live_stream_uri() (channel/start) and echoes the result.
1357 * Endpoint: wp_ajax_wpstream_give_me_live_uri.
1358 *
1359 * @return void Prints JSON then dies.
1360 * @since 3.0.1
1361 */
1362 public function wpstream_give_me_live_uri(){
1363
1364 // Target channel id from the request.
1365 $channel_id = intval($_POST['show_id']);
1366
1367 // Ownership gate: only the channel's author (or an admin) may start it.
1368 // Runs BEFORE the token check so an unauthorized caller is denied
1369 // regardless of whether this site is connected to wpstream.net.
1370 // NOTE: intentionally NOT nonce-gated — the start-event nonce is printed
1371 // into cacheable start-streaming markup, so a page cache serves a stale
1372 // token and check_ajax_referer would reject legitimate users (this was
1373 // tried on the abandoned `nonce-fixes` branch and reverted). This handler
1374 // also has no wp_ajax_nopriv_ registration, so guests cannot reach it;
1375 // for logged-in callers the ownership check is the real CSRF/IDOR guard.
1376 if( !wpstream_can_manage_channel( get_current_user_id(), $channel_id ) ){
1377 exit('You are not allowed to control this channel. Code 408');
1378 }
1379
1380 // Require a valid token; otherwise emit a "not connected" JSON and stop.
1381 $access_token = $this->require_token_or_die();
1382
1383 // Current user id (used as the request-by id for the API call).
1384 $current_user = wp_get_current_user();
1385 $userID = $current_user->ID;
1386
1387
1388 // Enforce the site's streaming-permission rule.
1389 global $wpstream_plugin;
1390 if( !$wpstream_plugin->main->wpstream_check_user_can_stream() ){
1391 exit('You are not allowed to stream.Code 407');
1392 }
1393
1394 // Quota lookup determines whether advanced (non-basic) features are allowed.
1395 $pack_details = $wpstream_plugin->main->quota_manager->get_live_quota_data( 'wpstream_give_me_live_uri' );
1396 $basic_streaming = $wpstream_plugin->main->quota_manager->is_basic_streaming_mode( $pack_details );
1397
1398 // Optional onboarding flag threaded into the start request metadata.
1399 $on_boarding = '';
1400 if(isset($_POST['start_onboarding'])){
1401 $on_boarding = sanitize_text_field($_POST['start_onboarding']);
1402 }
1403
1404
1405 // Decide whether to use this channel's local options or the global set.
1406 $channel_configuration = $this->channel_settings->prepare_baker( $channel_id );
1407 $is_autostart = $channel_configuration['autostart'];
1408 $is_record = $channel_configuration['record'];
1409 $is_encrypt = $channel_configuration['encrypt'] ? 'true' : 'false';
1410 $low_latency = $channel_configuration['low_latency'] ? 'true' : 'false';
1411 $adaptive_bitrate = 'common' === $channel_configuration['abr'] ? 'true' : 'false';
1412 $corsorigin = $channel_configuration['allow_access_from'];
1413
1414 /**
1415 * Fires when a go-live request passed ownership and streaming permission,
1416 * just before Baker is asked to start the channel. Quota has only been
1417 * read to pick the feature tier; it is enforced upstream and an exhausted
1418 * account surfaces through wpstream_channel_start_failed.
1419 *
1420 * @since 4.14.0
1421 *
1422 * @param int $channel_id Live Channel post ID.
1423 * @param int $user_id User who asked to go live.
1424 * @param array $configuration Resolved Channel Settings in Baker's
1425 * flag conventions (record, encrypt, ...).
1426 */
1427 do_action( 'wpstream_channel_start_requested', intval( $channel_id ), intval( $userID ), $channel_configuration );
1428
1429 // Translate the resolved option map into the API flag strings.
1430 // Fire the channel/start request with the resolved flags.
1431 $event_data = $this->wpstream_request_live_stream_uri(
1432 $channel_id,
1433 $is_autostart,
1434 $is_record,
1435 $is_encrypt,
1436 $low_latency,
1437 $adaptive_bitrate,
1438 $userID,
1439 $corsorigin,
1440 $on_boarding,
1441 $basic_streaming
1442 );
1443
1444 // Success => return the event data with connected:true.
1445 if( isset($event_data['success']) && $event_data['success']===true ){
1446 /**
1447 * Fires when Baker issued the ingest for a channel — earlier than
1448 * wpstream_channel_became_active, which waits for the status poll.
1449 * The ingest credentials are deliberately not passed.
1450 *
1451 * @since 4.14.0
1452 *
1453 * @param int $channel_id Live Channel post ID.
1454 * @param int $user_id User who went live.
1455 */
1456 do_action( 'wpstream_channel_started', intval( $channel_id ), intval( $userID ) );
1457
1458 wp_send_json( array(
1459 'is_record' => $is_record,
1460 'connected' => true,
1461 'event_data' => $event_data,
1462 ) );
1463 }else{
1464 // Failure => map known error codes to a friendly message.
1465 $default_error= 'Failed to turn channel ON. Please try again in a few minutes.';
1466 $plumer_error = '';
1467 if( isset($event_data['error'])){
1468 $plumer_error = is_scalar( $event_data['error'] ) ? (string) $event_data['error'] : 'unknown';
1469 switch ($plumer_error) {
1470 case 'NOT_ENOUGH_TRAFFIC':
1471 // Quota exhausted: prompt an upgrade.
1472 $default_error= 'You do not have enough Streaming Data to turn ON a live channel. Please upgrade your subscription for more resources.' ;
1473 break;
1474
1475 }
1476
1477 }
1478
1479 /**
1480 * Fires when Baker refused to start a channel.
1481 *
1482 * @since 4.14.0
1483 *
1484 * @param int $channel_id Live Channel post ID.
1485 * @param string $error_code Upstream error code (e.g. NOT_ENOUGH_TRAFFIC,
1486 * provisioning_failed), '' when none was given,
1487 * 'unknown' when it was not a scalar.
1488 * @param int $user_id User who asked to go live.
1489 */
1490 do_action( 'wpstream_channel_start_failed', intval( $channel_id ), $plumer_error, intval( $userID ) );
1491 // The raw upstream response stays server-side: the JS failure path
1492 // only reads `error`, and upstream objects can carry API internals.
1493 wp_send_json( array(
1494 'is_record' => $is_record,
1495 'connected' => false,
1496 'event_data' => '',
1497 'error' => $default_error,
1498 ) );
1499 }
1500 }
1501
1502 /**
1503 * Send the channel/start request that turns a channel live.
1504 *
1505 * Endpoint: channel/start. Assembles domain/CORS/key-access/metadata and the
1506 * feature flags, gated by $basic_streaming (advanced flags are forced off
1507 * when the user is not in basic-streaming mode).
1508 *
1509 * @param int $schannel_id Channel post ID.
1510 * @param string $is_autostart Autostart flag (unused; 'true' is sent literally).
1511 * @param string $is_record "true"/"false" record flag.
1512 * @param string $is_encrypt "true"/"false" encryption flag.
1513 * @param string $low_latency "true"/"false" low-latency flag (coerced to bool below).
1514 * @param string $adaptive_bitrate "true"/"false" ABR flag.
1515 * @param int $request_by_userid Requesting user id (unused in the body).
1516 * @param string $corsorigin Allowed origin ('*' or scheme://host).
1517 * @param string $on_boarding Non-empty adds on_boarding metadata.
1518 * @param bool $basic_streaming Whether advanced flags are permitted.
1519 * @return array|null Decoded channel/start response.
1520 * @since 3.0.1
1521 */
1522
1523
1524 public function wpstream_request_live_stream_uri(
1525 $schannel_id,
1526 $is_autostart,
1527 $is_record,
1528 $is_encrypt,
1529 $low_latency,
1530 $adaptive_bitrate,
1531 $request_by_userid,
1532 $corsorigin,
1533 $on_boarding,
1534 $basic_streaming
1535 ) {
1536 if ( $this->streaming_content_creation ) {
1537 $provisioning = $this->streaming_content_creation->ensure_provisioned( intval( $schannel_id ) );
1538 if ( empty( $provisioning['success'] ) ) {
1539 return array(
1540 'success' => false,
1541 'error' => 'provisioning_failed',
1542 );
1543 }
1544 }
1545
1546 // Resolve site host + scheme for CORS. The scheme comes from the
1547 // site URL, not is_ssl(): the latter describes the current request
1548 // and is false under wp-cli/cron and behind SSL-terminating
1549 // proxies, which would pin the origin to http:// on an https site.
1550 $domain = parse_url ( get_site_url() );
1551 $domain_scheme = isset( $domain['scheme'] ) ? $domain['scheme'] : 'http';
1552
1553 // The origin comes resolved from the Channel Settings Module (site
1554 // origin when domain-locked, `*` or a filtered origin otherwise);
1555 // only an empty value falls back to this site.
1556 if ( '' === trim( (string) $corsorigin ) ) {
1557 $corsorigin = $domain_scheme . '://' . $domain['host'];
1558 }
1559
1560 // Adaptive bitrate maps to the 'common' ABR profile.
1561 $abr='none';
1562 if($adaptive_bitrate=="true"){
1563 $abr='common';
1564 }
1565
1566 // Coerce the low-latency string flag into a real boolean.
1567 if($low_latency=="true"){
1568 $low_latency=true;
1569 }else{
1570 $low_latency = false;
1571 }
1572
1573 // Normalise basic-streaming to a string flag used as the gate below.
1574 $basic_streaming = $basic_streaming ? 'true' : 'false';
1575
1576 // Endpoint; the auth token is injected by the transport.
1577 $url = 'channel/start';
1578
1579 // Metadata sent with the start request (plugin version, onboarding, permalink).
1580 $metadata_array=array(
1581 'pluginVersion'=>WPSTREAM_PLUGIN_VERSION
1582 );
1583
1584 if($on_boarding!=''){
1585 $metadata_array['on_boarding']='yes';
1586 }
1587 $permalink = get_permalink($schannel_id);
1588 if ($permalink !== false) {
1589 $metadata_array['permalink'] = $permalink;
1590 }
1591
1592 /**
1593 * Filter the metadata sent with channel/start.
1594 *
1595 * Stored by the WpStream cloud alongside the session (plugin
1596 * version, onboarding flag, permalink); add tenant or campaign
1597 * identifiers here. Never put credentials in it.
1598 *
1599 * @since 4.14.0
1600 *
1601 * @param array $metadata Key/value metadata (JSON-encoded for transport).
1602 * @param int $channel_id Live Channel post ID.
1603 */
1604 $metadata_array = (array) apply_filters( 'wpstream_channel_start_metadata', $metadata_array, (int) $schannel_id );
1605
1606 // Build the channel/start body; advanced flags gated by $basic_streaming.
1607 $curl_post_fields=array(
1608 'channel_id' => $schannel_id,
1609 'domain' => $domain['host'],
1610 'allow_access_from' => $corsorigin,
1611 'record' => $basic_streaming ? $is_record : 'false',
1612 'encrypt' => $basic_streaming ? $is_encrypt : 'false',
1613 'low_latency' => $basic_streaming ? $low_latency : 'false',
1614 'abr' => $basic_streaming ? $abr : 'none',
1615 'hls_keys_url_prefix' => $this->hls_keys_url_prefix( $schannel_id ),
1616 'allow_key_access_from' => $this->allow_key_access_from( $schannel_id ),
1617 'metadata' => json_encode($metadata_array),
1618 'autostart' => 'true',
1619 // 'fakeError' => 'init'
1620 );
1621
1622
1623 // POST to channel/start; callers read success/error array keys,
1624 // so map a WP_Error (not-connected included) onto that shape.
1625 $response = $this->authorized_request( $url, $curl_post_fields );
1626 if ( is_wp_error( $response ) ) {
1627 return array( 'success' => false, 'error' => $response->get_error_message() );
1628 }
1629 return $response;
1630
1631
1632
1633 }
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645 /**
1646 * Return a cached API auth token, fetching a fresh one on cache miss.
1647 *
1648 * The token is stored in the `wpstream_token_api` transient for ~58 min;
1649 * failed logins are cached for 1s to avoid hammering the API.
1650 *
1651 * @return string|false Bearer token, or false when authentication failed.
1652 * @since 3.0.1
1653 */
1654 public function wpstream_get_token(){
1655 // Try the cached token first.
1656 $token = get_transient('wpstream_token_api');
1657 // Cache miss / empty => attempt a fresh login.
1658 if ( false === $token || $token === '' || $token=== NULL ) {
1659 $token = $this->wpstream_club_get_token();
1660 if ($token !== false){
1661 /**
1662 * Filter how long a good cloud access token is cached, in seconds.
1663 *
1664 * Clamped to [1, 3500]: the cloud expires the token itself after
1665 * an hour, so the cache can only be shortened. The token value
1666 * and transient name are never filterable.
1667 *
1668 * @since 4.14.0
1669 *
1670 * @param int $seconds Lifetime; default 3500.
1671 */
1672 $ttl = wpstream_tunable( 'wpstream_api_token_ttl', 3500, 3500 );
1673 set_transient( 'wpstream_token_api', $token, $ttl );
1674 }
1675 else {
1676 /**
1677 * Filter how long a failed login is remembered before the next
1678 * attempt, in seconds. Clamped to [1, 300]: without it every
1679 * request would retry the login.
1680 *
1681 * @since 4.14.0
1682 *
1683 * @param int $seconds Cool-down; default 1.
1684 */
1685 $failure_ttl = wpstream_tunable( 'wpstream_api_token_failure_ttl', 1, 5 * MINUTE_IN_SECONDS );
1686 set_transient( 'wpstream_token_api', 'failed', $failure_ttl );
1687
1688 /**
1689 * Fires when the WpStream account could not be authenticated
1690 * (wrong or revoked credentials, API down).
1691 *
1692 * Deliberately has no arguments: neither credentials nor the
1693 * token are ever exposed.
1694 *
1695 * @since 4.14.0
1696 */
1697 do_action( 'wpstream_api_authentication_failed' );
1698 }
1699 }
1700 // The 'failed' sentinel maps to false for callers.
1701 $ret = $token === 'failed' ? false : $token;
1702 return $ret;
1703 }
1704
1705 /**
1706 * Authenticate against the API and return a fresh access token.
1707 *
1708 * Endpoint: access_token (password grant using the stored WpStream
1709 * username/password options).
1710 *
1711 * @return string|false|null Access token, false on rejected login, null when credentials are unset.
1712 * @since 3.0.1
1713 */
1714 protected function wpstream_club_get_token(){
1715 // WpStream credentials: wp-config constants win over the stored options.
1716 $username = wpstream_get_api_username();
1717 $password = wpstream_get_api_password();
1718
1719 // No credentials configured: nothing to do.
1720 if ( $username=='' || $password==''){
1721 return;
1722 }
1723
1724 // Password-grant login body.
1725 $url = 'access_token';
1726 $curl_post_fields = array(
1727 'grant_type' => 'password',
1728 'username' => $username,
1729 'password' => $password
1730 );
1731 // POST to access_token.
1732 $response = $this->request( $url, $curl_post_fields );
1733
1734 // Return the token when present, otherwise signal failure.
1735 if ( ! is_wp_error( $response ) && isset( $response['access_token'] ) && $response['access_token'] != '' ) {
1736 return $response['access_token'];
1737 } else {
1738 return false;
1739 }
1740 }
1741
1742 /*
1743 *
1744 * Return token for api version 3.0
1745 *
1746 */
1747
1748
1749
1750 /**
1751 * Accessor for the composed user-quota service.
1752 *
1753 * @return Wpstream_User_Quota_Service
1754 */
1755 public function get_user_quota_service() {
1756 return $this->user_quota_service;
1757 }
1758
1759 /**
1760 * Accessor for the composed channel service.
1761 *
1762 * @return Wpstream_Channel_Service
1763 */
1764 public function get_channel_service() {
1765 return $this->channel_service;
1766 }
1767
1768 /**
1769 * Create a remote channel (delegated to the channel service).
1770 *
1771 * @param int $channel_id Channel post ID.
1772 * @param string|null $domain Optional domain override.
1773 * @return mixed Result from the channel service.
1774 */
1775 public function wpstream_create_channel( $channel_id, $domain = null ) {
1776 return $this->channel_service->create_channel( $channel_id, $domain );
1777 }
1778
1779 /**
1780 * Delete a remote channel (delegated to the channel service).
1781 *
1782 * @param int $channel_id Channel post ID.
1783 * @param string|null $domain Optional domain override.
1784 * @return mixed Result from the channel service.
1785 */
1786 public function wpstream_delete_channel( $channel_id, $domain = null ) {
1787 return $this->channel_service->delete_channel( $channel_id, $domain );
1788 }
1789
1790
1791 /**
1792 * AJAX: return the current user's live quota data as JSON.
1793 *
1794 * Endpoint: wp_ajax_wpstream_check_user_quota. Delegates to the quota manager.
1795 *
1796 * @return void Prints JSON then dies.
1797 */
1798 public function wpstream_check_user_quota() {
1799 // Fetch quota via the shared quota manager.
1800 global $wpstream_plugin;
1801 $pack_data = $wpstream_plugin->main->quota_manager->get_live_quota_data( 'wpstream_check_user_quota' );
1802 // On missing/failed data, return a generic error payload.
1803 if ( ! $pack_data || ! isset( $pack_data['success'] ) || ! $pack_data['success'] ) {
1804 print json_encode(
1805 array(
1806 'success' => false,
1807 'error' => esc_html__('Couldn\'t get user quota.', 'wpstream'),
1808 )
1809 );
1810 } else {
1811 // Otherwise echo the full quota payload.
1812 print json_encode($pack_data);
1813 }
1814 die();
1815 }
1816
1817
1818
1819 /**
1820 * Return the current user's active live events, keyed by channel id.
1821 *
1822 * Gated by the site's stream permission; delegates the API call to
1823 * wpstream_request_live_stream_for_user() (channel/list).
1824 *
1825 * @return array Map of channel_id => event data (empty when none/not permitted).
1826 * @since 3.0.1
1827 */
1828 public function wpstream_get_live_event_for_user(){
1829 // Identify the current user.
1830 $current_user = wp_get_current_user();
1831 $userID = $current_user->ID;
1832
1833 // Bail (empty) when the user is not allowed to stream.
1834 global $wpstream_plugin;
1835 if( !$wpstream_plugin->main->wpstream_check_user_can_stream() ){
1836 return;
1837 }
1838
1839
1840 // Fetch active events and re-key them by their channel_id.
1841 $event_data = $this->wpstream_request_live_stream_for_user($userID);
1842 $return_event = array();
1843 if(is_array($event_data)):
1844 foreach ($event_data as $key=>$event){
1845 $return_event[$event['channel_id']]=$event;
1846 }
1847 endif;
1848 return $return_event;
1849 }
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861 /**
1862 * Fetch the list of active channels for this site from the API.
1863 *
1864 * Endpoint: channel/list (status=active).
1865 *
1866 * @param int|string $user_id Requesting user id (not sent in the body).
1867 * @return array|false Array of channel entries, or false on no token/failure.
1868 * @since 3.0.1
1869 */
1870 public function wpstream_request_live_stream_for_user($user_id){
1871
1872 global $wpstream_plugin;
1873
1874
1875 // This site's host is sent so the API scopes results correctly.
1876 $domain = parse_url ( get_site_url() );
1877
1878
1879 $url = 'channel/list';
1880
1881 // Request only currently-active channels; the token is injected by the transport.
1882 $curl_post_fields=array(
1883 'domain' => $domain['host'],
1884 'status' => 'active'
1885 );
1886 // POST to channel/list; not-connected and transport failures both map to false below.
1887 $response = $this->authorized_request( $url, $curl_post_fields );
1888
1889 // Return the channels array on success, false otherwise.
1890 if ( ! is_wp_error( $response ) && isset( $response['success'], $response['channels'] ) && $response['success'] == true ) {
1891 return $response['channels'];
1892 } else {
1893 return false;
1894 }
1895
1896
1897 }
1898
1899
1900 /**
1901 * Return active live channel ids for shortcode use, cached in a transient.
1902 *
1903 * Wraps wpstream_request_live_stream_for_user() behind a 30s transient to
1904 * avoid repeated API calls when the shortcode renders.
1905 *
1906 * @param string $outside Unused caller marker.
1907 * @return array List of channel_id values for currently-active events.
1908 * @since 3.0.1
1909 */
1910 public function api20_wpstream_request_live_stream_for_user_for_shortcode($outside=''){
1911 global $wpstream_plugin;
1912 $return_array=array();
1913
1914 // Serve from the 30s cache when present.
1915 $result = get_transient('wpstream_live_stream_for_user_for_shortcode');
1916
1917 // Cache miss: fetch fresh channel list and cache it.
1918 if($result===false){
1919 $result = $this->wpstream_request_live_stream_for_user('');
1920 /**
1921 * Filter how long the account's active-channel list is cached for
1922 * shortcodes, in seconds. Clamped to [1, 300] so a channel that
1923 * just went live shows up within minutes.
1924 *
1925 * @since 4.14.0
1926 *
1927 * @param int $seconds Lifetime; default 30.
1928 */
1929 $ttl = wpstream_tunable( 'wpstream_active_channels_cache_ttl', 30, 5 * MINUTE_IN_SECONDS );
1930 set_transient( 'wpstream_live_stream_for_user_for_shortcode', $result, $ttl );
1931 }
1932
1933 // Reduce the channel entries to just their ids.
1934 if(is_array($result)):
1935 foreach($result as $key=>$event){
1936 $return_array[]=$event['channel_id'];
1937 }
1938 endif;
1939 return $return_array;
1940 }
1941
1942
1943
1944 /**
1945 * Get a signed upload form (S3/AWS) for pushing a recording to storage.
1946 *
1947 * Endpoint: video/upload. Admin only.
1948 *
1949 * @return array|false Decoded signed-form data, false when no token, or a
1950 * {success:false,error:'not_connected'} array.
1951 * @since 3.0.1
1952 */
1953 public function wpstream_get_signed_form_upload_data(){
1954 // Admin-only guard.
1955 if( !current_user_can('administrator') ){
1956 exit('not admin on wpstream_get_signed_form_upload_data');
1957 }
1958
1959
1960 $url = 'video/upload';
1961
1962 // POST to video/upload with an empty body (token injected by the
1963 // transport); return the signed form. Not-connected keeps the literal
1964 // 'not_connected' error string the admin JS checks.
1965 $response = $this->authorized_request( $url, array() );
1966 if ( is_wp_error( $response ) ) {
1967 $error = 'wpstream_not_connected' === $response->get_error_code() ? 'not_connected' : $response->get_error_message();
1968 return array( 'success' => false, 'error' => $error );
1969 }
1970
1971 return $response;
1972 }
1973
1974
1975
1976
1977
1978
1979
1980 /**
1981 * Return a name=>name map of stored videos for admin dropdowns.
1982 *
1983 * Fetches the raw list from the API, sorts newest-first by 'time' and
1984 * reduces it to video names. Admin only.
1985 *
1986 * @return array Map of video name => video name.
1987 * @since 3.0.1
1988 */
1989 public function wpstream_get_videos(){
1990 // Admin-only guard (returns empty for non-admins).
1991 if( !current_user_can('administrator') ){
1992 return;
1993 }
1994
1995
1996 $video_options = array();
1997 // Pull the raw video listing from the API.
1998 $video_array = $this->wpstream_get_videos_from_api();
1999 $video_list_raw_array = false;
2000
2001 // Extract the 'items' array if the response shape is as expected.
2002 if ( is_array($video_array) && isset($video_array['items']) && is_array($video_array['items']) ) {
2003 $video_list_raw_array = $video_array['items'];
2004 }
2005
2006 if(is_array($video_list_raw_array)){
2007 // Sort the list by timestamp descending (newest first).
2008 $keys = array_column($video_list_raw_array, 'time');
2009 array_multisort($keys, SORT_DESC , $video_list_raw_array);
2010
2011 // Build the name=>name options map, skipping unnamed entries.
2012 foreach ($video_list_raw_array as $key => $videos){
2013 if($videos['name']!=''):
2014 $video_options[$videos['name']]=$videos['name'];
2015 endif;
2016 }
2017
2018 }
2019 return $video_options;
2020 }
2021
2022
2023
2024 /**
2025 * Fetch the raw stored-video listing from the API.
2026 *
2027 * Endpoint: video/list. Admin only.
2028 *
2029 * @return array|false Decoded listing on success, false when no token, empty array on API failure.
2030 * @since 3.0.1
2031 */
2032 public function wpstream_get_videos_from_api( ){
2033
2034 // Admin-only guard.
2035 if( !current_user_can('administrator') ){
2036 exit('not admin on wpstream_get_videos_from_api');
2037 }
2038
2039
2040
2041 $url = 'video/list';
2042
2043 // POST to video/list with an empty body (token injected by the transport).
2044 $response = $this->authorized_request( $url, array() );
2045
2046 // Not-connected keeps the legacy false return so callers can tell it
2047 // from an API failure (empty array).
2048 if ( is_wp_error( $response ) && 'wpstream_not_connected' === $response->get_error_code() ) {
2049 return false;
2050 }
2051
2052 // Return full payload on success, empty array on failure.
2053 if ( ! is_wp_error( $response ) && isset( $response['success'] ) && $response['success'] == true ) {
2054 /**
2055 * Filter the cloud recordings listing shown on the Recordings screen.
2056 *
2057 * @since 4.14.0
2058 *
2059 * @param array $listing Decoded video/list payload (`videos` holds the entries).
2060 */
2061 return (array) apply_filters( 'wpstream_recordings_list', $response );
2062 } else {
2063 return array();
2064 }
2065
2066 }
2067
2068
2069
2070 /**
2071 * AJAX: get a signed download link for a stored recording.
2072 *
2073 * Endpoint: video/download (wp_ajax_wpstream_get_download_link). Admin only.
2074 * Echoes the raw API response.
2075 *
2076 * @return void Prints the API response then exits.
2077 * @since 3.0.1
2078 */
2079
2080 function wpstream_get_download_link(){
2081
2082 // CSRF gate: the recordings-list JS sends this nonce as 'security'.
2083 check_ajax_referer( 'wpstream_recordings_nonce', 'security' );
2084 // Admin-only guard.
2085 if( !current_user_can('administrator') ){
2086 exit('not admin on get_download_link');
2087 }
2088
2089 // Target video name from the request.
2090 $video_name = sanitize_text_field($_POST['video_name']);
2091
2092 // POST to video/download and return the link payload as JSON; the
2093 // token is injected by the transport. Not-connected keeps the legacy
2094 // silent return (no JSON emitted).
2095 $response = $this->authorized_request( 'video/download', array( 'name' => $video_name ) );
2096 if ( is_wp_error( $response ) ) {
2097 if ( 'wpstream_not_connected' === $response->get_error_code() ) {
2098 return false;
2099 }
2100 wp_send_json( array( 'success' => false, 'error' => $response->get_error_message() ) );
2101 }
2102 wp_send_json( $response );
2103 }
2104
2105
2106 /**
2107 * AJAX: delete a stored recording from cloud storage.
2108 *
2109 * Endpoint: video/delete (wp_ajax_wpstream_get_delete_file). Admin only.
2110 * Echoes the raw API response.
2111 *
2112 * @return void Prints the API response then exits.
2113 * @since 3.0.1
2114 */
2115 public function wpstream_get_delete_file(){
2116 // CSRF gate: the recordings-list JS sends this nonce as 'security'.
2117 check_ajax_referer( 'wpstream_recordings_nonce', 'security' );
2118 // Admin-only guard.
2119 if( !current_user_can('administrator') ){
2120 exit('not admin on get_delete_file');
2121 }
2122
2123 // Target video name from the request.
2124 $video_name = esc_html($_POST['video_name']);
2125
2126
2127 // POST to video/delete and return the outcome payload as JSON; the
2128 // token is injected by the transport. Not-connected keeps the legacy
2129 // silent return (no JSON emitted).
2130 $response = $this->authorized_request( 'video/delete', array( 'name' => $video_name ) );
2131 if ( is_wp_error( $response ) ) {
2132 if ( 'wpstream_not_connected' === $response->get_error_code() ) {
2133 return false;
2134 }
2135 wp_send_json( array( 'success' => false, 'error' => $response->get_error_message() ) );
2136 }
2137 if ( ! empty( $response['success'] ) ) {
2138 /**
2139 * Fires after the cloud confirmed a recording was deleted.
2140 *
2141 * @since 4.14.0
2142 *
2143 * @param string $video_name Recording file name.
2144 */
2145 do_action( 'wpstream_recording_deleted', $video_name );
2146 }
2147 wp_send_json( $response );
2148 }
2149
2150
2151 /**
2152 * AJAX: return the raw stored-video listing (used to poll for processing recordings).
2153 *
2154 * Endpoint: wp_ajax_wpstream_check_pending_videos. Admin only.
2155 *
2156 * @return void Emits JSON success/error.
2157 */
2158 public function wpstream_check_pending_videos() {
2159 // Admin-only guard.
2160 if (!current_user_can('administrator')) {
2161 wp_send_json_error(__('Unauthorized', 'wpstream'));
2162 }
2163
2164 // Return the raw video/list payload to the poller.
2165 $videos_list_raw = $this->wpstream_get_videos_from_api();
2166 wp_send_json_success($videos_list_raw);
2167 }
2168
2169
2170 }// end class
2171