Showing posts with label PHP. Show all posts
Showing posts with label PHP. Show all posts

Friday, August 17, 2007

Automatically Minimize (Minify) JavaScript

One of the 13 Simple Rules for Speeding Up Your Website is to Minify your JavaScript.

Sidenote: These rules can be easily checked using the YSlow add-on to Firebug.

The recommended tool to minify your JavaScript is JSMin. However, the default use of JSMin is to minimize your JavaScript in a separate step before you deploy your code and then have two JavaScript files - the original JavaScript you use to develop the code and the production "minified" version. For me the problem with that is the fact you have two versions to keep track of and keep in sync. Plus before you deploy your code, you need to make sure all your HTML pages use the minified filename not the original version.

Fortunately, there's a port of JSMin to PHP which allows you minify your JavaScript on the fly, but the usage instructions make it appear that you need to change your script source to point to separate PHP file for each JavaScript, or have a single PHP with all your JavaScript files hard-coded in it -- which is a nice option to get multiple JavaScript files with a single request.

What I'm going to show in this post is how to use jsmin-php to create a filter on your server so that all your JavaScript files are transparently minified as your retrieve them.

First off, download the current version of jsmin-php and save it in your directory. As the current version at the time of this writing is jsmin-1.1.0.php, I'll assume that is the file name.

Next, save the script below as jsmin.php:


<?php
// PHP filter to invoke JSMin on all JavaScript files
// see http://code.google.com/p/jsmin-php/
require_once('jsmin-1.1.0.php');

$js_file = pathinfo($_SERVER['ORIG_PATH_INFO']);
$js_path = substr($js_file['dirname'], 11);
$js_file = $js_file['basename'];

$charset = "utf-8";
$mime = "text/javascript";

header("Content-Type: $mime;charset=$charset");

// Output a minified version of JavaScript file
if (file_exists($js_file)) {
echo JSMin::minify(file_get_contents($js_file));
} else {
echo JSMin::minify(file_get_contents($js_path . '/' . $js_file));
}

?>


Note: If you change the name of this script, you'll probably need to change the 11 on the $js_path definition. That rips the script name (/jsmin.php/) off the beginning of the incoming path on the request so I can use a relative path to find scripts in a subdirectory. It should be the length of the script name plus 2 (for the leading and trailing '/'s).

Now, update your .htaccess file and add the lines:

# make all JavaScript files be minimized via JSMin
AddHandler jsmin .js
Action jsmin /jsmin.php


That should be it! If you fetch a JavaScript file from your server, it should now be minified.


Here are a few quick additions you could add to make YSlow happy.

At the beginning of jsmin.php, adding the line:

ob_start("ob_gzhandler");

should GZIP the output for you.



Also, where the Content-Type header is set, adding the following lines will add an expiration of 49 hours (needs to be > 48 hours to by sufficiently "far future" to make YSlow happy. If you really want to cut it close you could use (60 * 60 * 48) + 1):

$offset = 60 * 60 * 49;
$ExpStr = "Expires: " . gmdate("D, d M Y H:i:s", time() + $offset) . " GMT";
header($ExpStr);

Friday, February 23, 2007

Ajax Cross-Domain Issue

A common solution to the XMLHttpRequest cross-domain issue (aka cross site scripting or XSS), where you can only communicate with the originating domain, is to configure Apache to use either mod_proxy or mod_rewrite for URL rewriting and a pass-through proxy.

With Apache you need to enable the proxy functions (configure --enable-proxy). If you want to handle it all with mod_proxy, then define you proxy settings in httpd.conf such as:


ProxyPass /doExt http://external.com/doAction
ProxyPassReverse /doExt http://external.com/doAction

If you want to use mod_rewrite then turn on mod_rewrite in httpd.conf or .htaccess with RewriteEngine on and define your rewriting rules such as:

RewriteRule ^/doExt$ http://external.com/doAction [P]

Then when you post to your server via /doExt, Apache will pass the request on to the external site.

That's all well and good if you have access to the Apache configuration or the server is already configured with the proper modules. But, when your on a hosted environment, that's not always feasible. Another solution would be to provide a proxy servlet to make the remote request for you and return the result. What I am providing is in Java and PHP, but it could be written you language of choice.

Java proxy


The following servlet class will invoke some external URL on behalf of the original request and return the result from the external request as the result of the original local request. Note: In the following code I hard-coded the real destination. However, it could be determined from a parameter in the original request just as easily.


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.util.Enumeration;

/**
* This is a simply proxy class in order to bypass AJAX security contraints.
*
*/

public class ServerProxy extends javax.servlet.http.HttpServlet {

private static final long serialVersionUID = 1L;

public void doGet(javax.servlet.http.HttpServletRequest request,
javax.servlet.http.HttpServletResponse response)
throws javax.servlet.ServletException, java.io.IOException {
performTask(request, response);
}

public void doPost(javax.servlet.http.HttpServletRequest request,
javax.servlet.http.HttpServletResponse response)
throws javax.servlet.ServletException, java.io.IOException {
performTask(request, response);
}

public void performTask(javax.servlet.http.HttpServletRequest request,
javax.servlet.http.HttpServletResponse response)
throws javax.servlet.ServletException, java.io.IOException {

// Get the external URL to invoke
String realUrl = "http://www.external.com";

// Copy the request parameters from the original request
Enumeration paramNames = request.getParameterNames();
String paramName;
while (paramNames.hasMoreElements()) {
paramName = (String) paramNames.nextElement();
realUrl = realUrl.concat(paramName + "="
+ request.getParameter(paramName));
if (paramNames.hasMoreElements()) {
realUrl = realUrl.concat("&");
}
}

// real URL will be returning XML data
response.setContentType("text/xml");
PrintWriter out = response.getWriter();

// invoke the real URL and copy the result into the response
// for the original request
URL real = new URL(realUrl);
BufferedReader in = new BufferedReader(
new InputStreamReader(real.openStream()));
String inputLine;
while ((inputLine = in.readLine()) != null)
out.println(inputLine);
in.close();

return;
}

}


PHP proxy


Here is a similar solution using PHP:

$remoteServer = "http://www.external.com";
$path = $_GET[”path”];
$proxyTarget = $remoteServer.$path;
$connection = curl_init($proxyTarget);
curl_setopt($connection, CURLOPT_HEADER, false);
curl_setopt($connection, CURLOPT_RETURNTRANSFER, true);
$data = curl_exec($connection);
curl_close($connection);
header(”Content-Type: text/xml”);
echo $data;

Saturday, June 03, 2006

Compressing HTML, CSS, and JavaScript files

Adding gzip compression to files received from your web server can enhance the network latency of your web application. As simple way to do this is to define a handler in your .htaccess file to compress your HTML, CSS, and JavaScript files. To do this, add these lines to your .htaccess file:


AddHandler application/x-httpd-php .css .html .js
php_value auto_prepend_file /path/to/gzip-page.php
php_flag zlib.output_compression On

where the path is specific to your installation.

The gzip-page.php is needed to define the content-type for the compressed data so that they are not assumed to be PHP files. The code should look something like:


<?php
$pathinfo = pathinfo($PHP_SELF);
$extension = $pathinfo['extension'];
switch ($extension) {
case "css" :
header("Content-type: text/css");
break;
case "html" :
header("Content-type: text/html");
break;
case "js" :
header("Content-type: text/javascript");
break;

default :
break;
}
?>


Now all your HTML, CSS, and JavaScript content will be compressed if the browser supports compressed data (which most do). For Ajax applications this is most useful since you'll probably have a decent amount of JavaScript code to transmit and potentially a fair amount of CSS as well.

Wednesday, January 25, 2006

Using XML in Ajax (with PHP)

In support of my earlier post about using the XMLHttpRequest object for XML, here is a nice article demonstrating the use of the responseXML attribute of the XMLHttpRequest with PHP. Here is another.

Also, just for reference here are a few nice sites discussing developments in AJAX:
Ajaxian
AJAX blog
AJAX Matters
ajax info

As well as some good usage guideline articles:
Ajax Mistakes
XMLHttpRequest Usability Guidelines

Friday, January 20, 2006

Changing FeedCreator.class.php for Podcasting

As I mentioned in my Podcasting HowTo, I used Kai Blankenhorn's FeedCreator.class.php as the basis of my podcasting feed. In this posting I will detail my additions to his code to support podcasting and iTunes.

You may download the final file here

First, at line 31, I updated the change log:


v1.7.2-iTunes
added support for iTunes tags
v1.7.2-podcast
added support for podcast (added enclosure)
made the Item category be an Array as you are allowed to specify
multiple category elements in the RSS spec

Then, at what becomes line 173 (after the changelog changes), I added
these classes:

/**
* Version string.
**/
define("FEEDCREATOR_VERSION", "FeedCreator 1.7.2-iTunes");
/**
* An Enclosure is a part of an Item
*
* @author Steven Pothoven <steven@pothoven.net>
* @since 1.7.2-podcast
*/
class Enclosure {
/**
* Attributes of an enclosure
*/
var $url, $length, $type = "audio/mpeg";
}
/**
* iTunes extensions to RSS 2.0
*
* @author Steven Pothoven <steven@pothoven.net>
* @since 1.7.2-iTunes
*/
class iTunes {
/**
* This tag can only be populated using iTunes specific categories.
*/
var $category, $subcategory;
/**
* This tag should be used to note whether or not your Podcast contains explicit material.
* There are 2 possible values for this tag: Yes or No
*/
var $explicit;
/*
* At the Channel level, this tag is a short description that provides general information about the Podcast. It will appear next to your Podcast as users browse through listings of Podcasts
* At the Item level, this tag is a short description that provides specific information for each episode.
* Limited to 255 characters or less, plain text, no HTML
*/
var $subtitle;
/*
* At the Channel level, this tag is a long description that will appear next to your Podcast cover art when a user selects your Podcast.
* At the Item level, this tag is a long description that will be displayed in an expanded window when users click on an episode.
* Limited to 4000 characters or less, plain text, no HTML
*/
var $summary;
/*
* At the Channel level this tag contains the name of the person or company that is most widely attributed to publishing the Podcast and will be displayed immediately underneath the title of the Podcast.
* If applicable, at the item level, this tag can contain information about the person(s) featured on a specific episode.
*/
var $author;
/*
* This tag is for informational purposes only and will allow users to know the duration prior to download
* The tag is formatted: HH:MM:SS
*/
var $duration;
/*
* This tag allows users to search on text keywords
* Limited to 255 characters or less, plain text, no HTML, words must be separated by spaces
*/
var $keywords;
/*
* This tag contains the e-mail address that will be used to contact the owner of the Podcast for communication specifically about their Podcast on iTunes.
* Required element specifying the email address of the owner.
*/
var $owner_email;
/*
* Optional element specifying the name of the owner.
*/
var $owner_name;
/*
* This tag specifies the artwork for the Channel and Item(s). This artwork can be larger than the maximum allowed by RSS.
* Preferred size: 300 x 300 at 72 dpi
* Minimum size: 170 pixels x 170 pixels square at 72 dpi
* Format: JPG, PNG, uncompressed
*/
var $image;
/*
* This tag is used to block a podcast or an episode within a podcast from being posted to iTunes. Only use this tag when you want a podcast or an episode to appear within the iTunes podcast directory.
*/
var $block;
}


In the FeedItem class I added these lines at the new line 283:

/**
* Support for attachments
*/
var $enclosure;
/**
* Support for iTunes
*/
var $itunes;


In the _setFormat function, I added a new case at the new line 491:

case "PODCAST":
$this->_feed = new PodcastCreator();
break;


In the createFeed function, I added the iTunes extentions at line
1102:

/* iTunes add iTunes specific tags */
if ($this->itunes!="") {
if ($this->itunes->category!="") {
$feed.= " <itunes:category text=\"".htmlspecialchars($this->itunes->category)."\">\n";
if ($this->itunes->subcategory!="") {
$feed.= " <itunes:category text=\"".htmlspecialchars($this->itunes->subcategory)."\"/>\n";
}
$feed.= " </itunes:category>\n";
}
if ($this->itunes->explicit!="") {
$feed.= " <itunes:explicit>".$this->itunes->explicit."</itunes:explicit>\n";
}
if ($this->itunes->subtitle!="") {
$feed.= " <itunes:subtitle>".htmlspecialchars($this->itunes->subtitle)."</itunes:subtitle>\n";
}
if ($this->itunes->summary!="") {
$feed.= " <itunes:summary>".htmlspecialchars($this->itunes->summary)."</itunes:summary>\n";
}
if ($this->itunes->author!="") {
$feed.= " <itunes:author>".htmlspecialchars($this->itunes->author)."</itunes:author>\n";
}
if ($this->itunes->keywords!="") {
$feed.= " <itunes:keywords>".htmlspecialchars($this->itunes->keywords)."</itunes:keywords>\n";
}
if ($this->itunes->owner_email!="") {
$feed.= " <itunes:owner>\n";
$feed.= " <itunes:email>".$this->itunes->owner_email."</itunes:email>\n";
if ($this->itunes->owner_name!="") {
$feed.= " <itunes:name>".$this->itunes->owner_name."</itunes:name>\n";
}
$feed.= " </itunes:owner>\n";
}
if ($this->itunes->image!="") {
$feed.= " <itunes:link rel=\"image\" type=\"image/jpeg\" href=\"".$this->itunes->image."\">[image]</itunes:link>\n";
}


and additional iTunes and podcast extensions at line 1154:

/* podcasts add the enclosure element */
if ($this->items[$i]->enclosure!="") {
$feed.= " <enclosure url=\"".str_replace(" ", "%20", htmlspecialchars($this->items[$i]->enclosure->url)). "\" length=\"".htmlspecialchars($this->items[$i]->enclosure->length). "\" type=\"".htmlspecialchars($this->items[$i]->enclosure->type). "\"/>\n";
}
/* iTunes add iTunes specific tags */
if ($this->items[$i]->itunes!="") {
if ($this->items[$i]->itunes->category!="") {
$feed.= " <itunes:category text=\"".htmlspecialchars($this->items[$i]->itunes->category)."\">\n";
if ($this->items[$i]->itunes->subcategory!="") {
$feed.= " <itunes:category text=\"".htmlspecialchars($this->items[$i]->itunes->subcategory)."\"/>\n";
}
$feed.= " </itunes:category>\n";
}
if ($this->items[$i]->itunes->explicit!="") {
$feed.= " <itunes:explicit>".$this->items[$i]->itunes->explicit."</itunes:explicit>\n";
}
if ($this->items[$i]->itunes->subtitle!="") {
$feed.= " <itunes:subtitle>".htmlspecialchars($this->items[$i]->itunes->subtitle)."</itunes:subtitle>\n";
}
if ($this->items[$i]->itunes->summary!="") {
$feed.= " <itunes:summary>".htmlspecialchars($this->items[$i]->itunes->summary)."</itunes:summary>\n";
}
if ($this->items[$i]->itunes->author!="") {
$feed.= " <itunes:author>".htmlspecialchars($this->items[$i]->itunes->author)."</itunes:author>\n";
}
if ($this->items[$i]->itunes->keywords!="") {
$feed.= " <itunes:keywords>".htmlspecialchars($this->items[$i]->itunes->keywords)."</itunes:keywords>\n";
}
if ($this->items[$i]->itunes->duration!="") {
$feed.= " <itunes:duration>".$this->items[$i]->itunes->duration."</itunes:duration>\n";
}
if ($this->items[$i]->itunes->image!="") {
$feed.= " <itunes:link rel=\"image\" type=\"image/jpeg\" href=\"".$this->items[$i]->itunes->image."\">[image]</itunes:link>\n";
}


Finally, I added the PodcastCreator class at line 1235:

/**
* PodcastCreator is a FeedCreator that implements Podcast
*
* @see http://backend.userland.com/rss
* @since 1.7.2-podcast
* @author Steven Pothoven <steven@pothoven.net>
*/
class PodcastCreator extends RSSCreator20 {
function PodcastCreator() {
parent::_setRSSVersion("2.0");
parent::_setXMLNS("itunes=\"http://www.itunes.com/DTDs/Podcast-1.0.dtd\"");
}
}

Podcasting HowTo

As I mentioned in my last post, I added podcasting to our church web site. Today I thought I'd share how I did that for those interested in doing something similar.

To start with, I place all the sermon MP3 files into a separate directory. The MP3 files themselves adhere to the naming standard of:

SermonDate_Speaker_Passage.mp3

Where the date is in YYMMDD format. So, this past weeks sermons file is named:

060115_John Keen_Mark 1;1-8.mp3

This makes it very easy add sermons to the list by just uploading the file. Note: the passage uses a ';' to delineate chapter and verse instead of a ':', this is because a ':' is not allowed in a filename on some systems (Windows). The code will correct that when displayed.

I started with a simple table to be shown on the web page. Shortly after that I thought an RSS feed of the contents would be handy so people could be motified of new sermons without visiting the web page. The RSS feed lead to the podcast feed, as they are nearly the same thing.

They all essentially do the same thing, so I'll skip right to the podcast version. However, I'll share one little tidbit from the web page version. From the table on the web page, you can either stream the MP3, or download the file. Streaming an MP3 file from your server doesn't take any special software. I wrote a simple PHP script which I name m3u.php which allows me to stream any MP3 file. The contents of the m3u.php looks like:


<?php
header("Content-Type: audio/x-mpegurl");
print 'http://www.hostname.com/path/to/mp3/' . $_GET['filename'];
?>

So, in my table code, invoke it as (in PHP):

<a href="sermons/m3u.php?filename=' . $filename . '">

To make the podcast feed, I started with Kai Blankenhorn's FeedCreator.class.php. It worked very well for the RSS feed, but it doesn't support the podcast extensions. So, I updated it to add the podcast and iTunes extensions to RSS. If you're interested, I'll be happy to provide my updated version. I'll summarize the changes in a separate post so I don't make this post too long.

Now, I'll walk through my podcast generating PHP class. First, I need a function to list all the files in my mp3 directory:

// Function list_dir will list all the files in a given directory
//
function list_dir($dirname)
{
static $result_array = array();
$handle=opendir($dirname);
while ($file=readdir($handle)) {
// ignore the current and parent directory references ('.' and '..')
if (0 == strpos($file, '.')) {
continue;
}
$result_array[]=$file;
}
closedir ($handle);
return $result_array;
}

I then set up the common header portion of the podcast feed:

$rss = new UniversalFeedCreator();
// if cache is less than 12 hours (43200 seconds) old, use it
// this is because the feed only changes once a week to begin with, and this
// script does a lot of ID3 tag manipulation which can take a while
$rss->useCached("PODCAST", "sermons/podcast.xml", 43200);
$rss->title = "Seminole PCA Sermons";
$rss->description = "Recent sermons from the pulpit of Seminole Presbyterian Church";
$rss->language = "en-us";
$rss->link = "http://www.seminolepca.org";
$rss->webmaster = "webadmin@seminolepca.org";
$rss->docs = "http://blogs.law.harvard.edu/tech/rss";
$rss->syndicationURL = "http://www.seminolepca.org/".$PHP_SELF;
$rss->category = "Religion";

$image = new FeedImage();
$image->title = "Seminole PCA, Tampa, FL";
$image->url = "http://www.seminolepca.org/images/rss-SPC.jpg";
$image->link = "http://www.seminolepca.org";
$image->description = "Feed provided by seminolepca.org. Click to visit.";
$image->width = 144;
$image->height = 204;
$rss->image = $image;

$itunes = new iTunes();
$itunes->category = "Religion & Spirituality";
$itunes->subcategory = "Christianity";
$itunes->subtitle = $rss->description;
$itunes->owner_email = "church@seminolepca.org";
$itunes->owner_name = "Seminole Presbyterian Church";
$itunes->author = "Seminole Presbyterian Church";
$itunes->image = "http://www.seminolepca.org/images/podcast-SPC.jpg";
$rss->itunes = $itunes;

Now, I loop through my list of mp3 files to build the contents of the podcast feed:

// PHP code to dynamically generate RSS feed
//
// Start of code -------
$mp3dir="sermons/mp3";
// get a list of the "mp3" directory
$array=list_dir($mp3dir);
// reverse sort the array (puts newest sermon first)
rsort ($array);
reset ($array);

// for each file in the filelist, generate the appropriate HTML table code
foreach ($array as $filename) {
// break apart the filename into the 3 data fields; date, speaker,
// and subject using file naming convention:
// YYMMDD_Speaker_Passage.mp3
//
$tok = strtok($filename, "_");
$unixdate = mktime(0, 0, 0, substr($tok, 2, 2), substr($tok, 4, 2), substr($tok, 0, 2));
$sermondate=date("F d, Y", $unixdate);

$tok=strtok("_");
$speaker=$tok;
$tok=strtok(".");
$passage=$tok;
$chapter = strtok($passage, ";");
$verse = strtok(".");
if ($verse !== false) {
$passage = $chapter . ':' . $verse;
} else {
$passage = $chapter;
}

at this point in my PHP code, I also use the getid3 PHP library to ensure that all the ID3 tags are set correctly in the mp3 file. This is not important for actually generating the podcast feed, so I'll omit it here.

$item = new FeedItem();
$item->title = $speaker . ' on ' . $passage;
$item->link = "http://www.seminolepca.org/" . $mp3dir . "/" . $filename;
$item->description = $sermondate . ' - ' . $speaker . ' expounds on ' . $passage;
$item->date = $unixdate;
$item->source = "http://www.seminolepca.org";
$item->author = $speaker;
$item->enclosure = new Enclosure();
$item->enclosure->url = "http://www.seminolepca.org/" . $mp3dir . "/" . $filename;
$item->enclosure->length= $id3['filesize'];
$item->category[] = $itunes->category;

$item->itunes = new iTunes();
$item->itunes->category = $itunes->category;
$item->itunes->subcategory = $itunes->subcategory;
$item->itunes->keywords = "Seminole Presbyterian Church PCA Reformed Christian Sermon Tampa Florida FL";
$item->itunes->summary = $item->description;
$item->itunes->author = $item->author;
$item->itunes->duration = $id3['playtime_string'];
$item->itunes->image = "http://www.seminolepca.org/images/podcast-SPC.jpg";
$rss->addItem( $item);
}

Finally, I save feed for caching purposes and return it:

$rss->saveFeed("PODCAST", "sermons/podcast.xml");

The resulting podcast XML can be seen here.

Once that's working, go register you feed at the various podcast directories such as:
iPassage (if you podcasting sermons)
iPodder
Podcast.net
Podcast Alley
iTunes (HowTo)

I published my feed in iPodder, Podcast.net, and Podcast Alley and was automatically propagated to other directories including the iTunes Store.

For reference, here is the official Apple iTunes/Podcasting specification.