PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / 260927
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions v260927
260927 260917 260913 260909 260829 260814 260805 110710 110731 110812 110815 110912 110913 110915 110926 110927 111002 111003 111011 111017 111029 111105 111206 111216 111220 All 190 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 260927, at src/includes/classes/sc-files-in.inc.php

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