'',
'content_type' => '',
);
}
return array(
'kind' => 'epos-xml',
'content_type' => $this->content_type(),
);
}
/**
* Build an ePOS-Print XML diagnostic.
*
* @param string $printer_name Printer display name.
*
* @return array{content_type:string, payload:string}
*/
public function diagnostic( string $printer_name ): array {
$name = (string) preg_replace( '/[\x00-\x1F\x7F]/', '', $printer_name );
$text = "WCPOS - Cloud Print Test\nPrinter: " . $name . "\n";
$text .= 'Date: ' . gmdate( 'Y-m-d H:i' ) . "\nIf you can read this, printing works!\n";
// Three blank lines before the cut, the same bottom margin the receipt
// templates use, so the last line is not flush against the blade.
$xml = ''
. '' . esc_html( $text ) . '';
return array(
'content_type' => $this->content_type(),
'payload' => base64_encode( $xml ),
);
}
/**
* Resolve polling status.
*
* @param array $printer Printer configuration.
* @param array $context Runtime status context.
*
* @return string
*/
public function status( array $printer, array $context ): string {
$relay = $context['relay_status'] ?? null;
if ( \is_array( $relay ) && 'blocked' === ( $relay['origin_status'] ?? '' ) ) {
return 'blocked';
}
if ( \is_array( $relay ) && null !== ( $relay['last_seen_seconds_ago'] ?? null ) && (int) $relay['last_seen_seconds_ago'] <= (int) $context['seen_ttl'] ) {
return 'connected';
}
$seen = (int) ( $context['seen'] ?? 0 );
if ( 0 === $seen ) {
return 'waiting';
}
return ( (int) $context['now'] - $seen ) <= (int) $context['seen_ttl'] ? 'connected' : 'offline';
}
/**
* Parse the vendor request.
*
* @param array $request Request data.
*/
public function parse( array $request ): array {
$params = $request['params'];
// Server Direct Print multiplexes three different request types onto the
// one configured URL, as URL-encoded form data distinguished by
// ConnectionType (User's Manual Rev.K, ch.3 and the Test_print.php
// reference implementation):
//
// GetRequest — poll for a job
// SetResponse — printing result; the XML rides in the ResponseFile field
// SetStatus — status notification; the XML rides in the Status field
//
// Answering a status notification with print data hands the job to a
// request that discards it, so the printer never prints and the job stays
// claimed. Dispatching on ConnectionType is what keeps the job on the
// GetRequest that is actually asking for one.
$connection_type = (string) ( $params['ConnectionType'] ?? '' );
$result_xml = (string) ( $params['ResponseFile'] ?? '' );
if ( '' === $result_xml && false !== strpos( $request['body'], ' $phase,
'route' => $request['route'],
'result_xml' => $result_xml,
'busy' => 'advertise' === $phase,
);
}
/**
* Build an offer or acknowledgement.
*
* @param array $printer Printer data.
* @param array $poll Parsed request.
* @param array|null $next_job Available job.
*/
public function advertise( array $printer, array $poll, ?array $next_job ): array {
return array(
'status' => 200,
'headers' => array( 'Content-Type' => 'text/xml; charset=utf-8' ),
'body' => '',
);
}
/**
* Choose a format before claiming.
*
* @param array $printer Printer data.
* @param array $poll Parsed request.
* @param array $job Candidate job.
*/
public function negotiate( array $printer, array $poll, array $job ): array {
// SDP uses the provider default render, not an HTTP Accept media type.
return array( 'media_type' => '' );
}
/**
* Build the print-data response, including empty renders.
*
* @param array $job Claimed job.
* @param array $render Rendered bytes.
* @param array $poll Parsed request.
*/
public function deliver( array $job, array $render, array $poll ): array {
if ( '' === $render['body'] ) {
return $this->advertise( array(), $poll, null );
}
// SDP 1.00 is supported by every printer family; this is not a SOAP envelope.
$envelope = '';
$envelope .= '';
$envelope .= 'local_printer' . self::EPSON_SDP_PRINT_TIMEOUT_MS . '';
$envelope .= '' . $render['body'] . '';
$envelope .= '';
return array(
'status' => 200,
'headers' => array( 'Content-Type' => 'text/xml; charset=utf-8' ),
'body' => $envelope,
);
}
/**
* Identify the result target.
*
* @param array $poll Parsed request.
*/
public function match_result( array $poll ): array {
// SDP 1.00 carries no job token: match the active or latest unconfirmed claim.
return array( 'job_id' => null );
}
/**
* Decode the printer result.
*
* @param array $poll Parsed request.
*/
public function interpret_result( array $poll ): array {
$result_xml = $poll['result_xml'];
$code = 'unknown';
if ( 1 === preg_match( '/\bcode="([^"]*)"/', $result_xml, $matches ) ) {
$code = sanitize_text_field( $matches[1] );
}
$status = null;
if ( 1 === preg_match( '/\bstatus="(\d+)"/', $result_xml, $matches ) ) {
// Do not misdecode unsigned bit 31 on a 32-bit PHP build.
$status = (float) $matches[1] <= PHP_INT_MAX ? (int) $matches[1] : null;
}
$flags = null === $status ? '' : implode( ', ', self::describe_epson_status( $status ) );
$detail = null === $status ? '' : sprintf( ' (0x%08X%s)', $status, '' === $flags ? '' : ': ' . $flags );
return array(
'ok' => false !== strpos( $result_xml, 'success="true"' ),
'code' => $code,
'detail' => $detail,
);
}
/**
* Decode an Epson ePOS-Print response status bitmask.
*
* @param int $status Decimal ASB status bitmask.
*
* @return array
*/
private static function describe_epson_status( int $status ): array {
// Epson ePOS-Print XML User's Manual, response `status` table (ASB bits).
// Fault bits only: informational ones (print complete, drawer pin, feed
// button, panel switch, buzzer) say nothing about why a print failed.
$labels = array(
0x00000001 => __( 'no response from printer', 'woocommerce-pos' ),
0x00000008 => __( 'offline', 'woocommerce-pos' ),
0x00000020 => __( 'cover open', 'woocommerce-pos' ),
0x00000100 => __( 'waiting for online recovery', 'woocommerce-pos' ),
0x00000400 => __( 'mechanical error', 'woocommerce-pos' ),
0x00000800 => __( 'autocutter error', 'woocommerce-pos' ),
0x00002000 => __( 'unrecoverable error', 'woocommerce-pos' ),
0x00004000 => __( 'auto-recoverable error', 'woocommerce-pos' ),
0x00020000 => __( 'paper near end', 'woocommerce-pos' ),
0x00080000 => __( 'paper end', 'woocommerce-pos' ),
0x80000000 => __( 'spooler stopped', 'woocommerce-pos' ),
);
$descriptions = array();
foreach ( $labels as $bit => $label ) {
if ( 0 !== ( $status & $bit ) ) {
$descriptions[] = $label;
}
}
return $descriptions;
}
}