Deny Access Based on Accept-Language Header

It can be difficult to determine the source of a request to a server. You can use geolocation to lookup an IP address, one excellent service is IP2Location.

However, if the goal is to deny access to servers from outside the country, language may be an effective identifier.

Whether the language is sent in the headers is an unknown, however, if it is sent, and it isn’t the language of the site, access can be denied with these commands:

In this case, if ‘en’ (English) is not one of the languages the client will accept, the request will be denied.

        RewriteCond %{HTTP:Accept-Language} !(en) [NC]
        RewriteRule ^.*  - [F]

This should be part of a layered strategy, with other rules ensuring additional protection.

A word of caution. If the Accept-Language header is missing, access will also be denied. Search engine bots often omit the Accept-Language header.

eZ Publish Workflow - electronic media delivery

I’m helping on a site that sells music.

I wanted a way to bundle a person’s purchases into a single file that could be downloaded once. There are two types of electronic media, mp3_product and album. Each may have an image associated with it. An mp3_product is an .mp3 files, an album is a .zip file, containing .mp3s.

The link above is a great resource. Using it, I created a custom workflow:

<?php
 
class CreateDownloadFileType extends eZWorkflowEventType
{
    const WORKFLOW_TYPE_STRING = "createdownloadfile";
    public function __construct()
    {
        parent::__construct( CreateDownloadFileType::WORKFLOW_TYPE_STRING, 'Create Download File');
    }
 
    public function execute( $process, $event )
    {
        $parameters = $process->attribute( 'parameter_list' );

	$order_id = $parameters['order_id'];
	$user_id = $parameters['user_id'];

	$order = eZShopFunctionCollection::fetchOrder ($order_id);

	$items = eZProductCollectionItem::fetchList( array( 'productcollection_id' => $order['result']->ProductCollectionID ) );

	$order_number = $order['result']->OrderNr;

	$zip_file='/home/account/www/download/'.md5($order_number.$user_id).'.zip';
	$zip = new ZipArchive();
	if ($zip->open($zip_file,ZIPARCHIVE::CREATE)!==true) {
		eZDebug::writeError('Order: '.$order_id.' - could not create zip file '.$zip_file);
	}

	$storageDir = eZSys::storageDirectory();
	$attributes_to_include = array('file','image');

	foreach ($items as $item)
	{
		$product = eZContentObject::fetch($item->ContentObjectID);

		if (in_array($product->ClassIdentifier,array('mp3_product','album')))
		{
			$product_data = $product->dataMap();
			$name = preg_replace('/[^\w]/','_',trim($product_data['name']->DataText));

			foreach ($attributes_to_include as $v)
			{
				if ($product_data[$v]!==null)
				{
					$attribute_data = $product_data[$v]->Content();
					$file = $attribute_data->Filename;
					$mime_type = $attribute_data->MimeType;
				        list( $group, $type ) = explode( '/', $mime_type );
					$filePath = $storageDir . '/original/' . $group . '/' . $file;
					$pathInfo = pathinfo($filePath);
					if ($zip->addFile($filePath,$name.'.'.$pathInfo['extension'])!==true) {
						eZDebug::writeError('Order: '.$order_id.' - failed to add '.$filePath.' to '.$zip_file);
					}
				}
			}
		}
		$zip->close();
	}

        return eZWorkflowType::STATUS_ACCEPTED;
    }
}
eZWorkflowEventType::registerEventType( CreateDownloadFileType::WORKFLOW_TYPE_STRING, 'createdownloadfiletype' );
?>

The zip file created is named with an md5 of the order number and user id, and placed in a download directory. It’s probably not the most secure approach, but I think it is okay for this application, and it can be changed later.

This triggers on shop/checkout/after, which means the person has paid.

The download link for the zip file is placed in both the order view and order email templates.

    {def $current_user=fetch( 'user', 'current_user' )}
    {if $current_user.is_logged_in}
    <h2>{"Download"|i18n("design/base/shop")}:</h2>
    <p><a href="/download/{concat($order.order_nr,$current_user.contentobject_id)|md5()}.zip">{"Click to download"|i18n("design/base/shop")}</
a></p>
    {/if}
    {undef $current_user}

You may need an Alias and a RewriteRule:


Alias download /home/account/www/download
RewriteRule ^download/.*\.zip - [L]

The final step was to create a mechanism to delete the files after they are downloaded. There are at least two ways to do this. The first approach is to use an Apache filter, with a bash script:

Apache configuration code:

ExtFilterDefine delete mode=output
cmd="/home/account/webfilter.sh"

<Directory /home/account/www/download>
SetOutputFilter delete
</Directory>


FILENAME="/home/account/www$DOCUMENT_URI";
cat -
if [ -e $FILENAME ]; then
        rm $FILENAME;
fi

The Apache filter code is courtesy of Lyrix, Inc..

If you can’t use a filter - which is likely if you are using shared hosting, you can also use a PHP script. This uses a RewriteRule to route the request through PHP.

Options -Indexes
<IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteRule ^(.*\.zip)$ zip.php?path=$1 [L]
</IfModule>

Finally, this is the PHP code that will deliver the file, then delete it.

<?php
// downloading a file and then deleting it
$filename = $_GET['path'];
if (is_file($filename))
{
        // fix for IE catching or PHP bug issue
        header("Pragma: public");
        header("Expires: 0");
        header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
        header("Cache-Control: public");
        header("Content-Description: File Transfer");
        // browser must download file from server instead of cache

        // force download dialog
        header("Content-Type: application/zip");

        // use the Content-Disposition header to supply a recommended filename and
        // force the browser to display the save dialog.
        header("Content-Disposition: attachment; filename=".basename($filename).";");

        /*
        The Content-transfer-encoding header should be binary, since the file will be read
        directly from the disk and the raw bytes passed to the downloading computer.
        The Content-length header is useful to set for downloads. The browser will be able to
        show a progress meter as a file downloads. The content-lenght can be determines by
        filesize function returns the size of a file.
        */
        header("Content-Transfer-Encoding: binary");
        header("Content-Length: ".filesize($filename));

        @readfile($filename);
        unlink($filename);
        exit();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Requested Content Not Available</title>
</head>
<body>
<h1>Requested Content Not Available</h1>
<p>The content you requested is not available.</p>
<p>If you were unable to download your purchases, please contact us for assistance.</p>
</body>
</html>

jQuery eZ publish Mobile pagelayout.tpl

This code has been updated - use the github link above to get the latest code.

Using the book Head First Mobile Web to learn and as a reference, I created a simple mobile interface for an existing eZ publish installation.

pagelayout.tpl

pagelayout.tpl includes three ‘pages’, content, search, and menu.

{*?template charset=utf-8?*}
<!DOCTYPE html>
<html manifest="/manifest.appcache">
<head>
<meta NAME="robots" CONTENT="noindex,nofollow" />
<meta http-equiv="content-language" content="en" />
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.css" />
<link rel="stylesheet" href="/design/mobile/stylesheets/core.css" />
<script src="http://code.jquery.com/jquery-1.6.4.min.js"></script>
<script src="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.js"></script>
{include uri="design:page_head.tpl" enable_link=false}
</head>
<body>
{default current_user=fetch('user','current_user')}
<div data-role="page" id="content">
        {include uri="design:header.tpl"}
        <div data-role="content">
                {$module_result.content}
        </div>
        {include uri="design:footer.tpl" page="content"}
        <div id="footer">
                <address>{"Powered by %linkStartTag eZ publish&reg; open source content management system %linkEndTag and development framework."|i18n("design/base",,hash('%linkStartTag',"<a href='http://ez.no'
>",'%linkEndTag',"</a>" ))}<br />Copyright &copy;  2006-{currentdate()|datetime('custom','%Y')} <b>{ezini('SiteSettings','MetaDataArray','site.ini').copyright}</b><br />
                </address>
        </div><!--/footer-->
</div><!--/content-->
<div data-role="page" id="search">
        {include uri="design:header.tpl"}
        <div data-role="content">
                {include uri="design:searchbox.tpl"}
        </div>
        {include uri="design:footer.tpl" page="search"}
</div><!--/search-->
<div data-role="page" id="menu">
        {include uri="design:header.tpl"}
        {cache-block keys=array('nav',$uri_string, $current_user.role_id_list|implode( ',' ), $current_user.limited_assignment_value_list|implode( ',' ))}
        <div data-role="content">
                {menu name="LeftMenu"}
        </div>
        {include uri="design:footer.tpl" page="menu"}
        {/cache-block}
</div><!--/menu-->
{/default}
</body>
</html>

There are several supporting templates.

searchbox.tpl

{*?template charset=utf-8?*}
<form action={"/content/search/"|ezurl} method="get">
<h2>{"Search"|i18n("design/standard/toolbar/search")}</h2>
<input class="searchinput" type="text" name="SearchText" id="SearchText" value="" placeholder="{'Search'|i18n('/design/standard/toolbar/search')}">
<input type="submit" value="{'Submit'|i18n('/design/standard/node')}" data-icon="search" data-iconpos="left" />
</form>

header.tpl


{*?template charset=utf-8?*}
<div data-role="header" data-position="fixed">
<a href="/" data-rel="back" data-icon="back" data-role="button">{'Back'|i18n('design/standard/node')}</a>
<h1>{'Example'|i18n('design/standard/node')}</h1>
<a href="http://example.com/?notmobile" data-rel="external" data-icon="home" data-role="button" data-theme="b" data-iconpos="right">{'Full Site'|i18n('design/st
andard/node')}</a>
</div>

footer.tpl

{*?template charset=utf-8?*}
{if not(is_set($page))}
{def $page='unknown'}
{/if}
<div data-role="footer" data-position="fixed">
<div data-role="navbar">
<ul>
<li><a href="/Home" data-icon="home" data-role="button">{'Home'|i18n('design/standard/node')}</a></li>
{if $page|ne('menu')}
<li><a href="#menu" data-icon="menu" data-role="button">{'Menu'|i18n('design/standard/node')}</a></li>
{/if}
{if $page|ne('search')}
<li><a href="#search" data-icon="search" data-role="button">{'Search'|i18n('design/standard/node')}</a></li>
{/if}
</ul>
</div>
</div>

menu/flat_left.tpl

{*?template charset=utf-8?*}
{let docs=treemenu( $module_result.path,
                    is_set( $module_result.node_id )|choose( 2, $module_result.node_id ),
                    ezini( 'MenuContentSettings', 'LeftIdentifierList', 'menu.ini' ),
                    0, 5 )
                    depth=1
                    last_level=0}
        <ul data-role="listview" data-inset="true">
        {section var=menu loop=$:docs last-value}
            {set last_level=$menu.last|is_array|choose( $menu.level, $menu.last.level )}
            {section show=and( $last_level|eq( $menu.level ), $menu.number|gt( 1 ) )}
                </li>
            {section-else}
            {section show=and( $last_level|gt( $menu.level ), $menu.number|gt( 1 ) )}
                </li>
                    {"</ul>
                </li>"|repeat(sub( $last_level, $menu.level ))}
            {/section}
            {/section}

            {section show=and( $last_level|lt( $menu.level ), $menu.number|gt( 1 ) )}
                {'<ul><li>'|repeat(sub($menu.level,$last_level,1))}
                <ul>
                    <li class="menu-level-{$menu.level}">
            {section-else}
                <li class="menu-level-{$menu.level}">
            {/section}
            {let menu_node=fetch('content','node',hash('node_id',$menu.id))}
              {if $menu_node.class_identifier|ne('link')}
                <a {$menu.is_selected|choose( '', 'class="selected"' )} href={$menu.url_alias|ezurl}>{$menu.text|shorten( 25 )}</a>
              {else}
                <a href={$menu_node.data_map.location.content|ezurl} target="_blank">{$menu.text|shorten( 25 )}</a>
              {/if}
            {/let}
            {set depth=$menu.level}
        {/section}
           </li>

        {section show=sub( $depth, 0 )|gt( 0 ) loop=sub( $depth, 0 )}
            </ul>
        </li>
        {/section}
        </ul>

{/let}

The site includes .mp3 files, so an HTML5 audio tag is included. The .mp3s are also offered as links for native device playback.

datatype/ezbinaryfile.tpl

{default icon_size='normal' icon_title=$attribute.content.mime_type icon='no'}
{if $attribute.has_content}
{if $attribute.content}
{if $attribute.content.original_filename|ends_with('.mp3')}
<audio controls="controls">
        <source src="{$fileurl|ezurl('no','full')}" type="audio/mp3" />
        Audio playback not available.
</audio>
<div class="break"></div>
{/if}
{switch match=$icon}
    {case match='no'}
        <a href={concat("content/download/",$attribute.contentobject_id,"/",$attribute.id,"/file/",$attribute.content.original_filename)|ezurl}>{$attribute.content.original_filename|wash(xhtml)}</a> {$attribute
.content.filesize|si(byte)}
    {/case}
    {case}
        <a href={concat("content/download/",$attribute.contentobject_id,"/",$attribute.id,"/file/",$attribute.content.original_filename)|ezurl}>{$attribute.content.mime_type|mimetype_icon( $icon_size, $icon_tit
le )} {$attribute.content.original_filename|wash(xhtml)}</a> {$attribute.content.filesize|si(byte)}
    {/case}
{/switch}
{else}
        <div class="message-error"><h2>{"The file could not be found."|i18n("design/base")}</h2></div>
{/if}
{/if}
{/default}

core.css is included to support the navigation. Any other CSS required for proper page display could be added.

core.css


/* CORE CSS 20040217 */

/* BODY */

/* PAGE NAVIGATION */

div.pagenavigator
{
    text-align: center;    
}

div.pagenavigator span.previous
{
    float: left;
}

div.pagenavigator span.next
{
    float: right;
}
div#footer
{
    padding: 10px 0 10px 0;
    text-align: center;
}

I used eZ 4.7 (community edition equivalent), which includes auto device detection. This works extremely well.

Notes

Naming the mobile siteaccess and enabling autodetection in site.ini.


settings/override/site.ini.append.php

[SiteAccessSettings]
CheckValidity=false
AvailableSiteAccessList[]=site
AvailableSiteAccessList[]=mobile
AvailableSiteAccessList[]=edit
ForceVirtualHost=true
MatchOrder=host
HostMatchType=map
HostMatchMapItems[]=domain.com;site
HostMatchMapItems[]=m.domain.com;mobile
HostMatchMapItems[]=edit.domain.com;edit
HostMatchMapItems[]=www.edit.domain.com;edit
HostMatchMapItems[]=www.domain.com;site
MobileSiteAccessList[]=mobile
MobileSiteAccessURL=http://m.domain.com
DetectMobileDevice=enabled

The manifest.appcache file is used to reduce bandwidth requirements and speed pages. In this case, the Home page, css, and jQuery are cached.

manifest.appcache


CACHE MANIFEST

NETWORK:
content/search*
*

CACHE:
Home
design/mobile/stylesheets/core.css
http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.css
http://code.jquery.com/jquery-1.6.4.min.js
http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.js

You will probably need to add or update a RewriteRule to ensure proper delivery of the manifest.appcache file:

RewriteRule ^(robots\.txt|favicon\.ico|manifest\.appcache)$ - [L] 

It is a good idea to keep the mobile site out of search engines. This RewriteRule will allow you to deliver a different robots.txt file.

Excerpt from .htaccess

RewriteCond %{HTTP_HOST} ^m\..*
RewriteRule ^robots\.txt$ m.robots.txt [L]

m.robots.txt

User-agent: *
Disallow: /

Installing ffmpeg-php on CentOS 5.8

Follow the instructions in the link above, they’re excellent.

If you get a few compile errors related to missing files, add these RPMs (excerpt from /var/log/yum.log):

faad2-devel-2.7-1.el5.rf.i386
x264-devel-0.0.0-0.4.20101111.el5.rf.i386
lame-devel-3.99.5-1.el5.rf.i386
a52dec-devel-0.7.4-8.el5.rf.i386
dirac-devel-1.0.2-1.el5.rf.i386
faac-devel-1.26-1.el5.rf.i386
alsa-lib-devel-1.0.17-1.el5.i386
SDL-devel-1.2.10-9.el5.i386
gsm-devel-1.0.13-1.el5.rf.i386
opencore-amr-devel-0.1.2-1.el5.rf.i386
imlib2-1.4.4-1.el5.rf.i386
imlib2-devel-1.4.4-1.el5.rf.i386
ffmpeg-devel-0.6.5-1.el5.rf.i386
libXpm-devel-3.5.5-3.i386
gd-devel-2.0.33-9.4.el5_4.2.i386

I recommend using the package manager, it will save you a lot of time.

If you get an error referencing PIX_FMT_RGBA32, follow the instructions at: http://blog.hostonnet.com/ffmpeg-php-error-pix_fmt_rgba32

How to Keep a Mobile Site Out of Search Engines

I have a mobile version of a site, which is accessed through auto-detection of mobile user-agents.

Since the content is virtually the same as the regular site, I didn’t want it indexed by search engines, both to prevent site visitors from landing on a mobile site, and to avoid SEO penalties for duplicate content.

I used the following RewriteCond/RewriteRule to deliver an alternate robots.txt file for the mobile site.

        RewriteCond %{HTTP_HOST} ^m\..*
        RewriteRule ^robots\.txt$ m.robots.txt [L]

The alternate robots.txt file is:

User-agent: *
Disallow: /

In this case, the site is run by eZ publish, but the same strategy should work with any other mobile site that is run as a subdomain.