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-ajax.php

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

1,170 lines 41.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Dashboard / channel AJAX endpoint surface.
5 *
6 * Registers and implements the admin-ajax.php handlers behind the front-end
7 * user dashboard: editing channel data, saving the user's account/address,
8 * managing profile attachments, selecting/creating channels, the "watch later"
9 * list, and live-quota lookups. Also enqueues and localizes the dashboard JS.
10 *
11 * Handlers echo/`wp_send_json_*` a JSON payload and terminate the request.
12 * Several handlers are noted in code comments where their nonce/ownership
13 * checks are weaker than others in this file.
14 *
15 * @package Wpstream
16 * @subpackage Wpstream/includes
17 */
18
19
20 // Exit if accessed directly.
21 if ( ! defined( 'ABSPATH' ) ) {
22 exit;
23 }
24
25 /**
26 * Wires up the dashboard AJAX actions and provides their callbacks.
27 */
28 class WpStream_Ajax {
29
30 /**
31 * Store plugin main class to allow public access.
32 *
33 * @since 20180622
34 * @var object The main plugin class, exposing shared services such as
35 * the live-API connection and the quota manager.
36 */
37 public $main;
38
39 /**
40 * Constructor.
41 *
42 * Stashes the main plugin instance and registers every AJAX action this
43 * class answers, plus the dashboard script enqueue hook. All actions here
44 * are `wp_ajax_` only (logged-in users); none are exposed to `nopriv`.
45 *
46 * @param object $plugin_main The main class.
47 */
48 public function __construct( $plugin_main ) {
49 // Keep a reference to the main plugin so handlers can reach its services.
50 $this->main = $plugin_main;
51
52 // Onboarding: video list lookup and broadcaster RTMP info lookup.
53 add_action( 'wp_ajax_wpstream_get_videos_list', [$this,'wpstream_get_videos_list'] );
54 add_action('wp_ajax_wpstream_get_broadcaster_info', array($this, 'wpstream_get_broadcaster_info'));
55
56 // Add the dashboard AJAX actions
57 add_action( 'wp_ajax_wpstream_dashboard_save_channel_data', [$this, 'wpstream_dashboard_save_channel_data'] );
58 add_action( 'wp_ajax_wpstream_dashboard_save_user_address', [$this, 'wpstream_dashboard_save_user_address'] );
59 add_action( 'wp_ajax_wpstream_delete_profile_attachment', [$this, 'wpstream_delete_profile_attachment'] );
60 add_action( 'wp_ajax_wpstream_dashboard_save_user_data', [$this, 'wpstream_dashboard_save_user_data'] );
61 add_action( 'wp_ajax_wpstream_handle_channel_selection', [$this, 'wpstream_handle_channel_selection'] );
62 add_action( 'wp_ajax_wpstream_handle_channel_creation', [$this, 'wpstream_handle_channel_creation'] );
63 add_action( 'wp_ajax_wpstream_handle_channel_details_saving', [$this, 'wpstream_handle_channel_details_saving'] );
64 add_action( 'wp_ajax_wpstream_remove_post_id', [$this, 'wpstream_remove_post_id_callback'] );
65 add_action( 'wp_ajax_wpstream_get_live_quota_data', [$this, 'wpstream_get_live_quota_data'] );
66
67 // Enqueue dashboard scripts
68 add_action( 'wp_enqueue_scripts', [$this, 'wpstream_enqueue_dashboard_scripts'] );
69 }
70
71 /**
72 * Enqueue and localize the dashboard JavaScript.
73 *
74 * Only loads on the front-end dashboard page. Passes the admin-ajax URL and
75 * a couple of translated validation strings to the script via
76 * `wpstream_dashboard_script_vars`.
77 */
78 public function wpstream_enqueue_dashboard_scripts() {
79 // Only enqueue when we are on the plugin's dashboard page.
80 if ( function_exists('wpstream_is_dashboard_page') && wpstream_is_dashboard_page() ) {
81
82 // Register the dashboard script (jQuery-dependent, loaded in footer).
83 wp_enqueue_script(
84 'wpstream-dashboard-script',
85 plugin_dir_url( dirname( __FILE__ ) ) . 'js/dashboard-script.js',
86 array( 'jquery' ),
87 $this->main->get_version(),
88 true
89 );
90
91 // Expose the AJAX endpoint and translated password-validation messages to JS.
92 wp_localize_script( 'wpstream-dashboard-script', 'wpstream_dashboard_script_vars', array(
93 'ajaxurl' => admin_url( 'admin-ajax.php' ),
94 'currentPassEmpty' => esc_html__( 'Please enter your current password.', 'wpstream' ),
95 'passNoMatch' => esc_html__( 'Passwords do not match!', 'wpstream' ),
96 ));
97 }
98 }
99
100 /**
101 * Fetch the account's VOD list from the WpStream cloud API.
102 *
103 * Reads: nonce field `security`. Returns JSON `{success, videos}` when an
104 * API token is available, otherwise `{success:false, error:'Token not found'}`.
105 * Used by the onboarding video picker.
106 */
107 public function wpstream_get_videos_list() {
108 // Validate the onboarding nonce before hitting the API.
109 check_ajax_referer( 'wpstream_onboarding_video_list_nonce', 'security' );
110
111 // Check connectivity and pull the remote video list.
112 $connected = $this->main->wpstream_live_connection->is_connected();
113 $videos_list = $this->main->wpstream_live_connection->wpstream_get_videos();
114
115 // cleanup any previous echo before sending json
116 ob_end_clean();
117
118 // A connected account means the list is trustworthy; return it.
119 if ( $connected ) {
120 echo json_encode( array(
121 'success' => true,
122 'videos' => $videos_list,
123 ));
124 } else {
125 // No token: signal the failure so the UI can prompt to reconnect.
126 echo json_encode( array(
127 'success' => false,
128 'error' => 'Token not found',
129 ));
130 }
131 // Terminate the AJAX request.
132 die();
133 }
134
135 /**
136 * Return a channel's RTMP publish URL and stream key.
137 *
138 * Reads: nonce field `nonce`, POST `channel_id`. Requires the `publish_posts`
139 * capability. Returns JSON success `{rtmp_url, stream_key}` pulled from the
140 * channel post's `obs_uri` / `obs_stream` meta, or an error otherwise.
141 */
142 public function wpstream_get_broadcaster_info() {
143 // Verify nonce
144 check_ajax_referer('wpstream_broadcaster_nonce', 'nonce');
145
146 // Read the target channel post ID (0 when missing).
147 $channel_id = isset($_POST['channel_id']) ? intval($_POST['channel_id']) : 0;
148
149 if (empty($channel_id)) {
150 // No channel supplied: nothing to look up.
151 wp_send_json_error('Invalid channel ID');
152 return;
153 }
154
155 // Ownership gate: RTMP credentials belong to the channel's owner only.
156 // Replaces the old publish_posts-only check, which let any author-level
157 // user read another broadcaster's stream key (IDOR).
158 if ( ! wpstream_can_manage_channel( get_current_user_id(), $channel_id ) ) {
159 wp_send_json_error('You are not allowed to control this channel.');
160 return;
161 }
162
163 // Get RTMP URL from post meta
164 $obs_uri = get_post_meta($channel_id, 'obs_uri', true);
165 $obs_stream = get_post_meta($channel_id, 'obs_stream', true);
166
167 if (empty($obs_uri) || empty($obs_stream)) {
168 // Channel has no stored RTMP endpoint/key yet.
169 wp_send_json_error('RTMP information not available');
170 return;
171 }
172
173 // Hand back the RTMP endpoint and stream key for the broadcaster UI.
174 wp_send_json_success([
175 'rtmp_url' => $obs_uri,
176 'stream_key' => $obs_stream
177 ]);
178 }
179
180 /**
181 * Saves channel data from the dashboard.
182 *
183 * Handles the saving of channel data from the dashboard, including title, description,
184 * thumbnail ID, images, category terms, and whether the channel is paid.
185 *
186 * Reads: nonce field `nonce`, POST `postID`, `thumb_id`, `title`,
187 * `description`, `channel_paid`, `channel_price`, `images`, and
188 * `selected_categories`. Toggling `channel_paid` switches the post between
189 * the `wpstream_product` and WooCommerce `product` post types. Returns JSON
190 * success with the rebuilt thumbnail/gallery/taxonomy HTML and trailer/preview URLs.
191 */
192 public function wpstream_dashboard_save_channel_data() {
193 // Verify the nonce for security
194 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'wpstream_edit_channel_nonce' ) ) {
195 die( 'Permission denied.' );
196 }
197 // Must be an authenticated user to edit a channel.
198 if ( ! is_user_logged_in() ) {
199 wp_send_json_error( array( 'error' => 'User is not logged in.' ) );
200 die();
201 }
202
203 // Ownership check: the caller must be the real author of the target
204 // channel (admins allowed). This uses post_author via
205 // wpstream_can_manage_channel() rather than the old
206 // wpstream_start_streaming_channel user meta — that meta is client-writable
207 // (see wpstream_dashboard_save_user_address), so trusting it let a user
208 // point it at a foreign post id and edit/hijack that post (SEC-02).
209 $postID = isset( $_POST['postID'] ) ? intval( $_POST['postID'] ) : 0;
210 if ( $postID === 0 ) {
211 // A zero/missing postID is not a valid channel to edit.
212 wp_send_json_error( array( 'error' => 'Invalid channel ID.' ) );
213 die();
214 }
215 if ( ! wpstream_can_manage_channel( get_current_user_id(), $postID ) ) {
216 wp_send_json_error( array( 'error' => 'Can\'t edit this channel.' ) );
217 die();
218 }
219
220 // Collect and sanitize the submitted channel fields; every key may be
221 // absent on a malformed request, so read them with safe defaults.
222 $thumb_id = isset( $_POST['thumb_id'] ) ? intval( $_POST['thumb_id'] ) : 0;
223 $title = sanitize_text_field( $_POST['title'] ?? '' );
224 $description = sanitize_text_field( $_POST['description'] ?? '' );
225 $channel_paid = intval( $_POST['channel_paid'] ?? 0 );
226 $images = sanitize_text_field( $_POST['images'] ?? '' );
227 // Strip stray commas and enforce the gallery limit on the CSV image-id list.
228 $images = wpstream_limit_gallery_images( $images );
229 $channel_price=0;
230 // Paid channels carry a price; read it as a float when supplied.
231 if ( isset( $_POST['channel_price'] ) ) {
232 $channel_price = floatval( $_POST['channel_price'] );
233 }
234 // $postID was already read and ownership-checked above.
235
236 // Free channels use the custom post type; paid ones become WooCommerce products.
237 $previous_access = 'product' === get_post_type( $postID ) ? 'paid' : 'free';
238 $new_post_type = 'wpstream_product';
239 if ( $channel_paid == 1 ) {
240 // Paid: switch to the product post type and persist the price meta.
241 $new_post_type = 'product';
242 update_post_meta( $postID, '_price', $channel_price );
243 update_post_meta( $postID, '_regular_price', $channel_price );
244
245 }
246
247 // Only proceed when we have a real post to update.
248 if ( $postID != '0' ) {
249 // Write the core post fields, applying any post-type switch from above.
250 $post_data = array(
251 'ID' => $postID,
252 'post_title' => $title,
253 'post_content' => $description,
254 'post_type' => $new_post_type,
255 );
256 wp_update_post( $post_data );
257 // Set the channel's featured image / thumbnail.
258 set_post_thumbnail( $postID, $thumb_id );
259
260
261 /*
262 * Manage images
263 */
264 if ( $channel_paid == 1 ) {
265 // Paid channels store their gallery in WooCommerce product meta.
266 update_post_meta( $postID, '_product_type', 'live_stream' );
267 wp_set_post_terms( $postID, 'live_stream', 'product_type' );
268 update_post_meta( $postID, '_product_image_gallery', $images );
269 } else {
270
271 // Free channels store each image id as a separate gallery meta row.
272 $images_array = explode( ',', $images );
273 // Clear the old gallery before re-adding the current image ids.
274 delete_post_meta( $postID, 'wpstream_theme_gallery' );
275 foreach ( $images_array as $key => $value ) :
276 add_post_meta( $postID, 'wpstream_theme_gallery', $value, false );
277 endforeach;
278 }
279 // Read the gallery back in the normalized shape used for rendering.
280 $gallery_images = $this->wpstream_return_image_gallery( $postID );
281
282
283 /*
284 * Manage categories
285 */
286 if(isset( $_POST['selected_categories']) && is_array( $_POST['selected_categories'])):
287 // Assign the submitted terms per taxonomy.
288 $categories = $_POST['selected_categories'];
289 foreach ( $categories as $taxonomy => $term_ids ) {
290
291 // Normalize a single term id into an array.
292 if ( ! is_array( $term_ids ) ) {
293 $term_ids = array( $term_ids );
294 }
295 // Force term ids to integers.
296 $term_ids = array_map( 'intval', $term_ids );
297
298 // Replace the post's terms in this taxonomy with the selection.
299 wp_set_object_terms( $postID, $term_ids, $taxonomy );
300 }
301 endif;
302
303 // Build the taxonomy summary HTML for the dashboard response.
304 $taxonomy_information = $this->wpstream_return_taxoomy_information( $postID );
305
306 // Resolve the trailer and preview video URLs (empty when unset).
307 $video_trailer = $this->wpstream_theme_return_trailer_video( $postID );
308
309 $video_preview = $this->wpstream_theme_return_preview_video( $postID );
310
311 $access = $channel_paid == 1 ? 'paid' : 'free';
312 if ( $access !== $previous_access ) {
313 /**
314 * Fires when a dashboard save moved a channel between free and paid.
315 *
316 * The post type has already switched (`wpstream_product` ↔ `product`).
317 *
318 * @since 4.14.0
319 *
320 * @param int $channel_id Channel post ID.
321 * @param string $access New access: `free` or `paid`.
322 * @param string $previous Access before the save.
323 */
324 do_action( 'wpstream_channel_access_changed', intval( $postID ), $access, $previous_access );
325 }
326
327 /**
328 * Fires after a channel was saved from the front-end dashboard.
329 *
330 * @since 4.14.0
331 *
332 * @param int $channel_id Channel post ID.
333 * @param int $user_id User who saved it.
334 */
335 do_action( 'wpstream_dashboard_channel_saved', intval( $postID ), get_current_user_id() );
336
337 // Return the refreshed thumbnail, gallery, taxonomy HTML and video URLs.
338 wp_send_json_success(
339 array(
340 'success' => true,
341 's' => $images,
342 'thumburl' => get_the_post_thumbnail_url( $postID, 'wpstream_featured_unit_cards' ),
343 'images' => $this->wpstream_build_html_gallery_dashboard( $gallery_images ),
344 'taxonomies' => $taxonomy_information['html'],
345 'channel_paid'=>$channel_paid,
346 'channel_price'=>$channel_price,
347 'video_trailer' => $video_trailer,
348 'video_preview' => $video_preview,
349 'message' => esc_html__( 'Changes saved successfully.', 'wpstream' ),
350 )
351 );
352 die();
353 }
354 }
355
356 /**
357 * Return the image gallery for a post.
358 *
359 * This function returns the image gallery for a post based on the post type.
360 *
361 * @param int $post_id The ID of the post.
362 * @return array The array of image gallery for the post.
363 */
364 public function wpstream_return_image_gallery( $post_id ) {
365 // The gallery is stored differently for WooCommerce products vs custom channels.
366 $post_type = get_post_type( $post_id );
367 $gallery_images = array();
368
369 if ( 'product' === $post_type ) {
370 // Products keep a comma-separated id list in a single meta value.
371 $gallery_images_source = get_post_meta( $post_id, '_product_image_gallery', true );
372 $gallery_images = explode( ',', $gallery_images_source );
373 } else {
374 // Non-products use Meta Box's repeated gallery field (when available).
375 if(function_exists('rwmb_meta')){
376 $gallery_images = rwmb_meta( 'wpstream_theme_gallery', array(), $post_id );
377
378 // Meta Box may return an id-keyed array, a single value, or empty.
379 if ( is_array( $gallery_images ) ) {
380 $gallery_images = array_keys( $gallery_images );
381 } elseif ( ! empty( $gallery_images ) ) {
382 $gallery_images = array( $gallery_images );
383 } else {
384 $gallery_images = array();
385 }
386 }
387 }
388
389 // Drop empty entries before returning the id list.
390 return array_filter( $gallery_images );
391 }
392
393 /**
394 * Returns information about taxonomies for the specified post.
395 *
396 * @param int $post_id The post ID.
397 * @return array An array containing information about taxonomies and HTML markup.
398 */
399 public function wpstream_return_taxoomy_information( $post_id ) {
400 // Enumerate every taxonomy registered for this post's type.
401 $post_type = get_post_type( $post_id );
402 $taxonomies = get_object_taxonomies( $post_type );
403 $all_terms = array();
404 $return_array = array();
405
406 // Loop through each taxonomy and get terms attached to the post.
407 foreach ( $taxonomies as $taxonomy_slug ) {
408 // Skip WooCommerce's internal product_type/product_visibility taxonomies.
409 if ( 'product_type' !== $taxonomy_slug && 'product_visibility' !== $taxonomy_slug ) {
410 $taxonomy_obj = get_taxonomy( $taxonomy_slug );
411 $taxonomy_name = $taxonomy_obj->labels->name; // This fetches the name of the taxonomy.
412 $terms = wp_get_post_terms( $post_id, $taxonomy_slug, array( 'fields' => 'all' ) ); // fetch all fields of the term.
413
414 // Record the terms keyed by the taxonomy's display name.
415 if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
416 $all_terms[ $taxonomy_name ] = $terms;
417 }
418 }
419 }
420
421 // Build the read-only HTML block shown in the dashboard details panel.
422 $return_html = '';
423
424 foreach ( $all_terms as $taxonomy => $terms ) {
425 // One details section per taxonomy, headed by its name.
426 $return_html .= ' <div class="wpstream-dashboard-details" >';
427 $return_html .= '<div class="wpstream-dashboard-details-header">' . $taxonomy . '</div>';
428 $return_html .= '<div class="wpstream_account_details_value" id="wpstream_' . sanitize_key( $taxonomy ) . '">';
429
430 // Render each selected term as a chip.
431 foreach ( $terms as $term ) {
432 $return_html .= '<span class="wpstream_term_selected">' . $term->name . '</span>';
433 }
434
435 $return_html .= ' </div>
436
437 </div> ';
438 }
439
440 // Return both the raw term data and the rendered HTML.
441 $return_array['tax_information'] = $all_terms;
442 $return_array['html'] = $return_html;
443
444 return $return_array;
445 }
446
447 /**
448 * Return the trailer video for a post.
449 *
450 * This function returns the trailer video for a post based on the post type.
451 *
452 * @param int $post_id The ID of the post.
453 * @return string The URL of the trailer video.
454 */
455 function wpstream_theme_return_trailer_video( $post_id ) {
456 // Resolve the attachment stored in the channel's `video_trailer` meta.
457 $trailer_video_id = get_post_meta( $post_id, 'video_trailer', true );
458 $attachment_url = wp_get_attachment_url( $trailer_video_id );
459
460 // Return the URL when the attachment exists, otherwise an empty string.
461 if ( ! empty( $attachment_url ) ) {;
462 return $attachment_url;
463 }
464
465 return '';
466 }
467
468 /**
469 * Build HTML for the video trailer in the dashboard.
470 *
471 * This function builds HTML for the video trailer in the dashboard based on the provided video URL.
472 *
473 * @param string $video_url The URL of the video.
474 * @return string The HTML string for the video trailer.
475 */
476 function wpstream_theme_build_html_video_trailer_dashboard( $video_url ) {
477 // Wrap a non-empty URL in an HTML5 video player; empty URL yields no markup.
478 if ( ! empty( $video_url ) ) {
479 return '<div class="wpstream-video-trailer" id="wpstream-video-trailer"><video height="240" controls><source src="' . esc_url( $video_url ) . '" type="video/mp4"></video></div>';
480 }
481
482 return '';
483 }
484
485 /**
486 * Return the trailer video for a post.
487 *
488 * This function returns the trailer video for a post based on the post type.
489 *
490 * @param int $post_id The ID of the post.
491 * @return string The URL of the trailer video.
492 */
493 function wpstream_theme_return_preview_video( $post_id ) {
494 // Resolve the attachment stored in the channel's `video_preview` meta.
495 $preview_video_id = get_post_meta( $post_id, 'video_preview', true );
496 $attachment_url = wp_get_attachment_url( $preview_video_id );
497
498 // Return the URL when the attachment exists, otherwise an empty string.
499 if ( ! empty( $attachment_url ) ) {;
500 return $attachment_url;
501 }
502
503 return '';
504 }
505
506 /**
507 * Build HTML for the video preview in the dashboard.
508 *
509 * This function builds HTML for the video preview in the dashboard based on the provided video URL.
510 *
511 * @param string $video_url The URL of the video.
512 * @return string The HTML string for the video preview.
513 */
514 function wpstream_theme_build_html_video_preview_dashboard( $video_url ) {
515 // Wrap a non-empty URL in an HTML5 video player; empty URL yields no markup.
516 if ( ! empty( $video_url ) ) {
517 return '<div class="wpstream-video-preview" id="wpstream-video-preview"><video height="240" controls><source src="' . esc_url( $video_url ) . '" type="video/mp4"></video></div>';
518 }
519
520 return '';
521 }
522
523 /**
524 * Build HTML for the gallery in the dashboard.
525 *
526 * This function builds HTML for the gallery in the dashboard based on the provided array of image IDs.
527 *
528 * @param array $gallery_images An array of image IDs.
529 * @return string The HTML string for the gallery.
530 */
531 public function wpstream_build_html_gallery_dashboard( $gallery_images ) {
532 $return_string = '';
533
534 // Render one thumbnail tile per attachment id that resolves to an image.
535 if ( is_array( $gallery_images ) ) {
536 foreach ( $gallery_images as $attachment_id ) {
537 // Look up the sized image source for this attachment.
538 $preview = wp_get_attachment_image_src( $attachment_id, 'wpstream_featured_unit_cards' );
539
540 // Only emit markup when a real image URL came back.
541 if ( $preview && '' !== $preview[0] ) {
542 $return_string .= '<div class="wpstream_uploaded_images" data-imageid="' . esc_attr( $attachment_id ) . '">';
543 $return_string .= '<img src="' . esc_url( $preview[0] ) . '" alt="' . esc_html__( 'thumb', 'wpstream' ) . '" /></div>';
544 }
545 }
546 }
547
548 return $return_string;
549 }
550
551 /**
552 * Save user address data from dashboard.
553 *
554 * This function handles saving user address data from the dashboard.
555 *
556 * Reads: nonce field `nonce`, POST `inputData` (an array of `{id, value}`
557 * pairs). Each pair is stored as user meta on the current user. Returns a
558 * JSON success message.
559 */
560 public function wpstream_dashboard_save_user_address() {
561 // Verify the nonce for security.
562 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'wpstream_edit_addr_nonce' ) ) {
563 die( 'Permission denied.' );
564 }
565
566 // Require an authenticated user.
567 if ( ! is_user_logged_in() ) {
568 wp_send_json_error( array( 'error' => 'User is not logged in.' ) );
569 }
570
571 // Allowlist of user-meta keys this address form is permitted to write.
572 // The key comes from a form input id and is fully client-controlled, so
573 // without this allowlist any logged-in user could set arbitrary meta —
574 // e.g. wpstream_start_streaming_channel, which channel-edit ownership used
575 // to trust (SEC-02). Only the standard WooCommerce billing/shipping
576 // address fields are accepted; anything else is ignored.
577 $allowed_address_meta = array(
578 'billing_first_name', 'billing_last_name', 'billing_company',
579 'billing_address_1', 'billing_address_2', 'billing_city',
580 'billing_state', 'billing_postcode', 'billing_country',
581 'billing_phone', 'billing_email',
582 'shipping_first_name', 'shipping_last_name', 'shipping_company',
583 'shipping_address_1', 'shipping_address_2', 'shipping_city',
584 'shipping_state', 'shipping_postcode', 'shipping_country',
585 'shipping_phone',
586 );
587
588 // Persist each submitted field as user meta on the current user.
589 $userID = get_current_user_id();
590 foreach ($_POST['inputData'] as $item){
591 // Only store entries whose key is an allowlisted address field.
592 if( isset( $item['id'] ) && in_array( $item['id'], $allowed_address_meta, true ) ){
593 update_user_meta($userID, sanitize_text_field( $item['id']) , sanitize_text_field( $item['value']) );
594 }
595 }
596
597
598 // Report success back to the dashboard.
599 wp_send_json_success(
600 array(
601 'success' => true,
602 'message' => esc_html__( 'Changes saved successfully.', 'wpstream' ),
603 )
604 );
605
606 die();
607 }
608
609 /**
610 * Delete the current user's profile picture attachment.
611 *
612 * Reads: nonce field `security`, POST `image_id`. Deletes the attachment only
613 * when the current user is its author, then clears the `custom_picture` /
614 * `custom_picture_small` user meta. Returns JSON with the default avatar URL.
615 */
616 public function wpstream_delete_profile_attachment() {
617 // Verify the nonce for security.
618 if ( ! isset( $_POST['security'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['security'] ) ), 'wpstream_profile_image_upload' ) ) {
619 wp_send_json_error(
620 array(
621 'success' => false,
622 'message' => esc_html__( 'Permission denied nonce', 'wpstream' ),
623 )
624 );
625
626 die();
627 }
628
629 // Read the attachment id to delete (0 when absent — never a valid attachment).
630 $image_id = isset( $_POST['image_id'] ) ? intval( wp_unslash( $_POST['image_id'] ) ) : 0;
631 $user_id = get_current_user_id();
632
633 // Apply the shared, null-safe deletion policy (framework/ajax-upload.php):
634 // existence + attachment type + ownership or delete_post capability. Using
635 // the helper avoids dereferencing a null attachment and no longer leaks the
636 // owner's user id back to a non-owner in the error payload.
637 $can_delete = function_exists( 'wpstream_user_can_delete_attachment' )
638 && wpstream_user_can_delete_attachment( $image_id );
639
640 if ( ! $can_delete ) {
641 wp_send_json_error(
642 array(
643 'success' => false,
644 'message' => esc_html__( 'Permission denied!!!', 'wpstream' ),
645 )
646 );
647
648 die();
649
650 }
651
652 // Delete the attachment (you can customize this part).
653 wp_delete_attachment( $image_id, true );
654 // Clear the cached profile-picture meta so the default avatar is used.
655 delete_user_meta( $user_id, 'custom_picture' );
656 delete_user_meta( $user_id, 'custom_picture_small' );
657
658 // Return success plus the fallback avatar URL for the UI to swap in.
659 wp_send_json_success(
660 array(
661 'success' => true,
662 'default' => function_exists('wpstream_get_author_profile_image_url_by_author_id') ? wpstream_get_author_profile_image_url_by_author_id($user_id) : '',
663 'message' => esc_html__( 'Changes saved successfully.', 'wpstream' ),
664 )
665 );
666
667 die();
668 }
669
670 /**
671 * Save the current user's account profile fields from the dashboard.
672 *
673 * Reads: nonce field `nonce`, POST `firstName`, `lastName`, `displayName`,
674 * `email`, `aboutMe`, `newPassword1`, `newPassword2`, `currentPassword`.
675 * Updates only the non-empty fields, validates the email for uniqueness and
676 * format, and changes the password when both new-password fields match and
677 * the supplied current password checks out. Returns JSON success/failure.
678 */
679 public function wpstream_dashboard_save_user_data() {
680 // Verify the nonce for security.
681
682 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'wpstream_edit_account_nonce' ) ) {
683 die( 'Permission denied.' );
684 }
685
686 // Require an authenticated user.
687 if ( ! is_user_logged_in() ) {
688 wp_send_json_error( array( 'error' => 'User is not logged in.' ) );
689 }
690
691 // Get the user's ID.
692 $user_id = get_current_user_id();
693 $current_user = wp_get_current_user();
694
695 // Get the data from the AJAX request.
696 // Each field is read and sanitized only when present in the request.
697 if ( isset( $_POST['firstName'] ) ) {
698 $first_name = sanitize_text_field( wp_unslash( $_POST['firstName'] ) );
699 }
700 if ( isset( $_POST['lastName'] ) ) {
701 $last_name = sanitize_text_field( wp_unslash( $_POST['lastName'] ) );
702 }
703 if ( isset( $_POST['displayName'] ) ) {
704 $display_name = sanitize_text_field( wp_unslash( $_POST['displayName'] ) );
705 }
706 if ( isset( $_POST['email'] ) ) {
707 $email = sanitize_email( wp_unslash( $_POST['email'] ) );
708 }
709 // Bio/about text is stored in the user's description.
710 if (isset( $_POST['aboutMe'])){
711 $description = sanitize_textarea_field( wp_unslash( $_POST['aboutMe'] ) );
712 }
713 // Password change inputs: the two new-password fields plus the current one.
714 if ( isset( $_POST['newPassword1'] ) ) {
715 $new_password1 = sanitize_text_field( wp_unslash( $_POST['newPassword1'] ) );
716 }
717 if ( isset( $_POST['newPassword2'] ) ) {
718 $new_password2 = sanitize_text_field( wp_unslash( $_POST['newPassword2'] ) );
719 }
720 if ( isset( $_POST['currentPassword'] ) ) {
721 $current_password = sanitize_text_field( wp_unslash( $_POST['currentPassword'] ) );
722 }
723
724 // Track whether a password change actually happened for the response.
725 $passwordchanged = false;
726
727 // Only update fields that are not empty.
728 $user_data = array();
729
730 // Stage each provided profile field for the wp_update_user() call.
731 if ( ! empty( $first_name ) ) {
732 $user_data['first_name'] = $first_name;
733 }
734
735 if ( ! empty( $last_name ) ) {
736 $user_data['last_name'] = $last_name;
737 }
738
739 if ( !empty( $description ) ){
740 $user_data['description'] = $description;
741 }
742
743 if ( ! empty( $display_name ) ) {
744 $user_data['display_name'] = $display_name;
745 }
746
747 // Reject the email if it already belongs to a different account.
748 $existing_user = get_user_by( 'email', $email );
749
750 if ( $existing_user && $existing_user->ID !== $user_id ) {
751 wp_send_json_error(
752 array(
753 'success' => false,
754 'failaccount' => esc_html__( 'Email already exists.', 'wpstream' ),
755 )
756 );
757 }
758
759 // Reject an empty or malformed email address.
760 if ( empty( $email ) || ! filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
761 wp_send_json_error(
762 array(
763 'success' => false,
764 'failaccount' => esc_html__( 'Invalid Email Format', 'wpstream' ),
765 )
766 );
767 }
768
769 // Stage the validated email for update.
770 if ( ! empty( $email ) ) {
771 $user_data['user_email'] = $email;
772 }
773
774 // Update the user's data.
775 // Persist the staged profile fields in one call when any exist.
776 if ( ! empty( $user_data ) ) {
777 $user_data['ID'] = $user_id;
778 wp_update_user( $user_data );
779 }
780
781 // Handle a password change only when both new-password fields are filled.
782 if ( ! empty( $new_password1 ) && ! empty( $new_password2 ) ) {
783 // The two new passwords must be identical.
784 if ( $new_password1 !== $new_password2 ) {
785 wp_send_json_error(
786 array(
787 'success' => false,
788 'failpass' => esc_html__( 'Passwords do not match!', 'wpstream' ),
789 )
790 );
791
792 die();
793
794 } elseif ( ! wp_check_password( $current_password, $current_user->data->user_pass, $current_user->ID ) ) {
795 // The supplied current password must verify against the stored hash.
796 wp_send_json_error(
797 array(
798 'success' => false,
799 'failpass' => esc_html__( 'Current Password is not right!', 'wpstream' ),
800 )
801 );
802
803 die();
804
805 } else {
806 // Both checks passed: set the new password.
807 wp_set_password( $new_password1, $user_id );
808 $passwordchanged = true;
809 }
810 }
811
812 // Send a response to the client.
813 wp_send_json_success(
814 array(
815 'success' => true,
816 'passwordchanged' => $passwordchanged,
817 'message' => esc_html__( 'Changes saved successfully.', 'wpstream' ),
818 )
819 );
820 }
821
822 /**
823 * Handle the selection of a channel.
824 *
825 * This function handles the AJAX request for selecting a channel.
826 * It checks if the user is logged in, validates the security nonce,
827 * and updates the user meta with the selected channel if the user is the owner of the channel.
828 * It returns a JSON response indicating success or failure.
829 *
830 * @return void
831 */
832 public function wpstream_handle_channel_selection() {
833 // Require an authenticated user.
834 if ( ! is_user_logged_in() ) {
835 wp_die();
836 }
837
838 // Validate the channel-list nonce.
839 check_ajax_referer( 'wpstream_user_channel_list', 'security' );
840 // The channel post id the user wants to make active.
841 if ( isset( $_POST['selected_value'] ) ) {
842 $selected_value = intval( $_POST['selected_value'] );
843 }
844 $current_user = wp_get_current_user();
845 // Look up the author of the chosen channel for the ownership check.
846 $post_author_id = intval( get_post_field( 'post_author', $selected_value ) );
847
848 // Only the channel's owner may select it as their active channel.
849 if ( $current_user->ID !== $post_author_id ) {
850 echo wp_json_encode(
851 array(
852 'success' => false,
853 'message' => esc_html__( 'You are not the owner of this channel', 'wpstream' ),
854 )
855 );
856
857 } else {
858 // Record the selected channel as the user's active streaming channel.
859 update_user_meta( $current_user->ID, 'wpstream_start_streaming_channel', $selected_value );
860
861 echo wp_json_encode(
862 array(
863 'success' => true,
864 'message' => esc_html__( 'Channel updated', 'wpstream' ),
865 )
866 );
867 }
868
869 wp_die();
870 }
871
872 /**
873 * Create a new channel post for the current user.
874 *
875 * Reads: nonce field `security`, POST `channel_type` ('paid' or free).
876 * Enforces the per-user channel limit (admins bypass it) and the paid-channel
877 * permission, inserts a `wpstream_product` or WooCommerce `product`, and sets
878 * it as the user's active streaming channel. Echoes a JSON success/error.
879 */
880 public function wpstream_handle_channel_creation() {
881 // Require an authenticated user.
882 if ( ! is_user_logged_in() ) {
883 wp_die();
884 }
885
886 // Validate the channel-list nonce.
887 check_ajax_referer( 'wpstream_user_channel_list', 'security' );
888 // Requested channel type ('paid' triggers a WooCommerce product).
889 $channel_type = isset( $_POST['channel_type'] ) ? sanitize_text_field( wp_unslash( $_POST['channel_type'] ) ) : 'free';
890 $current_user = wp_get_current_user();
891 $access = 'paid' === $channel_type ? 'paid' : 'free';
892 $request = array(
893 'actor_id' => intval( $current_user->ID ),
894 'owner_id' => intval( $current_user->ID ),
895 'kind' => 'live_channel',
896 'access' => $access,
897 'title' => 'paid' === $access ? 'My New Paid Channel' : 'My New Free Channel',
898 );
899 if ( 'paid' === $access ) {
900 // The existing dashboard creates an unpriced product that users price later.
901 $request['price'] = 0;
902 }
903
904 $result = $this->main->streaming_content_creation->create( $request );
905 if ( ! empty( $result['success'] ) ) {
906 $post_id = intval( $result['content_id'] );
907 // Selection is a dashboard workflow effect, not a creation effect.
908 update_user_meta( $current_user->ID, 'wpstream_start_streaming_channel', $post_id );
909 echo wp_json_encode(
910 array(
911 'success' => true,
912 'message' => 'Post created with ID: ' . $post_id,
913 )
914 );
915 } elseif ( 'channel_limit_reached' === ( $result['error'] ?? '' ) ) {
916 echo wp_json_encode(
917 array(
918 'success' => false,
919 'message' => esc_html__( 'Your reached the maximum number of channels', 'wpstream' ),
920 )
921 );
922 } else {
923 echo wp_json_encode(
924 array(
925 'success' => false,
926 'message' => esc_html__( 'Error creating channel.', 'wpstream' ),
927 )
928 );
929 }
930
931 wp_die();
932 }
933
934 /**
935 * Handle AJAX request to save channel details.
936 *
937 * This function handles the AJAX request to save the details of a channel, including its title, description,
938 * price, images, featured status, and taxonomies.
939 *
940 * @return void Outputs JSON-encoded response indicating success or failure of the operation.
941 */
942 public function wpstream_handle_channel_details_saving() {
943 // Require an authenticated user.
944 if ( ! is_user_logged_in() ) {
945 wp_die();
946 }
947
948 // Validate the channel-list nonce.
949 check_ajax_referer( 'wpstream_user_channel_list', 'security' );
950
951 // Target channel post id.
952 if ( isset( $_POST['postID'] ) ) {
953 $post_id = intval( $_POST['postID'] );
954 }
955 $current_user = wp_get_current_user();
956 // Ownership check: compare the channel's author to the current user.
957 $post_author_id = intval( get_post_field( 'post_author', $post_id ) );
958
959 if ( $current_user->ID !== $post_author_id ) {
960 // Not the owner: refuse and echo back the ids for debugging.
961 echo wp_json_encode(
962 array(
963 'success' => false,
964 '$postID' => $post_id,
965 '$post_author_id' => $post_author_id,
966 'message' => esc_html__( 'You are not the owner of this channel', 'wpstream' ),
967 )
968 );
969 } else {
970 // Owner confirmed: read and sanitize the submitted channel fields.
971 if ( isset( $_POST['title'] ) ) {
972 $title = sanitize_text_field( wp_unslash( $_POST['title'] ) );
973 }
974 if ( isset( $_POST['description'] ) ) {
975 // Description allows post-safe HTML.
976 $sanitized_content = wp_kses_post( wp_unslash( $_POST['description'] ) );
977 }
978 $price = 0;
979
980 if ( isset( $_POST['price'] ) ) {
981 $price = sanitize_text_field( wp_unslash( $_POST['price'] ) );
982 }
983
984 if ( isset( $_POST['images'] ) ) {
985 $images = sanitize_text_field( wp_unslash( $_POST['images'] ) );
986 }
987
988 if ( isset( $_POST['featured'] ) ) {
989 $featured = intval( $_POST['featured'] );
990 }
991
992 if ( isset( $_POST['taxonomies'] ) ) {
993 // Sanitize each taxonomy value when an array was submitted.
994 $taxonomies_raw = sanitize_text_field( wp_unslash( $_POST['taxonomies'] ) );
995 $taxonomies = is_array( $taxonomies_raw ) ? array_map( 'sanitize_text_field', $taxonomies_raw ) : array();
996 }
997 // Strip stray commas and enforce the gallery limit on the image id list.
998 $images = wpstream_limit_gallery_images( $images );
999
1000 // Store the gallery/price differently for products vs custom channels.
1001 if ( get_post_type( $post_id ) === 'product' ) {
1002 update_post_meta( $post_id, '_product_image_gallery', $images );
1003 update_post_meta( $post_id, '_regular_price', $price );
1004 } else {
1005 // Free channels store each image id as its own gallery meta row;
1006 // clear the old gallery first so the limited list replaces it.
1007 $images_array = explode( ',', $images );
1008 delete_post_meta( $post_id, 'wpstream_theme_gallery' );
1009 foreach ( $images_array as $key => $value ) :
1010 add_post_meta( $post_id, 'wpstream_theme_gallery', $value );
1011 endforeach;
1012 }
1013
1014 // Apply the featured image / thumbnail.
1015 set_post_thumbnail( $post_id, $featured );
1016
1017 // Update the core post title/content.
1018 $post_data = array(
1019 'ID' => $post_id,
1020 'post_title' => $title,
1021 'post_content' => $sanitized_content,
1022 );
1023
1024 // Update the post.
1025 wp_update_post( $post_data );
1026
1027 // Reassign terms for each submitted taxonomy.
1028 foreach ( $taxonomies as $taxonomy => $term_ids ) {
1029 // Clear existing terms in this taxonomy first.
1030 wp_remove_object_terms( $post_id, '', $taxonomy );
1031
1032 if ( is_array( $term_ids ) ) {
1033 foreach ( $term_ids as $key => $term_id ) {
1034 // Tags are set by name (appended), other taxonomies by term id.
1035 if ( 'product_tag' === $taxonomy || 'post_tag' === $taxonomy ) {
1036 $tagterm = get_term( $term_id );
1037
1038 if ( $tagterm && ! is_wp_error( $tagterm ) ) {
1039 $tag_term_name = $tagterm->name;
1040 wp_set_post_terms( $post_id, $tag_term_name, $taxonomy, true );
1041 }
1042 } elseif ( -1 !== $term_id ) {
1043 // Skip the -1 "none" sentinel; otherwise assign the term id.
1044 wp_set_post_terms( $post_id, intval( $term_id ), $taxonomy, true );
1045 }
1046 }
1047 }
1048 }
1049
1050 // Echo the saved values back for the dashboard to confirm/update state.
1051 echo wp_json_encode(
1052 array(
1053 'success' => true,
1054 '$price' => $price,
1055 'message' => esc_html__( 'Channel updated', 'wpstream' ),
1056 'title' => $title,
1057 '$sanitized_content' => $sanitized_content,
1058 '$images' => $images,
1059 '$featured' => $featured,
1060 '$taxonomies' => $taxonomies,
1061 )
1062 );
1063 }
1064
1065 wp_die();
1066 }
1067
1068 /**
1069 * Callback handler to remove a post ID from the "watch later" list.
1070 *
1071 * Reads: nonce field `wpstream_nonce`, POST `postID`. Filters the given post
1072 * id out of the user's `wpstream_user_watch_later_items` meta array and saves
1073 * it. Returns JSON success/failure.
1074 */
1075 public function wpstream_remove_post_id_callback() {
1076 // Require an authenticated user.
1077 if ( ! is_user_logged_in() ) {
1078 wp_send_json_error( 'You must be logged in to perform this action.' );
1079 die();
1080 }
1081
1082 // Verify the nonce.
1083 if ( isset( $_POST['wpstream_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['wpstream_nonce'] ) ), 'wpstream-watch-later-nonce' ) ) {
1084 // Proceed only when a post id to remove was supplied.
1085 if ( isset( $_POST['postID'] ) ) {
1086 $post_id_to_remove = intval( $_POST['postID'] );
1087 $meta_key = 'wpstream_user_watch_later_items';
1088 $current_user = wp_get_current_user();
1089 $user_id = $current_user->ID;
1090
1091 // Get the current array of post IDs; users who never saved
1092 // anything have '' here, which array_filter() rejects.
1093 $watch_later_item_ids = get_user_meta( $user_id, $meta_key, true );
1094 if ( ! is_array( $watch_later_item_ids ) ) {
1095 $watch_later_item_ids = array();
1096 }
1097
1098 // Remove the specific ID from the array.
1099 $watch_later_item_ids = array_filter(
1100 $watch_later_item_ids,
1101 function ( $id ) use ( $post_id_to_remove ) {
1102 // Keep every id except the one being removed.
1103 return $id !== $post_id_to_remove;
1104 }
1105 );
1106
1107 // Update the user's metadata with the modified array.
1108 update_user_meta( $user_id, $meta_key, $watch_later_item_ids );
1109
1110 // Success payload.
1111 $response = array(
1112 'success' => true,
1113 'message' => 'Item removed',
1114 );
1115
1116 } else {
1117 // No post id was provided.
1118 $response = array(
1119 'success' => false,
1120 'message' => 'Invalid postID format.',
1121 );
1122 }
1123 } else {
1124 // Missing or invalid nonce.
1125 $response = array(
1126 'success' => false,
1127 'message' => 'Nonce verification failed.',
1128 );
1129 }
1130
1131 // Emit whichever response was built above and end the request.
1132 wp_send_json( $response );
1133
1134 wp_die();
1135 }
1136
1137 /**
1138 * Return the account's live-streaming quota data for the dashboard.
1139 *
1140 * Reads: nonce field `security`. Delegates to the quota manager, augments the
1141 * result with an `is_basic_streaming` flag, and returns it as JSON success,
1142 * or an error when no data is available.
1143 */
1144 public function wpstream_get_live_quota_data() {
1145 // Validate the notice nonce (null-coalesced when the field is absent).
1146 if ( ! wp_verify_nonce( $_POST['security'] ?? '', 'wpstream_notice_nonce' ) ) {
1147 wp_send_json_error( 'Nonce verification failed.' );
1148 die();
1149 }
1150
1151 // Require an authenticated user.
1152 if ( ! is_user_logged_in() ) {
1153 wp_send_json_error( 'You must be logged in to perform this action.' );
1154 die();
1155 }
1156
1157 // Ask the quota manager for the current live quota figures.
1158 $quota_data = $this->main->quota_manager->get_live_quota_data( 'wpstream_get_live_quota_data' );
1159
1160 if ( $quota_data ) {
1161 // Add the basic-streaming flag derived from the quota data.
1162 $quota_data['is_basic_streaming'] = $this->main->quota_manager->is_basic_streaming_mode( $quota_data );
1163 wp_send_json_success( $quota_data );
1164 } else {
1165 // No quota data returned by the manager.
1166 wp_send_json_error( 'Could not retrieve quota data.' );
1167 }
1168 }
1169 }
1170