Changeset 3366689
- Timestamp:
- 09/23/2025 04:51:48 PM (5 months ago)
- Location:
- plugnmeet
- Files:
-
- 8 edited
- 1 copied
-
tags/v1.2.15 (copied) (copied from plugnmeet/trunk)
-
tags/v1.2.15/README.txt (modified) (2 diffs)
-
tags/v1.2.15/plugnmeet.php (modified) (2 diffs)
-
tags/v1.2.15/public/class-plugnmeet-public.php (modified) (3 diffs)
-
tags/v1.2.15/public/partials/plugnmeet-public-display-client.php (modified) (3 diffs)
-
trunk/README.txt (modified) (2 diffs)
-
trunk/plugnmeet.php (modified) (2 diffs)
-
trunk/public/class-plugnmeet-public.php (modified) (3 diffs)
-
trunk/public/partials/plugnmeet-public-display-client.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
plugnmeet/tags/v1.2.15/README.txt
r3363798 r3366689 5 5 Requires at least: 5.9 6 6 Tested up to: 6.8.2 7 Stable tag: 1.2.1 47 Stable tag: 1.2.15 8 8 Requires PHP: 7.4 9 9 License: GPLv2 or later … … 24 24 1. **Self-Host:** Create your own server for maximum control and privacy by following the [official installation instructions](https://www.plugnmeet.org/docs/installation). 25 25 2. **Use the Cloud:** Get started in minutes with a ready-to-use [plugNmeet cloud subscription](https://www.plugnmeet.cloud). 26 27 **Note:** The plugin includes pre-configured demo credentials to help you test its features immediately. This demo server is a shared resource and is **not intended for production use** as it can be unreliable. For any important meetings, we strongly recommend using one of the options above to ensure a stable and professional experience for you and your users. 26 28 27 29 --- -
plugnmeet/tags/v1.2.15/plugnmeet.php
r3363798 r3366689 17 17 * Description: Plug-N-Meet web conference integration with WordPress 18 18 * x-release-please-start-version 19 * Version: 1.2.1 419 * Version: 1.2.15 20 20 * x-release-please-end 21 21 * Author: Jibon L. Costa <[email protected]> … … 41 41 */ 42 42 // x-release-please-start-version 43 define( 'PLUGNMEET_VERSION', '1.2.1 4' );43 define( 'PLUGNMEET_VERSION', '1.2.15' ); 44 44 // x-release-please-end 45 45 /** -
plugnmeet/tags/v1.2.15/public/class-plugnmeet-public.php
r3080964 r3366689 92 92 93 93 public function start_session() { 94 // Don't start a session for WP-CLI or REST API requests. 95 if ( ( defined( 'WP_CLI' ) && WP_CLI ) || defined( 'REST_REQUEST' ) ) { 96 return; 97 } 98 99 // Don't start a session on "true" admin pages, but DO allow it for AJAX requests. 100 if ( is_admin() && ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) ) { 101 return; 102 } 103 94 104 if ( ! session_id() ) { 95 105 session_start(); … … 232 242 public function plugnmeet_shortcode_room_view( $atts, $content = null, $tag = "" ) { 233 243 234 /**235 * Combine user attributes with known attributes.236 *237 * @see https://developer.wordpress.org/reference/functions/shortcode_atts/238 *239 * Pass third paramter $shortcode to enable ShortCode Attribute Filtering.240 * @see https://developer.wordpress.org/reference/hooks/shortcode_atts_shortcode/241 */242 243 244 $atts = shortcode_atts( 244 245 array( 245 'id' => 1,246 'id' => 0, // Default to 0 to better handle cases where no ID is provided. 246 247 ), 247 248 $atts, … … 249 250 ); 250 251 252 $id = intval( $atts['id'] ); 253 251 254 /** 252 * Build our ShortCode output. 253 * Remember to sanitize all user input. 254 * In this case, we expect a integer value to be passed to the ShortCode attribute. 255 * 256 * @see https://developer.wordpress.org/themes/theme-security/data-sanitization-escaping/ 257 */ 258 $id = intval( $atts['id'] ); 259 260 /** 261 * If the shortcode is enclosing, we may want to do something with $content 255 * If the shortcode is enclosing, the content will be used as the ID, 256 * overriding the 'id' attribute. 257 * e.g. [plugnmeet_room_view]123[/plugnmeet_room_view] 262 258 */ 263 259 if ( ! empty( $content ) ) { 264 $id = do_shortcode( $content );// We can parse shortcodes inside $content. 265 $id = intval( $atts['id'] ) . ' ' . sanitize_text_field( $id );// Remember to sanitize your user input. 260 $id = intval( do_shortcode( $content ) ); 266 261 } 267 262 268 263 if ( ! $id ) { 269 return null; 264 // If no ID is provided via attribute or content, there's nothing to show. 265 return __( 'Room ID not provided for the shortcode.', 'plugnmeet' ); 270 266 } 271 267 -
plugnmeet/tags/v1.2.15/public/partials/plugnmeet-public-display-client.php
r3363798 r3366689 1 1 <?php 2 2 /** 3 * This template provides a "blank slate" environment for the external React application. 4 * It aggressively removes all WordPress theme/plugin assets to prevent conflicts. 3 5 * 4 6 * @link https://www.mynaparrot.com … … 10 12 11 13 if ( ! defined( 'PLUGNMEET_BASE_NAME' ) ) { 12 die;14 die; 13 15 } 14 16 17 // Remove unnecessary meta tags and actions from wp_head for a cleaner output. 15 18 remove_action( 'wp_head', 'rsd_link' ); 16 19 remove_action( 'wp_head', 'wp_generator' ); … … 28 31 remove_action( 'wp_head', 'index_rel_link' ); 29 32 30 function insert_plnm_js_css() { 31 global $wp_styles, $wp_scripts;33 // This global will hold the handle of the main JS module to identify it in the filter. 34 $pnm_main_module_handle = ''; 32 35 33 $setting_params = (object) get_option( "plugnmeet_settings" ); 34 if ( ! isset( $setting_params->client_load ) || $setting_params->client_load === "remote" ) { 35 if ( ! class_exists( "plugNmeetConnect" ) ) { 36 require PLUGNMEET_ROOT_PATH . '/helpers/plugNmeetConnect.php'; 37 } 38 $connect = new plugNmeetConnect( $setting_params ); 39 $files = $connect->getClientFiles(); 40 $jsFiles = $files->getJSFiles() ?? []; 41 $cssFiles = $files->getCSSFiles() ?? []; 42 $path = $setting_params->plugnmeet_server_url . "/assets"; 43 } else { 44 $clientPath = PLUGNMEET_ROOT_PATH . "/public/client/dist/assets"; 45 $jsFiles = preg_grep( '~\.(js)$~', scandir( $clientPath . "/js", SCANDIR_SORT_DESCENDING ) ); 46 $cssFiles = preg_grep( '~\.(css)$~', scandir( $clientPath . "/css", SCANDIR_SORT_DESCENDING ) ); 47 $path = plugins_url( 'public/client/dist/assets', PLUGNMEET_BASE_NAME ); 48 } 36 /** 37 * This is the core function for creating the "blank slate". 38 * It runs late, enqueues the app's assets, and then overwrites the global script/style queues 39 * to ensure nothing else is loaded. 40 */ 41 function pnm_final_asset_loader() { 42 global $wp_styles, $wp_scripts, $pnm_main_module_handle; 49 43 50 foreach ( $jsFiles as $file ) { 51 wp_enqueue_script( $file, $path . '/js/' . $file, array(), null ); 52 } 53 foreach ( $cssFiles as $file ) { 54 wp_enqueue_style( $file, $path . '/css/' . $file, array(), null ); 55 } 44 // 1. Define the assets your app needs. 45 $setting_params = (object) get_option( "plugnmeet_settings" ); 46 if ( ! isset( $setting_params->client_load ) || $setting_params->client_load === "remote" ) { 47 if ( ! class_exists( "plugNmeetConnect" ) ) { 48 require PLUGNMEET_ROOT_PATH . '/helpers/plugNmeetConnect.php'; 49 } 50 $connect = new plugNmeetConnect( $setting_params ); 51 $files = $connect->getClientFiles(); 52 $jsFiles = $files->getJSFiles() ?? []; 53 $cssFiles = $files->getCSSFiles() ?? []; 54 $path = $setting_params->plugnmeet_server_url . "/assets"; 55 } else { 56 $clientPath = PLUGNMEET_ROOT_PATH . "/public/client/dist/assets"; 57 $jsFiles = array_values( preg_grep( '~\.(js)$~', scandir( $clientPath . "/js", SCANDIR_SORT_DESCENDING ) ) ); 58 $cssFiles = array_values( preg_grep( '~\.(css)$~', scandir( $clientPath . "/css", SCANDIR_SORT_DESCENDING ) ) ); 59 $path = plugins_url( 'public/client/dist/assets', PLUGNMEET_BASE_NAME ); 60 } 56 61 57 foreach ( $wp_styles->queue as $style ) { 58 if ( in_array( $style, $cssFiles ) ) { 59 continue; 60 } 61 $wp_styles->remove( $style ); 62 } 63 foreach ( $wp_scripts->queue as $script ) { 64 if ( in_array( $script, $jsFiles ) ) { 65 continue; 66 } 67 $wp_scripts->remove( $script ); 68 } 62 $allowed_js_handles = []; 63 foreach ( $jsFiles as $file ) { 64 $handle = $file; // Use the filename as the handle, matching original logic. 65 if ( str_starts_with( $handle, 'main-module.' ) ) { 66 $pnm_main_module_handle = $handle; 67 } 68 $allowed_js_handles[] = $handle; 69 // Enqueue the script to register it. We'll force it into the queue later. 70 wp_enqueue_script( $handle, $path . '/js/' . $file, [], null, false ); 71 } 72 73 $allowed_css_handles = []; 74 foreach ( $cssFiles as $file ) { 75 $handle = $file; 76 $allowed_css_handles[] = $handle; 77 wp_enqueue_style( $handle, $path . '/css/' . $file, [], null ); 78 } 79 80 // 2. Aggressively overwrite the queues. 81 // This ensures ONLY your assets are listed for printing. 82 $wp_scripts->queue = $allowed_js_handles; 83 $wp_styles->queue = $allowed_css_handles; 69 84 } 70 85 71 add_action( 'wp_print_styles', ' insert_plnm_js_css', 100);86 add_action( 'wp_print_styles', 'pnm_final_asset_loader', 9999 ); 72 87 73 function add_custom_attr( $tag ) {74 if ( str_contains( $tag, "main-module." ) ) {75 return str_replace( 'src=', 'type="module" src=', $tag );76 }77 88 78 return str_replace( 'src=', 'defer="defer" src=', $tag ); 89 /** 90 * Filters the script tag to add `type="module"` or `defer` attributes. 91 * 92 * @param string $tag The <script> tag for the enqueued script. 93 * @param string $handle The script's handle. 94 * 95 * @return string The modified <script> tag. 96 */ 97 function pnm_script_loader_tag_filter( $tag, $handle ) { 98 global $pnm_main_module_handle; 99 100 // Check if the current script is our main module and add type="module". 101 if ( $pnm_main_module_handle === $handle ) { 102 return str_replace( ' src=', ' type="module" src=', $tag ); 103 } 104 105 // For all other scripts on this page, add 'defer'. 106 return str_replace( ' src=', ' defer="defer" src=', $tag ); 79 107 } 80 108 81 add_filter( 'script_loader_tag', 'add_custom_attr', 10, 3 ); 109 add_filter( 'script_loader_tag', 'pnm_script_loader_tag_filter', 10, 2 ); 110 82 111 ?> 83 112 <!DOCTYPE html> 84 <html lang="en">113 <html <?php language_attributes(); ?>> 85 114 <head> 86 <meta charset=" UTF-8"/>115 <meta charset="<?php bloginfo( 'charset' ); ?>"/> 87 116 <meta http-equiv="X-UA-Compatible" content="IE=edge"/> 88 117 <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> 89 <?php wp_head(); ?> 90 <?php wp_print_inline_script_tag( $jsOptions ); ?> 118 <title>plugNmeet</title> 119 <?php wp_head(); ?> 120 <?php wp_print_inline_script_tag( $jsOptions ); ?> 91 121 </head> 92 122 <body> 93 123 <div id="plugNmeet-app"></div> 124 <?php // Do not include wp_footer() to ensure a clean environment. ?> 94 125 </body> 95 126 </html> -
plugnmeet/trunk/README.txt
r3363798 r3366689 5 5 Requires at least: 5.9 6 6 Tested up to: 6.8.2 7 Stable tag: 1.2.1 47 Stable tag: 1.2.15 8 8 Requires PHP: 7.4 9 9 License: GPLv2 or later … … 24 24 1. **Self-Host:** Create your own server for maximum control and privacy by following the [official installation instructions](https://www.plugnmeet.org/docs/installation). 25 25 2. **Use the Cloud:** Get started in minutes with a ready-to-use [plugNmeet cloud subscription](https://www.plugnmeet.cloud). 26 27 **Note:** The plugin includes pre-configured demo credentials to help you test its features immediately. This demo server is a shared resource and is **not intended for production use** as it can be unreliable. For any important meetings, we strongly recommend using one of the options above to ensure a stable and professional experience for you and your users. 26 28 27 29 --- -
plugnmeet/trunk/plugnmeet.php
r3363798 r3366689 17 17 * Description: Plug-N-Meet web conference integration with WordPress 18 18 * x-release-please-start-version 19 * Version: 1.2.1 419 * Version: 1.2.15 20 20 * x-release-please-end 21 21 * Author: Jibon L. Costa <[email protected]> … … 41 41 */ 42 42 // x-release-please-start-version 43 define( 'PLUGNMEET_VERSION', '1.2.1 4' );43 define( 'PLUGNMEET_VERSION', '1.2.15' ); 44 44 // x-release-please-end 45 45 /** -
plugnmeet/trunk/public/class-plugnmeet-public.php
r3080964 r3366689 92 92 93 93 public function start_session() { 94 // Don't start a session for WP-CLI or REST API requests. 95 if ( ( defined( 'WP_CLI' ) && WP_CLI ) || defined( 'REST_REQUEST' ) ) { 96 return; 97 } 98 99 // Don't start a session on "true" admin pages, but DO allow it for AJAX requests. 100 if ( is_admin() && ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) ) { 101 return; 102 } 103 94 104 if ( ! session_id() ) { 95 105 session_start(); … … 232 242 public function plugnmeet_shortcode_room_view( $atts, $content = null, $tag = "" ) { 233 243 234 /**235 * Combine user attributes with known attributes.236 *237 * @see https://developer.wordpress.org/reference/functions/shortcode_atts/238 *239 * Pass third paramter $shortcode to enable ShortCode Attribute Filtering.240 * @see https://developer.wordpress.org/reference/hooks/shortcode_atts_shortcode/241 */242 243 244 $atts = shortcode_atts( 244 245 array( 245 'id' => 1,246 'id' => 0, // Default to 0 to better handle cases where no ID is provided. 246 247 ), 247 248 $atts, … … 249 250 ); 250 251 252 $id = intval( $atts['id'] ); 253 251 254 /** 252 * Build our ShortCode output. 253 * Remember to sanitize all user input. 254 * In this case, we expect a integer value to be passed to the ShortCode attribute. 255 * 256 * @see https://developer.wordpress.org/themes/theme-security/data-sanitization-escaping/ 257 */ 258 $id = intval( $atts['id'] ); 259 260 /** 261 * If the shortcode is enclosing, we may want to do something with $content 255 * If the shortcode is enclosing, the content will be used as the ID, 256 * overriding the 'id' attribute. 257 * e.g. [plugnmeet_room_view]123[/plugnmeet_room_view] 262 258 */ 263 259 if ( ! empty( $content ) ) { 264 $id = do_shortcode( $content );// We can parse shortcodes inside $content. 265 $id = intval( $atts['id'] ) . ' ' . sanitize_text_field( $id );// Remember to sanitize your user input. 260 $id = intval( do_shortcode( $content ) ); 266 261 } 267 262 268 263 if ( ! $id ) { 269 return null; 264 // If no ID is provided via attribute or content, there's nothing to show. 265 return __( 'Room ID not provided for the shortcode.', 'plugnmeet' ); 270 266 } 271 267 -
plugnmeet/trunk/public/partials/plugnmeet-public-display-client.php
r3363798 r3366689 1 1 <?php 2 2 /** 3 * This template provides a "blank slate" environment for the external React application. 4 * It aggressively removes all WordPress theme/plugin assets to prevent conflicts. 3 5 * 4 6 * @link https://www.mynaparrot.com … … 10 12 11 13 if ( ! defined( 'PLUGNMEET_BASE_NAME' ) ) { 12 die;14 die; 13 15 } 14 16 17 // Remove unnecessary meta tags and actions from wp_head for a cleaner output. 15 18 remove_action( 'wp_head', 'rsd_link' ); 16 19 remove_action( 'wp_head', 'wp_generator' ); … … 28 31 remove_action( 'wp_head', 'index_rel_link' ); 29 32 30 function insert_plnm_js_css() { 31 global $wp_styles, $wp_scripts;33 // This global will hold the handle of the main JS module to identify it in the filter. 34 $pnm_main_module_handle = ''; 32 35 33 $setting_params = (object) get_option( "plugnmeet_settings" ); 34 if ( ! isset( $setting_params->client_load ) || $setting_params->client_load === "remote" ) { 35 if ( ! class_exists( "plugNmeetConnect" ) ) { 36 require PLUGNMEET_ROOT_PATH . '/helpers/plugNmeetConnect.php'; 37 } 38 $connect = new plugNmeetConnect( $setting_params ); 39 $files = $connect->getClientFiles(); 40 $jsFiles = $files->getJSFiles() ?? []; 41 $cssFiles = $files->getCSSFiles() ?? []; 42 $path = $setting_params->plugnmeet_server_url . "/assets"; 43 } else { 44 $clientPath = PLUGNMEET_ROOT_PATH . "/public/client/dist/assets"; 45 $jsFiles = preg_grep( '~\.(js)$~', scandir( $clientPath . "/js", SCANDIR_SORT_DESCENDING ) ); 46 $cssFiles = preg_grep( '~\.(css)$~', scandir( $clientPath . "/css", SCANDIR_SORT_DESCENDING ) ); 47 $path = plugins_url( 'public/client/dist/assets', PLUGNMEET_BASE_NAME ); 48 } 36 /** 37 * This is the core function for creating the "blank slate". 38 * It runs late, enqueues the app's assets, and then overwrites the global script/style queues 39 * to ensure nothing else is loaded. 40 */ 41 function pnm_final_asset_loader() { 42 global $wp_styles, $wp_scripts, $pnm_main_module_handle; 49 43 50 foreach ( $jsFiles as $file ) { 51 wp_enqueue_script( $file, $path . '/js/' . $file, array(), null ); 52 } 53 foreach ( $cssFiles as $file ) { 54 wp_enqueue_style( $file, $path . '/css/' . $file, array(), null ); 55 } 44 // 1. Define the assets your app needs. 45 $setting_params = (object) get_option( "plugnmeet_settings" ); 46 if ( ! isset( $setting_params->client_load ) || $setting_params->client_load === "remote" ) { 47 if ( ! class_exists( "plugNmeetConnect" ) ) { 48 require PLUGNMEET_ROOT_PATH . '/helpers/plugNmeetConnect.php'; 49 } 50 $connect = new plugNmeetConnect( $setting_params ); 51 $files = $connect->getClientFiles(); 52 $jsFiles = $files->getJSFiles() ?? []; 53 $cssFiles = $files->getCSSFiles() ?? []; 54 $path = $setting_params->plugnmeet_server_url . "/assets"; 55 } else { 56 $clientPath = PLUGNMEET_ROOT_PATH . "/public/client/dist/assets"; 57 $jsFiles = array_values( preg_grep( '~\.(js)$~', scandir( $clientPath . "/js", SCANDIR_SORT_DESCENDING ) ) ); 58 $cssFiles = array_values( preg_grep( '~\.(css)$~', scandir( $clientPath . "/css", SCANDIR_SORT_DESCENDING ) ) ); 59 $path = plugins_url( 'public/client/dist/assets', PLUGNMEET_BASE_NAME ); 60 } 56 61 57 foreach ( $wp_styles->queue as $style ) { 58 if ( in_array( $style, $cssFiles ) ) { 59 continue; 60 } 61 $wp_styles->remove( $style ); 62 } 63 foreach ( $wp_scripts->queue as $script ) { 64 if ( in_array( $script, $jsFiles ) ) { 65 continue; 66 } 67 $wp_scripts->remove( $script ); 68 } 62 $allowed_js_handles = []; 63 foreach ( $jsFiles as $file ) { 64 $handle = $file; // Use the filename as the handle, matching original logic. 65 if ( str_starts_with( $handle, 'main-module.' ) ) { 66 $pnm_main_module_handle = $handle; 67 } 68 $allowed_js_handles[] = $handle; 69 // Enqueue the script to register it. We'll force it into the queue later. 70 wp_enqueue_script( $handle, $path . '/js/' . $file, [], null, false ); 71 } 72 73 $allowed_css_handles = []; 74 foreach ( $cssFiles as $file ) { 75 $handle = $file; 76 $allowed_css_handles[] = $handle; 77 wp_enqueue_style( $handle, $path . '/css/' . $file, [], null ); 78 } 79 80 // 2. Aggressively overwrite the queues. 81 // This ensures ONLY your assets are listed for printing. 82 $wp_scripts->queue = $allowed_js_handles; 83 $wp_styles->queue = $allowed_css_handles; 69 84 } 70 85 71 add_action( 'wp_print_styles', ' insert_plnm_js_css', 100);86 add_action( 'wp_print_styles', 'pnm_final_asset_loader', 9999 ); 72 87 73 function add_custom_attr( $tag ) {74 if ( str_contains( $tag, "main-module." ) ) {75 return str_replace( 'src=', 'type="module" src=', $tag );76 }77 88 78 return str_replace( 'src=', 'defer="defer" src=', $tag ); 89 /** 90 * Filters the script tag to add `type="module"` or `defer` attributes. 91 * 92 * @param string $tag The <script> tag for the enqueued script. 93 * @param string $handle The script's handle. 94 * 95 * @return string The modified <script> tag. 96 */ 97 function pnm_script_loader_tag_filter( $tag, $handle ) { 98 global $pnm_main_module_handle; 99 100 // Check if the current script is our main module and add type="module". 101 if ( $pnm_main_module_handle === $handle ) { 102 return str_replace( ' src=', ' type="module" src=', $tag ); 103 } 104 105 // For all other scripts on this page, add 'defer'. 106 return str_replace( ' src=', ' defer="defer" src=', $tag ); 79 107 } 80 108 81 add_filter( 'script_loader_tag', 'add_custom_attr', 10, 3 ); 109 add_filter( 'script_loader_tag', 'pnm_script_loader_tag_filter', 10, 2 ); 110 82 111 ?> 83 112 <!DOCTYPE html> 84 <html lang="en">113 <html <?php language_attributes(); ?>> 85 114 <head> 86 <meta charset=" UTF-8"/>115 <meta charset="<?php bloginfo( 'charset' ); ?>"/> 87 116 <meta http-equiv="X-UA-Compatible" content="IE=edge"/> 88 117 <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> 89 <?php wp_head(); ?> 90 <?php wp_print_inline_script_tag( $jsOptions ); ?> 118 <title>plugNmeet</title> 119 <?php wp_head(); ?> 120 <?php wp_print_inline_script_tag( $jsOptions ); ?> 91 121 </head> 92 122 <body> 93 123 <div id="plugNmeet-app"></div> 124 <?php // Do not include wp_footer() to ensure a clean environment. ?> 94 125 </body> 95 126 </html>
Note: See TracChangeset
for help on using the changeset viewer.