<?php
// taxr.php
// Output tax data in JSON format.
// INFO 343, Autumn 2012
// Morgan Doocy
error_reporting(E_ALL);
ini_set('display_errors', true);
$PARAMS = array(
'year' => '/^(?:191[3-9]|19[23456789][0-9]|200[0-9]|201[01])$/',
'format' => '/^(?:xml|json)$/',
'dollars' => '/^(?:nominal|adjusted)$/',
'delay' => '/^(?:[0-5]?[0-9]|60)$/'
);
$ERRORS = array(
'year' => 'a 4-digit year between 1913 and 2011 (inclusive)',
'format' => "either 'xml' (default) or 'json'",
'dollars' => "either 'nominal' (default) or 'adjusted'",
'delay' => 'an integer number of seconds between 0 and 60'
);
// Disallow anything other than GET requests.
if ($_SERVER["REQUEST_METHOD"] != "GET") {
http_die(400, "Invalid Request", "This service accepts only GET requests.");
}
// Error if unknown parameters passed.
$unknown = array_diff(array_keys($_REQUEST), array_keys($PARAMS));
if (count($unknown)) {
http_die(400, "Invalid Request", "Unrecognized parameter(s) passed: " . implode(", ", $unknown));
}
// Connect to database.
try {
if (isset($_SERVER['RDS_HOSTNAME'])) {
$dbhost = $_SERVER['RDS_HOSTNAME'];
$dbport = $_SERVER['RDS_PORT'];
$username = $_SERVER['RDS_USERNAME'];
$password = $_SERVER['RDS_PASSWORD'];
} else {
$dbhost = '127.0.0.1';
$dbport = 3306;
$username = 'root';
$password = '';
}
$db = new PDO("mysql:dbname=taxr;host=$dbhost;port=$dbport;charset=utf8", $username, $password);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
} catch (PDOException $ex) {
http_die(500, "Internal Server Error", "Could not connect to MySQL database.");
}
// Delay specified number of seconds (optional).
if (isset($_REQUEST['delay'])) {
sleep(check_param('delay'));
}
if (isset($_REQUEST['year'])) {
$year = check_param('year');
$dollars = isset($_REQUEST['dollars']) ? check_param('dollars') : 'nominal';
$format = isset($_REQUEST['format']) ? check_param('format') : 'xml';
$brackets_sql = "SELECT `status`, `rate`, `from`, IFNULL(`to`, '-') AS `to` FROM `brackets`
WHERE `year` = ? AND `dollars` = ? ORDER BY `status`, `rate`, `from`, `to`";
$notes_sql = "SELECT `note` FROM `notes` WHERE `year` = ? ";
try {
$sth1 = $db->prepare($brackets_sql);
$sth2 = $db->prepare($notes_sql);
$sth1->setFetchMode(PDO::FETCH_NAMED);
$sth2->setFetchMode(PDO::FETCH_NAMED);
$sth1->execute(array($year, $dollars));
$sth2->execute(array($year));
$brackets = $sth1->fetchAll();
$notes = $sth2->fetchAll();
$data = structure($year, $brackets, $notes, $dollars);
switch ($format) {
case 'xml': print_year_xml($data); break;
case 'json': print_year_json($data); break;
}
} catch (PDOException $ex) {
http_die(500, "Internal Server Error", "There was an error fetching the data. (MySQL)");
}
} else {
$format = isset($_REQUEST['format']) ? check_param('format') : 'xml';
$years_sql =
"SELECT
`year`,
MIN(`rate`) AS `minrate`,
MAX(`rate`) AS `maxrate`,
COUNT(`rate`) as `brackets`,
`cpi`,
`president`,
`party`
FROM `brackets` JOIN `presidents` ON
(`year` >= `presidents`.`from` AND `year` <= `presidents`.`to`)
WHERE `status` = 'Single' OR `status` = 'All' AND `dollars` = 'nominal'
GROUP BY `year` ORDER BY `year` ASC;";
try {
$sth = $db->query($years_sql, PDO::FETCH_NAMED);
$data = $sth->fetchAll();
switch ($format) {
case 'xml': print_years_xml($data); break;
case 'json': print_years_json($data); break;
}
} catch (PDOException $ex) {
http_die(500, "Internal Server Error", "There was an error fetching the data. (MySQL)");
}
}
// === Functions ===
// Output the given data as JSON, with an application/json header.
function print_year_json($data) {
print_years_json($data);
}
function print_years_json($data) {
header("Content-type: application/json");
print pretty_print(json_encode($data));
}
function print_year_xml($data) {
header("Content-type: application/xml");
print '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
?>
<brackets year="<?= $data['year'] ?>" dollars="<?= $data['dollars'] ?>">
<?php
foreach ($data['statuses'] as $status => $brackets) {
?>
<statuses>
<status label="<?= $status ?>">
<?php
foreach ($brackets as $bracket) {
?>
<bracket>
<rate><?= $bracket['rate'] ?></rate>
<from><?= $bracket['from'] ?></from>
<to><?= $bracket['to'] == '-' ? '—' : intval($bracket['to']) ?></to>
</bracket>
<?php
}
?>
</status>
</statuses>
<notes>
<?php
foreach ($data['notes'] as $note) {
?>
<note><![CDATA[<?= $note ?>]]></note>
<?php
}
?>
</notes>
<?php
}
?>
</brackets>
<?php
}
function print_years_xml($data) {
header("Content-type: application/xml");
print '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
?>
<years>
<?php
foreach ($data as $row) {
?>
<year
<?php foreach (array_diff_key($row, array('year' => '')) as $attr => $val) { ?>
<?= htmlspecialchars($attr) ?>="<?= htmlspecialchars($val) ?>"
<?php } ?>
><?= $row['year'] ?></year>
<?php
}
?>
</years>
<?php
}
function structure($year, $brackets, $notes, $dollars) {
return array(
'year' => $year,
'dollars' => $dollars,
'statuses' => group_by($brackets, 'status'),
'notes' => column_data($notes, 'note')
);
}
function column_data($rows, $field) {
$return = array();
foreach ($rows as $row) {
array_push($return, $row[$field]);
}
return $return;
}
function group_by($rows, $field, $remove_field = true) {
$return = array();
foreach ($rows as $row) {
$value = $row[$field];
if ($remove_field) {
unset($row[$field]);
}
if (isset($return[$value])) {
array_push($return[$value], $row);
} else {
$return[$value] = array($row);
}
}
return $return;
}
// Die with the given HTTP status code and message.
function http_die($code, $status, $msg) {
header("HTTP/1.1 $code $status");
header("Content-type: text/plain");
die("ERROR $code: $status - $msg");
}
// Validate the parameter of the given name.
function check_param($name) {
global $PARAMS, $ERRORS;
if (!preg_match($PARAMS[$name], $_REQUEST[$name])) {
http_die(400, "Invalid Request", "Invalid value for parameter '$name': '{$_REQUEST[$name]}'.\n(Must be {$ERRORS[$name]}.)");
}
return $_REQUEST[$name];
}
// Return a human-readable version of the given JSON.
// Adapted from http://www.php.net/manual/en/function.json-encode.php#80339
function pretty_print($input) {
$tab = "\t";
$nl = "\n";
$indent = 0;
$in_string = false;
$indented_array = false;
$output = "";
for ($i = 0; $i < strlen($input); $i++) {
$char = $input[$i];
$before = $after = '';
if (!$in_string) {
switch ($char) {
case '{':
case '[':
$after = $nl . str_repeat($tab, ++$indent);
break;
case '}':
case ']':
$before = $nl . str_repeat($tab, --$indent);
break;
case ',':
$after = $nl . str_repeat($tab, $indent);
break;
case ':':
$after = " ";
break;
case '"':
if ($i == 0 || $input[$i-1] != '\\') {
$in_string = true;
}
}
} else if ($char == '"' && $input[$i-1] != '\\') {
$in_string = false;
}
$output .= $before . $char . $after;
}
return $output;
}
?>