1<?php 2/** 3 * This file is part of escpos-php: PHP receipt printer library for use with 4 * ESC/POS-compatible thermal and impact printers. 5 * 6 * Copyright (c) 2014-18 Michael Billington < michael.billington@gmail.com >, 7 * incorporating modifications by others. See CONTRIBUTORS.md for a full list. 8 * 9 * This software is distributed under the terms of the MIT license. See LICENSE.md 10 * for details. 11 */ 12 13namespace Mike42\Escpos\PrintConnectors; 14 15use Exception; 16use BadMethodCallException; 17 18/** 19 * Connector for sending print jobs to 20 * - local ports on windows (COM1, LPT1, etc) 21 * - shared (SMB) printers from any platform (smb://server/foo) 22 * For USB printers or other ports, the trick is to share the printer with a 23 * generic text driver, then connect to the shared printer locally. 24 */ 25class WindowsPrintConnector implements PrintConnector 26{ 27 /** 28 * @var array $buffer 29 * Accumulated lines of output for later use. 30 */ 31 private $buffer; 32 33 /** 34 * @var string $hostname 35 * The hostname of the target machine, or null if this is a local connection. 36 */ 37 private $hostname; 38 39 /** 40 * @var boolean $isLocal 41 * True if a port is being used directly (must be Windows), false if network shares will be used. 42 */ 43 private $isLocal; 44 45 /** 46 * @var int $platform 47 * Platform we're running on, for selecting different commands. See PLATFORM_* constants. 48 */ 49 private $platform; 50 51 /** 52 * @var string $printerName 53 * The name of the target printer (eg "Foo Printer") or port ("COM1", "LPT1"). 54 */ 55 private $printerName; 56 57 /** 58 * @var string $userName 59 * Login name for network printer, or null if not using authentication. 60 */ 61 private $userName; 62 63 /** 64 * @var string $userPassword 65 * Password for network printer, or null if no password is required. 66 */ 67 private $userPassword; 68 69 /** 70 * @var string $workgroup 71 * Workgroup that the printer is located on 72 */ 73 private $workgroup; 74 75 /** 76 * Represents Linux 77 */ 78 const PLATFORM_LINUX = 0; 79 80 /** 81 * Represents Mac 82 */ 83 const PLATFORM_MAC = 1; 84 85 /** 86 * Represents Windows 87 */ 88 const PLATFORM_WIN = 2; 89 90 /** 91 * Valid local ports. 92 */ 93 const REGEX_LOCAL = "/^(LPT\d|COM\d)$/"; 94 95 /** 96 * Valid printer name. 97 */ 98 const REGEX_PRINTERNAME = "/^[\d\w-]+(\s[\d\w-]+)*$/"; 99 100 /** 101 * Valid smb:// URI containing hostname & printer with optional user & optional password only. 102 */ 103 const REGEX_SMB = "/^smb:\/\/([\s\d\w-]+(:[\s\d\w+-]+)?@)?([\d\w-]+\.)*[\d\w-]+\/([\d\w-]+\/)?[\d\w-]+(\s[\d\w-]+)*$/"; 104 105 /** 106 * @param string $dest 107 * @throws BadMethodCallException 108 */ 109 public function __construct($dest) 110 { 111 $this -> platform = $this -> getCurrentPlatform(); 112 $this -> isLocal = false; 113 $this -> buffer = null; 114 $this -> userName = null; 115 $this -> userPassword = null; 116 $this -> workgroup = null; 117 if (preg_match(self::REGEX_LOCAL, $dest) == 1) { 118 // Straight to LPT1, COM1 or other local port. Allowed only if we are actually on windows. 119 if ($this -> platform !== self::PLATFORM_WIN) { 120 throw new BadMethodCallException("WindowsPrintConnector can only be " . 121 "used to print to a local printer ('".$dest."') on a Windows computer."); 122 } 123 $this -> isLocal = true; 124 $this -> hostname = null; 125 $this -> printerName = $dest; 126 } elseif (preg_match(self::REGEX_SMB, $dest) == 1) { 127 // Connect to samba share, eg smb://host/printer 128 $part = parse_url($dest); 129 $this -> hostname = $part['host']; 130 /* Printer name and optional workgroup */ 131 $path = ltrim($part['path'], '/'); 132 if (strpos($path, "/") !== false) { 133 $pathPart = explode("/", $path); 134 $this -> workgroup = $pathPart[0]; 135 $this -> printerName = $pathPart[1]; 136 } else { 137 $this -> printerName = $path; 138 } 139 /* Username and password if set */ 140 if (isset($part['user'])) { 141 $this -> userName = $part['user']; 142 if (isset($part['pass'])) { 143 $this -> userPassword = $part['pass']; 144 } 145 } 146 } elseif (preg_match(self::REGEX_PRINTERNAME, $dest) == 1) { 147 // Just got a printer name. Assume it's on the current computer. 148 $hostname = gethostname(); 149 if (!$hostname) { 150 $hostname = "localhost"; 151 } 152 $this -> hostname = $hostname; 153 $this -> printerName = $dest; 154 } else { 155 throw new BadMethodCallException("Printer '" . $dest . "' is not a valid " . 156 "printer name. Use local port (LPT1, COM1, etc) or smb://computer/printer notation."); 157 } 158 $this -> buffer = []; 159 } 160 161 public function __destruct() 162 { 163 if ($this -> buffer !== null) { 164 trigger_error("Print connector was not finalized. Did you forget to close the printer?", E_USER_NOTICE); 165 } 166 } 167 168 public function finalize() 169 { 170 $data = implode($this -> buffer); 171 $this -> buffer = null; 172 if ($this -> platform == self::PLATFORM_WIN) { 173 $this -> finalizeWin($data); 174 } elseif ($this -> platform == self::PLATFORM_LINUX) { 175 $this -> finalizeLinux($data); 176 } else { 177 $this -> finalizeMac($data); 178 } 179 } 180 181 /** 182 * Send job to printer -- platform-specific Linux code. 183 * 184 * @param string $data Print data 185 * @throws Exception 186 */ 187 protected function finalizeLinux($data) 188 { 189 /* Non-Windows samba printing */ 190 $device = "//" . $this -> hostname . "/" . $this -> printerName; 191 if ($this -> userName !== null) { 192 $user = ($this -> workgroup != null ? ($this -> workgroup . "\\") : "") . $this -> userName; 193 if ($this -> userPassword == null) { 194 // No password 195 $command = sprintf( 196 "smbclient %s -U %s -c %s -N -m SMB2", 197 escapeshellarg($device), 198 escapeshellarg($user), 199 escapeshellarg("print -") 200 ); 201 $redactedCommand = $command; 202 } else { 203 // With password 204 $command = sprintf( 205 "smbclient %s %s -U %s -c %s -m SMB2", 206 escapeshellarg($device), 207 escapeshellarg($this -> userPassword), 208 escapeshellarg($user), 209 escapeshellarg("print -") 210 ); 211 $redactedCommand = sprintf( 212 "smbclient %s %s -U %s -c %s -m SMB2", 213 escapeshellarg($device), 214 escapeshellarg("*****"), 215 escapeshellarg($user), 216 escapeshellarg("print -") 217 ); 218 } 219 } else { 220 // No authentication information at all 221 $command = sprintf( 222 "smbclient %s -c %s -N -m SMB2", 223 escapeshellarg($device), 224 escapeshellarg("print -") 225 ); 226 $redactedCommand = $command; 227 } 228 $retval = $this -> runCommand($command, $outputStr, $errorStr, $data); 229 if ($retval != 0) { 230 throw new Exception("Failed to print. Command \"$redactedCommand\" " . 231 "failed with exit code $retval: " . trim($errorStr) . trim($outputStr)); 232 } 233 } 234 235 /** 236 * Send job to printer -- platform-specific Mac code. 237 * 238 * @param string $data Print data 239 * @throws Exception 240 */ 241 protected function finalizeMac($data) 242 { 243 throw new Exception("Mac printing not implemented."); 244 } 245 246 /** 247 * Send data to printer -- platform-specific Windows code. 248 * 249 * @param string $data 250 */ 251 protected function finalizeWin($data) 252 { 253 /* Windows-friendly printing of all sorts */ 254 if (!$this -> isLocal) { 255 /* Networked printing */ 256 $device = "\\\\" . $this -> hostname . "\\" . $this -> printerName; 257 if ($this -> userName !== null) { 258 /* Log in */ 259 $user = "/user:" . ($this -> workgroup != null ? ($this -> workgroup . "\\") : "") . $this -> userName; 260 if ($this -> userPassword == null) { 261 $command = sprintf( 262 "net use %s %s", 263 escapeshellarg($device), 264 escapeshellarg($user) 265 ); 266 $redactedCommand = $command; 267 } else { 268 $command = sprintf( 269 "net use %s %s %s", 270 escapeshellarg($device), 271 escapeshellarg($user), 272 escapeshellarg($this -> userPassword) 273 ); 274 $redactedCommand = sprintf( 275 "net use %s %s %s", 276 escapeshellarg($device), 277 escapeshellarg($user), 278 escapeshellarg("*****") 279 ); 280 } 281 $retval = $this -> runCommand($command, $outputStr, $errorStr); 282 if ($retval != 0) { 283 throw new Exception("Failed to print. Command \"$redactedCommand\" " . 284 "failed with exit code $retval: " . trim($errorStr)); 285 } 286 } 287 /* Final print-out */ 288 $filename = tempnam(sys_get_temp_dir(), "escpos"); 289 file_put_contents($filename, $data); 290 if (!$this -> runCopy($filename, $device)) { 291 throw new Exception("Failed to copy file to printer"); 292 } 293 unlink($filename); 294 } else { 295 /* Drop data straight on the printer */ 296 if (!$this -> runWrite($data, $this -> printerName)) { 297 throw new Exception("Failed to write file to printer at " . $this -> printerName); 298 } 299 } 300 } 301 302 /** 303 * @return string Current platform. Separated out for testing purposes. 304 */ 305 protected function getCurrentPlatform() 306 { 307 if (PHP_OS == "WINNT") { 308 return self::PLATFORM_WIN; 309 } 310 if (PHP_OS == "Darwin") { 311 return self::PLATFORM_MAC; 312 } 313 return self::PLATFORM_LINUX; 314 } 315 316 /* (non-PHPdoc) 317 * @see PrintConnector::read() 318 */ 319 public function read($len) 320 { 321 /* Two-way communication is not supported */ 322 return false; 323 } 324 325 /** 326 * Run a command, pass it data, and retrieve its return value, standard output, and standard error. 327 * 328 * @param string $command the command to run. 329 * @param string $outputStr variable to fill with standard output. 330 * @param string $errorStr variable to fill with standard error. 331 * @param string $inputStr text to pass to the command's standard input (optional). 332 * @return number 333 */ 334 protected function runCommand($command, &$outputStr, &$errorStr, $inputStr = null) 335 { 336 $descriptors = [ 337 0 => ["pipe", "r"], 338 1 => ["pipe", "w"], 339 2 => ["pipe", "w"], 340 ]; 341 $process = proc_open($command, $descriptors, $fd); 342 if (is_resource($process)) { 343 /* Write to input */ 344 if ($inputStr !== null) { 345 fwrite($fd[0], $inputStr); 346 } 347 fclose($fd[0]); 348 /* Read stdout */ 349 $outputStr = stream_get_contents($fd[1]); 350 fclose($fd[1]); 351 /* Read stderr */ 352 $errorStr = stream_get_contents($fd[2]); 353 fclose($fd[2]); 354 /* Finish up */ 355 $retval = proc_close($process); 356 return $retval; 357 } else { 358 /* Method calling this should notice a non-zero exit and print an error */ 359 return -1; 360 } 361 } 362 363 /** 364 * Copy a file. Separated out so that nothing is actually printed during test runs. 365 * 366 * @param string $from Source file 367 * @param string $to Destination file 368 * @return boolean True if copy was successful, false otherwise 369 */ 370 protected function runCopy($from, $to) 371 { 372 return copy($from, $to); 373 } 374 375 /** 376 * Write data to a file. Separated out so that nothing is actually printed during test runs. 377 * 378 * @param string $data Data to print 379 * @param string $filename Destination file 380 * @return boolean True if write was successful, false otherwise 381 */ 382 protected function runWrite($data, $filename) 383 { 384 return file_put_contents($filename, $data) !== false; 385 } 386 387 public function write($data) 388 { 389 $this -> buffer[] = $data; 390 } 391} 392