From df50fde8c7555fbf24468256bd9a59cae80617e7 Mon Sep 17 00:00:00 2001 From: Andrew Berry Date: Wed, 1 Jun 2011 16:38:00 -0400 Subject: [PATCH 1/5] #1173692: Add settings for content protection. --- includes/jplayer.admin.inc | 21 +++++++++++++++++++++ 1 files changed, 21 insertions(+), 0 deletions(-) diff --git a/includes/jplayer.admin.inc b/includes/jplayer.admin.inc index 1ab75b7..b15333a 100644 --- a/includes/jplayer.admin.inc +++ b/includes/jplayer.admin.inc @@ -31,6 +31,19 @@ function jplayer_settings_form() { '#default_value' => variable_get('jplayer_autoplay', ''), ); + $form['options']['jplayer_protected'] = array( + '#title' => t('Protect audio files from direct downloads'), + '#type' => 'checkbox', + '#default_value' => variable_get('jplayer_protected', FALSE), + ); + $form['options']['jplayer_access_time'] = array( + '#title' => t('Access delay'), + '#type' => 'textfield', + '#default_value' => variable_get('jplayer_access_time', 5), + '#size' => 5, + '#description' => t('The number of seconds that a client will have access to download a protected file after it is requested by jPlayer'), + ); + $form = system_settings_form($form); $form['#validate'][] = 'jplayer_settings_form_validate'; $form['#submit'][] = 'jplayer_settings_form_submit'; @@ -45,6 +58,14 @@ function jplayer_settings_form_validate($form, &$form_state) { if (!$form_state['jplayer_version']) { form_error($form['jplayer_directory'], t('The directory specified does not seem to contain the jPlayer library. Check to make sure that the jquery.player.min.js file is located within this directory.')); } + + $time = $form_state['values']['jplayer_access_time']; + if ($form_state['values']['jplayer_protected'] && !is_numeric($time)) { + form_error($form['options']['jplayer_access_time'], t('Access time must be a value in seconds.')); + } + if (intval($time) < 0) { + form_error($form['options']['jplayer_access_time'], t('Access time must be at least 0 seconds.')); + } } /** -- 1.7.5.2 From 9518ac6907f37202c4ce0aa1a7ebf53ca297470e Mon Sep 17 00:00:00 2001 From: Andrew Berry Date: Thu, 2 Jun 2011 11:17:12 -0400 Subject: [PATCH 2/5] #1173692: Implement basic content protection for filefields using jPlayer. --- jplayer.module | 109 +++++++++++++++++++++++++++++++++++++++++++++++ theme/jplayer.js | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+), 0 deletions(-) diff --git a/jplayer.module b/jplayer.module index c8e5bcf..fa7251a 100644 --- a/jplayer.module +++ b/jplayer.module @@ -23,10 +23,119 @@ function jplayer_menu() { 'file' => 'includes/jplayer.admin.inc', ); + $items['jplayer/authorize'] = array( + 'title' => 'jPlayer content authorization', + 'page callback' => 'jplayer_authorize', + 'access arguments' => array('access content'), + 'description' => 'jPlayer callback to authorize a sound file to be accessed.', + 'type' => MENU_CALLBACK, + ); + return $items; } /** + * Menu callback to authorize access to a file. + */ +function jplayer_authorize($filepath, $timestamp) { + $filepath = base64_decode($filepath); + $timestamp = base64_decode($timestamp); + + if (!isset($_SESSION['jplayer_protect'])) { + $_SESSION['jplayer_protect'] = array(); + } + $_SESSION['jplayer_protect'] = array(); + + $_SESSION['jplayer_protect'][$filepath] = (int)$timestamp; + + drupal_json(TRUE); +} + +/** + * Implementation of hook_file_download(). + */ +function jplayer_file_download($filepath) { + if (!variable_get('jplayer_protected', FALSE)) { + return NULL; + } + + // We need to determine if we are responsible for this file. + $filepath = file_create_path($filepath); + $result = db_query("SELECT * FROM {files} WHERE filepath = '%s'", $filepath); + + // Ensure case-sensitivity of uploaded file names. + while ($file = db_fetch_object($result)) { + if (strcmp($file->filepath, $filepath) == 0) { + break; + } + } + + // If the file is not found in the database, we're not responsible for it. + if (empty($file)) { + return NULL; + } + + // Find out if any file field contains this file, and if so, which field + // and node it belongs to. Required for later access checking. + $cck_files = array(); + foreach (content_fields() as $field) { + if ($field['type'] == 'filefield' || $field['type'] == 'image') { + $db_info = content_database_info($field); + $table = $db_info['table']; + $fid_column = $db_info['columns']['fid']['column']; + + $columns = array('vid', 'nid'); + foreach ($db_info['columns'] as $property_name => $column_info) { + $columns[] = $column_info['column'] .' AS '. $property_name; + } + $result = db_query("SELECT ". implode(', ', $columns) ." + FROM {". $table ."} + WHERE ". $fid_column ." = %d", $file->fid); + + while ($content = db_fetch_array($result)) { + $content['field'] = $field; + $cck_files[$field['field_name']][$content['vid']] = $content; + } + } + } + + // If any of the displays for this field are for jPlayer, then we need to + // protect the file. + foreach ($cck_files as $field_name => $field_files) { + foreach ($field_files as $revision_id => $content) { + $teaser_format = $content['field']['display_settings']['teaser']['format']; + $full_format = $content['field']['display_settings']['full']['format']; + // Neither the teaser or the full formatter for this field is a jPlayer + // display. + if (!($teaser_format == 'single' || $teaser_format == 'playlist' || $full_format == 'single' || $full_format == 'playlist')) { + return NULL; + } + } + } + + // We need to figure out how the browser would have URL-encoded the file + // name. + $encoded = str_replace($file->filename, rawurlencode($file->filename), $filepath); + $encoded = str_replace('sites/default/files', 'system/files', $encoded); + // For some reason ampersands are encoded twice by the browser. + $encoded = str_replace("%26", "%2526", $encoded); + $started = (int)$_SESSION['jplayer_protect'][$GLOBALS['base_url'] . '/' . $encoded]; + + // Now we know that content protection is enabled, at least one display for + // the field uses jPlayer, and we know when the player last started to access + // the file. + if ($started) { + if (time() <= $started + variable_get('jplayer_access_time', 5)) { + // Allow access. + return NULL; + } + } + + // Otherwise, deny access as the last played time is too far in the past. + return -1; +} + +/** * Implementation of hook_theme(). */ function jplayer_theme() { diff --git a/theme/jplayer.js b/theme/jplayer.js index 955e7cd..d898dc6 100644 --- a/theme/jplayer.js +++ b/theme/jplayer.js @@ -63,6 +63,20 @@ Drupal.behaviors.jPlayer = function(context) { }); } + $(wrapper).find('a.jp-play').click(function() { + // Generate the authorization URL to ping. + var time = new Date; + var authorize_url = Drupal.settings.basePath + 'jplayer/authorize/' + base64_encode($(player).attr('rel')) + '/' + base64_encode(parseInt(time.getTime() / 1000).toString()); + + // Ping the authorization URL. We need to disable async so that this + // command finishes before thisandler returns. + $.ajax({ + url: authorize_url, + async: false, + }); + return false; + }); + // Actually initialize the player. $(player).jPlayer({ ready: function() { @@ -140,4 +154,114 @@ Drupal.jPlayer.previous = function(wrapper, player, playlist, current) { return index; } +Drupal.jPlayer.base64Encode = function(data) { + // From http://phpjs.org/functions/base64_encode:358 where it is + // dual licensed under GPL/MIT. + // + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Bayron Guevara + // + improved by: Thunder.m + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // - depends on: utf8_encode + // * example 1: base64_encode('Kevin van Zonneveld'); + // * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + enc = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data = utf8_encode(data + ''); + + do { // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = o1 << 16 | o2 << 8 | o3; + + h1 = bits >> 18 & 0x3f; + h2 = bits >> 12 & 0x3f; + h3 = bits >> 6 & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(''); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + '=='; + break; + case 2: + enc = enc.slice(0, -1) + '='; + break; + } + + return enc; +}; + +Drupal.jPlayer.utf8_encode = function(argString) { + // From http://phpjs.org/functions/utf8_encode:577 where it is dual-licensed + // under GPL/MIT. + // http://kevin.vanzonneveld.net + // + original by: Webtoolkit.info (http://www.webtoolkit.info/) + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: sowberry + // + tweaked by: Jack + // + bugfixed by: Onno Marsman + // + improved by: Yves Sucaet + // + bugfixed by: Onno Marsman + // + bugfixed by: Ulrich + // + bugfixed by: Rafal Kukawski + // * example 1: utf8_encode('Kevin van Zonneveld'); + // * returns 1: 'Kevin van Zonneveld' + + if (argString === null || typeof argString === "undefined") { + return ""; + } + + var string = (argString + ''); // .replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + var utftext = "", + start, end, stringl = 0; + + start = end = 0; + stringl = string.length; + for (var n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if (c1 > 127 && c1 < 2048) { + enc = String.fromCharCode((c1 >> 6) | 192) + String.fromCharCode((c1 & 63) | 128); + } else { + enc = String.fromCharCode((c1 >> 12) | 224) + String.fromCharCode(((c1 >> 6) & 63) | 128) + String.fromCharCode((c1 & 63) | 128); + } + if (enc !== null) { + if (end > start) { + utftext += string.slice(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.slice(start, stringl); + } + + return utftext; +}; + })(jQuery); + -- 1.7.5.2 From 7b8b0f34421678d737acc99585bab222aa197903 Mon Sep 17 00:00:00 2001 From: Andrew Berry Date: Thu, 2 Jun 2011 11:17:48 -0400 Subject: [PATCH 3/5] #1173692: Add missing semicolons in javascript. --- theme/jplayer.js | 10 +++++----- 1 files changed, 5 insertions(+), 5 deletions(-) diff --git a/theme/jplayer.js b/theme/jplayer.js index d898dc6..55cab98 100644 --- a/theme/jplayer.js +++ b/theme/jplayer.js @@ -128,31 +128,31 @@ Drupal.jPlayer.setActive = function(wrapper, player, playlist, index) { Drupal.jPlayer.play = function(wrapper, player) { $(player).jPlayer('play'); Drupal.jPlayer.active = true; -} +}; Drupal.jPlayer.pause = function(wrapper, player) { $(player).jPlayer('pause'); Drupal.jPlayer.active = false; -} +}; Drupal.jPlayer.stop = function(wrapper, player) { $(player).jPlayer('stop'); Drupal.jPlayer.active = false; -} +}; Drupal.jPlayer.next = function(wrapper, player, playlist, current) { var index = (current + 1 < playlist.length) ? current + 1 : 0; Drupal.jPlayer.setActive(wrapper, player, playlist, index); Drupal.jPlayer.play(wrapper, player); return index; -} +}; Drupal.jPlayer.previous = function(wrapper, player, playlist, current) { var index = (current - 1 >= 0) ? current - 1 : playlist.length - 1; Drupal.jPlayer.setActive(wrapper, player, playlist, index); Drupal.jPlayer.play(wrapper, player); return index; -} +}; Drupal.jPlayer.base64Encode = function(data) { // From http://phpjs.org/functions/base64_encode:358 where it is -- 1.7.5.2 From b67f6754e47f59d5170b0c386a978c97d79d2e91 Mon Sep 17 00:00:00 2001 From: Andrew Berry Date: Thu, 2 Jun 2011 11:26:07 -0400 Subject: [PATCH 4/5] #1173692: Only add the content protection handler if content protection is enabled. --- jplayer.module | 1 + theme/jplayer.js | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/jplayer.module b/jplayer.module index fa7251a..a12e1f8 100644 --- a/jplayer.module +++ b/jplayer.module @@ -223,6 +223,7 @@ function jplayer_add($add = TRUE) { $settings = array('jPlayer' => array( 'swfPath' => base_path() . variable_get('jplayer_directory', 'sites/all/libraries/jplayer'), 'autoPlay' => (int) variable_get('jplayer_autoplay', ''), + 'protected' => variable_get('jplayer_protected', ''), )); if ($add) { drupal_add_js($filepath); diff --git a/theme/jplayer.js b/theme/jplayer.js index 55cab98..ceb0fd8 100644 --- a/theme/jplayer.js +++ b/theme/jplayer.js @@ -63,19 +63,21 @@ Drupal.behaviors.jPlayer = function(context) { }); } - $(wrapper).find('a.jp-play').click(function() { - // Generate the authorization URL to ping. - var time = new Date; - var authorize_url = Drupal.settings.basePath + 'jplayer/authorize/' + base64_encode($(player).attr('rel')) + '/' + base64_encode(parseInt(time.getTime() / 1000).toString()); - - // Ping the authorization URL. We need to disable async so that this - // command finishes before thisandler returns. - $.ajax({ - url: authorize_url, - async: false, + if (Drupal.settings.jPlayer.protected) { + $(wrapper).find('a.jp-play').click(function() { + // Generate the authorization URL to ping. + var time = new Date; + var authorize_url = Drupal.settings.basePath + 'jplayer/authorize/' + base64_encode($(player).attr('rel')) + '/' + base64_encode(parseInt(time.getTime() / 1000).toString()); + + // Ping the authorization URL. We need to disable async so that this + // command finishes before thisandler returns. + $.ajax({ + url: authorize_url, + async: false, + }); + return false; }); - return false; - }); + } // Actually initialize the player. $(player).jPlayer({ -- 1.7.5.2 From 9344e4eb16920a69f8ecf595e665be077b8d63c9 Mon Sep 17 00:00:00 2001 From: Andrew Berry Date: Thu, 2 Jun 2011 11:26:29 -0400 Subject: [PATCH 5/5] #1173692: Fix renaming of the base64 function. --- theme/jplayer.js | 6 +++--- 1 files changed, 3 insertions(+), 3 deletions(-) diff --git a/theme/jplayer.js b/theme/jplayer.js index ceb0fd8..55c1906 100644 --- a/theme/jplayer.js +++ b/theme/jplayer.js @@ -67,7 +67,7 @@ Drupal.behaviors.jPlayer = function(context) { $(wrapper).find('a.jp-play').click(function() { // Generate the authorization URL to ping. var time = new Date; - var authorize_url = Drupal.settings.basePath + 'jplayer/authorize/' + base64_encode($(player).attr('rel')) + '/' + base64_encode(parseInt(time.getTime() / 1000).toString()); + var authorize_url = Drupal.settings.basePath + 'jplayer/authorize/' + Drupal.jPlayer.base64Encode($(player).attr('rel')) + '/' + Drupal.jPlayer.base64Encode(parseInt(time.getTime() / 1000).toString()); // Ping the authorization URL. We need to disable async so that this // command finishes before thisandler returns. @@ -180,7 +180,7 @@ Drupal.jPlayer.base64Encode = function(data) { return data; } - data = utf8_encode(data + ''); + data = Drupal.jPlayer.utf8Encode(data + ''); do { // pack three octets into four hexets o1 = data.charCodeAt(i++); @@ -212,7 +212,7 @@ Drupal.jPlayer.base64Encode = function(data) { return enc; }; -Drupal.jPlayer.utf8_encode = function(argString) { +Drupal.jPlayer.utf8Encode = function(argString) { // From http://phpjs.org/functions/utf8_encode:577 where it is dual-licensed // under GPL/MIT. // http://kevin.vanzonneveld.net -- 1.7.5.2