self::$max_file_size) { return new WP_Error('file_too_large', 'File exceeds 10 MB limit.'); } $mime = mime_content_type($file['tmp_name']); if (!in_array($mime, self::$image_types, true)) { return new WP_Error('invalid_type', 'Only JPEG, PNG, GIF, and WebP are allowed.'); } $existing = self::get_images($post_id); if (count($existing) >= self::$max_images) { return new WP_Error('too_many', 'Maximum ' . self::$max_images . ' images per beep.'); } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; $file_return = wp_handle_sideload($file, [ 'test_form' => false, 'mimes' => [ 'jpg|jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp', ], ]); if (isset($file_return['error'])) { return new WP_Error('sideload_error', $file_return['error']); } $attachment = [ 'post_mime_type' => $file_return['type'], 'post_title' => preg_replace('/\.[^.]+$/', '', basename($file_return['file'])), 'post_content' => '', 'post_status' => 'inherit', 'guid' => $file_return['url'], ]; $attachment_id = wp_insert_attachment($attachment, $file_return['file'], $post_id); if (is_wp_error($attachment_id)) { return $attachment_id; } $attach_data = wp_generate_attachment_metadata($attachment_id, $file_return['file']); wp_update_attachment_metadata($attachment_id, $attach_data); add_post_meta($post_id, self::META_KEY_IMAGES, $attachment_id); return [ 'id' => $attachment_id, 'url' => wp_get_attachment_url($attachment_id), 'thumb' => self::get_image_url($attachment_id, 'thumbnail'), 'sizes' => self::get_image_sizes($attachment_id), ]; } /** * Remove a single image from a beep post. */ public static function remove_image($post_id, $attachment_id) { $ids = self::get_images($post_id); $ids = array_values(array_filter($ids, fn($id) => (int) $id !== (int) $attachment_id)); delete_post_meta($post_id, self::META_KEY_IMAGES); foreach ($ids as $id) { add_post_meta($post_id, self::META_KEY_IMAGES, $id); } wp_delete_attachment($attachment_id, true); } /** * Get image attachment IDs for a beep post. */ public static function get_images($post_id) { $ids = get_post_meta($post_id, self::META_KEY_IMAGES, false); return array_filter(array_map('intval', (array) $ids)); } /** * Get image srcset-ready sizes for an attachment. */ public static function get_image_sizes($attachment_id) { $sizes = []; foreach (['thumbnail', 'medium', 'large', 'full'] as $size) { $img = wp_get_attachment_image_src($attachment_id, $size); if ($img) { $sizes[$size] = ['url' => $img[0], 'width' => $img[1], 'height' => $img[2]]; } } return $sizes; } /** * Get image URL at a given size. */ public static function get_image_url($attachment_id, $size = 'medium') { $img = wp_get_attachment_image_src($attachment_id, $size); return $img ? $img[0] : ''; } /** * Set a video URL for a beep post. */ public static function set_video_url($post_id, $url) { $url = esc_url_raw($url); if (!$url) return new WP_Error('invalid_url', 'Invalid video URL.'); update_post_meta($post_id, self::META_KEY_VIDEO, $url); return true; } /** * Get the video URL for a beep post. */ public static function get_video_url($post_id) { return esc_url_raw(get_post_meta($post_id, self::META_KEY_VIDEO, true)); } /** * Detect if a URL is a supported video oEmbed. */ public static function detect_video_url($url) { $patterns = [ 'youtube' => '#https?://(?:www\.)?youtube\.com/watch\?v=[^\s&#]+#', 'yt_s' => '#https?://youtu\.be/[^\s&#]+#', 'vimeo' => '#https?://vimeo\.com/\d+#', 'twitter' => '#https?://(?:twitter\.com|x\.com)/[^\s#/]+/status/\d+#', ]; foreach ($patterns as $name => $pattern) { if (preg_match($pattern, $url)) return $name; } return false; } /** * Render the Twitter-style image gallery. * * @param int[] $image_ids * @return string HTML */ public static function render_image_gallery($image_ids) { if (empty($image_ids)) return ''; $count = count($image_ids); $grid_class = 'beep-gallery'; if ($count === 1) $grid_class .= ' beep-gallery-1'; elseif ($count === 2) $grid_class .= ' beep-gallery-2'; elseif ($count === 3) $grid_class .= ' beep-gallery-3'; else $grid_class .= ' beep-gallery-4'; $html = '
'; foreach ($image_ids as $i => $id) { $full = self::get_image_url($id, 'large'); $thumb = self::get_image_url($id, 'medium'); if (!$thumb) $thumb = $full; if (!$full) continue; $alt = get_post_meta($id, '_wp_attachment_image_alt', true); $html .= ''; } $html .= '
'; return $html; } /** * Render video embed via oEmbed. */ public static function render_video_embed($url) { if (!$url) return ''; $embed = wp_oembed_get($url, ['width' => 560, 'height' => 315]); if ($embed) { return '
' . $embed . '
'; } return ''; } /** * Delete all media for a beep post (when beep is deleted). */ public static function delete_media($post_id) { foreach (self::get_images($post_id) as $id) { wp_delete_attachment($id, true); } delete_post_meta($post_id, self::META_KEY_IMAGES); delete_post_meta($post_id, self::META_KEY_VIDEO); } /* ============================================================ * Parent-agnostic wrappers (v1.9.0) * $parent_type = 'post' (default) or 'comment'. * Routes to post_meta or comment_meta automatically. * ============================================================ */ public static function images_meta_key() { return self::META_KEY_IMAGES; } /** * Resolve the right meta storage for a parent (post or comment). * * @return array{get:string,add:string,del:string}|WP_Error */ private static function meta_store($parent_id, $parent_type = 'post') { if ($parent_type === 'comment') { return [ 'get' => 'get_comment_meta', 'add' => 'add_comment_meta', 'del' => 'delete_comment_meta', ]; } if ($parent_type === 'post') { return [ 'get' => 'get_post_meta', 'add' => 'add_post_meta', 'del' => 'delete_post_meta', ]; } return new WP_Error('invalid_parent', 'parent_type must be post or comment.'); } /** Image upload wrapper that works for posts or comments. */ public static function handle_upload_for_parent($parent_id, $file, $parent_type = 'post') { $store = self::meta_store($parent_id, $parent_type); if (is_wp_error($store)) return $store; if (!is_array($file) || $file['error'] !== UPLOAD_ERR_OK) { return new WP_Error('upload_error', 'Upload failed.'); } if ($file['size'] > self::$max_file_size) { return new WP_Error('file_too_large', 'File exceeds 10 MB limit.'); } $mime = mime_content_type($file['tmp_name']); if (!in_array($mime, self::$image_types, true)) { return new WP_Error('invalid_type', 'Only JPEG, PNG, GIF, and WebP are allowed.'); } $existing = call_user_func($store['get'], $parent_id, self::META_KEY_IMAGES, false); $existing = array_filter(array_map('intval', (array) $existing)); if (count($existing) >= self::$max_images) { return new WP_Error('too_many', 'Maximum ' . self::$max_images . ' images per beep.'); } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; $file_return = wp_handle_sideload($file, [ 'test_form' => false, 'mimes' => [ 'jpg|jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp', ], ]); if (isset($file_return['error'])) { return new WP_Error('sideload_error', $file_return['error']); } $attachment = [ 'post_mime_type' => $file_return['type'], 'post_title' => preg_replace('/\.[^.]+$/', '', basename($file_return['file'])), 'post_content' => '', 'post_status' => 'inherit', 'guid' => $file_return['url'], ]; // Attachments still need a post parent for WP core; use the comment's post ID when on a comment. $attach_parent = $parent_type === 'comment' ? (int) get_comment($parent_id)->comment_post_ID : $parent_id; $attachment_id = wp_insert_attachment($attachment, $file_return['file'], $attach_parent); if (is_wp_error($attachment_id)) { return $attachment_id; } $attach_data = wp_generate_attachment_metadata($attachment_id, $file_return['file']); wp_update_attachment_metadata($attachment_id, $attach_data); call_user_func($store['add'], $parent_id, self::META_KEY_IMAGES, $attachment_id); return [ 'id' => $attachment_id, 'url' => wp_get_attachment_url($attachment_id), 'thumb' => self::get_image_url($attachment_id, 'thumbnail'), 'sizes' => self::get_image_sizes($attachment_id), ]; } /** Image removal wrapper. */ public static function remove_image_from_parent($parent_id, $attachment_id, $parent_type = 'post') { $store = self::meta_store($parent_id, $parent_type); if (is_wp_error($store)) return $store; $ids = call_user_func($store['get'], $parent_id, self::META_KEY_IMAGES, false); $ids = array_values(array_filter((array) $ids, fn($id) => (int) $id !== (int) $attachment_id)); call_user_func($store['del'], $parent_id, self::META_KEY_IMAGES); foreach ($ids as $id) { call_user_func($store['add'], $parent_id, self::META_KEY_IMAGES, $id); } wp_delete_attachment($attachment_id, true); return true; } /** Voice upload wrapper. */ public static function handle_voice_upload_for_parent($parent_id, $file, $parent_type = 'post') { $store = self::meta_store($parent_id, $parent_type); if (is_wp_error($store)) return $store; if (!is_array($file) || $file['error'] !== UPLOAD_ERR_OK) { return new WP_Error('upload_error', 'Upload failed.'); } if ($file['size'] > self::$max_audio_size) { return new WP_Error('file_too_large', 'Voice note exceeds 20 MB limit.'); } $mime = mime_content_type($file['tmp_name']); if (!in_array($mime, self::$audio_types, true)) { return new WP_Error('invalid_type', 'Only MP3, OGG, WAV, WebM, and M4A audio are allowed.'); } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; $file_return = wp_handle_upload($file, [ 'test_form' => false, 'mimes' => [ 'mp3' => 'audio/mpeg', 'ogg' => 'audio/ogg', 'wav' => 'audio/wav', 'webm' => 'audio/webm', 'm4a' => 'audio/mp4', ], ]); if (isset($file_return['error'])) { return new WP_Error('upload_error', $file_return['error']); } $attachment = [ 'post_mime_type' => $file_return['type'], 'post_title' => preg_replace('/\.[^.]+$/', '', basename($file_return['file'])), 'post_content' => '', 'post_status' => 'inherit', 'guid' => $file_return['url'], ]; $attach_parent = $parent_type === 'comment' ? (int) get_comment($parent_id)->comment_post_ID : $parent_id; $attachment_id = wp_insert_attachment($attachment, $file_return['file'], $attach_parent); if (is_wp_error($attachment_id)) { return $attachment_id; } $attach_data = wp_generate_attachment_metadata($attachment_id, $file_return['file']); wp_update_attachment_metadata($attachment_id, $attach_data); $url = wp_get_attachment_url($attachment_id); call_user_func($store['add'], $parent_id, 'beep_reply_voice_url', $url); call_user_func($store['add'], $parent_id, 'beep_reply_voice_id', $attachment_id); return ['id' => $attachment_id, 'url' => $url]; } public static function handle_voice_upload($post_id, $file) { if (!is_array($file) || $file['error'] !== UPLOAD_ERR_OK) { return new WP_Error('upload_error', 'Upload failed.'); } if ($file['size'] > self::$max_audio_size) { return new WP_Error('file_too_large', 'Voice note exceeds 20 MB limit.'); } $mime = mime_content_type($file['tmp_name']); if (!in_array($mime, self::$audio_types, true)) { return new WP_Error('invalid_type', 'Only MP3, OGG, WAV, WebM, and M4A audio are allowed.'); } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; $file_return = wp_handle_upload($file, [ 'test_form' => false, 'mimes' => [ 'mp3' => 'audio/mpeg', 'ogg' => 'audio/ogg', 'wav' => 'audio/wav', 'webm' => 'audio/webm', 'm4a' => 'audio/mp4', ], ]); if (isset($file_return['error'])) { return new WP_Error('upload_error', $file_return['error']); } $attachment = [ 'post_mime_type' => $file_return['type'], 'post_title' => preg_replace('/\.[^.]+$/', '', basename($file_return['file'])), 'post_content' => '', 'post_status' => 'inherit', 'guid' => $file_return['url'], ]; $attachment_id = wp_insert_attachment($attachment, $file_return['file'], $post_id); if (is_wp_error($attachment_id)) { return $attachment_id; } $attach_data = wp_generate_attachment_metadata($attachment_id, $file_return['file']); wp_update_attachment_metadata($attachment_id, $attach_data); update_post_meta($post_id, 'beep_voice_url', wp_get_attachment_url($attachment_id)); update_post_meta($post_id, 'beep_voice_id', $attachment_id); return [ 'id' => $attachment_id, 'url' => wp_get_attachment_url($attachment_id), ]; } public static function get_voice_url($post_id) { return esc_url_raw(get_post_meta($post_id, 'beep_voice_url', true)); } public static function remove_voice($post_id) { $id = (int) get_post_meta($post_id, 'beep_voice_id', true); if ($id) { wp_delete_attachment($id, true); } delete_post_meta($post_id, 'beep_voice_url'); delete_post_meta($post_id, 'beep_voice_id'); } public static function render_voice_note($post_id) { $url = self::get_voice_url($post_id); if (!$url) return ''; $duration = ''; $meta = get_post_meta($post_id, 'beep_voice_id', true); if ($meta) { $metadata = wp_get_attachment_metadata($meta); if ($metadata && isset($metadata['length_formatted'])) { $duration = $metadata['length_formatted']; } } $dur_html = $duration ? '' . esc_html($duration) . '' : ''; return '
' . '' . '
Voice note
' . $dur_html . '
'; } // Video file upload support public static function handle_video_upload($post_id, $file) { if (!is_array($file) || $file['error'] !== UPLOAD_ERR_OK) { return new WP_Error('upload_error', 'Upload failed.'); } if ($file['size'] > self::$max_video_size) { return new WP_Error('file_too_large', 'Video exceeds 100 MB limit.'); } $mime = mime_content_type($file['tmp_name']); if (!in_array($mime, self::$video_types, true)) { return new WP_Error('invalid_type', 'Only MP4, WebM, OGG, and MOV are allowed.'); } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; $file_return = wp_handle_upload($file, [ 'test_form' => false, 'mimes' => [ 'mp4' => 'video/mp4', 'webm' => 'video/webm', 'ogg' => 'video/ogg', 'mov' => 'video/quicktime', ], ]); if (isset($file_return['error'])) { return new WP_Error('upload_error', $file_return['error']); } $attachment = [ 'post_mime_type' => $file_return['type'], 'post_title' => preg_replace('/\.[^.]+$/', '', basename($file_return['file'])), 'post_content' => '', 'post_status' => 'inherit', 'guid' => $file_return['url'], ]; $attachment_id = wp_insert_attachment($attachment, $file_return['file'], $post_id); if (is_wp_error($attachment_id)) { return $attachment_id; } $attach_data = wp_generate_attachment_metadata($attachment_id, $file_return['file']); wp_update_attachment_metadata($attachment_id, $attach_data); update_post_meta($post_id, 'beep_video_url', wp_get_attachment_url($attachment_id)); update_post_meta($post_id, 'beep_video_id', $attachment_id); return [ 'id' => $attachment_id, 'url' => wp_get_attachment_url($attachment_id), ]; } public static function get_upload_video_url($post_id) { return esc_url_raw(get_post_meta($post_id, 'beep_video_url', true)); } public static function remove_video($post_id) { $id = (int) get_post_meta($post_id, 'beep_video_id', true); if ($id) { wp_delete_attachment($id, true); } delete_post_meta($post_id, 'beep_video_url'); delete_post_meta($post_id, 'beep_video_id'); } public static function render_video_player($post_id) { $url = self::get_upload_video_url($post_id); if (!$url) return ''; return '
' . '
'; } }