404]); ticketdrum_token_throttle(); $request = sanitize_text_field(wp_unslash($_GET['ticketdrum_wallet'])); if (!preg_match('/^([a-zA-Z0-9]{16,64})-(\d+)-(\d+)$/',$request,$m)) { ticketdrum_token_failed(); wp_die(esc_html__('Invalid ticket','ticketdrum'),esc_html__('Not found','ticketdrum'),['response'=>404]); } $purchase = $wpdb->get_row($wpdb->prepare("SELECT * FROM %i WHERE access_token=%s AND status='paid' LIMIT 1",TICKETDRUM_PURCHASES_TABLE,$m[1]),ARRAY_A); if (!$purchase) { ticketdrum_token_failed(); wp_die(esc_html__('Ticket not found','ticketdrum'),esc_html__('Not found','ticketdrum'),['response'=>404]); } $unit = false; foreach (ticketdrum_build_ticket_units($purchase) as $u) if ((int)$u['ticket_id']==(int)$m[2] && (int)$u['index']==(int)$m[3]) $unit = $u; if (!$unit) wp_die(esc_html__('Ticket not found','ticketdrum'),esc_html__('Not found','ticketdrum'),['response'=>404]); $code = $unit['code']; $pkpass = ticketdrum_generate_pkpass($purchase,$unit); if (!$pkpass) wp_die(esc_html__('Could not generate pass','ticketdrum'),esc_html__('Error','ticketdrum'),['response'=>500]); header('Content-Type: application/vnd.apple.pkpass'); header('Content-Disposition: attachment; filename="ticket-'.$code.'.pkpass"'); echo $pkpass; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- non-HTML file output (binary .pkpass download) die(); } add_action('template_redirect','ticketdrum_wallet_endpoint'); //A solid-colour PNG, assembled directly rather than drawn with GD and captured from the output //buffer. Wallet passes need flat brand-colour placeholder icons at three sizes, so there is //nothing to render, and doing it this way keeps the pass generator free of both the GD //extension and an output buffer opened inside a function that also writes files and signs them, //and that can return early at several points along the way. function ticketdrum_solid_png($size,$r,$g,$b) { $chunk = function($type,$data) { return pack('N',strlen($data)).$type.$data.pack('N',crc32($type.$data)); }; $ihdr = pack('NN',$size,$size)."\x08\x02\x00\x00\x00"; //8-bit truecolour RGB, no interlace $row = "\x00".str_repeat(chr($r).chr($g).chr($b),$size); //filter type 0 (None), then the pixels $idat = gzcompress(str_repeat($row,$size),9); return "\x89PNG\r\n\x1a\n".$chunk('IHDR',$ihdr).$chunk('IDAT',$idat).$chunk('IEND',''); } function ticketdrum_generate_pkpass($purchase,$unit) { $pass = [ 'formatVersion' => 1, 'passTypeIdentifier' => TICKETDRUM_APPLE_PASS_TYPE_ID, 'teamIdentifier' => TICKETDRUM_APPLE_TEAM_ID, 'organizationName' => get_bloginfo('name'), 'serialNumber' => $unit['code'], 'description' => $unit['event_title'].' - '.$unit['ticket_title'], 'foregroundColor' => 'rgb(250,245,234)', 'backgroundColor' => 'rgb(22,36,27)', 'barcodes' => [['message'=>$unit['code'],'format'=>'PKBarcodeFormatQR','messageEncoding'=>'iso-8859-1']], 'eventTicket' => [ 'primaryFields' => [['key'=>'event','label'=>__('EVENT','ticketdrum'),'value'=>$unit['event_title']]], 'secondaryFields' => [ ['key'=>'when','label'=>__('WHEN','ticketdrum'),'value'=>$unit['event_datetime']], ['key'=>'type','label'=>__('TICKET','ticketdrum'),'value'=>$unit['ticket_title']], ], 'auxiliaryFields' => [ ['key'=>'name','label'=>__('NAME','ticketdrum'),'value'=>stripslashes($purchase['name'])], ['key'=>'order','label'=>__('ORDER','ticketdrum'),'value'=>'#'.sprintf('%04d',$purchase['id'])], ], ], ]; $files = ['pass.json'=>json_encode($pass)]; //Minimal required icons (solid brand-colour PNGs, built in memory) foreach (['icon.png'=>29,'icon@2x.png'=>58,'logo.png'=>50] as $icon_name=>$icon_size) $files[$icon_name] = ticketdrum_solid_png($icon_size,22,36,27); //Manifest of SHA1s, signed with the pass certificate $manifest = []; foreach ($files as $name=>$content) $manifest[$name] = sha1($content); $files['manifest.json'] = json_encode($manifest); $certs = []; if (!openssl_pkcs12_read(file_get_contents(TICKETDRUM_APPLE_PASS_CERT),$certs,TICKETDRUM_APPLE_PASS_CERT_PASSWORD)) return false; $manifest_file = ticketdrum_tmpdir().'manifest-'.$unit['code'].'-'.wp_generate_password(12,false).'.json'; $signature_file = $manifest_file.'.sig'; file_put_contents($manifest_file,$files['manifest.json']); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- short-lived signing input written to the plugin's own uploads folder $signed = openssl_pkcs7_sign($manifest_file,$signature_file,$certs['cert'],[$certs['pkey'],''],[],PKCS7_BINARY|PKCS7_DETACHED,TICKETDRUM_APPLE_WWDR_CERT); if (!$signed) { @wp_delete_file($manifest_file); return false; } //Apple wants the raw DER PKCS#7, but openssl_pkcs7_sign() can only write S/MIME, so the //signature has to be lifted back out of it. It is the last MIME part: a Content-Type of //application/x-pkcs7-signature, then the rest of that part's headers, a blank line, and the //base64 body up to the closing boundary. Matching from the content type rather than from //the "base64" of the transfer-encoding header matters - OpenSSL puts a Content-Disposition //line after that one, so the blank line is not where a naive match expects it to be. $smime = str_replace("\r\n","\n",(string)file_get_contents($signature_file)); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_get_contents -- reading back the signature this function just wrote to the plugin's own uploads folder @wp_delete_file($manifest_file); @wp_delete_file($signature_file); if (!preg_match('#Content-Type:\s*application/x-pkcs7-signature.*?\n\n(.*?)\n-{2,}#s',$smime,$sig_m)) return false; $files['signature'] = base64_decode(preg_replace('/\s+/','',$sig_m[1]),true); //A PKCS#7 structure is a DER SEQUENCE, so it always starts 0x30. Anything else means the //extraction went wrong and the pass would be rejected by Wallet rather than fail here. if (!is_string($files['signature']) || $files['signature']==='' || substr($files['signature'],0,1)!=="\x30") return false; //Zip it all up, in the plugin's own folder under uploads $zip_file = ticketdrum_tmpdir().'pass-'.$unit['code'].'-'.wp_generate_password(12,false).'.zip'; $zip = new ZipArchive(); if ($zip->open($zip_file,ZipArchive::CREATE|ZipArchive::OVERWRITE)!==true) return false; foreach ($files as $name=>$content) $zip->addFromString($name,$content); $zip->close(); $pkpass = file_get_contents($zip_file); @wp_delete_file($zip_file); return $pkpass; }