PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / 260917
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions v260917
260917 260913 260909 260829 260814 260805 110710 110731 110812 110815 110912 110913 110915 110926 110927 111002 111003 111011 111017 111029 111105 111206 111216 111220 120213 All 189 releases
s2member / src / includes / classes / sc-files-in.inc.php

sc-files-in.inc.php in s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions 260917, at src/includes/classes/sc-files-in.inc.php

829 lines 45.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // @codingStandardsIgnoreFile
3 /**
4 * Shortcode `[s2File /]` (inner processing routines).
5 *
6 * Copyright: © 2009-2011
7 * {@link http://websharks-inc.com/ WebSharks, Inc.}
8 * (coded in the USA)
9 *
10 * Released under the terms of the GNU General Public License.
11 * You should have received a copy of the GNU General Public License,
12 * along with this software. In the main directory, see: /licensing/
13 * If not, see: {@link http://www.gnu.org/licenses/}.
14 *
15 * @package s2Member\s2File
16 * @since 110926
17 */
18 if(!defined('WPINC')) // MUST have WordPress.
19 exit('Do not access this file directly.');
20
21 if(!class_exists('c_ws_plugin__s2member_sc_files_in'))
22 {
23 /**
24 * Shortcode `[s2File /]` (inner processing routines).
25 *
26 * @package s2Member\s2File
27 * @since 110926
28 */
29 class c_ws_plugin__s2member_sc_files_in
30 {
31 /**
32 * Handles the Shortcode for: `[s2File /]`.
33 *
34 * @package s2Member\s2File
35 * @since 110926
36 *
37 * @attaches-to ``add_shortcode('s2File');``
38 *
39 * @param array $attr An array of Attributes.
40 * @param string $content Content inside the Shortcode.
41 * @param string $shortcode The actual Shortcode name itself.
42 *
43 * @return string Value of requested File Download URL, streamer array element; or null on failure.
44 */
45 public static function sc_get_file($attr = array(), $content = '', $shortcode = '')
46 {
47 foreach(array_keys(get_defined_vars()) as $__v) $__refs[$__v] =& $$__v;
48 do_action('ws_plugin__s2member_before_sc_get_file', get_defined_vars());
49 unset($__refs, $__v); // Housekeeping.
50
51 $attr = c_ws_plugin__s2member_utils_strings::trim_qts_deep((array)$attr); // Force array; trim quote entities.
52
53 $attr = shortcode_atts(array('download' => '', 'download_key' => '',
54 'stream' => '', 'inline' => '', 'storage' => '',
55 'remote' => '', 'ssl' => '', 'rewrite' => '', 'rewrite_base' => '',
56 'skip_confirmation' => '', 'url_to_storage_source' => '',
57 'count_against_user' => '', 'check_user' => '',
58 'get_streamer_json' => '', 'get_streamer_array' => ''), $attr);
59
60 //260811 Validate download key.
61 if(!in_array($attr['download_key'], array('ip-forever', 'universal'), true))
62 $attr['download_key'] = filter_var($attr['download_key'], FILTER_VALIDATE_BOOLEAN) ? 'yes' : '';
63
64 foreach(array_keys(get_defined_vars()) as $__v) $__refs[$__v] =& $$__v;
65 do_action('ws_plugin__s2member_before_sc_get_file_after_shortcode_atts', get_defined_vars());
66 unset($__refs, $__v); // Housekeeping.
67
68 $get_streamer_json = filter_var($attr['get_streamer_json'], FILTER_VALIDATE_BOOLEAN);
69 $get_streamer_array = filter_var($attr['get_streamer_array'], FILTER_VALIDATE_BOOLEAN);
70 $get_streamer_json = $get_streamer_array = ($get_streamer_array || $get_streamer_json) ? TRUE : FALSE;
71
72 foreach($attr as $key => $value) // Now we need to go through and a `file_` prefix to certain Attribute keys, for compatibility.
73 if(strlen($value) && in_array($key, array('download', 'download_key', 'stream', 'inline', 'storage', 'remote', 'ssl', 'rewrite', 'rewrite_base')))
74 $config['file_'.$key] = $value; // Set prefixed config parameter here so we can pass properly in ``$config`` array.
75 else if(strlen($value) && !in_array($key, array('get_streamer_json', 'get_streamer_array')))
76 $config[$key] = $value;
77
78 unset($key, $value); // We don't want these bleeding into Hooks/Filters anyway.
79
80 if(!empty($config) && isset($config['file_download'])) // Looking for a File Download URL?
81 {
82 $_get = c_ws_plugin__s2member_files::create_file_download_url($config, $get_streamer_array);
83
84 if($get_streamer_array && $get_streamer_json && is_array($_get))
85 $get = json_encode($_get);
86
87 else if($get_streamer_array && $get_streamer_json)
88 $get = 'null'; // Null object value.
89
90 else if(!empty($_get))
91 $get = $_get;
92 }
93 return apply_filters('ws_plugin__s2member_sc_get_file', isset($get) ? $get : NULL, get_defined_vars());
94 }
95
96 /**
97 * Handles the Shortcode for: `[s2Stream /]`.
98 *
99 * @package s2Member\s2File
100 * @since 130119
101 *
102 * @attaches-to ``add_shortcode('s2Stream');``
103 *
104 * @param array $attr An array of Attributes.
105 * @param string $content Content inside the Shortcode.
106 * @param string $shortcode The actual Shortcode name itself.
107 *
108 * @return string HTML markup that produces an audio/video stream for a specific player.
109 */
110 public static function sc_get_stream($attr = array(), $content = '', $shortcode = '')
111 {
112 foreach(array_keys(get_defined_vars()) as $__v) $__refs[$__v] =& $$__v;
113 do_action('ws_plugin__s2member_before_sc_get_stream', get_defined_vars());
114 unset($__refs, $__v); // Housekeeping.
115
116 $attr = c_ws_plugin__s2member_utils_strings::trim_qts_deep((array)$attr);
117
118 $attr = shortcode_atts(array('download' => '', 'file_download' => '', 'download_key' => '',
119 'stream' => 'yes', 'inline' => 'yes', 'storage' => '',
120 'remote' => '', 'ssl' => '', 'rewrite' => 'yes', 'rewrite_base' => '',
121 'skip_confirmation' => '', 'url_to_storage_source' => 'yes',
122 'count_against_user' => 'yes', 'check_user' => 'yes',
123
124 // Configuration
125 'player' => 'jwplayer-v7-rtmp', 'player_id' => 's2-stream-'.md5(uniqid('', TRUE)),
126 'player_path' => '/jwplayer/jwplayer.js', 'player_key' => '', 'player_title' => '',
127 'player_image' => '', 'player_mediaid' => '', 'player_description' => '', 'player_captions' => '', 'player_tracks' => '',
128 'player_resolutions' => '', // A comma-delimited list of resolution options.
129
130 // Layout
131 'player_controls' => 'yes', 'player_skin' => '', 'player_stretching' => 'uniform',
132 'player_width' => '480', 'player_height' => '270', 'player_aspectratio' => '',
133
134 // Playback
135 'player_autostart' => 'no', 'player_fallback' => 'yes', 'player_mute' => 'no',
136 'player_primary' => (($attr['player'] === 'jw-player-v7' || $attr['player'] === 'jw-player-v6') ? 'html5' : 'flash'),
137 'player_repeat' => 'no', 'player_startparam' => '', // `startparam` seems to be JW Player v6 only.
138
139 // Advanced Option Blocks
140 'player_option_blocks' => ''), $attr);
141
142 $attr['download'] = (!empty($attr['file_download'])) ? $attr['file_download'] : $attr['download'];
143
144 //260811 Validate download key.
145 if(!in_array($attr['download_key'], array('ip-forever', 'universal'), true))
146 $attr['download_key'] = filter_var($attr['download_key'], FILTER_VALIDATE_BOOLEAN) ? 'yes' : '';
147
148 foreach(array_keys(get_defined_vars()) as $__v) $__refs[$__v] =& $$__v;
149 do_action('ws_plugin__s2member_before_sc_get_stream_after_shortcode_atts', get_defined_vars());
150 unset($__refs, $__v); // Housekeeping.
151
152 //260805 Validate the final player configuration after shortcode hooks, before it affects paths or generated markup.
153 $player_templates = array('jwplayer-v6', 'jwplayer-v6-rtmp', 'jwplayer-v6-rtmp-only', 'jwplayer-v7', 'jwplayer-v7-rtmp', 'jwplayer-v7-rtmp-only');
154 if(!in_array($attr['player'], $player_templates, TRUE))
155 $attr['player'] = 'jwplayer-v7-rtmp';
156
157 //260805 Shortcode content may select only an exact player script path allowlisted through trusted PHP.
158 $player_paths = array_map('strval', (array)apply_filters('ws_plugin__s2member_sc_get_stream_player_paths', array('/jwplayer/jwplayer.js'), $attr));
159 $player_paths = array_values(array_unique(array_filter(array_map('trim', $player_paths), 'strlen')));
160 if(!$player_paths)
161 $player_paths = array('/jwplayer/jwplayer.js');
162 if(!in_array((string)$attr['player_path'], $player_paths, TRUE))
163 $attr['player_path'] = $player_paths[0];
164
165 //260805 Preserve supported enums and numeric formats; invalid values fall back instead of entering JavaScript syntax.
166 $attr['player_primary'] = strtolower(trim((string)$attr['player_primary']));
167 if(!in_array($attr['player_primary'], array('html5', 'flash'), TRUE))
168 $attr['player_primary'] = 'flash';
169 $attr['player_stretching'] = strtolower(trim((string)$attr['player_stretching']));
170 if(!in_array($attr['player_stretching'], array('uniform', 'exactfit', 'fill', 'none'), TRUE))
171 $attr['player_stretching'] = 'uniform';
172 //260805 Keep JW Player v6 query parameter names as strings; complete output encoding prevents JavaScript-string breakout.
173 $attr['player_startparam'] = trim((string)$attr['player_startparam']);
174 if(!preg_match('/^(?:[0-9]+|[0-9]+(?:\.[0-9]+)?%)$/D', (string)$attr['player_width']))
175 $attr['player_width'] = '480';
176 if(!preg_match('/^(?:[0-9]+|[0-9]+(?:\.[0-9]+)?%)$/D', (string)$attr['player_height']))
177 $attr['player_height'] = '270';
178 if($attr['player_aspectratio'] && (!preg_match('/^([0-9]+):([0-9]+)$/D', (string)$attr['player_aspectratio'], $_player_aspectratio) || !(int)$_player_aspectratio[1] || !(int)$_player_aspectratio[2]))
179 $attr['player_aspectratio'] = '';
180 unset($_player_aspectratio); //260805 Housekeeping.
181
182 //260805 Resolution tokens become filename suffixes and labels, so discard tokens outside the supported token format.
183 $_player_resolutions = array();
184 foreach(preg_split('/[,;\s]+/', (string)$attr['player_resolutions'], -1, PREG_SPLIT_NO_EMPTY) as $_player_resolution)
185 {
186 $_player_resolution = ltrim(trim($_player_resolution), 'Rr');
187 if($_player_resolution !== '' && preg_match('/^[A-Za-z0-9][A-Za-z0-9_-]*$/D', $_player_resolution))
188 $_player_resolutions[] = $_player_resolution;
189 }
190 $attr['player_resolutions'] = implode(',', $_player_resolutions);
191 unset($_player_resolutions, $_player_resolution); //260805 Housekeeping.
192
193 //260805 Encode complete JavaScript string literals once; templates receive generated literals instead of shortcode text.
194 $player_json_strings = array();
195 foreach(array('player_id', 'player_key', 'player_title', 'player_image', 'player_mediaid', 'player_description', 'player_skin', 'player_aspectratio', 'player_stretching', 'player_primary', 'player_startparam') as $_player_json_string_key)
196 {
197 $_player_json_string = wp_json_encode((string)$attr[$_player_json_string_key], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
198 $player_json_strings[$_player_json_string_key] = is_string($_player_json_string) ? $_player_json_string : '""';
199 }
200 unset($_player_json_string_key, $_player_json_string); //260805 Housekeeping.
201
202 foreach($attr as $key => $value) // Now we need to go through and a `file_` prefix to certain Attribute keys, for compatibility.
203 if(strlen($value) && in_array($key, array('download', 'download_key', 'stream', 'inline', 'storage', 'remote', 'ssl', 'rewrite', 'rewrite_base')))
204 $config['file_'.$key] = $value; // Set prefixed config parameter here so we can pass properly in ``$config`` array.
205 else if(strlen($value) && !in_array($key, array('file_download', 'player')) && strpos($key, 'player_') !== 0)
206 $config[$key] = $value;
207
208 unset($key, $value); // Ditch these now. We don't want these bleeding into Hooks/Filters anyway.
209
210 if(!empty($config) && isset($config['file_download'])) // Looking for a File Download URL?
211 {
212 if($attr['player_resolutions'] && c_ws_plugin__s2member_utils_conds::pro_is_installed() /* Pro serves SMIL files. */)
213 {
214 $file_download_extension = strtolower(ltrim((string)strrchr(basename($config['file_download']), '.'), '.'));
215 $file_download_resolution_wo_extension = substr($config['file_download'], 0, -(strlen($file_download_extension) + 1) /* For the dot. */);
216 $file_download_wo_resolution_extension = preg_replace('/\-r[0-9]+([^.]*)$/i', '', $file_download_resolution_wo_extension); // e.g., `r720p-HD` is removed here.
217
218 $file_download_resolutions = array(); // Initialize the array of resolutions.
219 foreach(preg_split('/[,;\s]+/', $attr['player_resolutions'], -1, PREG_SPLIT_NO_EMPTY) as $_player_resolution)
220 {
221 $_player_resolution = ltrim($_player_resolution, 'Rr'); // Remove R|r prefix.
222 $file_download_resolutions[$_player_resolution] = $file_download_wo_resolution_extension.'-r'.$_player_resolution.'.'.$file_download_extension;
223 }
224 unset($_player_resolution); // Housekeeping.
225
226 $file_download_urls = array(); // Initialize array of all file download urls.
227 foreach($file_download_resolutions as $_player_resolution => $_file_download_resolution) // NOTE: these ARE in a specific order.
228 {
229 $_file_download_config = array_merge($config, array('file_download' => $_file_download_resolution));
230
231 if($file_download_urls) // If this is a ANOTHER resolution, don't count it against the user.
232 $_file_download_config = array_merge($_file_download_config, array('check_user' => FALSE, 'count_against_user' => FALSE));
233
234 if(!($file_download_urls[str_replace(array('_', '-'), ' ', $_player_resolution)] = c_ws_plugin__s2member_files::create_file_download_url($_file_download_config, TRUE)))
235 return apply_filters('ws_plugin__s2member_sc_get_stream', NULL, get_defined_vars()); // Failure.
236 }
237 unset($_player_resolution, $_file_download_resolution, $_file_download_config); // Housekeeping.
238 }
239 else $file_download_urls = array(c_ws_plugin__s2member_files::create_file_download_url($config, TRUE)); // Default behavior.
240
241 if($file_download_urls && $attr['player'] && is_file($template = dirname(dirname(__FILE__)).'/templates/players/'.$attr['player'].'.php') && $attr['player_id'] && $attr['player_path'])
242 {
243 $template = (is_file(TEMPLATEPATH.'/'.basename($template))) ? TEMPLATEPATH.'/'.basename($template) : $template;
244 $template = (is_file(get_stylesheet_directory().'/'.basename($template))) ? get_stylesheet_directory().'/'.basename($template) : $template;
245 $template = (is_file(WP_CONTENT_DIR.'/'.basename($template))) ? WP_CONTENT_DIR.'/'.basename($template) : $template;
246
247 if(strpos($attr['player'], 'jwplayer-v7') === 0) // JW Player (new v7).
248 {
249 $player = trim(c_ws_plugin__s2member_utilities::evl(file_get_contents($template)));
250
251 $_first_file_download_url = array(); // Holds the first one.
252 $_last_file_download_url = array(); // Holds the last one.
253 $_uses_rtmp_streamers = FALSE; // Streamers use RTMP?
254
255 $_total_player_sources = count($file_download_urls); // Total sources.
256 $_player_sources_counter = 1; // Player sources counter; needed by the loop below.
257
258 $player_resolution_aspect_ratio_w = 16; // Default aspect ratio width.
259 $player_resolution_aspect_ratio_h = 9; // Default aspect ratio in height.
260 if($attr['player_aspectratio'] && preg_match('/^[0-9]+\:[0-9]+$/', $attr['player_aspectratio']))
261 list($player_resolution_aspect_ratio_w, $player_resolution_aspect_ratio_h) = explode(':', $attr['player_aspectratio']);
262 $player_resolution_aspect_ratio_w = (int)$player_resolution_aspect_ratio_w; // Force integer value.
263 $player_resolution_aspect_ratio_h = (int)$player_resolution_aspect_ratio_h; // Force integer value.
264
265 // See: <http://wsharks.com/1yzjAl6> and <http://wsharks.com/1yzkhea> regarging the SMIL bitrate hints given here.
266 $player_resolution_bitrates = array(2160 => '35000000', 1440 => '10000000', 1080 => '8000000', 720 => '5000000', 640 => '2500001', 480 => '2500000', 360 => '1000000', 320 => '999999', 240 => '500000', 180 => '300000');
267 $player_resolution_bitrates = apply_filters('ws_plugin__s2member_sc_get_stream_resolution_bitrates', $player_resolution_bitrates, get_defined_vars());
268
269 $player_resolution_sources_smil_file_id = md5(serialize($attr).c_ws_plugin__s2member_utils_ip::current()); // Initialize SMIL ID.
270 $player_resolution_sources_smil_file_url = home_url('/s2member-rsf-file.smil?s2member_rsf_file='.urlencode($player_resolution_sources_smil_file_id).'&s2member_rsf_file_ip='.urlencode(c_ws_plugin__s2member_utils_ip::current()));
271 $player_resolution_sources_smil_file_url = c_ws_plugin__s2member_utils_urls::add_s2member_sig($player_resolution_sources_smil_file_url);
272 $player_resolution_sources_smil_file_contents = ''; // Initialize player sources SMIL file contents.
273 $player_sources = array(); //260805 Build source configuration as PHP data before JSON serialization.
274
275 foreach($file_download_urls as $_file_download_url_label => $_file_download_url)
276 {
277 $_is_first_file_download_url = $_player_sources_counter <= 1;
278 $_is_last_file_download_url = $_player_sources_counter >= $_total_player_sources;
279
280 if($_is_first_file_download_url) // We base this conditional on the first streamer.
281 $_uses_rtmp_streamers = stripos($_file_download_url['streamer'], 'rtmp') === 0;
282
283 switch($attr['player'])// See: <http://wsharks.com/1Bd6tKy>
284 {
285 case 'jwplayer-v7': // New JW Player v7 (very simple).
286
287 //260805 Store source fields as data so the JSON encoder controls all JavaScript syntax.
288 $_player_source = array('file' => $_file_download_url['url']);
289 if(is_string($_file_download_url_label)) $_player_source['label'] = $_file_download_url_label;
290 if($_is_first_file_download_url) $_player_source['default'] = 'true';
291 $player_sources[] = $_player_source;
292
293 break; // Break switch loop.
294
295 case 'jwplayer-v7-rtmp': // RTMP w/ downloadable fallback (mobile compatibility).
296 case 'jwplayer-v7-rtmp-only': // RTMP streaming only (flash player only).
297
298 if($attr['player_resolutions'] && $_total_player_sources > 1 && $_uses_rtmp_streamers)
299 {
300 if($_is_first_file_download_url) // The first source is the SMIL file.
301 {
302 //260805 The generated SMIL URL is serialized as data with the other player sources.
303 $_player_source = array('file' => $player_resolution_sources_smil_file_url);
304 if($_is_first_file_download_url) $_player_source['default'] = 'true';
305 $player_sources[] = $_player_source;
306 }
307 $_file_download_url['smil']['height'] = (int)$_file_download_url_label; // e.g., `720p-HD` becomes `720`.
308 if(!$_file_download_url['smil']['height']) $_file_download_url['smil']['height'] = 720; // Use a default height if invalid.
309 $_file_download_url['smil']['width'] = ceil(($_file_download_url['smil']['height'] / $player_resolution_aspect_ratio_h) * $player_resolution_aspect_ratio_w);
310
311 $_file_download_url['smil']['system-bitrate'] = '1'; // Default value.
312 if(!empty($player_resolution_bitrates[$_file_download_url['smil']['height']]))
313 $_file_download_url['smil']['system-bitrate'] = $player_resolution_bitrates[$_file_download_url['smil']['height']];
314
315 $player_resolution_sources_smil_file_contents .= '<video src="'.esc_attr($_file_download_url['file']).'"'.
316 ' width="'.esc_attr($_file_download_url['smil']['width']).'"'.
317 ' height="'.esc_attr($_file_download_url['smil']['height']).'"'.
318 ' system-bitrate="'.esc_attr($_file_download_url['smil']['system-bitrate']).'" />';
319 }
320 else // Build them inline; i.e., don't create a SMIL file in this case; not necessary.
321 {
322 //260805 Store RTMP source fields as data before serialization.
323 $_player_source = array('file' => $_file_download_url['streamer'].'/'.$_file_download_url['prefix'].$_file_download_url['file']);
324 if(is_string($_file_download_url_label)) $_player_source['label'] = $_file_download_url_label;
325 if($_is_first_file_download_url) $_player_source['default'] = 'true';
326 $player_sources[] = $_player_source;
327 }
328 if($_is_last_file_download_url && $attr['player'] === 'jwplayer-v7-rtmp') // Provide a fallback also.
329 {
330 //260805 Store the downloadable fallback as data before serialization.
331 $player_sources[] = array('file' => $_file_download_url['url']);
332 }
333 break; // Break switch loop.
334 }
335 if($_is_first_file_download_url) // Record first one; also run back compat. replacements.
336 {
337 $_first_file_download_url = $_file_download_url; // Record for use later.
338 //260805 Use literal replacement for legacy placeholders in trusted custom player templates.
339 $player = str_replace(array('%%streamer%%', '%%prefix%%', '%%file%%', '%%url%%'), array($_file_download_url['streamer'], $_file_download_url['prefix'], $_file_download_url['file'], $_file_download_url['url']), $player);
340 }
341 if($_is_last_file_download_url) // Record last one; which could be the same as the first one.
342 {
343 $_last_file_download_url = $_file_download_url; // Record for use later.
344 }
345 $_player_sources_counter++; // Increment the counter.
346 }
347 //260805 Serialize the complete source list once, including HTML-safe escaping for the inline script context.
348 $player_sources = wp_json_encode($player_sources, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
349 if(!is_string($player_sources)) $player_sources = '[]';
350
351 if($player_resolution_sources_smil_file_contents && $_first_file_download_url) // Build SMIL file.
352 {
353 $player_resolution_sources_smil_file_contents = '<smil>'. // See: <http://wsharks.com/1ruqGVu>
354 ' <head><meta base="'.esc_attr($_first_file_download_url['streamer']).'" /></head>'.
355 ' <body><switch>'.$player_resolution_sources_smil_file_contents.'</switch></body>'.
356 '</smil>';
357 set_transient('s2m_rsf_'.$player_resolution_sources_smil_file_id, $player_resolution_sources_smil_file_contents, 86400);
358 }
359 unset($_first_file_download_url, $_last_file_download_url, $_uses_rtmp_streamers, // Housekeeping.
360 $_total_player_sources, $_player_sources_counter, $_is_first_file_download_url, $_is_last_file_download_url,
361 $_file_download_url_label, $_file_download_url, $_player_source);
362
363 //260805 Parse flexible attributes as data and substitute only values encoded for their exact output contexts.
364 $_player_tracks = self::sc_get_stream_json_data($attr['player_tracks'], 'array');
365 $_player_option_blocks = self::sc_get_stream_json_data($attr['player_option_blocks'], 'object-properties');
366 $_player_width = (strpos($attr['player_width'], '%') !== FALSE) ? wp_json_encode((string)$attr['player_width']) : (string)(int)$attr['player_width'];
367 $_player_height = $attr['player_aspectratio'] ? '""' : ((strpos($attr['player_height'], '%') !== FALSE) ? wp_json_encode((string)$attr['player_height']) : (string)(int)$attr['player_height']);
368 if(!is_string($_player_width)) $_player_width = '480';
369 if(!is_string($_player_height)) $_player_height = '270';
370
371 //260805 strtr() replaces literal placeholders without reprocessing placeholder-like text inside generated values.
372 $player = strtr($player, array(
373 "'%%player_id%%'" => $player_json_strings['player_id'],
374 '%%player_id%%' => esc_attr($attr['player_id']),
375 '%%player_path%%' => esc_url($attr['player_path']),
376 "'%%player_key%%'" => $player_json_strings['player_key'],
377 "'%%player_title%%'" => $player_json_strings['player_title'],
378 "'%%player_image%%'" => $player_json_strings['player_image'],
379 "'%%player_mediaid%%'" => $player_json_strings['player_mediaid'],
380 "'%%player_description%%'" => $player_json_strings['player_description'],
381 '%%player_tracks%%' => $_player_tracks,
382 '%%player_sources%%' => $player_sources,
383 '%%player_controls%%' => filter_var($attr['player_controls'], FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false',
384 '%%player_width%%' => $_player_width,
385 '%%player_height%%' => $_player_height,
386 "'%%player_aspectratio%%'" => $player_json_strings['player_aspectratio'],
387 "'%%player_stretching%%'" => $player_json_strings['player_stretching'],
388 '%%player_autostart%%' => filter_var($attr['player_autostart'], FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false',
389 '%%player_fallback%%' => filter_var($attr['player_fallback'], FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false',
390 '%%player_mute%%' => filter_var($attr['player_mute'], FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false',
391 '%%player_repeat%%' => filter_var($attr['player_repeat'], FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false',
392 "'%%player_primary%%'" => $player_json_strings['player_primary'],
393 '%%player_option_blocks%%' => $_player_option_blocks,
394 ));
395 unset($_player_tracks, $_player_option_blocks, $_player_width, $_player_height); //260805 Housekeeping.
396 }
397 else if(strpos($attr['player'], 'jwplayer-v6') === 0) // JW Player (old v6).
398 {
399 $player = trim(c_ws_plugin__s2member_utilities::evl(file_get_contents($template)));
400
401 $_first_file_download_url = array(); // Holds the first one.
402 $_last_file_download_url = array(); // Holds the last one.
403 $_uses_rtmp_streamers = FALSE; // Streamers use RTMP?
404
405 $_total_player_sources = count($file_download_urls); // Total sources.
406 $_player_sources_counter = 1; // Player sources counter; needed by the loop below.
407
408 $player_resolution_aspect_ratio_w = 16; // Default aspect ratio width.
409 $player_resolution_aspect_ratio_h = 9; // Default aspect ratio in height.
410 if($attr['player_aspectratio'] && preg_match('/^[0-9]+\:[0-9]+$/', $attr['player_aspectratio']))
411 list($player_resolution_aspect_ratio_w, $player_resolution_aspect_ratio_h) = explode(':', $attr['player_aspectratio']);
412 $player_resolution_aspect_ratio_w = (int)$player_resolution_aspect_ratio_w; // Force integer value.
413 $player_resolution_aspect_ratio_h = (int)$player_resolution_aspect_ratio_h; // Force integer value.
414
415 // See: <http://wsharks.com/1yzjAl6> and <http://wsharks.com/1yzkhea> regarging the SMIL bitrate hints given here.
416 $player_resolution_bitrates = array(2160 => '35000000', 1440 => '10000000', 1080 => '8000000', 720 => '5000000', 640 => '2500001', 480 => '2500000', 360 => '1000000', 320 => '999999', 240 => '500000', 180 => '300000');
417 $player_resolution_bitrates = apply_filters('ws_plugin__s2member_sc_get_stream_resolution_bitrates', $player_resolution_bitrates, get_defined_vars());
418
419 $player_resolution_sources_smil_file_id = md5(serialize($attr).c_ws_plugin__s2member_utils_ip::current()); // Initialize SMIL ID.
420 $player_resolution_sources_smil_file_url = home_url('/s2member-rsf-file.smil?s2member_rsf_file='.urlencode($player_resolution_sources_smil_file_id).'&s2member_rsf_file_ip='.urlencode(c_ws_plugin__s2member_utils_ip::current()));
421 $player_resolution_sources_smil_file_url = c_ws_plugin__s2member_utils_urls::add_s2member_sig($player_resolution_sources_smil_file_url);
422 $player_resolution_sources_smil_file_contents = ''; // Initialize player sources SMIL file contents.
423 $player_sources = array(); //260805 Build source configuration as PHP data before JSON serialization.
424
425 foreach($file_download_urls as $_file_download_url_label => $_file_download_url)
426 {
427 $_is_first_file_download_url = $_player_sources_counter <= 1;
428 $_is_last_file_download_url = $_player_sources_counter >= $_total_player_sources;
429
430 if($_is_first_file_download_url) // We base this conditional on the first streamer.
431 $_uses_rtmp_streamers = stripos($_file_download_url['streamer'], 'rtmp') === 0;
432
433 switch($attr['player'])// See: <http://wsharks.com/1Bd6tKy>
434 {
435 case 'jwplayer-v6': // Default w/ a direct URL (very simple).
436
437 //260805 Store source fields as data so the JSON encoder controls all JavaScript syntax.
438 $_player_source = array('file' => $_file_download_url['url']);
439 if(is_string($_file_download_url_label)) $_player_source['label'] = $_file_download_url_label;
440 if($_is_first_file_download_url) $_player_source['default'] = 'true';
441 $player_sources[] = $_player_source;
442
443 break; // Break switch loop.
444
445 case 'jwplayer-v6-rtmp': // RTMP w/ downloadable fallback (mobile compatibility).
446 case 'jwplayer-v6-rtmp-only': // RTMP streaming only (flash player only).
447
448 if($attr['player_resolutions'] && $_total_player_sources > 1 && $_uses_rtmp_streamers)
449 {
450 if($_is_first_file_download_url) // The first source is the SMIL file.
451 {
452 //260805 The generated SMIL URL is serialized as data with the other player sources.
453 $_player_source = array('file' => $player_resolution_sources_smil_file_url);
454 if($_is_first_file_download_url) $_player_source['default'] = 'true';
455 $player_sources[] = $_player_source;
456 }
457 $_file_download_url['smil']['height'] = (int)$_file_download_url_label; // e.g., `720p-HD` becomes `720`.
458 if(!$_file_download_url['smil']['height']) $_file_download_url['smil']['height'] = 720; // Use a default height if invalid.
459 $_file_download_url['smil']['width'] = ceil(($_file_download_url['smil']['height'] / $player_resolution_aspect_ratio_h) * $player_resolution_aspect_ratio_w);
460
461 $_file_download_url['smil']['system-bitrate'] = '1'; // Default value.
462 if(!empty($player_resolution_bitrates[$_file_download_url['smil']['height']]))
463 $_file_download_url['smil']['system-bitrate'] = $player_resolution_bitrates[$_file_download_url['smil']['height']];
464
465 $player_resolution_sources_smil_file_contents .= '<video src="'.esc_attr($_file_download_url['file']).'"'.
466 ' width="'.esc_attr($_file_download_url['smil']['width']).'"'.
467 ' height="'.esc_attr($_file_download_url['smil']['height']).'"'.
468 ' system-bitrate="'.esc_attr($_file_download_url['smil']['system-bitrate']).'" />';
469 }
470 else // Build them inline; i.e., don't create a SMIL file in this case; not necessary.
471 {
472 //260805 Store RTMP source fields as data before serialization.
473 $_player_source = array('file' => $_file_download_url['streamer'].'/'.$_file_download_url['prefix'].$_file_download_url['file']);
474 if(is_string($_file_download_url_label)) $_player_source['label'] = $_file_download_url_label;
475 if($_is_first_file_download_url) $_player_source['default'] = 'true';
476 $player_sources[] = $_player_source;
477 }
478 if($_is_last_file_download_url && $attr['player'] === 'jwplayer-v6-rtmp') // Provide a fallback also.
479 {
480 //260805 Store the downloadable fallback as data before serialization.
481 $player_sources[] = array('file' => $_file_download_url['url']);
482 }
483 break; // Break switch loop.
484 }
485 if($_is_first_file_download_url) // Record first one; also run back compat. replacements.
486 {
487 $_first_file_download_url = $_file_download_url; // Record for use later.
488 //260805 Use literal replacement for legacy placeholders in trusted custom player templates.
489 $player = str_replace(array('%%streamer%%', '%%prefix%%', '%%file%%', '%%url%%'), array($_file_download_url['streamer'], $_file_download_url['prefix'], $_file_download_url['file'], $_file_download_url['url']), $player);
490 }
491 if($_is_last_file_download_url) // Record last one; which could be the same as the first one.
492 {
493 $_last_file_download_url = $_file_download_url; // Record for use later.
494 }
495 $_player_sources_counter++; // Increment the counter.
496 }
497 //260805 Serialize the complete source list once, including HTML-safe escaping for the inline script context.
498 $player_sources = wp_json_encode($player_sources, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
499 if(!is_string($player_sources)) $player_sources = '[]';
500
501 if($player_resolution_sources_smil_file_contents && $_first_file_download_url) // Build SMIL file.
502 {
503 $player_resolution_sources_smil_file_contents = '<smil>'. // See: <http://wsharks.com/1ruqGVu>
504 ' <head><meta base="'.esc_attr($_first_file_download_url['streamer']).'" /></head>'.
505 ' <body><switch>'.$player_resolution_sources_smil_file_contents.'</switch></body>'.
506 '</smil>';
507 set_transient('s2m_rsf_'.$player_resolution_sources_smil_file_id, $player_resolution_sources_smil_file_contents, 86400);
508 }
509 unset($_first_file_download_url, $_last_file_download_url, $_uses_rtmp_streamers, // Housekeeping.
510 $_total_player_sources, $_player_sources_counter, $_is_first_file_download_url, $_is_last_file_download_url,
511 $_file_download_url_label, $_file_download_url, $_player_source);
512
513 //260805 Parse flexible attributes as data and substitute only values encoded for their exact output contexts.
514 $_player_captions = self::sc_get_stream_json_data($attr['player_captions'], 'array');
515 $_player_option_blocks = self::sc_get_stream_json_data($attr['player_option_blocks'], 'object-properties');
516 $_player_width = (strpos($attr['player_width'], '%') !== FALSE) ? wp_json_encode((string)$attr['player_width']) : (string)(int)$attr['player_width'];
517 $_player_height = $attr['player_aspectratio'] ? '""' : ((strpos($attr['player_height'], '%') !== FALSE) ? wp_json_encode((string)$attr['player_height']) : (string)(int)$attr['player_height']);
518 if(!is_string($_player_width)) $_player_width = '480';
519 if(!is_string($_player_height)) $_player_height = '270';
520
521 //260805 strtr() replaces literal placeholders without reprocessing placeholder-like text inside generated values.
522 $player = strtr($player, array(
523 "'%%player_id%%'" => $player_json_strings['player_id'],
524 '%%player_id%%' => esc_attr($attr['player_id']),
525 '%%player_path%%' => esc_url($attr['player_path']),
526 "'%%player_key%%'" => $player_json_strings['player_key'],
527 "'%%player_title%%'" => $player_json_strings['player_title'],
528 "'%%player_image%%'" => $player_json_strings['player_image'],
529 "'%%player_mediaid%%'" => $player_json_strings['player_mediaid'],
530 "'%%player_description%%'" => $player_json_strings['player_description'],
531 '%%player_captions%%' => $_player_captions,
532 '%%player_sources%%' => $player_sources,
533 '%%player_controls%%' => filter_var($attr['player_controls'], FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false',
534 "'%%player_skin%%'" => $player_json_strings['player_skin'],
535 "'%%player_stretching%%'" => $player_json_strings['player_stretching'],
536 '%%player_width%%' => $_player_width,
537 '%%player_height%%' => $_player_height,
538 "'%%player_aspectratio%%'" => $player_json_strings['player_aspectratio'],
539 '%%player_autostart%%' => filter_var($attr['player_autostart'], FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false',
540 '%%player_fallback%%' => filter_var($attr['player_fallback'], FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false',
541 '%%player_mute%%' => filter_var($attr['player_mute'], FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false',
542 "'%%player_primary%%'" => $player_json_strings['player_primary'],
543 '%%player_repeat%%' => filter_var($attr['player_repeat'], FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false',
544 "'%%player_startparam%%'" => $player_json_strings['player_startparam'],
545 '%%player_option_blocks%%' => $_player_option_blocks,
546 ));
547 unset($_player_captions, $_player_option_blocks, $_player_width, $_player_height); //260805 Housekeeping.
548 }
549 }
550 }
551 unset($player_json_strings, $player_templates, $player_paths); //260805 Housekeeping.
552 return apply_filters('ws_plugin__s2member_sc_get_stream', isset($player) ? $player : NULL, get_defined_vars());
553 }
554
555 /**
556 * Parses a structured player attribute and returns safe JSON for the existing template placeholder.
557 *
558 * @package s2Member\s2File
559 * @since 260805
560 *
561 * @param mixed $value Attribute value, optionally base64 encoded.
562 * @param string $container Expected top-level container: `array` or `object-properties`.
563 *
564 * @return string Safe JSON, or the appropriate empty value when invalid.
565 */
566 protected static function sc_get_stream_json_data($value = '', $container = 'array')
567 {
568 $value = trim((string)$value);
569 $empty = ($container === 'array') ? '[]' : '';
570 if($value === '')
571 return $empty;
572
573 //260805 Try the documented plain-text form first, then a canonical strict-base64 form for backward compatibility.
574 $candidates = array($value);
575 $_base64 = preg_replace('/\s+/', '', $value);
576 if($_base64 !== '' && preg_match('/^[A-Za-z0-9+\/]+={0,2}$/D', $_base64))
577 {
578 $_decoded = base64_decode($_base64, TRUE);
579 if($_decoded !== FALSE && rtrim(base64_encode($_decoded), '=') === rtrim($_base64, '='))
580 $candidates[] = trim($_decoded);
581 }
582 unset($_base64, $_decoded); //260805 Housekeeping.
583
584 foreach($candidates as $_candidate)
585 {
586 //260805 Bound parser work and reject oversized shortcode configuration instead of attempting partial recovery.
587 if($_candidate === '' || strlen($_candidate) > 65536)
588 continue;
589
590 $_candidate = trim($_candidate);
591 if($container === 'array')
592 $_input = (substr($_candidate, 0, 1) === '[') ? $_candidate : '['.$_candidate.']';
593 else $_input = (substr($_candidate, 0, 1) === '{' && substr($_candidate, -1) === '}') ? $_candidate : '{'.$_candidate.'}';
594
595 $_position = 0;
596 $_parsed = self::sc_parse_stream_data($_input, $_position);
597 while(isset($_input[$_position]) && strpos(" \t\r\n\f\v", $_input[$_position]) !== FALSE)
598 $_position++;
599 if(!$_parsed[0] || $_position !== strlen($_input))
600 continue;
601 if($container === 'array' && !is_array($_parsed[1]))
602 continue;
603 if($container === 'object-properties' && !is_object($_parsed[1]))
604 continue;
605
606 if($container === 'object-properties')
607 {
608 //260805 These top-level JW Player settings can select or load executable player/plugin code and are not accepted from post content.
609 foreach(array_keys(get_object_vars($_parsed[1])) as $_property)
610 if(in_array(strtolower($_property), array('plugins', 'html5player', 'flashplayer', 'flashloader', 'modes', 'base'), TRUE))
611 continue 2;
612 }
613
614 $_json = wp_json_encode($_parsed[1], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
615 if(!is_string($_json))
616 continue;
617
618 //260805 Option blocks already sit inside setup object braces, so return only the safely generated object properties there.
619 return ($container === 'array') ? $_json : substr($_json, 1, -1);
620 }
621 return $empty;
622 }
623
624 /**
625 * Parses the data-only subset of legacy JavaScript object notation used by player shortcode attributes.
626 *
627 * @package s2Member\s2File
628 * @since 260805
629 *
630 * @param string $input Input being parsed.
631 * @param integer $position Current byte offset, passed by reference.
632 * @param integer $depth Current nesting depth.
633 *
634 * @return array A `(success, value)` pair.
635 */
636 protected static function sc_parse_stream_data($input, &$position, $depth = 0)
637 {
638 $length = strlen($input);
639 while($position < $length && strpos(" \t\r\n\f\v", $input[$position]) !== FALSE)
640 $position++;
641 if($position >= $length || $depth > 32)
642 return array(FALSE, NULL);
643
644 $character = $input[$position];
645 if($character === '{')
646 {
647 $position++;
648 $object = new stdClass();
649 while(TRUE)
650 {
651 while($position < $length && strpos(" \t\r\n\f\v", $input[$position]) !== FALSE)
652 $position++;
653 if($position < $length && $input[$position] === '}')
654 {
655 $position++;
656 return array(TRUE, $object);
657 }
658
659 if($position < $length && ($input[$position] === "'" || $input[$position] === '"'))
660 {
661 $_key = self::sc_parse_stream_data($input, $position, $depth);
662 if(!$_key[0] || !is_string($_key[1]))
663 return array(FALSE, NULL);
664 $key = $_key[1];
665 }
666 else
667 {
668 if(!preg_match('/^[A-Za-z_$][A-Za-z0-9_$]*/', substr($input, $position), $_key))
669 return array(FALSE, NULL);
670 $key = $_key[0];
671 $position += strlen($key);
672 }
673 if(in_array(strtolower($key), array('__proto__', 'prototype', 'constructor'), TRUE))
674 return array(FALSE, NULL);
675
676 while($position < $length && strpos(" \t\r\n\f\v", $input[$position]) !== FALSE)
677 $position++;
678 if($position >= $length || $input[$position] !== ':')
679 return array(FALSE, NULL);
680 $position++;
681
682 $_value = self::sc_parse_stream_data($input, $position, $depth + 1);
683 if(!$_value[0])
684 return array(FALSE, NULL);
685 $object->{$key} = $_value[1];
686
687 while($position < $length && strpos(" \t\r\n\f\v", $input[$position]) !== FALSE)
688 $position++;
689 if($position < $length && $input[$position] === ',')
690 {
691 $position++;
692 continue;
693 }
694 if($position < $length && $input[$position] === '}')
695 {
696 $position++;
697 return array(TRUE, $object);
698 }
699 return array(FALSE, NULL);
700 }
701 }
702 if($character === '[')
703 {
704 $position++;
705 $array = array();
706 while(TRUE)
707 {
708 while($position < $length && strpos(" \t\r\n\f\v", $input[$position]) !== FALSE)
709 $position++;
710 if($position < $length && $input[$position] === ']')
711 {
712 $position++;
713 return array(TRUE, $array);
714 }
715
716 $_value = self::sc_parse_stream_data($input, $position, $depth + 1);
717 if(!$_value[0])
718 return array(FALSE, NULL);
719 $array[] = $_value[1];
720
721 while($position < $length && strpos(" \t\r\n\f\v", $input[$position]) !== FALSE)
722 $position++;
723 if($position < $length && $input[$position] === ',')
724 {
725 $position++;
726 continue;
727 }
728 if($position < $length && $input[$position] === ']')
729 {
730 $position++;
731 return array(TRUE, $array);
732 }
733 return array(FALSE, NULL);
734 }
735 }
736 if($character === "'" || $character === '"')
737 {
738 $quote = $character;
739 $string = '';
740 $position++;
741 while($position < $length)
742 {
743 $character = $input[$position++];
744 if($character === $quote)
745 {
746 //260805 Reject executable URL schemes even when hidden with whitespace or control characters inside structured data.
747 $_scheme = strtolower(preg_replace('/[\x00-\x20]+/', '', $string));
748 if(preg_match('/^(?:javascript|vbscript):/i', $_scheme))
749 return array(FALSE, NULL);
750 return array(TRUE, $string);
751 }
752 if($character === '\\')
753 {
754 if($position >= $length)
755 return array(FALSE, NULL);
756 $_escape = $input[$position++];
757 switch($_escape)
758 {
759 case "'": case '"': case '\\': case '/': $string .= $_escape; break;
760 case 'b': $string .= "\x08"; break;
761 case 'f': $string .= "\x0C"; break;
762 case 'n': $string .= "\n"; break;
763 case 'r': $string .= "\r"; break;
764 case 't': $string .= "\t"; break;
765 case 'v': $string .= "\x0B"; break;
766 case "\n": break;
767 case "\r": if($position < $length && $input[$position] === "\n") $position++; break;
768 case '0':
769 if($position < $length && ctype_digit($input[$position])) return array(FALSE, NULL);
770 $string .= "\0";
771 break;
772 case 'x':
773 $_hex = substr($input, $position, 2);
774 if(strlen($_hex) !== 2 || !ctype_xdigit($_hex)) return array(FALSE, NULL);
775 $_unicode = json_decode('"\\u00'.$_hex.'"');
776 if(!is_string($_unicode)) return array(FALSE, NULL);
777 $string .= $_unicode;
778 $position += 2;
779 break;
780 case 'u':
781 $_hex = substr($input, $position, 4);
782 if(strlen($_hex) !== 4 || !ctype_xdigit($_hex)) return array(FALSE, NULL);
783 $_unicode_escape = '\\u'.$_hex;
784 $position += 4;
785 if(hexdec($_hex) >= 0xD800 && hexdec($_hex) <= 0xDBFF)
786 {
787 if(substr($input, $position, 2) !== '\u') return array(FALSE, NULL);
788 $_low_hex = substr($input, $position + 2, 4);
789 if(strlen($_low_hex) !== 4 || !ctype_xdigit($_low_hex) || hexdec($_low_hex) < 0xDC00 || hexdec($_low_hex) > 0xDFFF) return array(FALSE, NULL);
790 $_unicode_escape .= '\\u'.$_low_hex;
791 $position += 6;
792 }
793 $_unicode = json_decode('"'.$_unicode_escape.'"');
794 if(!is_string($_unicode)) return array(FALSE, NULL);
795 $string .= $_unicode;
796 break;
797 default: $string .= $_escape; break;
798 }
799 }
800 else
801 {
802 if(ord($character) < 32)
803 return array(FALSE, NULL);
804 $string .= $character;
805 }
806 }
807 return array(FALSE, NULL);
808 }
809 if($character === '-' || ctype_digit($character))
810 {
811 if(!preg_match('/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+\-]?[0-9]+)?/', substr($input, $position), $_number))
812 return array(FALSE, NULL);
813 $position += strlen($_number[0]);
814 $_value = json_decode($_number[0]);
815 if(json_last_error() !== JSON_ERROR_NONE || (is_float($_value) && !is_finite($_value)))
816 return array(FALSE, NULL);
817 return array(TRUE, $_value);
818 }
819 foreach(array('true' => TRUE, 'false' => FALSE, 'null' => NULL) as $_literal => $_value)
820 if(substr($input, $position, strlen($_literal)) === $_literal)
821 {
822 $position += strlen($_literal);
823 return array(TRUE, $_value);
824 }
825 return array(FALSE, NULL);
826 }
827 }
828 }
829