As I have had too many problems trying to solve this, I want to post my solution for two reasons: first, help other people do the same; second, hearing oppinions and enhancements to my solution.

Problem: Some web content can only be accessed paying. Concretely, my paid content are movies, flv format.

Modules involved:

  1. flashvideo
  2. private_upload
  3. a custom module that drupal forced me to create

Context: Drupal has two filesystem settings: public and private. The names are totally wrong, because they should be called unmanaged and managed. Public file system is not managed by drupal, it is apache that serves files directly. Private file system is served by drupal and is considerably slower. If some protection has to be performed on files, then the drupal way is private file system, but with an efficiency cost. One solutions comes with the private_upload module, that implements a mixed public/private file system. Having a public file system, private_upload creates a protected directory. This way, the public file system will be served by apache (considerably faster) and the private part will be served by drupal (slower but only for a part of the contents that need some access control). In my case, the protected content are flv videos served by the flashvideo module.

Solution: Once done, it is not that difficult. What made it difficult was the lack on documentation in the drupal community regarding these aspects. Having properly installed flashvideo and private_upload modules, the first thing is is to theme flashvideo player and put it into template.php:

function mytheme_flashvideo_play_flash($video) {

   // Here begins the modification: we want an address starting with system/files so drupal takes control when the file is private, otherwise don't do anything
   // Get the FlashVars for this video.
   // we ask whether the url is private
   if (_private_upload_is_url_private($video['file'])) {
     //in this case, we add the system part
     $video['file']=_private_upload_replace_start_with($video['file'], 'http://mydomain/web/'._private_upload_path(), 'http://mydomain/web/system/'._private_upload_path()' );
   }
   //end of modification
   $flashvars = flashvideo_get_flashvars($video);

   // Creates an absolute path for the player.
   $loader_path = check_url(file_create_url(flashvideo_variable_get($video['nodetype'], 'player', 'Player.swf')));

   // Get the window mode
   $wmode = '';
   $wmode_param = '';
   switch(flashvideo_variable_get($video['nodetype'], 'mode', 'window')) {
     case 'transparent':
       $wmode = ' wmode="transparent"';
       $wmode_param = '<param name="wmode" value="transparent" />';
       break;
     case 'window':
       $wmode = ' wmode="window" allowfullscreen="true"';
       $wmode_param = '<param name="wmode" value="window" /><param name="allowfullscreen" value="true" />';
       break;
   }

   $output .= '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="'.$video['width'].'" height="'.$video['height'].'" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab">';
//   if ($flashvars[])
   $output .= '<param name="movie" value="'. $loader_path .'" />'. $wmode_param .'<param name="FlashVars" value="'.$flashvars.'" /><param name="quality" value="high" />';
   $output .= '<embed allowScriptAccess="always" src="'. $loader_path .'" width="'.$video['width'].'" height="'.$video['height'].'" border="0" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer"';
   $output .= $wmode . ' quality="high" flashvars="'.$flashvars.'" /></object>';
   $output .= (flashvideo_variable_get($video['nodetype'], 'embed', 0) == 1) ? '<br/>' . theme('flashvideo_embed', $output,
$video['nodetype']) : '';
   $output = theme( 'flashvideo_format_play', $video, $output, t('http://www.macromedia.com/go/getflashplayer'),
                                       t('Link to Macromedia Flash Player Download Page'),
                                       t('Download latest Flash Player'));
   return $output;
}

Having made sure drupal will take care of private files, all that is left is to establish some control over private files. First, when flashvideo processes videos, those private ones must be marked private. Then, the only way to control downloads is through hook_file_download. Information is too scarce on this, I hade to try many options until I noticed that hooks can only be controled from modules. A module must be created and activated so paid content is properly controlled. It is a very simple thing, such a small module that even I was able to do it. The module is called paiddownloads, and paiddownloads.module looks like this:

<?php
// $Id$

//This is a copy of the upload_file_download function in upload module with an added control
function paiddownloads_file_download($file) {
  $file = file_create_path($file);
  $result = db_query("SELECT f.* FROM {files} f WHERE filepath = '%s'", $file);
  if ($file = db_fetch_object($result)) {
    if (user_access('view uploaded files')) {
      $node = node_load($file->nid);
      if (node_access('view', $node)) {
        //this is the modification: an added control of a session variable indicating the user has paid for a video
        //this session variable must be set some other place. In my case, as I am using a pay platform, the platform sends info about the commited payment
        if ($node->type == "video" && $SESSION['video'][$file->nid] == 1 ) {
          // if a payment is only for one access, the session variable can be unset here
          $type = mime_header_encode($file->filemime);
          return array(
            'Content-Type: '. $type,
            'Content-Length: '. $file->filesize,
          );
        }
        else {
          return -1;
        }
        //end of modification
      }
      else {
        return -1;
      }
    }
    else {
      return -1;
    }
  }
}

Well, I hope this is useful for someone. I am using a payment plaftorm, ALLOPASS, for SMS micropayments. There is no module for interacting with it in drupal, but it is quite an easy thing. I am not a web programmer, but I have managed to solve communicating with ALLOPASS from my drupal site. If someone wants to create such a moduled solution, I am open to sending my solution to the person who wants to automate it (my solution must be handcoded into every pay node, which is far from the ideal).

Comments

vkr11’s picture

I will need to spend more time understanding the fix, but I like what I see :-) Subscribing to keep a track of this..

- Victor

meshackdotcom’s picture

I'm a newb, but may have use for this in the future... subscribing

zilla’s picture

this should be in the handbook - maybe a site recipe submission...

Farreres’s picture

Before adding to any handbook, I prefer to hear oppinions from some of the wise guys.

jakchapman@groups.drupal.org’s picture

Given that the user has paid for access, of course. I don't see any mention of this, but keeping the video out of the cache is pretty important in my case, and I would think in yours too since a customer paying for the video once and then passing it around to all his friends or posting it on his own website is probably not what you're after; so I'm hoping this feature is hidden somewhere in there :)

Farreres’s picture

The solution I have written counts that someone has paid previously. You can control the user has paid through a cookie or through a session variable. In the code I supposed you control it with a session variable.

About the user copying and passing the file, I don't think there is a sure fire solution. As long as you have sent the data, it might be copied by some strange ways. Every protection is breakable. But if you are sending the data, it's because the user has paid. Some users will NEVER pay no matter what you do. I think, instead of thinking in how to prevent non paying users from viewing free, I would think another way, how to make pay users feel it is worth paying again.

jakchapman@groups.drupal.org’s picture

Thanks for your reply. In my case, it's not about money, it's just about strength of protection for sensitive information or copyrighted materials. I think taking measures to keep it out of the cache by setting it to no-cache and expiring it in the past is probably strong enough for copyright laws, but when we are talking about information that truly must be kept private, I don't know that I would trust any Internet technology.

Farreres’s picture

Could you please explain how those features could be implemented? I mean, not storing in cache and expiring in the past. How can I add them into my code?

Farreres’s picture

Following JackChapman's suggestion, I suggest adding this line to paiddownloads.module:

return array(
'Content-Type: '. $type,
'Content-Length: '. $file->filesize,
// added line below to expire in the past and disable cache for this content
'Expires: -1', 'Pragma: no-cache', 'Cache-Control: no-cache'
);

Is this what you were suggesting?

jakchapman@groups.drupal.org’s picture

I don't know that much about it really. When I was testing it out I found people suggesting including the following both at the top in the real head section and after the </body> tag (with the repeated <head></head>):

<head>
<META HTTP-EQUIV="Expires" CONTENT="Mon, 04 Dec 1999 21:29:02 GMT">
<META HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE">
</head>

Apparently including it just at the top works for most browsers and including it at the bottom works for some non-conforming browser(s). This did work to keep it out of the cache in firefox 2 and IE7 on my machine. I didn't test it any further than that. My intuition is that what you are suggesting should work for most or all browsers since it's in the actual header and not in the document, but the depth of my knowledge on this subject is pretty limited. Of course, someone can always still get around it some way or other, but that's no reason not to make it difficult :)

Farreres’s picture

I have finally rewritten my function this way:

function paiddownloads_file_download($file) {
  $file = file_create_path($file);
  $result = db_query("SELECT f.* FROM {files} f WHERE filepath = '%s'", $file);
  if ($file = db_fetch_object($result)) {
    if (user_access('view uploaded files')) {
      $node = node_load($file->nid);
      if (node_access('view', $node)) {
        if ($node->type == "video" && $_SESSION['video'][$file->nid] == 1 ) {
          unset($GLOBALS['_SESSION']['video'][$file->nid]); // only allow one download, unset session variable
          $type = mime_header_encode($file->filemime);
          return array(
            'Content-Type: '. $type,
            'Content-Length: '. $file->filesize,
            'Expires: -1', // expire in the past
            'Pragma: no-cache', // don't cache
            'Cache-Control: no-cache' // again don't cache
          );
        }
        else {
          return -1;
        }
      }
      else {
        return -1;
      }
    }
    else {
      return -1;
    }
  }
}

Of course, there must be some way to ignore directives and someone with knowledge will do it, but no matter what we do, there will always be some way to break what your build, so I think this is enough. Adding more restrictions will harm users. I even have to consider if only allowing one download is perhaps harming users.

About the head addition of meta directives, I would make them conditional by theming page.tpl.php:

<head>
<?php if ($secretcontent) {?>
<META HTTP-EQUIV="Expires" CONTENT="Mon, 04 Dec 1999 21:29:02 GMT">
<META HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE">
<?php }?>
</head>

But I consider it is not the page that is secret but the information being sent. For me, it is ok that the user can access the page, although the file being downloaded is restricted.

socialnicheguru’s picture

thanks for your help!

http://SocialNicheGuru.com
Delivering inSITE(TM), we empower you to deliver the right product and the right message to the right NICHE at the right time across all product, marketing, and sales channels.

Dasha_V - old’s picture

In addition to this discussion about protecting video - here is integration of the Flowplayer API module with secure streaming plugin. This makes videos available for being played only and deny direct download of the file.

http://drupal.org/node/784662

I hope this can be useful for someone.

Farreres’s picture

You can check my new approach here: https://www.drupal.org/node/2672400