| 1 |
<?php |
| 2 |
/** |
| 3 |
* Cloud printer registry (reads woocommerce_pos_settings_cloud_print). |
| 4 |
* |
| 5 |
* @package WCPOS\WooCommercePOS\Services |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace WCPOS\WooCommercePOS\Services; |
| 9 |
|
| 10 |
/** |
| 11 |
* Cloud_Print_Registry class. |
| 12 |
*/ |
| 13 |
class Cloud_Print_Registry { |
| 14 |
const OPTION = 'woocommerce_pos_settings_cloud_print'; |
| 15 |
|
| 16 |
const RUNTIME_OPTION = 'woocommerce_pos_cloud_print_runtime'; |
| 17 |
const SEEN_TTL = 150; // Seconds; connected if seen within this window. |
| 18 |
const PN_STATUS_TTL = 60; // Seconds; PrintNode live-status cache window. |
| 19 |
|
| 20 |
/** |
| 21 |
* All registered cloud printers. |
| 22 |
* |
| 23 |
* @return array<int, array> |
| 24 |
*/ |
| 25 |
public function get_printers(): array { |
| 26 |
$settings = get_option( self::OPTION, array() ); |
| 27 |
|
| 28 |
return isset( $settings['printers'] ) && \is_array( $settings['printers'] ) ? $settings['printers'] : array(); |
| 29 |
} |
| 30 |
|
| 31 |
/** |
| 32 |
* Get a registered cloud printer by id. |
| 33 |
* |
| 34 |
* @param string $printer_id Printer id. |
| 35 |
* |
| 36 |
* @return array|null |
| 37 |
*/ |
| 38 |
public function get_printer( string $printer_id ): ?array { |
| 39 |
foreach ( $this->get_printers() as $printer ) { |
| 40 |
if ( isset( $printer['id'] ) && hash_equals( (string) $printer['id'], $printer_id ) ) { |
| 41 |
return $printer; |
| 42 |
} |
| 43 |
} |
| 44 |
|
| 45 |
return null; |
| 46 |
} |
| 47 |
|
| 48 |
/** |
| 49 |
* Verify a printer's poll token (constant-time). |
| 50 |
* |
| 51 |
* @param string $printer_id Printer id. |
| 52 |
* @param string $token Presented token. |
| 53 |
*/ |
| 54 |
public function verify_token( string $printer_id, string $token ): bool { |
| 55 |
$printer = $this->get_printer( $printer_id ); |
| 56 |
if ( null === $printer || empty( $printer['poll_token_hash'] ) || '' === $token ) { |
| 57 |
return false; |
| 58 |
} |
| 59 |
|
| 60 |
return hash_equals( (string) $printer['poll_token_hash'], self::hash_token( $token ) ); |
| 61 |
} |
| 62 |
|
| 63 |
/** |
| 64 |
* Generate a cryptographically strong poll token (returned to the admin once). |
| 65 |
*/ |
| 66 |
public static function generate_token(): string { |
| 67 |
return bin2hex( random_bytes( 24 ) ); |
| 68 |
} |
| 69 |
|
| 70 |
/** |
| 71 |
* Hash a poll token for at-rest storage (we never persist the plaintext). |
| 72 |
* |
| 73 |
* @param string $token Token. |
| 74 |
*/ |
| 75 |
public static function hash_token( string $token ): string { |
| 76 |
return hash( 'sha256', $token ); |
| 77 |
} |
| 78 |
|
| 79 |
/** |
| 80 |
* Derive a stable, URL-safe printer id from a display name, unique against existing ids. |
| 81 |
* |
| 82 |
* @param string $name Display name. |
| 83 |
* @param array<string> $existing_ids Already-used ids. |
| 84 |
* |
| 85 |
* @return string |
| 86 |
*/ |
| 87 |
public static function derive_id( string $name, array $existing_ids ): string { |
| 88 |
$base = sanitize_title( $name ); |
| 89 |
if ( '' === $base ) { |
| 90 |
$base = 'printer'; |
| 91 |
} |
| 92 |
$candidate = $base; |
| 93 |
$suffix = 2; |
| 94 |
while ( \in_array( $candidate, $existing_ids, true ) ) { |
| 95 |
$candidate = $base . '-' . $suffix; |
| 96 |
++$suffix; |
| 97 |
} |
| 98 |
|
| 99 |
return $candidate; |
| 100 |
} |
| 101 |
|
| 102 |
/** |
| 103 |
* Record that a printer polled just now. |
| 104 |
* |
| 105 |
* @param string $printer_id Printer id. |
| 106 |
*/ |
| 107 |
public function record_seen( string $printer_id ): void { |
| 108 |
$runtime = get_option( self::RUNTIME_OPTION, array() ); |
| 109 |
$runtime = \is_array( $runtime ) ? $runtime : array(); |
| 110 |
$runtime[ $printer_id ] = time(); |
| 111 |
update_option( self::RUNTIME_OPTION, $runtime, false ); // Autoload no. |
| 112 |
} |
| 113 |
|
| 114 |
/** |
| 115 |
* Get a printer's last-seen unix timestamp (0 if never). |
| 116 |
* |
| 117 |
* @param string $printer_id Printer id. |
| 118 |
* |
| 119 |
* @return int |
| 120 |
*/ |
| 121 |
public function get_seen( string $printer_id ): int { |
| 122 |
$runtime = get_option( self::RUNTIME_OPTION, array() ); |
| 123 |
|
| 124 |
return ( \is_array( $runtime ) && isset( $runtime[ $printer_id ] ) ) ? (int) $runtime[ $printer_id ] : 0; |
| 125 |
} |
| 126 |
|
| 127 |
/** |
| 128 |
* Drop runtime last-seen entries for printer ids that no longer exist. |
| 129 |
* |
| 130 |
* Prevents the runtime option from growing unbounded as printers are |
| 131 |
* removed, and stops a recreated id (slug reuse) from inheriting a deleted |
| 132 |
* printer's stale status. |
| 133 |
* |
| 134 |
* @param array<string> $keep_ids Printer ids to retain. |
| 135 |
*/ |
| 136 |
public function prune_seen( array $keep_ids ): void { |
| 137 |
$runtime = get_option( self::RUNTIME_OPTION, array() ); |
| 138 |
if ( ! \is_array( $runtime ) ) { |
| 139 |
return; |
| 140 |
} |
| 141 |
$pruned = array_intersect_key( $runtime, array_flip( $keep_ids ) ); |
| 142 |
if ( $pruned !== $runtime ) { |
| 143 |
update_option( self::RUNTIME_OPTION, $pruned, false ); |
| 144 |
} |
| 145 |
} |
| 146 |
|
| 147 |
/** |
| 148 |
* Connection status for a printer. |
| 149 |
* |
| 150 |
* For PrintNode printers this returns PrintNode's live vocabulary |
| 151 |
* ('online'|'offline'|'unknown'), cached briefly. For polling printers |
| 152 |
* (Star/Epson) it returns 'waiting' (never polled), 'connected' (polled |
| 153 |
* within SEEN_TTL), or 'offline' (polled, but stale). |
| 154 |
* |
| 155 |
* @param string $printer_id Printer id. |
| 156 |
* |
| 157 |
* @return string |
| 158 |
*/ |
| 159 |
public function status_for( string $printer_id ): string { |
| 160 |
$printer = $this->get_printer( $printer_id ); |
| 161 |
if ( null !== $printer && 'printnode' === ( $printer['provider'] ?? '' ) ) { |
| 162 |
return $this->printnode_status( $printer ); |
| 163 |
} |
| 164 |
|
| 165 |
if ( null !== $printer && 'star-online' === ( $printer['provider'] ?? '' ) ) { |
| 166 |
return $this->star_online_status( $printer ); |
| 167 |
} |
| 168 |
|
| 169 |
if ( null !== $printer && Provider::is_polling( (string) ( $printer['provider'] ?? '' ) ) ) { |
| 170 |
$relay_status = Cloud_Print_Relay_Service::status( $printer_id ); |
| 171 |
if ( null !== $relay_status && 'blocked' === $relay_status['origin_status'] ) { |
| 172 |
return 'blocked'; |
| 173 |
} |
| 174 |
if ( null !== $relay_status && null !== $relay_status['last_seen_seconds_ago'] && $relay_status['last_seen_seconds_ago'] <= self::SEEN_TTL ) { |
| 175 |
return 'connected'; |
| 176 |
} |
| 177 |
} |
| 178 |
|
| 179 |
$seen = $this->get_seen( $printer_id ); |
| 180 |
if ( 0 === $seen ) { |
| 181 |
return 'waiting'; |
| 182 |
} |
| 183 |
|
| 184 |
return ( time() - $seen ) <= self::SEEN_TTL ? 'connected' : 'offline'; |
| 185 |
} |
| 186 |
|
| 187 |
/** |
| 188 |
* The relay's block signal for a printer, when it reports one. |
| 189 |
* |
| 190 |
* Delegates to the relay service's transient-cached status, so this is |
| 191 |
* safe to call in any order relative to status_for(). |
| 192 |
* |
| 193 |
* @param string $printer_id Printer ID. |
| 194 |
* |
| 195 |
* @return string|null |
| 196 |
*/ |
| 197 |
public function status_detail_for( string $printer_id ): ?string { |
| 198 |
return Cloud_Print_Relay_Service::status_detail( $printer_id ); |
| 199 |
} |
| 200 |
|
| 201 |
/** |
| 202 |
* Resolve a PrintNode printer's live status, cached for PN_STATUS_TTL seconds. |
| 203 |
* |
| 204 |
* All outcomes are cached, including 'unknown'/'offline', to avoid hammering |
| 205 |
* the PrintNode API on every settings read. |
| 206 |
* |
| 207 |
* @param array $printer Registered PrintNode printer. |
| 208 |
* |
| 209 |
* @return string 'online', 'offline', or 'unknown'. |
| 210 |
*/ |
| 211 |
private function printnode_status( array $printer ): string { |
| 212 |
$key = 'wcpos_cloud_print_pn_status_' . md5( (string) $printer['id'] ); |
| 213 |
$cached = get_transient( $key ); |
| 214 |
if ( false !== $cached ) { |
| 215 |
return (string) $cached; |
| 216 |
} |
| 217 |
|
| 218 |
$api_key = (string) ( $printer['printnode_api_key'] ?? '' ); |
| 219 |
$pn_printer_id = (int) ( $printer['printnode_printer_id'] ?? 0 ); |
| 220 |
if ( '' === $api_key || 0 === $pn_printer_id ) { |
| 221 |
$status = 'unknown'; |
| 222 |
} else { |
| 223 |
$status = ( new PrintNode_Client( $api_key ) )->printer_state( $pn_printer_id ); |
| 224 |
} |
| 225 |
|
| 226 |
set_transient( $key, $status, self::PN_STATUS_TTL ); |
| 227 |
|
| 228 |
return $status; |
| 229 |
} |
| 230 |
/** |
| 231 |
* Resolve a Star Online device's live status, cached for PN_STATUS_TTL seconds. |
| 232 |
* |
| 233 |
* @param array $printer Registered star-online printer. |
| 234 |
* |
| 235 |
* @return string 'online', 'offline', or 'unknown'. |
| 236 |
*/ |
| 237 |
private function star_online_status( array $printer ): string { |
| 238 |
$key = 'wcpos_cloud_print_star_status_' . md5( (string) $printer['id'] ); |
| 239 |
$cached = get_transient( $key ); |
| 240 |
if ( false !== $cached ) { |
| 241 |
return (string) $cached; |
| 242 |
} |
| 243 |
|
| 244 |
$api_key = (string) ( $printer['star_api_key'] ?? '' ); |
| 245 |
$url = (string) ( $printer['star_cloudprnt_url'] ?? '' ); |
| 246 |
$device_id = (string) ( $printer['star_device_id'] ?? '' ); |
| 247 |
$api_base = Star_Online_Client::api_base_from_cloudprnt_url( $url ); |
| 248 |
$group = Star_Online_Client::group_from_cloudprnt_url( $url ); |
| 249 |
|
| 250 |
if ( '' === $api_key || null === $api_base || '' === $group || '' === $device_id ) { |
| 251 |
$status = 'unknown'; |
| 252 |
} else { |
| 253 |
$status = ( new Star_Online_Client( $api_base, $api_key ) )->device_state( $group, $device_id ); |
| 254 |
} |
| 255 |
|
| 256 |
set_transient( $key, $status, self::PN_STATUS_TTL ); |
| 257 |
|
| 258 |
return $status; |
| 259 |
} |
| 260 |
} |
| 261 |
|