Zend_Rest - Example

I built a REST API and calling code using Zend Framework. This code also uses Doctrine (http://www.doctrine-project.org/projects/orm).

Prior to coding, I reviewed several resources on the ‘net in an attempt to follow best practices. I visited the following, as well as others:

routes.ini

These settings ensure the request is properly organized when received by the REST controller. Be sure the ACL is configured to allow access as appropriate as well.

Code:

# REST routes
resources.router.routes.api.type = "Zend_Rest_Route"
resources.router.routes.api.route = "/api/:id"
resources.router.routes.api.defaults.module = "api"
resources.router.routes.api.defaults.controller = "index"
resources.router.routes.api.api = "admin, vendor, client"

Calling Code

The calling code is a subset, only the relevant code is included.

Authentication is handled by JSON encoding, then encrypting the username and password in the Authorization header.

The process has two phases, a GET to test whether the element exists and a POST, PUT or DELETE depending on existence, error, or requested action. Since the GET is a test, no data is returned. POST and PUT pass the data in the body text, JSON encoded.

PHP:

$client = new Zend_Http_Client();
            $iv_size mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256MCRYPT_MODE_ECB);
            $iv mcrypt_create_iv($iv_sizeMCRYPT_RAND);
            $authorization base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256$v['url'],
                Zend_Json::encode(array('username'=>$this->username,'password'=>$this->password)),
                MCRYPT_MODE_ECB$iv));
            $client->setHeaders(array('keepalive' => true,'Authorization' => $authorization));
            $client->setUri($api['protocol'].'://'.$url.'/'.$api['path'].'/admin/'.$admin->uuid);
            $client->setMethod(Zend_Http_Client::GET);
            $response $client->request();
            $status $response->getStatus();
            $body $response->getBody();
            switch ($status)
            {
                case 200:    /* Record exists */
                    $next_method = ($type == 'allow') ? Zend_Http_Client::PUT Zend_Http_Client::DELETE;
                    break;
                case 404:    /* Record does not exist */
                    if ($type == 'allow')
                    {
                        $client->setUri($api['protocol'].'://'.$url.'/'.$api['path'].'/admin');
                        $next_method Zend_Http_Client::POST;
                        break;
                    }
                default:    /* Error */
                    return false;
            }
    
            if (($next_method == Zend_Http_Client::POST) ||
                ($next_method == Zend_Http_Client::PUT))
                {
                    $values array('uuid','password','email','first_name','middle_name','last_name','status','last_modified_by');
                    $admin_data $admin->getData();
                    foreach ($values as $k => $v)
                        $data[$v] = $admin_data[$v];
                    $jsonData Zend_Json::encode($data);
                    $client->setRawData($jsonData,'text/json');
                }
        
            $client->setMethod($next_method);
            $response $client->request();
            $status $response->getStatus();
            $client->getAdapter()->close();
            $return in_array($status,array(200,201,204));

modules/api/controllers/AdminController.php

The REST controller uses a preDispatch override to ensure the submitted id, username and password are valid. Invalid requests are discarded with an HTTP/400. Unauthorized requests receive an HTTP/403.

GET requests use the submitted UUID to test for the existence of the data. Using a UUID ensures the data will be identified the same across different systems. The data has local identifiers as well, and some translation is applied, although it has been removed from the posted code.

The JSON data is read from the request body with a file_get_contents on php://input

PHP:

<?php
class Api_AdminController extends Zend_Rest_Controller
{
    private $model null;
    private $uuid null;
    private $data null;
    private $response null;
 
    public function init()
    {
        $this->apiBaseUrl $_SERVER['SERVER_NAME'].'/api/admin';
        $this->_helper->layout->disableLayout();
        $this->getHelper('viewRenderer')->setNoRender(true);
    }
 
    public function preDispatch()
    {
        // This ensures the required data is present and valid prior to processing any requests
        $hostname = new Zend_Validate_Hostname(array('allow'=>Zend_Validate_Hostname::ALLOW_DNS));
        $validators array(
            'uuid'=>array(array('Regex','pattern' => '/^[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}$/','allowEmpty'=>false,'presence'=>'required')),
            'username'=>array(array('EmailAddress','allowEmpty'=>false,'presence'=>'required','allow' => $hostname,'mx' => true)),
            'password'=>array(array('StringLength','min'=>8,'max'=>32)));
        $input = new Zend_Filter_Input(array(),$validators);
        $request $this->getRequest();
 
        $this->data Zend_Json::decode(file_get_contents('php://input'));
        if ($request->isPost())
            $this->uuid $this->data['uuid'];
        else
        {
            $this->uuid $request->getParam('id');
            $this->data['uuid'] = $this->uuid;
        }
 
        $iv_size mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256MCRYPT_MODE_ECB);
        $iv mcrypt_create_iv($iv_sizeMCRYPT_RAND);
        $authorization Zend_Json::decode(trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256$_SERVER['SERVER_NAME'],
            base64_decode($request->getHeader('Authorization')),
            MCRYPT_MODE_ECB$iv)));
        $input->setData(array(
            'uuid'=>$this->uuid,
            'username'=>$authorization['username'],
            'password'=>$authorization['password']));
 
        if (!$input->isValid())
            $this->error(400);
        else
        {
            // Data is valid, authenticate based on username and password
                $session = new Zend_Session_Namespace('global');
                $adapter = new Application_Auth_Adapter_Doctrine($input->username,$input->password);
            $auth Zend_Auth::getInstance();
            $result $auth->authenticate($adapter);
                if (!$result->isValid()) 
                $this->error(403);
            else
            {
                // Prepare the response object
                $api Zend_Registry::get('bootstrap')->getOption('rest');
                $this->response array(
                    'link'=>$api['protocol'].'://'.$this->apiBaseUrl.'/'.$input->id,
                    'title'=>'Admin',
                    'charset'=>'utf-8');
            }
        }
 
        $session=new Zend_Session_Namespace('global');
        switch ($session->type)
        {
            case 'client'
                $this->model 'Application_Model_Admin'
                break;
            case 'vendor'
                $this->model 'Application_Model_VendorAdmin'
                break;
            case 'admin'
                $this->model 'Application_Model_Admin'
                break;
            default: 
                $this->error(400);
                break;
        }
    }
    
    public function indexAction()
    {
        // Handles GET requests
    }
    
    public function getAction()
    {
        // Handles GET requests
        $q Doctrine_Query::create()
            ->from ($this->model)
            ->where('uuid = ?',$this->uuid);
        $result $q->fetchArray();
        $q->free();
        switch (count($result))
        {
            case 1:
                // The data is NOT sent back to the client.  The response serves only to acknowledge the existence of the record
                // In this application, the GET request is really querying for presence, not requesting data
                $this->response['updated'] =$result[0]['updated_at'];
                $this->_helper->json($this->response);
                break;
            case 0:
                $this->error(404);
                break;
            default:
                // Database should only have one instance of UUID
                $this->error(409,'Multiple matches - contact support');
                break;
        }
    }
 
    private function process()
    {
        $form = new Application_Access_Admin_View_Form();
        $form->populate($this->data);
        $values array('uuid','password','email','first_name','middle_name','last_name','status','last_modified_by');
 
        $data array();
        foreach ($elements as $k => $v)
        {
            $name $v->getName();
            if (!in_array($name,$values))
                $form->removeElement($name);
            else
                $data[$name] = $v->getValue();
        }
 
        if ($form->isValidPartial($data))
        {
            $request $this->getRequest();
            if (!$request->isPost())
            {
                $q Doctrine_Query::create()
                    ->from ($this->model)
                    ->where('uuid = ?',$this->uuid);
                $admin $q->fetchOne();
                $q->free();
            }
            else
            {
                $admin = new $this->model;
                $admin->assignDefaultValues();
            }
            $admin->fromArray($form->getValues());
            $response $this->getResponse();
            try
            {
                $admin->save();
                if ($request->isPost())
                    $response->setHttpResponseCode(201);
                else
                    $response->setHttpResponseCode(204);
            }
            catch(Doctrine_Exception $e)
            {
                   $this->error(400,var_export($admin->getErrorMessages(),true));
            }
            catch (Exception $e)
            {
                   $this->error(400,var_export($e->getMessage(),true));
            }
        }
        else
               $this->error(400,var_export($form->getMessages(),true));
    }
 
    public function postAction()
    {
        $this->process();
    }
 
    public function putAction()
    {
        $this->process();
    }
    
    public function deleteAction()
    {
        $q Doctrine_Query::create()
            ->from ($this->model)
            ->where('uuid = ?',$this->uuid);
        $result $q->fetchOne();
        $q->free();
        $result->delete();
        $response $this->getResponse();
        $response->setHttpResponseCode(204);
    }
 
    private function error($code,$message=null)
    {
        // If a message is returned, it is text in the body
        $response $this->getResponse();
        $response->setHttpResponseCode($code);
        if ($message != null)
            $response->setBody($message.PHP_EOL.PHP_EOL);
        $response->sendResponse();
        exit;
    }
}

Use curl to test the interface, or PHP.

Filtering Spam - Tips & Ideas

Link: http://docs.cpanel.net/twiki/bin/view/AllDocumentation/CpanelDocs/UserFiltering

SSH in to the server and find the affected inbox.

Quick Look at the inbox

This command shows the From, X-Spam-Bar, and Subject, without the file name. If you want to see the file names, remove the -h. You may have to use a different header for the spam score.

grep -h “From: \|X-Spam-Bar: \|Subject: ” * | more

The output should show give you an overview of what has been delivered, where it came from, and the subject.

Spam Score

The spam score is an appealing tool because it adapts to the current spam environ, and includes many parameters such as the sending IP address, domain, email address, SPF and reverse DNS information.

This is a very simple script that gives an overview of the spam bar values. It can be used as a first pass t set the level for filtering.

echo ‘Items in Inbox’
ls -1 | wc -l
echo ‘No Spam Bar (probably not spam)’
grep -L “X-Spam-Bar:” * | wc -l
echo ‘X-Spam-Bar Counts’
echo ‘+’
grep -m1 “X-Spam-Bar: +$” * | wc -l
echo ‘++’
grep -m1 “X-Spam-Bar: ++$” * | wc -l
echo ‘+++’
grep -m1 “X-Spam-Bar: +++$” * | wc -l
echo ‘++++’
grep -m1 “X-Spam-Bar: ++++$” * | wc -l
echo ‘+++++’
grep -m1 “X-Spam-Bar: +++++$” * | wc -l
echo ‘++++++’
grep -m1 “X-Spam-Bar: ++++++$” * | wc -l

It’s good to check the spam bar for valid emails as well. Many times, ‘real’ email is given a spam score.

From addresses

Look for patterns in the from addresses. Common patterns include ‘info@somedomain.info’ - the info, either as the sending email user or TLD is frequently used.

grep -h -m1 “From: ” * | sort

Subject

The subjects should be checked for the patterns in a similar manner as the From header.

Body

Using the results of the From and Subject checks, review a few of the message bodies (read the emails). Look for common text that would not be used by people. For example, Dear email@domain.com. People wouldn’t use an email address in a salutation, neither would real newsletter senders or other respectable sources.

Set the Filters

Read the directions. All the systems work differently.

Test the filters with some valid emails and some spam to be sure they behave as intended.

I created three filters.

  • Discard - The discard filter checks for glaring spam signs, such as a server that sent many spams messages, a from address pattern, distinct text patterns that simply wouldn’t come from people or other valid sources. These are discarded without warning to the sender.
  • Fail with message - Fail with message warnings were issued for anything that looked like spam, but might still be a valid email. Since the only ones that will read the fail with message text are people, it’s worth sending a friendly message, with a proposed solution. A good proposed solution is to suggest they use the site’s contact form. This will usually bypass the spam filters. It’s probably not a good idea to put a URL in the message, since a creative spammer may use it.

Denying access based on HTTP referer

Link: http://httpd.apache.org/docs/2.0/misc/rewriteguide.html#access

To deny access to a site based on the referer URL, you can use Apache rewrite rules.

In the sample below, any referral that ends with .ru, .ua, or .tv will be rejected and redirected to the forbidden page (a 403).

Code:

RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} \.(ru|ua|tv)$ [NC]
RewriteRule ^.*  - [F]

To test the rule, you can use curl, like so:

curl -e ‘http://test.tv’ domain.com

dojo - dijit Form - Create dijits programmatically

Link: http://davidwalsh.name/dojo-widget

This code populates an empty form with a radio button input that has three options. It can be modified to work with check boxes and other dijits.

This is intended to be used with Zend Framework’s Zend_Dojo_Form component, the tags are created using the id naming conventions of Zend, so styling can be applied more easily.

Test data is included, this code can be copied and used as is. It does assume dojo is in the dojo directory. It is coded for dojo 1.6.1 and should work for later versions as well. DOCTYPE is HTML5, although no HTML5 tags are used.

XML:

<!DOCTYPE html><head>  
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<META http-equiv="Content-Style-Type" content="text/css">
<title>dojo Test Code</title>
<style type="text/css">
<!--
    @import "dojo/dijit/themes/tundra/tundra.css";
-->
dl#dl-web_admin_select
{
width:625px;
}
dl#dl-web_admin_select dt
{
font-weight:bolder;
width:550px;
float:right;
}
dd
{
}
</style>
<script type="text/javascript">
//<!--
    var djConfig = {"parseOnLoad":true,"isDebug":true,"locale":"en_US"};
//-->
</script>
<script type="text/javascript" src="dojo/dojo/dojo.js"></script>
<script type="text/javascript" src="dojo/dijit/dijit.js"></script>
<script type="text/javascript" src="dojo/dijit/dijit-all.js"></script>
<script type="text/javascript">
dojo.require("dojo.parser");
dojo.require("dijit.form.RadioButton");
 
// Data to simulate Zend_Dojo_Data response
var data={"identifier":"id","items":[{"id":"1","url":"one.com","account_name":"AccountOne","name":"AccountOne - one.com"},{"id":"3","url":"three.com","account_name":"AccountOne","name":"AccountOne - three.com"},{"id":"2","url":"two.com","account_name":"AccountTwo","name":"AccountTwo - two.com"}]};
 
// Used to store the array of created inputs, so they can be destroyed if replaced
var dijit_input = new Array();
 
dojo.addOnLoad(function(){
    var i=0,input_type;
    // Destroy the HTML
    dojo.destroy('fieldset-web_admin_select');
    // Destroy the dijit widgets
    dijit_input.forEach(function(item){item.destroy()});
 
    fieldset = dojo.create('fieldset',{'id':'fieldset-web_admin_select'},'frmTest');
    dl = dojo.create('dl',{'id':'dl-web_admin_select'},'fieldset-web_admin_select');
    i=0;
    data.items.forEach(function(item){
      label = dojo.create('dt',{'id':'web_admin_select-label_'+i,'innerHTML':item.name},'dl-web_admin_select');
      input = dojo.create('dd',{'id':'dd_web_admin_select_'+i},'web_admin_select-label_'+i,'after');
      selector = dojo.create('input',{'id':'web_admin_select_'+i,'type':'radio','name':'web_admin_select'},'dd_web_admin_select_'+i);
      dijit_input.push(new dijit.form.RadioButton({
        'value': item.id,
        'id': "rb["+i+"]"},
        'web_admin_select_'+i));
      i++;
    });
});
 
</script>
</head>
<body class="tundra">
<h1>dojo Test</h1>
<form data-dojo-type="dijit.form.Form" data-dojo-props="'id':'frmTest','name':'frmTest','method':'post'">
</form>
</body>
</html>

Apache Mod_AutoIndex - IIS/ASP Implementation

Link: http://httpd.apache.org/docs/2.0/mod/mod_autoindex.html

This is a simple directory listing script for IIS, using ASP.

Features

  • Custom CSS for each folder, if available It reads a css directory and if there is a .css file with the requested directory name, it will include a <style> tag for it. For example, if the requested directory listing is forms, it will create a style tag for css/forms.css.
  • Single instance One copy of this code can reside at the top of a directory tree and allow navigation throughout sub folders.
  • Drag and drop content posting The administrator or person who is posting content can drag and drop the files using Windows Explorer, without modifying the script, or asking for help. New content will be displayed as copied.
  • Hidden aware Hidden directories and files aren’t listed, so content can be placed on the server, but not displayed. This includes files named with a leading underscore which are usually not content.
  • Sample CSS A sample CSS file is included

Code:

<!DOCTYPE HTML>
<html>
<head>
<%
Const ReadOnly=1
Const Hidden=2
 
Dim fs,fo,x
Set fs=Server.CreateObject("Scripting.FileSystemObject")
 
Dim strPage,strPageHTML,strPageEncode,i
strPage=Request.ServerVariables("QUERY_STRING")
If (strPage = Null) Then strPage = ""
If (InStr(strPage,".")) Then strPage = ""
strPage=Replace(strPage,"+"," ")
strPageHTML=Server.HTMLEncode(strPage)
strPageEscape=Server.URLEncode(strPage)
%>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<META http-equiv="Content-Style-Type" content="text/css">
<title>Page Title</title>
<base href="http://domain.com/top/" />
<link href="css/style.css" rel="stylesheet" type="text/css" />
<%
If (strPage <> "") Then
  Set fo=fs.GetFolder("D:\docs\top\css")
  For Each x In fo.Files
    i=InStr(LCase(x.Name),LCase(strPage))
    If (i <> 0) Then Response.Write("<link href=""css/" & x.Name & """ rel=""stylesheet"" type=""text/css"" />")
  Next
End If
%>
</head>
<body>
<div id="divHeader">
<a href="#" title="Go to Home Page"><h1 id="title">Page Title</h1></a>
<h2><% Response.write(strPageHTML) %></h2>
</div>
<div id="divMain">
<div id="divFolders">
<ul>
<li><a href="http://docs/">Intranet Home</a></li>
<li><a href="http://docs:8088/default.aspx">Search Intranet</a></li>
</ul>
<ul>
<%
Set fo=fs.GetFolder("D:\docs\top\")
 
For Each x In fo.SubFolders
  'Print the name of all files in the test folder
  If (((x.Attributes And Hidden) = 0) And _
  (Left(x.Name,1) <> "_")) Then
  Response.write("<li><a href='" & Request.ServerVariables("SCRIPT_NAME") & "?" & Server.URLEncode(x.Name) & "'>" & Server.HTMLEncode(x.Name) & "</a></li>")
  End If
Next
%>
</ul>
</div>
<div id="divContent">
<% If (strPage = "") Then %>
<p>Home or top page text.</p>
<% Else %>
<%
Set fo=fs.GetFolder("D:\docs\top\" & strPage)
If (fo.Files.Count > 0) Then
  Response.Write("<ul>")
  For Each x In fo.Files
    If (((x.Attributes And Hidden) = 0) And _
      (Left(x.Name,1) <> "_")) Then
      Response.write("<li><a href='" & strPageEscape & "/" & x.Name & "'>" & Server.HTMLEncode(x.Name) & "</a></li>")
    End If
  Next
  Response.Write("</ul>")
Else
  Response.Write("No files found")
End If
%>
<% End If %>
</div>
</div>
<div id="divFooter">
<span class="float-left bolder">&copy; 2008-<% Response.Write(Year(Now)) %>&nbsp;Company, Inc.</span>
<span class="float-right">This information is for private internal use only.</span>
</div>
</body>
</html>
<%
set fo=nothing
set fs=nothing
%>

Base CSS file

Name this file style.css and place it in the css directory. You may add additional CSS files for each page. The CSS filenames are case-insensitive.

CSS:

*
{
font-family: "Trebuchet MS","sans-serif";
margin:0;
padding:0;
}
body
{
width:1000px;
}
#divHeader
{
height:105px;
width:950px;
padding:25px 25px 0;
background:#fff url(../images/logo.jpg) no-repeat top right;
}
h1
{
color:#000;
}
#divMain
{
border-style:solid;
border-width:1px 0;
border-color:#000;
height:450px;
width:1000px;
}
#divFolders
{
width:200px;
float:left;
background-color:yellow;
height:450px;
overflow:auto;
}
ul
{
list-style-type:none;
padding:5px;
margin:10px;
}
#divContent
{
padding:10px;
width:780px;
float:left;
}
#divFooter
{
background-color:#fff;
padding:15px;
}
.break
{
float:none;
clear:both;
}
.float-left
{
float:left;
}
.float-right
{
float:right;
}
.center
{
margin:0 auto;
}
.bolder
{
font-weight:bolder;
}
p
{
padding:5px;
margin:5px;
}
a
{
color:#000;
text-decoration:none;
}
a:hover
{
text-decoration:underline;
}
a:visited
{
color:#000;
}

:: Next >>