taxr/taxr.php

<?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;
   }
?>