PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / 260805
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions v260805
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 260805, at src/includes/classes/sc-files-in.inc.php

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