Created
October 31, 2018 23:14
-
-
Save jjmontgo/2be75d3fb36d680563a6d7d40931d13d to your computer and use it in GitHub Desktop.
A PHP Class to fetch a PDF URL from the PDF Service
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Makes a POST request to a Lambda function through an API Gateway | |
* which encapsulates a call to wkhtmltopdf, rendering an HTML | |
* document as a PDF file and returning a signed URL to the | |
* file in an S3 bucket. | |
* | |
* The POST can contain the following parameters: | |
* body - A complete HTML5 document to be rendered in PDF (required) | |
* header - A complete HTML5 document to be rendered in the header of every page (optional) | |
* footer - A complete HTML5 document to be rendered in the footer of every page (optional) | |
* filename - The preferred filename for the rendered PDF document (optional) | |
* If not provided it will be 'generated.pdf' | |
* margin_left, margin_right, margin_top, margin_bottom - optional integer values, in mm | |
* | |
* Make sure the following definitions are added to a file outside version control: | |
* define("AWS_REGION", ""); | |
* define("PDF_API_GATEWAY_URL", "********.execute-api.us-east-1.amazonaws.com"); | |
* define("PDF_API_GATEWAY_STAGE", "***"); // usually "prod" | |
* define("PDF_ACCESS_KEY", ""); | |
* define("PDF_SECRET_KEY", ""); | |
* | |
* Then call the generator with: | |
* | |
* GeneratePDF::fromHTML("<!doctype html><html>...</html>") | |
* ->header("<!doctype html><html>...</html>") | |
* ->footer("<!doctype html><html>...</html>") | |
* ->leftMargin(0) | |
* ->rightMargin(0) | |
* ->andSendToBrowserAs("my-new-pdf-file.pdf"); // must be called, causes a redirect and exit | |
*/ | |
class GeneratePDF { | |
private static $instance; | |
private $postParams = array(); | |
/** | |
* Instantiate the class and set the required body POST parameter | |
*/ | |
public static function fromHTML($bodyHtml) { | |
if (!isset(static::$instance)) { | |
static::$instance = new GeneratePDF_V2(); | |
} | |
static::$instance->setParam("body", $bodyHtml); | |
return static::$instance; | |
} | |
/** | |
* Optionally put the PDF in a folder for the project | |
* @param string $projectName This should follow appropriate standards for directory names | |
*/ | |
public function projectName($projectName) { | |
$this->setParam("project_name", $projectName); | |
return $this; | |
} | |
public function header($headerHtml) { | |
$this->setParam("header", $headerHtml); | |
return $this; | |
} | |
public function footer($footerHtml) { | |
$this->setParam("footer", $footerHtml); | |
return $this; | |
} | |
public function topMargin($margin) { | |
$this->setParam("margin_top", $margin); | |
return $this; | |
} | |
public function leftMargin($margin) { | |
$this->setParam("margin_left", $margin); | |
return $this; | |
} | |
public function bottomMargin($margin) { | |
$this->setParam("margin_bottom", $margin); | |
return $this; | |
} | |
public function rightMargin($margin) { | |
$this->setParam("margin_right", $margin); | |
return $this; | |
} | |
public function andSendToBrowserAs($filename) { | |
$this->setParam("filename", $filename); | |
$signedPdfUrl = $this->getSignedPdfUrl(); | |
header("Location: {$signedPdfUrl}"); | |
exit(); | |
} | |
public function setParam($key, $value) { | |
$this->postParams[$key] = $value; | |
return $this; | |
} | |
/** | |
* Delivers HTML to AWS and receives a URL to the rendered PDF | |
*/ | |
private function getSignedPdfUrl() { | |
$postBody = http_build_query($this->postParams); | |
$date = new DateTime('UTC'); | |
$xAmzDate = $date->format('Ymd\THis\Z'); | |
$headers = array( | |
'Content-Type: application/x-www-form-urlencoded; charset=utf-8', | |
'Host: ' . PDF_API_GATEWAY_URL, | |
'X-Amz-Date: ' . $xAmzDate, | |
); | |
$authorizationHeader = $this->getAuthorizationHeader($xAmzDate, $headers, $postBody); | |
$headers[] = "Authorization: " . $authorizationHeader; | |
$ch = curl_init(); | |
curl_setopt($ch, CURLOPT_URL, "https://" . PDF_API_GATEWAY_URL . "/" . PDF_API_GATEWAY_STAGE); | |
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); | |
curl_setopt($ch, CURLOPT_POST, count($this->postParams)); | |
curl_setopt($ch, CURLOPT_ENCODING, "gzip"); | |
curl_setopt($ch, CURLOPT_POSTFIELDS, $postBody); // the data to post | |
curl_setopt($ch, CURLOPT_HEADER, 0); // dont include the header in the output | |
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // return the response as a string | |
// curl_setopt($ch, CURLOPT_CAINFO, '/usr/share/ca-certificates/cacert.pem'); | |
if (defined('CURLOPT_IPRESOLVE') && defined('CURL_IPRESOLVE_V4')){ | |
curl_setopt( $ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); | |
} | |
$info = curl_getinfo($ch); | |
$response = curl_exec($ch); | |
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); | |
if ($httpCode !== 200) { | |
throw new Exception('Failed to receive PDF file; remote server returned '.$httpCode.' status' . "\n\n" . $response); | |
} | |
return $response; | |
} | |
/** | |
* @see https://docs.aws.amazon.com/apigateway/api-reference/signing-requests/ | |
*/ | |
private function getAuthorizationHeader($xAmzDate, $headers, $postBody) { | |
// Task 1: Create a Canonical Request for Signature Version 4 | |
$canonicalHeaders = ""; | |
foreach ($headers as $header) { | |
$headerParts = explode(":", $header); | |
$canonicalHeaders .= trim(strtolower($headerParts[0])) . ":" . trim($headerParts[1]) . "\n"; | |
} | |
$signedHeaders = "content-type;host;x-amz-date"; | |
$canonicalRequest = | |
"POST\n" . | |
"/" . PDF_API_GATEWAY_STAGE . "\n" . | |
"\n" . | |
$canonicalHeaders . "\n" . | |
$signedHeaders . "\n" . | |
hash("sha256", $postBody); | |
$hashedCanonicalRequest = hash("sha256", $canonicalRequest); | |
// Task 2: Create a String to Sign for Signature Version 4 | |
$dateObj = new DateTime('UTC'); | |
$date = $dateObj->format('Ymd'); | |
$region = AWS_REGION; | |
$service = "execute-api"; | |
$alg = "sha256"; | |
$credentialScope = "{$date}/{$region}/{$service}/aws4_request"; | |
$stringToSign = | |
"AWS4-HMAC-SHA256\n" . | |
$xAmzDate . "\n" . | |
"{$credentialScope}\n" . | |
$hashedCanonicalRequest; | |
// Task 3: Calculate the Signature for AWS Signature Version 4 | |
$secretKey = PDF_SECRET_KEY; | |
$kSecret = "AWS4" . $secretKey; // @todo update this with secret key | |
$kDate = hash_hmac($alg, $date, $kSecret, true); | |
$kRegion = hash_hmac($alg, $region, $kDate, true); | |
$kService = hash_hmac($alg, $service, $kRegion, true ); | |
$kSigning = hash_hmac($alg, 'aws4_request', $kService, true); | |
$signature = hash_hmac($alg, $stringToSign, $kSigning); | |
// Task 4: Add the Signature to the HTTP Request | |
// Using authorization header method | |
$accessKey = PDF_ACCESS_KEY; // to update | |
$authorizationHeader = | |
'AWS4-HMAC-SHA256 ' . | |
'Credential=' . $accessKey . '/' . $credentialScope . ', ' . | |
'SignedHeaders=' . $signedHeaders . ', ' . 'Signature=' . $signature; | |
return $authorizationHeader; | |
} | |
/** | |
* Private constructor called internally | |
*/ | |
private function __construct() {} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment