Send Email via Exchange Web Services using Command Line

So I have several batch scripts (CLI) that need to send mail to external addresses, and the only way I can do that is via Exchange.

I remembered Exchange had some Web services built in. I originally made a VBS script, but found that it wouldn’t run when it’s called from a scheduled task. So I wrote a PHP CLI app. All you’d need is a CLI version of PHP with CURL (Xampp would work). I suppose this would work on Linux and OS X too.

All you’d need to change is the endpoint address at the top of the script.

 

<?php
/**
 * Uses sockets to make webmail calls to Webmail
 * 2014 Ben Evans - ben@nebev.net
 */
$base_url = "https://address_of_your_exchange_server/ews/exchange.asmx";
$shortopts  = "";
$longopts  = array(
	"username:",
    "password:",
    "to:",
    "subject:",
	"htmlfile:",
	"cc:",
	"attachment:"
);
$required_options = array("username", "password", "to", "subject", "htmlfile");
$create_message_template = '<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">  <soap:Body>    <CreateItem MessageDisposition="SaveOnly" xmlns="http://schemas.microsoft.com/exchange/services/2006/messages">		<SavedItemFolderId>			<t:DistinguishedFolderId Id="drafts" />		</SavedItemFolderId>		<Items>			<t:Message>				<t:Subject><!--SUBJECT--></t:Subject>				<t:Body BodyType="HTML"><!--MESSAGE--></t:Body>				<t:ToRecipients>					<t:Mailbox>						<t:EmailAddress><!--EMAILADDRESS--></t:EmailAddress>					</t:Mailbox>				</t:ToRecipients>			</t:Message>		</Items>    </CreateItem>  </soap:Body></soap:Envelope>';
$create_attachment_template = '<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"               xmlns:xsd="http://www.w3.org/2001/XMLSchema"               xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"               xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">  <soap:Body>    <CreateAttachment xmlns="http://schemas.microsoft.com/exchange/services/2006/messages"                      xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">      <ParentItemId Id="<!--PARENTID-->" ChangeKey="<!--PARENTCHANGEKEY-->" />      <Attachments>        <t:FileAttachment>          <t:Name><!--FILENAME--></t:Name>          <t:Content><!--FILECONTENTS--></t:Content>        </t:FileAttachment>      </Attachments>    </CreateAttachment>  </soap:Body></soap:Envelope>';
$send_item_template = '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"                xmlns:xsd="http://www.w3.org/2001/XMLSchema"                xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"                xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">  <soap:Body>    <SendItem xmlns="http://schemas.microsoft.com/exchange/services/2006/messages"               SaveItemToFolder="true">      <ItemIds>        <t:ItemId Id="<!--ITEMID-->" ChangeKey="<!--ITEMCHANGEKEY-->" />      </ItemIds>    </SendItem>  </soap:Body></soap:Envelope>';


// Main Program
$options = getopt($shortopts, $longopts);
$options_met = true;
foreach($required_options as $ro) {
	if( !array_key_exists($ro, $options) ) {
		echo "Required option $ro not specified.\n";
		$options_met = false;
	}
}
if( !$options_met ) {
echo "\nUsage: php webmail.php\n";
echo "\t--username=xxx --password=xxx --to=a@x.com --subject=\"Hi there\" \n\t--htmlfile=\"path\\to\\html.htm\"\n\n";
echo "Required Arguments:\n";
echo "\t--username\tYour Username (eg. ben)\n";
echo "\t--password\tYour Password\n";
echo "\t--to\t\tThe email address you want this message sent to\n";
echo "\t--subject\tEmail Subject\n";
echo "\t--htmlfile\tThe HTML file you want to send\n";
echo "\nOptional Arguments:\n";
echo "\t--cc\t\tEmail Address you want to CC to\n";
echo "\t--attachment\tFull filepath to the attachment you want to include\n";
	exit;
}

// Check if the HTML file exists
if(!file_exists($options['htmlfile'])) {
	die("HTML File does not exist");
}
$html_contents = file_get_contents($options['htmlfile']);
$html_contents = str_replace("<", "<", str_replace(">", ">", $html_contents));

// Create the Message
$message_xml = str_replace("<!--MESSAGE-->", $html_contents, $create_message_template);
$message_xml = str_replace("<!--SUBJECT-->", $options['subject'], $message_xml);
$message_xml = str_replace("<!--EMAILADDRESS-->", $options['to'], $message_xml);

// CC (Dodgy)
if( array_key_exists("cc", $options) ) {
	$message_xml = str_replace("</t:ToRecipients>", "</t:ToRecipients><t:CcRecipients><t:Mailbox><t:EmailAddress>". $options['to'] ."</t:EmailAddress></t:Mailbox></t:CcRecipients>", $message_xml); 
}

// Create the item:
$response = get_web_page($base_url, $options['username'], $options['password'], $message_xml);

//Acquire ItemID and ChangeKey
preg_match('/ItemId Id="([^"]+)"/', $response['content'], $item_id); // You want $item_id[1]
preg_match('/ChangeKey="([^"]+)"/', $response['content'], $change_key); // You want $change_key[1]

//Optionally Add attachments
if( array_key_exists("attachment", $options) ) {
	$attachment_xml = $create_attachment_template;
	$attachment_xml = str_replace("<!--PARENTID-->", $item_id[1], $attachment_xml);
	$attachment_xml = str_replace("<!--PARENTCHANGEKEY-->", $change_key[1], $attachment_xml);
	
	if( !file_exists($options['attachment']) ) {
		die("Attachment file specified does not exist!");
	}
	
	$attachment_file_name = basename($options['attachment']);
	$attachment_xml = str_replace("<!--FILENAME-->", $attachment_file_name, $attachment_xml);
	$attachment_xml = str_replace("<!--FILECONTENTS-->", base64_encode(file_get_contents($options['attachment'])), $attachment_xml);
	
	$attachment_response = get_web_page($base_url, $options['username'], $options['password'], $attachment_xml);
	
	//Read the ItemId and ChangeKey from the response - the ItemId should be the same, but the ChangeKey will be different
	preg_match('/RootItemId="([^"]+)"/', $attachment_response['content'], $item_id); // You want $item_id[1]
	preg_match('/RootItemChangeKey="([^"]+)"/', $attachment_response['content'], $change_key); // You want $change_key[1]
	
}

// Send Item
$send_xml = $send_item_template;
$send_xml = str_replace("<!--ITEMID-->", $item_id[1], $send_xml);
$send_xml = str_replace("<!--ITEMCHANGEKEY-->", $change_key[1], $send_xml);

// Check for response
$send_response = get_web_page($base_url, $options['username'], $options['password'], $send_xml);
if($send_response['errno']==0) {
	echo "Sent Message Successfully!";
}



/**
 * Get a web file (HTML, XHTML, XML, image, etc.) from a URL.  Return an
 * array containing the HTTP server response header fields and content.
 * A note that the bloody Exchange Server uses NTLM authentication
 */
function get_web_page( $url, $username, $password, $post )
{
    $options = array(
        CURLOPT_RETURNTRANSFER => true,     // return web page
        CURLOPT_HEADER         => false,    // don't return headers
        CURLOPT_FOLLOWLOCATION => true,     // follow redirects
		CURLOPT_HTTPAUTH	   => CURLAUTH_NTLM,
		CURLOPT_UNRESTRICTED_AUTH => true,
        CURLOPT_ENCODING       => "",       // handle all encodings
        CURLOPT_USERAGENT      => "spider", // who am i
        CURLOPT_AUTOREFERER    => true,     // set referer on redirect
        CURLOPT_CONNECTTIMEOUT => 120,      // timeout on connect
        CURLOPT_TIMEOUT        => 120,      // timeout on response
        CURLOPT_MAXREDIRS      => 10,       // stop after 10 redirects
		CURLOPT_SSL_VERIFYPEER => false,    // Disabled SSL Cert checks
		CURLOPT_USERPWD		   => $username . ":" . $password,
		CURLOPT_HTTPHEADER	   => array('Accept: text/xml','Content-Type: text/xml')
    );
	if(!is_null($post)) {
		$options[CURLOPT_POST] = 1;
        $options[CURLOPT_POSTFIELDS] = $post; 
	}
	
    $ch      = curl_init( $url );
    curl_setopt_array( $ch, $options );
	
    $content = curl_exec( $ch );
    $err     = curl_errno( $ch );
    $errmsg  = curl_error( $ch );
    $header  = curl_getinfo( $ch );
    curl_close( $ch );

    $header['errno']   = $err;
    $header['errmsg']  = $errmsg;
    $header['content'] = $content;
	
	if( $err != 0 ) {
		echo "WARNING: SOAP Error Occured: " . $errmsg . "\n";
	}
	
    return $header;
}

You’d call it by doing something like “C:\xampp\php\php.exe path\to\webmail.php –username=ben –password=………”

It’s not terribly efficient (eg. The XML is stored as a variable), but it works for me! Let me know if anyone finds it useful.

WKHTMLTOPDF / WKHTMLTOIMAGE Fonts on Windows

Up until recently, I’ve been using DomPDF for most of my PDF Generation.

However, it’s full of bugs and the development community doesn’t seem that interested in fixing them – my main gripe is that tables won’t span over multiple pages without failing miserably.

So I gave WKHTMLTOPDF a go. It’s a QT based cross-platform webkit renderer that outputs to PDF or Image. Long story short, WKHTMLTOPDF works a lot better, and is easy to use too. But I couldn’t get fonts to work on my Windows box.

I installed the fonts, referenced them 100 different ways, added a “src:” tag to the CSS. Nothing worked for me.

Then I remembered the @font-face CSS declaration. I tried that while referencing the real path to the file and it worked first time!

@font-face {
font-family: 'Open Sans Regular'; /* Call the font whatever you like here */
src: url('file:///c:/temp/osr.ttf') format('truetype');
}

body{
font-family: 'Open Sans Regular'; /* Refer to the name you declared in the font-face declaration */
font-size: 10pt;
font-weight: 300;
}

Seems obvious now, but I spent a good amount of time getting it to work. Perhaps it will help someone in the future.

Get latest tweets from a user without using OAUTH

So. Twitter updated their API and even the search function won’t return JSON anymore.
Some people just don’t want to have to use OAuth just to search publicly available tweets (fair enough, really).
So my workaround involves using this snippet of PHP. Of course, if you can cache the result of whatever this spits out, that would be more ideal than running it every time – as there’s a lot more data returned than the regular JSON feed used to return.

<?php
date_default_timezone_set("Australia/Sydney");

$username = "nebev";
$url = "https://twitter.com/search?q=from%3A" . $username;
$json = file_get_contents($url);
$tweet_max_count = 5;

$doc = new DOMDocument();
$doc->loadHTML($json);
$xpath = new DOMXpath($doc);
$all_tweets = array();

$tweets = $xpath->query("//*[contains(@class, 'tweet-text')]");
$timestamps = $xpath->query("//*[contains(@class, 'tweet-timestamp')]");

foreach( $tweets as $tweet_text ) {
	if( $tweet_text->tagName == "p" ) {
		$all_tweets[] = array("text" => $tweet_text->textContent);
	}
}
$counter = 0;
foreach( $timestamps as $timestamp ) {
	if( $timestamp->tagName == "a" ) {
		
		foreach( $timestamp->childNodes as $time_span ) {
			$time =  $time_span->getAttribute("data-time");
		}
		$all_tweets[$counter]['timestamp'] = new DateTime( "@" . $time );
		$counter++;
	}
}

$temp_tweet_counter = 0;
foreach($all_tweets as $result) {
	if( $temp_tweet_counter >= $tweet_max_count ) {
		break;
	}
	
	$time = $result['timestamp'];
	$now = new DateTime();
	$interval = $now->diff($time);
	$since_text = "";
        
	if( intval($interval->format("%d")) > 0 ) {
		if( intval($interval->format("%d")) == 1 ) {
			$since_text = "Yesterday";
		}else{
			$since_text = $interval->format("%d") . " days ago";
		}
	}elseif( intval($interval->format("%h")) > 0 ) {
		if( intval($interval->format("%h")) == 1 ) {
			$since_text = "1 hour ago";
		}else{
			$since_text = $interval->format("%h") . " hours ago";
		}
	}else{
		$since_text = "Just now";
	}
        
        // Some Regex to display links, hashtags & @handles
    
$result['text'] = preg_replace("#(^|[\n ])([\w]+?://[\w]+[^ \"\n\r\t< ]*)#", "\\1<a href=\"\\2\">\\2</a>", $result['text']);
$result['text'] = preg_replace("#(^|[\n ])((www|ftp)\.[^ \"\t\n\r< ]*)#", "\\1<a href=\"http://\\2\">\\2</a>", $result['text']);
$result['text'] = preg_replace("/@(\w+)/", "<a href=\"http://twitter.com/\\1\">@\\1</a>", $result['text']);
$result['text'] = preg_replace("/#(\w+)/", "<a href=\"http://twitter.com/search?q=%23\\1\">#\\1</a>", $result['text']);   
        
	echo "<div class='tweet'>\n";
	echo "\t<div class='tweet-text'>" . $result['text'] . "</div>\n";
	echo "\t<div class='tweet-time'>" . $since_text . "</div>\n";
	echo "</div>\n";
	$temp_tweet_counter++;
}

OpenLDAP Groups and PHP

200px-Database-openldapSo I usually use Active Directory when I go to work on any LDAP-based Directory service.

However, I’ve recently started to use OpenLDAP for some things. Unfortunately there’s not a pretty nice Frontend like Microsoft has.

There’s also a slightly different way of looking up groups and adding people to groups than AD. In the code below, I’ve set up a few groups of the Object Type ‘groupOfUniqueNames’.
You start by binding to the directory:

$ldap = ldap_connect("yourserver.example.com", 389);
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
$bind = @ldap_bind($ldap, "CN=admin,DC=example,DC=com", "youradminpassword");

// Lets set a user we want to query
$user = "uid=ben.evans,ou=users,dc=example,dc=com";

To query whether or not $user is part a group:

$result = ldap_search($ldap, "DC=example,DC=com", "(&amp;(objectClass=groupOfUniqueNames)(uniqueMember=". $user ."))", array("dn")) or die ("Error in search query");
$info = ldap_get_entries($ldap, $result);
foreach($info as $i) {
	// At this point, $i will have an array with the key [dn] containing the DN of the group the user is a member of
}

Or to add user to a group:

$group_name = "cn=myspecialgroup,ou=groups,dc=example,dc=com";
$group_info = array('uniqueMember' => $user);
ldap_mod_add($ldap,$group_name,$group_info);

Easy!

Ajaxplorer on Windows Servers Using SMB/Samba

So I’ve just spent 6 hours trying to troubleshoot Ajaxplorer on a Windows box, and I’ve come across a few (very irritating) issues. Hopefully this information might help someone else at some point. The issues were particularly related to $ (hidden) shares and spaces in filenames.

Firstly, in order to get ANYTHING working with SMB, you’re going to have to install smbclient for Windows. Doing this goes against every fiber in my body – installing a Windows client that uses Linux (Cygwin) to access a Windows client. Yuck! But it’s the easiest way – the best instructions are found here.

You should make sure that smbclient is in your PATH. You’ll know if you’ve configured it correctly if you open a new Command Prompt window and type in smbclient and you get the smbclient help screen.

I’m going to assume at this point that you’ve set up your Authentication with LDAP and a Share. I had a hidden share setup (eg: \\server\students$ ).

I’ve noticed that (mostly) if you use a NORMAL share (one without a $ sign), you’ll be able to browse things without an issue! However, I couldn’t.

The solution to this is to open the file in /plugins/access.smb/smb.php and look for the following lines:

        $descriptorspec = array(
            0 => array("pipe", "r"),  	// stdin is a pipe that the child will read from
            1 => array("pipe", "w"),  	// stdout is a pipe that the child will write to
            2 => array("pipe", "w") 	// stderr is a pipe to write to
        );

Change the last element of the array (stderr) to an “a” instead of a “w”. It turns out that sometimes windows hangs when you perform stream_get_contents() on STDOUT blocks when STDERR is filled – but only under some circumstances.

        $descriptorspec = array(
            0 => array("pipe", "r"),  	// stdin is a pipe that the child will read from
            1 => array("pipe", "w"),  	// stdout is a pipe that the child will write to
            2 => array("pipe", "a") 	// stderr is a pipe to write to
        );

OK. Now that that is done, we’ll have to make some other changes. The way that Ajaxplorer downloads your files is quite clever – it uses smbclient to download the files to a temporary directory and then reads that and outputs it to the client. Unfortunately the author of this seems to have hardcoded this line:

$this->tmpfile = tempnam('/tmp', 'smb.up.');

Yea. So. Windows doesn’t have that directory by default. What PHP attempts to do (under normal circumstances) is use the temporary directory you’ve defined in php.ini. But apparently it will use the temp directory as defined by the the environment variable TMP if it’s inside a stream wrapper (which it is).
Normally this wouldn’t be much of an issue, but the default temp directory (C:\windows\temp) isn’t writable by the IUSR user (Server 2008 R2; IIS 7.5).

So, you have two options. Either make the Windows temp directory writable, or (my preferred option), make a C:\tmp directory and give the web server full write access. You can test to see if this process is working by finding and commenting the line:

unlink ($this->tmpfile);

under the __destruct() function and seeing if a temp file is created when you click Download in the Ajaxplorer interface. Also look to make sure the Filesize isn’t 0. Remember to uncomment this once you’re satisfied that it works.

The last problem I had was with spaces. I noticed that smbclient was being called like this:

SMBCLIENT -N -O "TCP_NODELAY IPTOS_LOWDELAY SO_KEEPALIVE SO_RCVBUF=8192 SO_SNDBUF=8192"
   -O "TCP_NODELAY IPTOS_LOWDELAY SO_KEEPALIVE SO_RCVBUF=8192 SO_SNDBUF=8192" 
   -d 0 "//your.file.server/hiddenshare$" 
   -c "dir user_dir\folder with space\*" " -U "domain/username%password"

Unfortunately that folder with a space doesn’t really work that well with this command. I’m not sure if this is Cygwin or the command line or anything, but it just wouldn’t work for me. I looked again at the smb.php file and found the following function:

function execute ($command, $purl) {
        return smb::client ('-d 0 '
              . escapeshellarg ('//' . $purl['host'] . '/' . $purl['share'])
              . ' -c ' . escapeshellarg ($command), $purl
        );
    }

When it comes down to it, $command is actually ‘dir “user_dir\folder with space\*” ‘ which should work. But escapeshellarg (at least on my system) stripped those lovely double-quotes. So I changed it to this:

	/**
	 * Windows has an unfortunate tendency to not work as you want it with Spaces in Directories or files
	 * @param string $string
	 * @return string Escaped Argument
	 */
	function windowsEscapeShellArg($string) {
		$string = str_replace('"', "|", $string);	// We can use Pipe because Windows doesn't allow it in Filenames
		$string = escapeshellarg($string);
		$string = str_replace("|", '\"', $string);
		return $string;
	}
	
    function execute ($command, $purl) {
        return smb::client ('-d 0 '
              . escapeshellarg ('//' . $purl['host'] . '/' . $purl['share'])
              . ' -c ' . $this->windowsEscapeShellArg($command), $purl
        );
    }

Hacky? Hell yes! Does it work? Also yes… If you’ve got better ideas, I’d love to hear them.

After all of that, I was finally able to access and download all files from a Windows SMB share ON A WINDOWS MACHINE!

Zend Server 6 on Mac OSX 10.8 Mountain Lion

So today I thought I’d try out the new Zend Server 6.
Here are the problems I’ve encountered so far:

Zend Framework 1:

I was originally hoping that the new installation of the new Zend Server would bring about the latest version of ZF1. Nope. The bugs I wanted fixed in 1.12.2 (Namely a bug with SoapFault) were still there. My solution was to download the package and replace the /usr/local/zend/share/ZendFramework directory with the new 1.12.2 contents. I know it’s not that hard, but the whole idea of using this Zend Server package is so that you don’t have to do these things!

MySQL:

Well. In theory, MySQL Works. That is, unless you’re using MySQL Workbench. What happens is the process of fetching tables crashes with the error:

Cannot load from mysql.proc. The table is probably corrupted

Not happy.

The general consensus around the web is that it will go away if you use the mysql_upgrade command. Turns out it doesn’t appear to work in this case. My Solution was to install the much nicer and newer MySQL 5.6 directly from the MySQL site. Installation went off without a hitch, and doesn’t interfere with Zend’s instance of MySQL. The bonus part is that MySQL doesn’t run automatically when Zend Starts up, so you won’t have any conflicts! You’ll also have Geospatial and other neat things if you install 5.6. Again, easy workaround, but you really shouldn’t have to do this.

Mail:

Oh come on! This clearly wasn’t even tested. Mail just doesn’t work on Mountain Lion. I know it’s a Development machine I use, but this is a pretty basic feature!

Anyway, Zend threw an Exception when sending mail and told me “Unable to send mail.”

Upon closer inspection of the logs (very prettily displayed in the new Zend Server Interface), I see this message:

dyld: Library not loaded: /usr/lib/libsasl2.2.dylib
Referenced from: /usr/sbin/sendmail
Reason: Incompatible library version: sendmail requires version 3.0.0 or later, but libsasl2.2.dylib provides version 0.0.0

Well that was helpful. I downloaded the XCode Command-Line tools and used otool to see if my suspicions were correct.

Vader:lib ben$ otool -L /usr/lib/libsasl2.2.dylib
/usr/lib/libsasl2.2.dylib:
/usr/lib/libsasl2.2.dylib (compatibility version 3.0.0, current version 3.15.0)
/usr/lib/libcrypto.0.9.8.dylib (compatibility version 0.9.8, current version 47.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 169.3.0)
Vader:lib ben$ otool -L /usr/local/zend/lib/libsasl2.2.dylib
/usr/local/zend/lib/libsasl2.2.dylib:
/usr/local/zend/lib/libsasl2.2.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 159.1.0)
/usr/lib/libresolv.9.dylib (compatibility version 1.0.0, current version 46.1.0)

Yep. As you can see, Zend uses its own (broken) version of libsasl2.2.dylib. Solution?

sudo mv /usr/local/zend/lib/libsasl2.2.dylib /usr/local/zend/lib/libsasl2.2.dylib.old
sudo ln -s /usr/lib/libsasl2.2.dylib /usr/local/zend/lib/libsasl2.2.dylib

Elegant? No. Harmful? Not sure. Does it work? Yes.

Checking Variables using PHPDoc in Zend Framework

EDIT: Yes guys. I know PHP isn’t Java. I mainly made this because I have a lot of legacy code to maintain that often dies if it doesn’t have the right input. The same code can be called by many different methods in many different controllers. It’s not ideal, no. It’s not fast, no. God help you if you use it in production. But it’s made my life a little easier.

The thing I don’t like about PHP is that you can’t specify primitives in function declarations. It’s annoying. So something like this has the potential to fail miserably:

 

function addMe($x, $y) {
   return $x + $y;
}

echo addMe(3, 9); // No Problems
echo addMe(new stdClass(), 22); // Yea. Good luck

 

Often these things fail miserably, or worse, have an unexpected result that can cause data integrity headaches later on.
Enter the wonderful world of PHPDoc. PHPDoc makes PHP functions sort of LOOK like Java methods, in that you can do the following:

/**
* Adds two integers
* @param int $x
* @param int $y
* @return int
*/
function addMe($x, $y) {
   return $x + $y;
}

This is nice in theory – at least now the programmer knows that they should be passing 2 integers to this function, and expecting an integer back. However, it really doesn’t stop me putting in whatever I like. We also have issues with arrays. Take the following example:

 

/**
* Adds all elements of the array together and returns the sum
* @param multitype:int
* @return int
*/
function addArrayElements(array $a) {
   $sum = 0;
   foreach( $a as $a_element ) {
      $sum = $sum + $a_element;
   }
}

echo addArrayElements( array(4, 7, 6, 3) ); // No Problems
echo addArrayElements( array(4, new stdClass(), 6, 3) ); // Not as expected

Notice above that I’ve put in the lesser-used PHPDoc “multitype”. This essentially means that I’m expecting an array, but that each of the array elements is supposed to be an integer. Again, nice in theory, but there’s nothing stopping me putting in garbage into an array as opposed to integers.

We also have the scenario of where we might want to accept a couple of different variable types (but say limit it to two). For example, we could be outputting a date, and accept an integer or DateTime object as an input. Traditionally this was done with the mixed keyword, but PHPDoc (or Eclipse’s implementation of it anyway) allows you to separate valid types using the boolean | operator. eg:

/**
 * Outputs the date in YY-MM-DD format
 * @param int|DateTime $date_var
 * @return string
 */
function showDate( $date_var ) {
   if( is_object($date_var) ) {
      return $date_var->format("Y-m-d");
   }else{
      return date("Y-m-d", $date_var);
   }
}

echo showDate(new DateTime()); // No Problems
echo showDate( strtotime("now") ); // No Problems
echo showDate(new stdClass()); // Error.

Again, the Docblock tells us what to expect, but we don’t really get the Opportunity to validate our inputs in any way. Sure, we can right validation code right into our functions (and I did this for a long time for many many functions), but it’s quite impractical, especially when we’re just writing code to validate something that is easy-to-edit and easy-to-write, like PHPDoc.

So What’s the solution? PHP has an interesting quirky feature called Reflection that sort of allows you to reverse engineer classes. This is pretty neat.
I also use the Zend Framework, which have taken some of these Reflection features to the next level – specifically, it allows you to easily parse PHPDoc in classes.

So I developed my own class that essentially allows you to validate your inputs against the function’s PHPDoc. It’s not particularly fancy, but it does allow you to ensure that your variables are pretty much as expected with just one line. It can either return boolean FALSE, or throw a general Exception, which you can catch somewhere else… Code:

class Model_Helper_Verification {
	
	/**
	 * Checks the PHPDocBlock for errors and returns false or throws an exception
	 * @param string $method Method name in class::method format
	 * @param array $arguments Method Arguments
	 * @param boolean $exceptions Whether or not you want to throw an Exception on Error
	 * @param boolean $debug Whether or not you want detailed Debug messages when checking parameters (stdOut)
	 * @throws Exception
	 * @return boolean True if all is OK. False if type checking failed
	 */
	public static function checkPHPDocArguments( $method, $arguments, $exceptions = false, $debug = false ) {
		$method_elements = explode("::", $method);
		$reflection = new Zend_Reflection_Method($method_elements[0], $method_elements[1]);
		$function_parameters = $reflection->getParameters();
		$docblock = $reflection->getDocblock();
		$tags = $docblock->getTags("param");
		
		for( $counter = 0; $counter < sizeof($tags); $counter++ ) {
			$tag = $tags[$counter];/* @var $tag Zend_Reflection_Docblock_Tag_Param */
			if( array_key_exists($counter, $arguments) ) {
				
				// Sometimes a function definition is optional, and we pass the Default Value
				// eg. public function($value = null)
				$function_parameter = $function_parameters[ $counter ]; /* @var $function_parameter Zend_Reflection_Parameter */
				if( $function_parameter->isOptional() ) {
					if($function_parameter->getDefaultValue() === $arguments[$counter]) {
						// We've passed the default value (this doesn't have to be in the DocBlock)
						continue;
					}
				}
				
				$result = self::phpdoc_check($arguments[$counter], $tag, $debug);
				if( $result === false ) {
					if( $debug ) {
						echo $tag->getVariableName() . " is supposed to be of type " . $tag->getType() . ", but isn't\n";
					}
					if( $exceptions ) {
						throw new Exception("Type Checking Error for variable " . $tag->getVariableName(), 3000);
					}
					return false;
				}
			}
		}
		return true;
	}

	
	/**
	 * Checks against a Method's PHPDocBlock
	 * @param mixed $variable_value
	 * @param Zend_Reflection_Docblock_Tag_Param $tag
	 * @param boolean $debug
	 * @return boolean
	 */
	private static function phpdoc_check( $variable_value, Zend_Reflection_Docblock_Tag_Param $tag, $debug = false ) {
		$type = $tag->getType();
		if( strtolower($type) == "bool" || strtolower($type) == "boolean" ) {
			return is_bool($variable_value);
		}
		if( strtolower($type) == "null" ) {
			return is_null($variable_value);
		}
		if( strtolower($type) == "string" ) {
			return is_string($variable_value);
		}
		if( strtolower($type) == "int" ) {
			return is_int($variable_value);
		}
		if( strtolower($type) == "array" ) {
			return is_array($variable_value);
		}
		if( strtolower($type) == "number" ) {
			return is_numeric($variable_value);
		}
		if( strtolower($type) == "mixed" ) {
			return true;	// Obviously don't want checks
		}
		
		// Do the ORs
		if( strpos($type, "|") !== false ) {
			$exploded_types = explode("|", $type);
			foreach($exploded_types as $et) {
				$new_tag = new Zend_Reflection_Docblock_Tag_Param( "@param " .  $et . " " . $tag->getVariableName() . "_alternate_element" );
				$result = self::phpdoc_check($variable_value, $new_tag, $debug);
				if( $result === true ) {
					return true;	// Satisfied at least one of the critera
				}
			}
			if( $debug ) {
				echo "Encountered a Docblock with Boolean ORs. None satisfied the conditions\n";
			}
			return false;	// None of the required types worked
		}
		
		// Multitypes		
		if( strlen($type) > 9 &amp;amp;&amp;amp; substr( strtolower($type), 0, 10 ) == "multitype:") {
			$sub_type = substr($type, 10);
			if( is_array( $variable_value ) ) {
				$new_tag = new Zend_Reflection_Docblock_Tag_Param( "@param " . $sub_type . " " . $tag->getVariableName() . "_array_element" );
				foreach( $variable_value as $val ) {
					$result = self::phpdoc_check($val, $new_tag, $debug);
					if( $result == false ) {
						return false;
					}
				}
				return true;	// Went through all elements. They were as expected
			}
			if($debug) { "Encountered a Multitype, but variables weren't in an array"; }
			return false;
		}
		
		// Only thing left is an object
		$return = (is_object($variable_value) &amp;amp;&amp;amp; is_a($variable_value, $type));
		if( $debug &amp;amp;&amp;amp; !$return ) {
			echo "Wanted type: ". $type . ". Object: ";
			echo is_object($variable_value);
			if( is_object($variable_value) ) {
				echo ". Type Found: " . get_class($variable_value);
			}
			echo "\n";
		}
		
		return $return;
	}	
}

So how do you call this terrible piece of code, I hear you ask? Simple:


class demo{
   /**
    * @param DateTime|int
    * @return string
    */
   public static function showDate($date_var) {
      Model_Helper_Verification::checkPHPDocArguments(__METHOD__, func_get_args(), true); // Checks arguments for errors, and throws exception
      if( is_object($date_var) ) {
         return $date_var->format("Y-m-d");
      }else{
         return date("Y-m-d", $date_var);
      }
   }

   
   /**
    * Adds all elements of the array together and returns the sum
    * @param multitype:int
    * @return int
    */
   function addArrayElements(array $a) {
      Model_Helper_Verification::checkPHPDocArguments(__METHOD__, func_get_args(), true); // Checks arguments for errors, and throws exception
      $sum = 0;
      foreach( $a as $a_element ) {
         $sum = $sum + $a_element;
      }
   }

}

So that’s it! Check any function’s parameters and be sure that they’re correct before you do something silly, like write them to a database. Hope someone finds this useful.

Installing Zend CE on Mac OS X

I develop applications using the Zend Framework all day at work. My primary environment is OS X using Zend CE. I like the fact that it’s an all-in-one app from Zend that comes with everything I need (including Zend Debugger).

However, every time I go to set it up again, I forget exactly what it was that I had to do to get Zend CE working as expected. The installation “just works” for Windows, but I’ve found it really needs some tweaking to get it to work nicely in OS X. So I’m writing this here mostly for my own benefit. Maybe it might benefit you too. Keep in mind I’m setting this up for a development environment. If you’re wanting to use it for production, you’re better off going and finding someone who knows what they’re talking about.

We start, fairly obviously by downloading the latest Zend CE from Zend.

 

Might be worth noting that even though Zend says you MUST login to register, if you give some bogus info a couple of times it asks if you want to “download without registering”. Yes please.

Open the DMG, and follow the installer. There are no options you can choose anyway. Pretty standard stuff.

Once things have installed, you’ll see a Zend Server “App” in your applications Directory. It’s nothing special, and essentially redirects you to the web page http://localhost:10081/ - Go there and step through the wizard.

Now we get to the interesting parts. You may 0r may not be aware that MySQL in Zend CE doesn’t allow connection via TCP, but rather via a socket. I have apps that are far too stupid to be able to deal with that; plus I like having the option to connect via other machines if needed. So I’m going to change that.

Fire up a Terminal session and type in:

 sudo nano /usr/local/zend/mysql/data/my.cnf

Go to around line 45, and look for the line that says “skip_networking”. Comment it out by putting a # in front of it. Alternatively you can skip this step and just connect to the socket /usr/local/zend/mysql/tmp/mysql.sock
Now write the file (CTRL + O; Enter) and exit (CTRL + X).

Now lets actually start MySQL. Lets run

sudo /usr/local/zend/bin/zendctl.sh stop-mysql
sudo /usr/local/zend/bin/zendctl.sh start-mysql

OK. Now, if you’re confortable with putting your Application in /usr/local/zend/apache2/htdocs and accessing it at the URL http://localhost:10088 you can stop here. However, I personally like things on Port 80 and using my own directories. Just a matter of preference of course.

Next thing I’m going to do is edit Apache’s configuration. So lets run:

sudo nano /usr/local/zend/apache2/conf/httpd.conf

Scroll down a bit until you find the line that says Listen 10088. Just add another line saying Listen 80. eg:


# Listen: Allows you to bind Apache to specific IP addresses and/or
# ports, instead of the default. See also the <VirtualHost>
# directive.
#
# Change this to Listen on specific IP addresses as shown below to
# prevent Apache from glomming onto all bound IP addresses.
#
#Listen 12.34.56.78:80
Listen 10088
Listen 80

Write the changes, and restart Apache.

sudo /usr/local/zend/bin/apachectl restart

Ok. Now we probably have the same crappy demo website at http://localhost/ as we do at http://localhost:10088/ – great. If you don’t want to run multiple web apps on the same host and you’re happy putting things in the htdocs directory, leave now.

Lets set up some basic DNS so we can have multiple sites hosted by our local machine at different addresses. For example, I might want the address http://rqz.development.com to point to the app housed in ~/repositories/rqz. I might also want the address http://blog.development.com to point to the app housed in ~/repositories/blog. Using these examples, lets edit our hosts file:

sudo nano /etc/hosts

Now I’m going to add the following lines to ensure that both these addresses resolve to my local machine:


127.0.0.1 rqz.development.com
127.0.0.1 blog.development.com

Write the changes, and try to ping one of those addresses. It should resolve to 127.0.0.1.

Now, all of Apache’s configuration files for Zend CE live in /usr/local/zend/etc/sites.d/
The files that live in here won’t (by default) actually be included unless you have the file starting with vhost_ and ending with .conf. So lets try this:

sudo nano /usr/local/zend/etc/sites.d/vhost_rqz.conf

My Zend application is stored in /Users/ben/repositories/rqz and is a traditional Zend 1 app (so the Web Server base should be /public). So I’m going to write the following in that file:


# Listen for virtual host requests on all IP addresses
NameVirtualHost *:80

<VirtualHost *:80>
DocumentRoot /Users/ben/repositories/rqz/public
ServerName rqz.development.com

<Directory /Users/ben/repositories/rqz/public/>
Options FollowSymLinks MultiViews
AllowOverride None
Order allow,deny
allow from all

# Rewrite for search engine friendly URLs
RewriteEngine On

# Rules
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*) index.php
</Directory>
</VirtualHost>

Now restart Apache ( sudo /usr/local/zend/bin/apachectl restart ) and navigate to the server you specified. In my case, http://rqz.development.com – you should see the appropriate app (in my case the app located in /Users/ben/repositories/rqz/public

If I wanted to add my other site (blog.development.com), I’d simply replicate what was in the <VirtualHost> into another file (eg: /usr/local/zend/etc/sites.d/vhost_blog.conf), and change the appropriate paths. Easy!

Randomised Quiz Software for Programming

During my time at University, I had to do many assignments. The most interesting (as part of my Master of Information Technology at Macquarie Univeristy) was developing a Randomised Quiz System for Programming. The motivation behind it was simple:

  • Students plagiarise. A lot. Practical Exercises are given to every student every week, but since the answers to these questions are essentially the same (and easy to copy), students will share their work.
  • Students also don’t get enough practice doing questions. Lecturers will teach a concept, but not have time to go through making hundreds of variations on the same concept.
  • Students work best when they work at their own pace.

So taking all these things into account, I developed a program where you could put in your Question Template into an XML file, specify which bits are to be randomised, and consequently generate random questions for each student – eliminating the ability to copy from classmates.

Generally speaking, the program has 3 types of questions:

  • Read and Interpret (essentially like putting something into a compiler and giving the output)
  • Multiple Choice Questions based on Reading and Interpreting
  • Having questions where you ‘fill-in’ pieces of code to make the program execute as instructed in the question directive.

The third presents many interesting security concerns (many of which I didn’t bother to pay attention to due to time constraints), but the rest are quite interesting (and work quite well). I finished this project, and handed it in. If I recall correctly, I scraped in a High Distinction.

Over a year later, I was approached by a good friend who was to lecture an Undergraduate Computing unit. We got talking, and discussed the project I had completed in 2010. I told him that it really wasn’t “up to scratch” (I didn’t really know even what a Framework WAS back then), but that I’d rewrite it a little so that it at least conformed to a loose MVC architecture.
Several months later, I completed the bulk of this work, and after some collaboration with the University, I’ve decided to release my work as Open Source (GPL3) in the interest of helping the community.

I’m a bit embarrassed with a lot of the code (it’s funny how much you learn within a couple of years), but it (mostly) works.
If you’re interested, check out the original proposal here or the code on github here. If you like/hate it, leave a comment below or email me.
Even better, if you want to contribute please get in touch!

Simple LDAP (OpenLDAP) Password Changer in PHP

I was having issues trying to get a PHP script to change a password for a user in OpenLDAP. I wrote the following script. Hopefully it’s useful to someone. Parts taken from http://snippets.dzone.com/posts/show/4059

<?php

$ldap_server = "example.com.au";

$username = "test.user";
$old_password = "test";
$dn = "ou=testou,dc=example,dc=com,dc=au";
$new_password = "123456";
$user_dn = "uid=" . $username . "," . $dn;

$ldap = ldap_connect($ldap_server, 389);
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);

if ($ldap){

try{
$bind = @ldap_bind($ldap, $user_dn, $old_password);
if( !$bind ) {
throw new Exception();
}

}catch(Exception $e) {
@ldap_close($ldap);
throw new Exception("Incorrect Current Password");
}

if( $bind ) {

$filter = "uid=" . $username;
$ldap_result = ldap_list($ldap, $dn, $filter);
$info = ldap_get_entries($ldap, $ldap_result);

//print_r($info); die();

if( $info['count'] == 1 ) {

$userpassword = "{SHA}" . base64_encode( pack( "H*", sha1( $new_password ) ) );
$userdata = array( "userpassword" => array( 0 => $userpassword ) );
$result = ldap_mod_replace($ldap, $user_dn , $userdata);

if ($result) echo "Your password has been changed!" ;
else echo "There was a problem changing your password, please call IT for help";

}

}else{
echo "Invalid Password.";
}

}

@ldap_close($ldap);
?>