Automating SirsiDynix Patron Login

I want to build systems around the information that’s stored about me and the materials I’ve borrowed in the Provincial Library Service’s online system, but the SirsiDynix used for this doesn’t have any hooks to allow me to do this, so I’ll have to build my own.

By way of doing this, I need to understand to “login” to my library account in SirsiDynix from a script. Here’s what I’ve learned.

How SirsiDynix Authentication Works

The link SirsiDynix system from the front page of the Provincial Library Service’s website goes to:

http://library.pe.ca/catalogue-en

This URI is a 302 redirect to:

http://24.224.240.218/

which itself, in turn, gets 302 redirected to:

http://24.224.240.218/uhtbin/cgisirsi.exe/x/x/0/49/

This is the last URI in the journey that doesn’t contain parameter related to my “session”: this URI sends my browser a session_security cookie with a value of 259310010 and then 302 redirects me to a URI that has this value embedded in the ps parameter:

http://24.224.240.218/uhtbin/cgisirsi.exe/?ps=3SoTIEJTW9/PLS/259310010/38/1/X

which sends me a session_number cookie with the same value of 259310010 and then 302 redirects me to:

http://24.224.240.218/uhtbin/cgisirsi.exe/?ps=wyIB2k9pHm/PLS/259310010/60/1180/X

Entering my library card barcode and PIN in the header and clicking “Login” does an HTTP POST to:

http://24.224.240.218/uhtbin/cgisirsi.exe/?ps=8N5QS6vOI2/PLS/259310010/303

sending the card number as the user_id parameter and the PIN as the password parameter.

The response to this POST sets cookies of the same name (user_id and password) and updates the session_security and session_number cookies with a new value.

In the above URIs, the initial part of the ps parameter – 8N5QS6vOI2 for example – appears to be random and meaningless and I found that it could be changed or the same value used all the time without ill effect.

Automating SirsiDynix Authentication

With the above reverse-engineering, we have enough to automate the login process using PHP and cURL.

First, let’s set some variables that we’ll use later:

$user_id = "XXXXXXXXXXXXX"; // replace with your card number
$pin = "YYYY"; // replace with your PIN
$baseurl = "http://24.224.240.218"; // root URI of SirsiDynix server

The first task is to start a SirsiDynix session, and to grab the value of the session_security cookie that we’re sent back; we do this with cURL:

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseurl . "/uhtbin/cgisirsi.exe/x/x/0/49/");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_COOKIESESSION, true);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_COOKIEFILE, "/tmp/sirsidynix-cookies.txt");
curl_setopt($ch, CURLOPT_COOKIEJAR, "/tmp/sirsidynix-cookies.txt");
$result = curl_exec($ch);

preg_match('/^Set-Cookie:\s*([^;]*)/mi', $result, $m);
parse_str($m[1], $cookies);
$session_security_cookie = $cookies['session_security'];

The key ingredients here are the setting of the CURLOPT_HEADER open to true (so that we can parse the cookie out of the headers), and setting the CURLOPT_COOKIEFILE and CURLOPT_COOKIEJAR options so that subsequent requests will pass back the same cookies.

With the value of $session_security_cookie set, we can now login by doing an HTTP POST:

$loginurl = $baseurl . "/uhtbin/cgisirsi.exe/?ps=8N5QS6vOI2/PLS/" . $session_security_cookie . "/303";
$ch = curl_init(); 
curl_setopt($ch, CURLOPT_URL, $loginurl); 
curl_setopt($ch, CURLOPT_POSTFIELDS, "user_id=$user_id&password=$pin"); 
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
curl_setopt($ch, CURLOPT_COOKIEFILE, "/tmp/sirsidynix-cookies.txt"); 
curl_setopt($ch, CURLOPT_COOKIEJAR, "/tmp/sirsidynix-cookies.txt"); 
$result = curl_exec($ch);

That’s it: we’re now “logged in” to SirsiDynix, and we can use this session to interact with the system as though we were using a web browser.

Retrieving Checked Out Items

One thing we might want to do now that we’re “logged in” is retrieve a list of the items we have checked out. We can retrieve a list of the paths for all of our checked out items like this:

$itemsurl = $baseurl . "/uhtbin/cgisirsi.exe/?ps=07P15KtNyC/CHA/" . $session_security_cookie . "/30#";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $itemsurl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_COOKIEFILE, "/tmp/sirsidynix-cookies.txt");
curl_setopt($ch, CURLOPT_COOKIEJAR, "/tmp/sirsidynix-cookies.txt");
$result = curl_exec($ch);

$regex = '/\\Details\\<\/a\\>/';
preg_match_all($regex, $result, $matches);
$checked_out_items = $matches[1];

This results in a $checked_out_items array that looks like this:

Array
(
    [0] => /uhtbin/cgisirsi.exe/?ps=obViAQ0yfH/CHA/152430023/5/3?searchdata1=293432{CKEY}&searchfield1=GENERAL^SUBJECT^GENERAL^^
    [1] => /uhtbin/cgisirsi.exe/?ps=TGF3wAfAQt/CHA/152430023/5/3?searchdata1=104284{CKEY}&searchfield1=GENERAL^SUBJECT^GENERAL^^
)

We can now iterate over those paths and parse out the title, author, ISBN and date due:

foreach($checked_out_items as $key => $url) {
    $item = $baseurl . $url;
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $item);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_COOKIEFILE, "/tmp/sirsidynix-cookies.txt");
    curl_setopt($ch, CURLOPT_COOKIEJAR, "/tmp/sirsidynix-cookies.txt");
    $result = curl_exec($ch);

    $regex = '/(?:(?Us)\\(.{0,})\\<\/dd\\>)/';
    preg_match($regex, $result, $matches);
    $items[$key]['title'] = trim(html_entity_decode($matches[1]));

    $regex = '/(?:(?Us)\\\\n(.{0,}) .{0,}\\<\/dd\\>)/';
    preg_match($regex, $result, $matches);
    $items[$key]['author'] = trim(html_entity_decode($matches[1]));

    $regex = '/(?:(?Us)\\(.{0,})\\<\/dd\\>)/';
    preg_match($regex, $result, $matches);
    $items[$key]['isbn'] = trim(html_entity_decode($matches[1]));

    $regex = '/(?:(?Us)\\Due:(.{0,})\\<\/td\\>)/';
    preg_match($regex, $result, $matches);
    $items[$key]['datedue'] = trim(html_entity_decode($matches[1]));
    $items[$key]['datedue_unixtime'] = strtotime($items[$key]['datedue']);
}

Using regular expressions (put together and tested using the excellent Debuggex tool), the metadata for each item is parsed out; the result, after all items have been iterated over, is an $items array that looks like this:

Array
(
    [0] => Array
        (
            [title] => Reporting : writings from The New Yorker
            [author] => Remnick, David.
            [isbn] => 0307263584
            [datedue] => 6 Oct 2013
            [datedue_unixtime] => 1381028400
        )

    [1] => Array
        (
            [title] => Start & run a coffee bar
            [author] => Matzen, Thomas, 1963-
            [isbn] => 1551803542
            [datedue] => 6 Oct 2013
            [datedue_unixtime] => 1381028400
        )

)

Created iCalendar Events for Due Dates

Now that we’ve pulled out all the checked out items, we can create iCalendar events for each one; rather than using some sort of heavyweight iCalendar class, we just manually stitch together the iCalendar files:

foreach($items as $key => $item) {

    $ical = "";
    $ical .= "BEGIN:VCALENDAR\n";
    $ical .= "CALSCALE:GREGORIAN\n";
    $ical .= "PRODID:-//Date Due Processor //DateDue 1.1//EN\n";
    $ical .= "VERSION:2.0\n";
    $ical .= "METHOD:PUBLISH\n";
    $ical .= "BEGIN:VEVENT\n";
    $ical .= "TRANSP:TRANSPARENT\n";
    $ical .= "STATUS:CONFIRMED\n";
    $ical .= "SUMMARY:" . $item['title'] . "\n";
    $ical .= "DTSTART;VALUE=DATE:" . strftime("%Y%m%d", $item['datedue_unixtime']) . "\n";
    $ical .= "DTEND;VALUE=DATE:" . strftime("%Y%m%d", $item['datedue_unixtime']) . "\n";
    $ical .= "END:VEVENT\n";
    $ical .= "END:VCALENDAR\n";
    
    $fp = fopen("icalendar-" . $key . ".ics","w");
    fwrite($fp,$ical);
    fclose($fp);
}

When the resulting .ics files get loaded up into iCal on my Mac, they look like this:

iCal showing book dates due on calendar

Grab the Code

You can grab the code outlined above as sirsidynix-to-ical.php from GitHub; to use it in your particular SirsiDynix setup will require that you change the base URL and might require that you modify the URLs for logging in, retrieving items, and so on.

In theory you should be able to automate any action that a patron can otherwise perform interactively using a browser, from renewing loans to placing and reporting on holds. I welcome reports of the use of this code elsewhere.

Comments