Source

OutreachAanmeldScript / aanmeldscript-functions.php

The default branch has multiple heads

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
<?php //-*- tab-width: 4; indent-tabs-mode: t; -*-
/*
Geschreven door Jan Kanis <jan.code at jankanis punt nl> 
<http://bitbucket.org/JanKanis>, (C) 2010-2011. Beschikbaar gesteld zonder 
beperkingen voor elk gebruik aan de Outreach Utrecht en Youth for Christ, of 
openbaar te maken onder de GPL versie 3 of elke latere versie. 


code layout: 
Class AanmeldScript contains all data specific to the form. Everything is set up in its constructor. It has two instances of Sheet, one for the aanmeldingen sheet and one for the projecten sheet. The sheet data is available in AanmeldScript attributes aanmeldingendata and projectendata. The other public methods are 'plekvrij' which returns if there's space left in the specified project, and 'insertAanmelding', which inserts (after some checks) a new registration into the aanmeldingen spreadsheet. The other (private) methods deal with consistency and error checking. 
Class Sheet represents a reference to a google spreadsheet. It does not itself store the spreadsheet data, but that is available through the 'getData' method. 'insertRow' inserts a row into the sheet. 

Below that are a bunch of functions. Some important ones:
'do_request' wraps a remote call and retries it a few times if it fails. RequestMultiException is the associated exception if the request keeps failing. 
'filterfields', 'index', and 'plekvrij' are basic functions that are used to munge arrays with spreadsheet or configuration data into the right form. 
'parsecsv' parses a string with csv data into an array. 
'valid_email' checks an email address for validity.
I use 'parseparams' to simulate optional and named arguments to functions. 
'checkpoint' can be used to profile the script's time and memory usage. 
'log_error' logs an error to the error mail address, and optionally displays it in the page output. 
'warn_sheetbeheerder' sends an error message to the mail addres of the registration manager, and optionally displays it.
'debugprint' formats and prints an error message to the outputstream. 

*/

define('SCRIPT_VERSION', '2.0.2');

require_once 'aanmeldscript-config.php';

// for use with checkpointing
//$starttime = $time = microtime(1);

//checkpoint('prestartup');

if(count($ERROR_LOG_EMAIL)<1 or in_array(False, array_map('valid_email', $ERROR_LOG_EMAIL))) {
	die('Error: $ERROR_LOG_EMAIL is not correctly set. Set $ERROR_LOG_EMAIL in aanmeldscript-config.php to the email addresses to send error messages to');
}

require_once 'Zend/Gdata/Spreadsheets.php';
require_once 'Zend/Gdata/ClientLogin.php';
# If installed, the Zend Autoloader could also be used. 
//checkpoint('loaded Zend');


///////////////////////////////
// Policy and Driver section //
///////////////////////////////

class AanmeldScript {

	private $spreadsheetService, $spreadsheetFeed;
	public $projecten, $aanmeldingen;
	public $projectendata, $aanmeldingendata;

	private $errors;
	private $aanmeldingcolumns;

	function __construct($account, $password, $extra=array()) {
		extract(parseparams($extra, array(), array('aanmeldingen', 'projecten')));

		//checkpoint('preconnect');
		$service = Zend_Gdata_Spreadsheets::AUTH_SERVICE_NAME;
		$this->httpClient = do_request('Zend_Gdata_ClientLogin::getHttpClient', array($account, $password, $service));
		$this->spreadsheetService = new Zend_Gdata_Spreadsheets($this->httpClient);
		//checkpoint('connected');
		$this->spreadsheetFeed = do_request(array($this->spreadsheetService, 'getSpreadsheetFeed'), array());
		//checkpoint('got sheets feed');
		$this->aanmeldingen = new Sheet($this->spreadsheetService, $this->findsheet($this->spreadsheetFeed, trim($aanmeldingen)));
		$this->projecten = new Sheet($this->spreadsheetService, $this->findsheet($this->spreadsheetFeed, trim($projecten)));
		//checkpoint('got sheets');
		
		$projectencsv = $this->projecten->getData();
		$this->checkprojectenchars($projectencsv);
		$projectendata = index(filterfield(filterfield($projectencsv, 'naam'), 'code'), 'code');

		$this->aanmeldingendata = $this->aanmeldingen->getData(array('headerref'=>&$this->aanmeldingcolumns));
		$this->checkspreadsheets($projectencsv, $projectendata, $this->aanmeldingendata);

		$this->projectendata = plekvrij($projectendata, $this->aanmeldingendata);
	}

	function plekvrij($project) {
		assert(isset($this->projectendata[$project]));
		$plekvrij = $this->projectendata[$project]['plekvrij'];
		return is_null($plekvrij) or $plekvrij > 0;
	}

	function insertAanmelding($row) {
		$missing = array_values(array_diff(array_keys($row), $this->aanmeldingcolumns));
		if(count($missing) > 0) {
			$warnmsg = "De volgende kolom(men) missen in het aanmeldingen spreadsheet in Google Docs:\n";
			foreach($missing as $col) {
				$warnmsg .= " - '$col'\n"; }
			$warnmsg .= "\nDeze aanmelding is mogelijk niet volledig opgeslagen:\n";
			foreach($row as $key => $val) {
				$warnmsg .= "  $key: $val\n"; }
			warn_sheetbeheerder($warnmsg);
		}
		return $this->aanmeldingen->insertrow($row);
	}

	private function findsheet($spreadsheetFeed, $name) {
		$name = trim($name);
		$found = 0;
		foreach($spreadsheetFeed as $sheet) {
			if(trim($sheet->getTitle()) == $name) {
				$found++;
				break;
			}
		}
		if($found < 1) {
			throw new Exception("Spreadsheet met titel '$name' niet gevonden in Google Docs"); }
		elseif($found > 1) {
			throw new Exception("Meerdere spreadsheets met titel '$name' gevonden in Google Docs, zorg dat er maar één spreadsheet met die naam is"); }
		return $sheet;
	}

	// Checks for fatal errors in the Projecten spreadsheets, and throws an 
	// exception if any are found. The current checks are for illegal 
	// characters in project codes and illegal values in 'max deelnemers'.
	private function checkprojectenchars($projectencsv) {
		foreach($projectencsv as $project) {
			$code = $project['code'];
			$maxdeelnemers = $project['max deelnemers'];
			if($code !== htmlspecialchars($code, ENT_QUOTES)) {
				throw new Exception("niet toegestane tekens in Projecten configuratie: ".var_export_str($code).
					"\nIn projectcodes zijn de volgende tekens niet toegestaan: <>'\"&");
			}
			if(!($maxdeelnemers === '' || $maxdeelnemers === (string) (int) $maxdeelnemers)) {
				throw new Exception("'max deelnemers' moet een getal zijn of mag leeg gelaten worden: ".var_export_str($maxdeelnemers));
			}
		}
		return True;
	}

	// This function checks for non-fatal errors in the Google spreadsheets, and calls warn_sheetbeheerder if it finds any.
	// returns True when no errors found, False on errors.
	private function checkspreadsheets($projectendata_raw, $projectendata, $aanmeldingen) {
		$duplicatecode = array();
		foreach(array_count_values(array_map(fn('$x["code"]'), $projectendata_raw)) as $code => $count) {
			if($count > 1) {
				$duplicatecode[] = $code; }
		}

		$unknownprojects = array();
		foreach($aanmeldingen as $aanmelding) {
			if($aanmelding['ingedeeld'] && !isset($projectendata[$aanmelding['ingedeeld']])) {
				$unknownprojects[] = $aanmelding['ingedeeld']; }
		}

		// Return if no errors found
		if(!$duplicatecode and !$unknownprojects) {
			return True; }
		
		// Construct a (very verbose) warning message
		$warnmsg = "Fouten gevonden in spreadsheets!\n".
			"(Dit bericht betreft een waarschuwing, aanmeldingen kunnen ondertussen gewoon door gaan)\n";
		if($duplicatecode) {
			$warnmsg .= "\nDe volgende projectcodes worden voor meerdere projecten gebruikt:\n";
			foreach($duplicatecode as $dup) {
				$warnmsg .= "- $dup\n"; }
		}
		if($unknownprojects) {
			$warnmsg .= "\nEr zijn aanmeldingen geregistreerd voor de volgende project(en), maar deze projecten zijn onbekend:\n";
			foreach($unknownprojects as $unk) {
				$warnmsg .= "- '$unk'\n"; }
		}
		$warnmsg .= "\n\nMogelijke oplossingen:\n";
		if($duplicatecode) {
			$warnmsg .= "\nZorg er voor dat elke code in het Projecten spreadsheet maar bij één project hoort. ".
				"Geef de projecten die codes dubbel gebruiken een nieuwe code.\n"; }
		if($unknownprojects) {
			$warnmsg .= "\nZorg er voor dat iedereen die in het Aanmeldingen spreadsheet ergens is ingedeeld, ".
				"ingedeeld staat bij een project dat ook in het Projecten sheet voorkomt. Als het bedoelde project wel in het Projecten spreadsheet staat maar de codes komen ".
				"niet overeen, pas dan de code in het Aanmeldingen sheet aan. Als het project niet in het Projecten spreadsheet voorkomt, voeg het dan daaraan toe. ".
				"Als je iemand handmatig bij een project ingedeeld wil hebben staan maar niet wil dat andere mensen zich ook voor dit project in kunnen schrijven, ".
				"voeg dan de code van het project aan het Projecten spreadsheet toe maar laat de naam in het Projecten sheet leeg, dan verschijnt het project niet in het ".
				"aanmeldformulier op de website."; 
		}
		warn_sheetbeheerder($warnmsg);

		return False;
	}

}

//
// End of Policy and Driver section //
//


/* Note: It is not the responsibility of Sheet to maintain a local cache of the sheet data. Sheet only contains the keys necessary to get at the remote spreadsheet representation. 
*/
class Sheet {

	private $spreadsheetService, $sheet, $sheetname;
	private $key;
	// not used: private $sheet; 
	
	function __construct($spreadsheetService, $sheet) {

		$this->spreadsheetService = $spreadsheetService;
		$this->sheet = $sheet;
		$this->sheetname = trim($sheet->getTitle());
		//The @ to suppress an E_STRICT warning of "Only variables should be passed by reference"
		$this->key = end(@explode('/', $sheet->getId()));
	}

	function getData($extra=array()) {
		$pp = parseparams($extra, array('headerref'=>Null));

		// Reading spreadsheet data using ListQuery only reads until it 
		// encounters a blank row. The Google list API doesn't provide any way
		// around this apparently. Using CellQuery is a possibility, but uses 
		// too much memory and is slower. Using the document export API seems 
		// the best (or least bad) solution. Unfortunately there are still 
		// some edge cases in which the PHP csv parsing api doesn't interpret
		// the Google/Excel csv dialect correctly, but fortunately for our use 
		// case we can work around that. 

		// Using csv for parseability. Not specifying a 'gid' parameter uses the 
		// first worksheet.
		$url = "https://spreadsheets.google.com/feeds/download/spreadsheets/Export?key={$this->key}&exportFormat=csv";
		//$response = $this->spreadsheetService->performHttpRequest('GET', $url);
		$response = do_request(array($this->spreadsheetService, 'performHttpRequest'), array('GET', $url), "export {$this->sheetname} as csv");
		//checkpoint('got content');
		return parsecsv($response->getBody(), array('headerref'=>&$pp['headerref']));
	}

	function insertrow($row) {
		// The third parameter to insertRow (the worksheet ID) is optional
		return do_request(array($this->spreadsheetService, 'insertRow'), array($row, $this->key), "insert row into {$this->sheetname}");
	} 

}//end Spreadsheet class

// Wrapper around Gdata calls that cause web requests. If the request fails, 
// we retry up to two times before throwing an error. If a retry succeeds, the 
// original failure still gets logged. 
function do_request($callback, $params, $name=Null) {
	if(is_null($name)) {
		$name = $callback; }
	if(is_array($name)) {
		$name = (is_object($callback[0]) ? get_class($callback[0]) : $callback[0])."->{$callback[1]}"; }

	$rqdata = array();
	$tries = 1;
	while(True) {
		try {
			$e = Null;
			$starttime = microtime(True);
			// Do the actual call
			$res = call_user_func_array($callback, $params);
		} catch(Exception $e) {
			// use $e later
		}
		$time = microtime(True) - $starttime;
		if(!is_null($e)) {
			$rqdata[] = array('ex'=>$e, 'time'=>$time);
			if($tries < 4) {
				// Do exponential backoff, see http://code.google.com/apis/documents/docs/3.0/developers_guide_protocol.html#ExponentialBackoff
				usleep(pow(2,$tries-1)*1000000 + rand(0,1000)*1000);
				$tries++;
				continue;
			} else {
				throw new RequestMultiException($name, $rqdata);
			}
		} else {
			$rqdata[] = array('ex'=>'OK', 'time'=>$time);
			break;
		}
	}
	if($tries > 1) {
		log_error("Web request '$name' had to be retried:\n".requestLogStrval($rqdata)); }
	//checkpoint("do_request '$name'");
	//debugprint(sprintf("request '$name' took $tries trie(s) in %.3F seconds", $time));
	return $res;
}

// Thrown from do_request to encapsulate multiple exceptions. 
class RequestMultiException extends Exception {
	function __construct($name, $rqlog, $code=0) {
		$this->name = $name;
		$this->rqlog = $rqlog;
 		$message = "Web request $name failed ".count($rqlog)." times:\n";
		foreach($rqlog as $r) {
			$message .= "\t".get_class($r['ex']).": {$r['ex']->getMessage()}\n"; }

		// note: PHP 5.2 doesn't support the third parameter $previous.
		parent::__construct($message, $code);
	}

	function __toString() {
		return "exception ".__CLASS__.":\n".requestlogStrval($this->rqlog);
	}
}

// Format a request exception log as string
// @$log: an array of array('ex'=>Exception or string, 'time'=>float)'s. 
function requestlogStrval($log) {
	$ret = '';
	$i = 1;
	foreach($log as $l) {
		$ret .= sprintf("\nattempt $i (%.3F sec.):\n", $l['time']).strval($l['ex'])."\n";
		if($l['ex'] instanceof Zend_Gdata_App_HttpException) {
			$respbody = $l['ex']->getRawResponseBody();
			if(trim($respbody) != '') {
				$ret .= "\nResponse Body:\n".$respbody."\n"; }
		}
		$i++;
	}
	return $ret;
}


function filterfield($items, $field) {
	$out = array();
	foreach($items as $item) {
		// note @ and non-strict comparison
		if(@$item[$field] != '') {
			$out[] = $item;
		}
	}
	return $out;
}


function index($arr, $field) {
	$out = array();
	foreach($arr as $item) {
		$out[$item[$field]] = $item;
	}
	return $out;
}

/* In de array met projecten krijgt elk project er een veld 'plekvrij' bij, 
 * waarin staat hoeveel plek er nog vrij is. Als er geen maximum is (max 
 * deelnemers in het spreadsheet is leeg) is de waarde NULL. De nieuwe array
 * wordt teruggegeven. */
function plekvrij($projecten, $aanmeldingen) {
	$plekvrij = array();
	foreach($projecten as $project) {
		if($project['max deelnemers'] !== "") {
			$plekvrij[$project['code']] = (int) $project['max deelnemers'];
		} else {
			$project['plekvrij'] = null;
		}
	}
	foreach($aanmeldingen as $aanmelding){
		$key = @$aanmelding['ingedeeld'];
		if(array_key_exists($key, $plekvrij)) {
			$plekvrij[$key]--;
		}
	}
	foreach(array_keys($projecten) as $code) {
		$projecten[$code]['plekvrij'] = @$plekvrij[$code];
	}
	return $projecten;
}


// str_getcsv didn't exist yet in php 5.2. And this function is not entirely 
// the same as that. We also skip empty lines, use a header row, and hack 
// around crappy php escape character behaviour. 
function parsecsv($input, $extra=array()) {
	$pp = parseparams($extra, array('haveheader'=>True, 'headerref'=>Null));
	extract($pp);
	$header = &$pp['headerref'];

	// WTF
	// php fgetcsv recognizes \-escapes, while google doesn't use them. This 
	// could otherwise lead to escape character injections. I didn't find a 
	// way to fix this correctly because php apparently does not support  full
	// round-trippability in its csv parser. So we just insert a space and 
	// hope nobody minds. If php 5.2 support were dropped I could have a look
	// at the 'escape' parameter. 
	// This app currently doesn't use '\' chars in fields it cares about, and 
	// it is not a legal email char. 
	$input = preg_replace('#\\\\"#', '\\\\ "', $input);

	$fp = fopen('php://temp', 'r+');
	fwrite($fp, $input);
	fseek($fp, 0);
	$ret = array();
	if($haveheader) {
		$header = parsecsvline($fp);
		while (($line = parsecsvline($fp)) !== False) {
			$kline = array();
			for($i=0; $i<count($header); $i++) {
				$kline[$header[$i]] = isset($line[$i]) ? $line[$i] : ''; }
			$ret[] = $kline;
		}
	} else {
		while (($line = parsecsvline($fp)) !== False) {
			$ret[] = $line;
		}
	}
	fclose($fp);
	return $ret;
}

function parsecsvline($fp) {
	do {
		$line = fgetcsv($fp, 0, ',', '"');
		if($line === False) return False;
		if($line === array(null)) continue;
		//if($obeycomments and substr($line[0], 0, 1) === '#') continue;
	} while(False);
	return array_map('trim', $line);
}



///////////////////////////////////////
// Utility & Error related functions //
///////////////////////////////////////

function valid_email($email) {
	// het blijkt dat zo ongeveer alle printable ascii kararkters geldig zijn in het user deel van een
	// emailadres. Zelfs quoten mag, dus "jan kanis"@phil.uu.nl is een legaal adres, dat naar de mailbox van
	// jan kanis (zonder quotes) gedelevered moet worden. Deze regex implementeert de quotes niet.
	// ik gebruik hier de karakters van http://www.remote.org/jochen/mail/info/chars.html
	// de ' heb ik er niet ingezet. Om dat wel te doen moet die ook goed gequote worden in de gegenereerde
	// javascript.

	$email_pattern_ch = '[+&*\-\./0-9=?A-Za-z_\^{}~]';

	// Domain names mogen alleen uit letters en cijfers bestaan, en een - (streepje) mag er ook in zitten
	// maar niet als eerste of laatste karakter van een subdomein naam. Domeinnamen worden gescheiden door
	// puntjes (.) . Oorspronkelijk mochten domeinnamen ook niet met een cijfer beginnen, maar tegenwoordig
	// bestaan dat soort domeinen wel.

	$email_pattern_name = '\w([\w-]*\w)?';

	// dit patroon werkt niet als de regex met /-en wordt aangegeven. # werkt wel. dus: $regex = "#$icht_email_pattern#";
	$email_pattern = "$email_pattern_ch+@$email_pattern_name(\.$email_pattern_name)+";

	return (bool) preg_match("#^$email_pattern\$#", $email);
}

function pr($x) {
	echo "$x\n";
}

function id($x) {return $x;}

/* A wrapper around create_function. If called with a single argument, that argument is the body and the function has a single argument $x. If called with two parameters, the first is/are the name(s) of the formal parameters, using the syntax of create_function. */
function fn($var, $body=null) {
	if($body === null) {
		$body = $var;
		$var = '$x';
	}
	if(preg_match('/^[[:alnum:]_]+$/', $var)) {
		$var = "\$$var";
	}
	if(strpos('return', $body) === false) {
		$body = "return $body;";
	}
	return create_function($var, $body);
}

/* A helper function to use named arguments. Example: 
function myfunc($nonoptionalarg, $extraargs) {
	extract(parseparams($extraargs, array('firstoptarg'=>'firstdefault', 'secondoptarg'=>'seconddefault'), array('requiredkwarg')));	
	...
	// You can now use $firstoptarg, $secondoptarg, $requiredkwarg and $nonoptionalarg as normal variables
	...
}
$required is a list of keyword arguments that are required, so they don't need to be present in $defaults. 
*/
function parseparams($params, $defaults, $required=array()) {
	$knownparams = array_merge(array_keys($defaults), $required); 
	foreach(array_keys($params) as $p) {
		if(!in_array($p, $knownparams)) {
			throw new Exception("Undefined extra argument '$p' in ".var_export_str($params)."!"); }
	}
	$ret = $params + $defaults;
	foreach($required as $r) {
		if(!array_key_exists($r, $ret)) {
			throw new Exception("required array argument '$r' is missing!"); }
	}
	return $ret;
}


// Aan te roepen als er een fout in de spreadsheets wordt gevonden (en er dus 
// niet een fout in de php code zit). 
function warn_sheetbeheerder($msg) {
	global $aanmeldemail;
	if(!mail($aanmeldemail, "Waarschuwing van het outreach aanmeldsysteem op {$_SERVER['SERVER_NAME']}", 
			$msg, "From: website@{$_SERVER['SERVER_NAME']}\r\n")) {
		log_error("Mail naar \$aanmeldemail ($aanmeldemail) versturen mislukt:\n\n".$msg); }
	if(DEBUG) debugprint($msg);
}

// Aan te roepen bij een fout in de PHP code
function log_error($e, $servervars=True) {
	global $ERROR_LOG_EMAIL;
	$mail = "Foutmelding!\n\n".
		"fout:\n".$e."\n\nScript versie ".SCRIPT_VERSION."\n";
	if($servervars) {
		$mail .= "\n\n\$_SERVER:\n".var_export_str($_SERVER).
		"\n\n\$_POST:\n".var_export_str($_POST).
		"\n\n\$_GET:\n".var_export_str($_GET); }
	foreach($ERROR_LOG_EMAIL as $errmail) {
		mail($errmail, "Foutmelding van het outreach aanmeldscript v. ".SCRIPT_VERSION.
				" op {$_SERVER['SERVER_NAME']}", $mail);
	}
	if(DEBUG) debugprint($mail);
}

// Prints profiling information at that point in the run.
function checkpoint($name) {
	if(!DEBUG) return;
	global $mem, $realmem, $time, $starttime;
	$newmem = memory_get_usage();
	$newrealmem = memory_get_usage(True);
	$newtime = microtime(True);
	debugprint(sprintf("checkpoint \"$name\" time: %.3Fs (+%.3Fs) mem: %s (%s) realmem: %s (%s) peak mem: %s", 
			$newtime-$starttime, $newtime-$time, 
			fmt($newmem), dfmt($newmem-$mem), 
			fmt($newrealmem), dfmt($newrealmem-$realmem), 
			fmt(memory_get_peak_usage()))); 
	$time = $newtime;	
	$mem = $newmem;
	$realmem = $newrealmem;
}
function fmt($num) { return number_format($num, 0, '', '.'); }
function dfmt($num) { return ($num>=0?'+':'').number_format($num, 0, '', '.'); }

function error_handler($errno, $errstr, $errfile, $errline) {
	// This test is to recognize if we are inside an @-expression. @ sets 
	// error_reporting temporarily to 0, see also 
	// http://stackoverflow.com/questions/7380782/error-supression-operator-and-set-error-handler
	if(ini_get('error_reporting') == 0) return False;
	$stacktrace = debug_backtrace(False);
	foreach(array_keys($stacktrace) as $i) {
		if(isset($stacktrace[$i]['args'])) {
			$stacktrace[$i]['args'] = (string) $stacktrace[$i]['args']; }
	}
	log_error("PHP error of type $errno: $errstr (file $errfile line $errline)\n\nstacktrace:\n".var_export_str($stacktrace));
	return False;
}

function responsebody($feed) {
	return xmlpp($feed->getHttpClient()->getLastResponse()->getBody());
}

function var_export_str($arg) { return var_export($arg, True); }
function var_dump_str($arg) {
	// WTF (*%^(*&09*%&%^ PHP, is returnen ipv printen nou zo moeilijk!?
	ob_start();
	var_dump($arg);
	$out = ob_get_clean();
	return $out;
}

function debugprint($arg) {
	print "<pre>\n";
	if(is_string($arg)) { 
		//pass
	} elseif(is_array($arg)) {
		$arg = var_export_str($arg);
	} else {
		$arg = var_dump_str($arg);
	}
	print htmlspecialchars("$arg\n");
	print "</pre>\n";
	flush();
}

// xml pretty print from http://gdatatips.blogspot.com/2008/11/xml-php-pretty-printer.html
function xmlpp($xml, $html_output=false) {  
    $xml_obj = new SimpleXMLElement($xml);  
    $level = 4;  
    $indent = 0; // current indentation level  
    $pretty = array();  
      
    // get an array containing each XML element  
    $xml = explode("\n", preg_replace('/>\s*</', ">\n<", $xml_obj->asXML()));  
  
    // shift off opening XML tag if present  
    if (count($xml) && preg_match('/^<\?\s*xml/', $xml[0])) {  
      $pretty[] = array_shift($xml);  
    }
  
    foreach ($xml as $el):
      if (preg_match('/^<([\w])+[^>\/]*>$/U', $el)) {  
          // opening tag, increase indent  
          $pretty[] = str_repeat(' ', $indent) . $el;  
          $indent += $level;  
      } else {
        if (preg_match('/^<\/.+>$/', $el)) {              
          $indent -= $level;  // closing tag, decrease indent  
        }  
        if ($indent < 0) {  
          $indent += $level;  
        }  
        $pretty[] = str_repeat(' ', $indent) . $el;  
      }  
    endforeach;
    $xml = implode("\n", $pretty);     
    return ($html_output) ? htmlentities($xml) : $xml;  
} 	$starttime = microtime(True);