« Posts under php

Yii Rights Extension – Error 403. There must be at least one superuser!

The Problem

I am new to yii and wanted to use a php framework for a new personal site.  After reading a bunch and my own experience with CakePHP and CodeIgnitor I decided to try something new and Yii seemed like a good choice.

It seems pretty well organized and has tons of extension already.  Notably I was interested in their User Management plugins and social integration.  This led me to yii extensions: Rights, yii-user.

I installed yii-user without too much trouble, but still was not perfect and documentation for a newb was crappy to say the least.  But fumbled my way thought it with an hour or 2.  Still better than cakePHP ACL (but not as powerful).

So I needed an ACL (Access Control Lists), which led me to Rights extension.  Which seemed easy enough to install, but quickly got to:

Error 403.  There must be at least one superuser!

 Solving Error 403.  There must be at least one superuser!

What is not told by this error is this is actually a sql error due to constraint violations.  Unfortunately all these errors are nicely captured and completely hidden.  EVEN IN DEBUG MODE!!!!  Why the hello would anyone do that!

So after 1.5 hours of trying to find where the problem was I go to RInstaller->run() method.  Which i stepped thought the code at this point and found the error.

The easiest solution was to comment out the foreign key contrains in the data/schema.sql file.

NOTE: I also Commented this following out in RAuthorizer.php, but couldn’t tell you if this is necessary.

if( $superusers===array() )
throw new CHttpException(403, Rights::t(‘core’, ‘There must be at least one superuser!’));

Simple OAI Repository in PHP

Getting Started

If you don’t know what an OAI repository is, then go here.

If you just want the code, check it out here at github.

So a couple of months ago I needed to test against an OAI repository.  Basically we have an OAI harvester, but were having issues with a customer’s repository.  The main problem is the repository in question would fail deep into the harvest.  So I wanted to pinpoint where this error happens during the harvest, but the logs around the error were no help.  The errors were trapped and we use a 3rd party harvester jar, so I couldn’t easily get better error messaging.  So what was the error?

  • Is it bad XML?
  • Bad UTF-8?
  • Some other error?

Now as a java shop someone had written a clojure oai simulator, but if you have ever written some clojure you may feel my pain.  Im not against functional languages, but really the dependency management is a huge pain and java compounds these problems.  So it uses lein for this, but when this stopped working my clojure world came to an end.  Hence I decided to write a quick php example.

PHP Implementation

So we need to implement the basic functionality:

  • Identify
  • ListRecords
  • ListSets
  • Error Handling

The basic requests work on the URL var “verb”.  For example http://localhost/my-oai/oai.php?verb=Identify

Getting URL vars in PHP

Anytime you use variables submitted by users, you should be careful.  So lets make a utility class that will handle some of the basic functionality we will be using.

<?php
class Utility{
	/**
	 * Wrapper for getting a URL variable.  Gurantees a string is returned.  Either
	 * the variable requested or '' if the URL variable doesnt exist.
	 */
	public static function myGet($index){
		return (isset($_GET[$index])) ? $_GET[$index] : '';
	}
}
?>

Now we made things static so it is a little easier to use. And as we require more helper functions in the future we can easily place them here. Really all we did was protect ourselves from accessing an index that doesn’t exist.

Now that we have a safe GET command, how should we set things up?  Since we have several actions that will do certain things, this sounds like we should use switch() statement.  Here is the start of my oai.php file.

<?php
//we are returning xml, so we need to set the header
//for "Content-Type"
header("Content-type: text/xml; charset=utf-8");

require_once 'Utility.php';

//handle the verb
$verb = Utility::myGet('verb');
$set = Utility::myGet('set');

/**
 * Handle the verbs
 */
switch($verb){
	case 'resumptiontoken':

	case 'ListRecords':
		break;

	case 'Identify':
		break;

	case 'ListMetadataFormats':
		break;

	case 'ListSets':
		break;

	default:
		break;		
}
?>

So based on these actions we need to display some xml. We could do this via building an XML document using PHP, but really this is overkill. The approach we will take is storing the XML as strings and doing some basic variable replacement. For this we can use PHP sprintf.

How to Structure our Verb / Actions

So as we discussed everything will use this sprintf command, but we want to extend the functionality a little.  So we will create a base class which has this shared functionality so we can extend it and re-use the functionality.  And to be a little over the top I included an interface.

<?php
interface Verb{
	public function doVerb();
}

class Base{
	public function getRequest(){
		$prot = ($_SERVER['SERVER_PORT'] === '443') ? 'https://' : 'http://';
		return $prot . $_SERVER["SERVER_NAME"] . htmlentities($_SERVER['REQUEST_URI']);
	}
}

class VerbBase extends Base{

	public function sprintf($args){

		$args = func_get_args();
		$file = array_shift($args);

		$str = file_get_contents($file);

		if(empty($str) || $str < 0)
			throw new Exception('Failed to read file ('.$file.').');

		array_unshift($args, $str);

		return call_user_func_array('sprintf', $args);
	}
}
?>

Implementing Our Verbs

Lets create a ListRecords.php file which implements our Verb interface and extends our VerbBase class.

<?php

/**
 * List Sets class
 */
 require_once 'Verb.php';

 class ListSets 
		extends VerbBase
		implements Verb{

	public function ListSets(){
		//empty
	}	

	public function doVerb(){
		$sets = "";
		foreach(scandir('sets') as $file)
			if($file[0] !== '.' && $file !== 'defaults')
				$sets .= "\n\t<set>\n\t\t<setSpec>$file</setSpec>\n\t\t<setName>Set $file</setName>\n\t</set>";

		return $this->sprintf('sets/defaults/listsets/listsets.xml', date(DATE_ATOM), $sets);
	}
 }

?>

As you can see the doVerb() implementation scans a directory listing called ‘sets’ and creates a string.  We then use our sprintf implementation to create the xml.

Our Remaining Verbs

Since the remainder of the verbs do basically the same thing we can use a single class to perform the rest.  I named the file ListRecords.php but this is misleading.

<?php

/**
 * List records class
 */
 require_once 'Verb.php';

 class ListRecords 
		extends VerbBase
		implements Verb{

	private $rawSet;
	private $set;
	private $setDir = 'sets/';

	public function ListRecords($set = null){
		$this->rawSet = $set;
		$this->set = $this->setDir . ((empty($set)) ? 'badset' : $set);
	}

	public function doVerb($resumption = null){
		//set doesnt exist
		if(!is_dir($this->set))
			return $this->doBadSet();

		$loadFiles = array();
		$files = scandir($this->set);

		foreach($files as $file){
			if($file[0] !== '.' && substr($file,-3) === 'xml'){
				array_push($loadFiles, $file);
			}
		}

		//no xml to load
		if(count($loadFiles) <= 0)
			return $this->doBadSet();

		$key = 0;
		if(!empty($resumption)){
			$parts = explode(':',$resumption);
			$key = $parts[1];
		}

		$content = file_get_contents($this->set . '/' . $loadFiles[$key]);

		//for convienence we remove any resumption tokens
		$content = preg_replace('/\<resumptionToken\>.*\<\/resumptionToken\>/i','',$content);
		$content = preg_replace('/\<resumptionToken[\s+]?\/\>/i','',$content);

		if(count($loadFiles) > 1){		
			$key++;

			//remove all end stuff
			$content = str_replace('</ListRecords>','',$content);
			$content = str_replace('</OAI-PMH>','',$content);

			$content .= ($key < count($loadFiles)) ? "<resumptionToken>{$this->rawSet}:$key</resumptionToken>" : '<resumptionToken />';

			$content .= "</ListRecords></OAI-PMH>";

		}

		return $content;
	}

	private function doBadSet(){
		$url = $this->getRequest();
		return $this->sprintf('sets/defaults/badset/badset.xml', date(DATE_ATOM),$this->set,$url);
	}

 }

?>

The only part that is unique to ListRecords verb is the resumption code logic.  It may have made send to try and split this out a little better, but I didn’t see a clear way to do this that didn’t result in a lot of code duplication.

The End Result

So if you remember the oai.php we can easily fill in our code now to get the following.

<?php
header("Content-type: text/xml; charset=utf-8");

require_once 'Utility.php';

//handle the verb
$verb = Utility::myGet('verb');
if(isset($_GET['resumptionToken']))
	$verb = 'resumptiontoken';

$set = Utility::myGet('set');

/**
 * Handle the verbs
 */
switch($verb){
	case 'resumptiontoken':
		$parts = explode(':', Utility::myGet('resumptionToken'));
		$set = $parts[0];

	case 'ListRecords':
		require_once 'ListRecords.php';

		$verb = new ListRecords($set);
		echo $verb->doVerb(Utility::myGet('resumptionToken'));
		break;

	case 'Identify':
		require_once 'ListRecords.php';

		$verb = new ListRecords('defaults/identify');
		echo $verb->doVerb();
		break;

	case 'ListMetadataFormats':
		require_once 'ListRecords.php';

		$verb = new ListRecords('defaults/listmetadataformats');
		echo $verb->doVerb();
		break;

	case 'ListSets':
		require_once 'ListSets.php';

		$verb = new ListSets();
		echo $verb->doVerb();
		break;

	default:
		require_once 'Verb.php';

		$verb = new VerbBase();
		$url = $verb->getRequest();
		echo $verb->sprintf('sets/defaults/badrequest/badrequest.xml', date(DATE_ATOM), $url);
		break;

}

?>

So you can see we have a very simple clear oai.php file which handles our basic required actions.  Enjoy and ask any questions if you have any.

So Why did I Bother?

There are already other Open Source OAI implementations out there.  And some of them see good.  But what I found was they were overly complicated and were intended to be a “real” repository that people wanted to hook up to a database and have all kinds of admin functionality.  So I decided I needed a real bare bones repository that met my needs.

Hopefully this saves someone else time out there.  You can download all the code and some more utility functionality than what I discussed in the article at github.