1 /* 2 * PPD utilities for CUPS. 3 * 4 * Copyright 2007-2015 by Apple Inc. 5 * Copyright 1997-2006 by Easy Software Products. 6 * 7 * These coded instructions, statements, and computer programs are the 8 * property of Apple Inc. and are protected by Federal copyright 9 * law. Distribution and use rights are outlined in the file "LICENSE.txt" 10 * which should have been included with this file. If this file is 11 * missing or damaged, see the license at "http://www.cups.org/". 12 * 13 * This file is subject to the Apple OS-Developed Software exception. 14 */ 15 16 /* 17 * Include necessary headers... 18 */ 19 20 #include "cups-private.h" 21 #include "ppd-private.h" 22 #include <fcntl.h> 23 #include <sys/stat.h> 24 #if defined(WIN32) || defined(__EMX__) 25 # include <io.h> 26 #else 27 # include <unistd.h> 28 #endif /* WIN32 || __EMX__ */ 29 30 31 /* 32 * Local functions... 33 */ 34 35 static int cups_get_printer_uri(http_t *http, const char *name, 36 char *host, int hostsize, int *port, 37 char *resource, int resourcesize, 38 int depth); 39 40 41 /* 42 * 'cupsGetPPD()' - Get the PPD file for a printer on the default server. 43 * 44 * For classes, @code cupsGetPPD@ returns the PPD file for the first printer 45 * in the class. 46 * 47 * The returned filename is stored in a static buffer and is overwritten with 48 * each call to @code cupsGetPPD@ or @link cupsGetPPD2@. The caller "owns" the 49 * file that is created and must @code unlink@ the returned filename. 50 */ 51 52 const char * /* O - Filename for PPD file */ 53 cupsGetPPD(const char *name) /* I - Destination name */ 54 { 55 _ppd_globals_t *pg = _ppdGlobals(); /* Pointer to library globals */ 56 time_t modtime = 0; /* Modification time */ 57 58 59 /* 60 * Return the PPD file... 61 */ 62 63 pg->ppd_filename[0] = '\0'; 64 65 if (cupsGetPPD3(CUPS_HTTP_DEFAULT, name, &modtime, pg->ppd_filename, 66 sizeof(pg->ppd_filename)) == HTTP_STATUS_OK) 67 return (pg->ppd_filename); 68 else 69 return (NULL); 70 } 71 72 73 /* 74 * 'cupsGetPPD2()' - Get the PPD file for a printer from the specified server. 75 * 76 * For classes, @code cupsGetPPD2@ returns the PPD file for the first printer 77 * in the class. 78 * 79 * The returned filename is stored in a static buffer and is overwritten with 80 * each call to @link cupsGetPPD@ or @code cupsGetPPD2@. The caller "owns" the 81 * file that is created and must @code unlink@ the returned filename. 82 * 83 * @since CUPS 1.1.21/macOS 10.4@ 84 */ 85 86 const char * /* O - Filename for PPD file */ 87 cupsGetPPD2(http_t *http, /* I - Connection to server or @code CUPS_HTTP_DEFAULT@ */ 88 const char *name) /* I - Destination name */ 89 { 90 _ppd_globals_t *pg = _ppdGlobals(); /* Pointer to library globals */ 91 time_t modtime = 0; /* Modification time */ 92 93 94 pg->ppd_filename[0] = '\0'; 95 96 if (cupsGetPPD3(http, name, &modtime, pg->ppd_filename, 97 sizeof(pg->ppd_filename)) == HTTP_STATUS_OK) 98 return (pg->ppd_filename); 99 else 100 return (NULL); 101 } 102 103 104 /* 105 * 'cupsGetPPD3()' - Get the PPD file for a printer on the specified 106 * server if it has changed. 107 * 108 * The "modtime" parameter contains the modification time of any 109 * locally-cached content and is updated with the time from the PPD file on 110 * the server. 111 * 112 * The "buffer" parameter contains the local PPD filename. If it contains 113 * the empty string, a new temporary file is created, otherwise the existing 114 * file will be overwritten as needed. The caller "owns" the file that is 115 * created and must @code unlink@ the returned filename. 116 * 117 * On success, @code HTTP_STATUS_OK@ is returned for a new PPD file and 118 * @code HTTP_STATUS_NOT_MODIFIED@ if the existing PPD file is up-to-date. Any other 119 * status is an error. 120 * 121 * For classes, @code cupsGetPPD3@ returns the PPD file for the first printer 122 * in the class. 123 * 124 * @since CUPS 1.4/macOS 10.6@ 125 */ 126 127 http_status_t /* O - HTTP status */ 128 cupsGetPPD3(http_t *http, /* I - HTTP connection or @code CUPS_HTTP_DEFAULT@ */ 129 const char *name, /* I - Destination name */ 130 time_t *modtime, /* IO - Modification time */ 131 char *buffer, /* I - Filename buffer */ 132 size_t bufsize) /* I - Size of filename buffer */ 133 { 134 int http_port; /* Port number */ 135 char http_hostname[HTTP_MAX_HOST]; 136 /* Hostname associated with connection */ 137 http_t *http2; /* Alternate HTTP connection */ 138 int fd; /* PPD file */ 139 char localhost[HTTP_MAX_URI],/* Local hostname */ 140 hostname[HTTP_MAX_URI], /* Hostname */ 141 resource[HTTP_MAX_URI]; /* Resource name */ 142 int port; /* Port number */ 143 http_status_t status; /* HTTP status from server */ 144 char tempfile[1024] = ""; /* Temporary filename */ 145 _cups_globals_t *cg = _cupsGlobals(); /* Pointer to library globals */ 146 147 148 /* 149 * Range check input... 150 */ 151 152 DEBUG_printf(("cupsGetPPD3(http=%p, name=\"%s\", modtime=%p(%d), buffer=%p, " 153 "bufsize=%d)", http, name, modtime, 154 modtime ? (int)*modtime : 0, buffer, (int)bufsize)); 155 156 if (!name) 157 { 158 _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("No printer name"), 1); 159 return (HTTP_STATUS_NOT_ACCEPTABLE); 160 } 161 162 if (!modtime) 163 { 164 _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("No modification time"), 1); 165 return (HTTP_STATUS_NOT_ACCEPTABLE); 166 } 167 168 if (!buffer || bufsize <= 1) 169 { 170 _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("Bad filename buffer"), 1); 171 return (HTTP_STATUS_NOT_ACCEPTABLE); 172 } 173 174 #ifndef WIN32 175 /* 176 * See if the PPD file is available locally... 177 */ 178 179 if (http) 180 httpGetHostname(http, hostname, sizeof(hostname)); 181 else 182 { 183 strlcpy(hostname, cupsServer(), sizeof(hostname)); 184 if (hostname[0] == '/') 185 strlcpy(hostname, "localhost", sizeof(hostname)); 186 } 187 188 if (!_cups_strcasecmp(hostname, "localhost")) 189 { 190 char ppdname[1024]; /* PPD filename */ 191 struct stat ppdinfo; /* PPD file information */ 192 193 194 snprintf(ppdname, sizeof(ppdname), "%s/ppd/%s.ppd", cg->cups_serverroot, 195 name); 196 if (!stat(ppdname, &ppdinfo) && !access(ppdname, R_OK)) 197 { 198 /* 199 * OK, the file exists and is readable, use it! 200 */ 201 202 if (buffer[0]) 203 { 204 unlink(buffer); 205 206 if (symlink(ppdname, buffer) && errno != EEXIST) 207 { 208 _cupsSetError(IPP_STATUS_ERROR_INTERNAL, NULL, 0); 209 210 return (HTTP_STATUS_SERVER_ERROR); 211 } 212 } 213 else 214 { 215 int tries; /* Number of tries */ 216 const char *tmpdir; /* TMPDIR environment variable */ 217 struct timeval curtime; /* Current time */ 218 219 /* 220 * Previously we put root temporary files in the default CUPS temporary 221 * directory under /var/spool/cups. However, since the scheduler cleans 222 * out temporary files there and runs independently of the user apps, we 223 * don't want to use it unless specifically told to by cupsd. 224 */ 225 226 if ((tmpdir = getenv("TMPDIR")) == NULL) 227 # ifdef __APPLE__ 228 tmpdir = "/private/tmp"; /* /tmp is a symlink to /private/tmp */ 229 # else 230 tmpdir = "/tmp"; 231 # endif /* __APPLE__ */ 232 233 /* 234 * Make the temporary name using the specified directory... 235 */ 236 237 tries = 0; 238 239 do 240 { 241 /* 242 * Get the current time of day... 243 */ 244 245 gettimeofday(&curtime, NULL); 246 247 /* 248 * Format a string using the hex time values... 249 */ 250 251 snprintf(buffer, bufsize, "%s/%08lx%05lx", tmpdir, 252 (unsigned long)curtime.tv_sec, 253 (unsigned long)curtime.tv_usec); 254 255 /* 256 * Try to make a symlink... 257 */ 258 259 if (!symlink(ppdname, buffer)) 260 break; 261 262 tries ++; 263 } 264 while (tries < 1000); 265 266 if (tries >= 1000) 267 { 268 _cupsSetError(IPP_STATUS_ERROR_INTERNAL, NULL, 0); 269 270 return (HTTP_STATUS_SERVER_ERROR); 271 } 272 } 273 274 if (*modtime >= ppdinfo.st_mtime) 275 return (HTTP_STATUS_NOT_MODIFIED); 276 else 277 { 278 *modtime = ppdinfo.st_mtime; 279 return (HTTP_STATUS_OK); 280 } 281 } 282 } 283 #endif /* !WIN32 */ 284 285 /* 286 * Try finding a printer URI for this printer... 287 */ 288 289 if (!http) 290 if ((http = _cupsConnect()) == NULL) 291 return (HTTP_STATUS_SERVICE_UNAVAILABLE); 292 293 if (!cups_get_printer_uri(http, name, hostname, sizeof(hostname), &port, 294 resource, sizeof(resource), 0)) 295 return (HTTP_STATUS_NOT_FOUND); 296 297 DEBUG_printf(("2cupsGetPPD3: Printer hostname=\"%s\", port=%d", hostname, 298 port)); 299 300 if (cupsServer()[0] == '/' && !_cups_strcasecmp(hostname, "localhost") && port == ippPort()) 301 { 302 /* 303 * Redirect localhost to domain socket... 304 */ 305 306 strlcpy(hostname, cupsServer(), sizeof(hostname)); 307 port = 0; 308 309 DEBUG_printf(("2cupsGetPPD3: Redirecting to \"%s\".", hostname)); 310 } 311 312 /* 313 * Remap local hostname to localhost... 314 */ 315 316 httpGetHostname(NULL, localhost, sizeof(localhost)); 317 318 DEBUG_printf(("2cupsGetPPD3: Local hostname=\"%s\"", localhost)); 319 320 if (!_cups_strcasecmp(localhost, hostname)) 321 strlcpy(hostname, "localhost", sizeof(hostname)); 322 323 /* 324 * Get the hostname and port number we are connected to... 325 */ 326 327 httpGetHostname(http, http_hostname, sizeof(http_hostname)); 328 http_port = httpAddrPort(http->hostaddr); 329 330 DEBUG_printf(("2cupsGetPPD3: Connection hostname=\"%s\", port=%d", 331 http_hostname, http_port)); 332 333 /* 334 * Reconnect to the correct server as needed... 335 */ 336 337 if (!_cups_strcasecmp(http_hostname, hostname) && port == http_port) 338 http2 = http; 339 else if ((http2 = httpConnect2(hostname, port, NULL, AF_UNSPEC, 340 cupsEncryption(), 1, 30000, NULL)) == NULL) 341 { 342 DEBUG_puts("1cupsGetPPD3: Unable to connect to server"); 343 344 return (HTTP_STATUS_SERVICE_UNAVAILABLE); 345 } 346 347 /* 348 * Get a temp file... 349 */ 350 351 if (buffer[0]) 352 fd = open(buffer, O_CREAT | O_TRUNC | O_WRONLY, 0600); 353 else 354 fd = cupsTempFd(tempfile, sizeof(tempfile)); 355 356 if (fd < 0) 357 { 358 /* 359 * Can't open file; close the server connection and return NULL... 360 */ 361 362 _cupsSetError(IPP_STATUS_ERROR_INTERNAL, NULL, 0); 363 364 if (http2 != http) 365 httpClose(http2); 366 367 return (HTTP_STATUS_SERVER_ERROR); 368 } 369 370 /* 371 * And send a request to the HTTP server... 372 */ 373 374 strlcat(resource, ".ppd", sizeof(resource)); 375 376 if (*modtime > 0) 377 httpSetField(http2, HTTP_FIELD_IF_MODIFIED_SINCE, 378 httpGetDateString(*modtime)); 379 380 status = cupsGetFd(http2, resource, fd); 381 382 close(fd); 383 384 /* 385 * See if we actually got the file or an error... 386 */ 387 388 if (status == HTTP_STATUS_OK) 389 { 390 *modtime = httpGetDateTime(httpGetField(http2, HTTP_FIELD_DATE)); 391 392 if (tempfile[0]) 393 strlcpy(buffer, tempfile, bufsize); 394 } 395 else if (status != HTTP_STATUS_NOT_MODIFIED) 396 { 397 _cupsSetHTTPError(status); 398 399 if (buffer[0]) 400 unlink(buffer); 401 else if (tempfile[0]) 402 unlink(tempfile); 403 } 404 else if (tempfile[0]) 405 unlink(tempfile); 406 407 if (http2 != http) 408 httpClose(http2); 409 410 /* 411 * Return the PPD file... 412 */ 413 414 DEBUG_printf(("1cupsGetPPD3: Returning status %d", status)); 415 416 return (status); 417 } 418 419 420 /* 421 * 'cupsGetServerPPD()' - Get an available PPD file from the server. 422 * 423 * This function returns the named PPD file from the server. The 424 * list of available PPDs is provided by the IPP @code CUPS_GET_PPDS@ 425 * operation. 426 * 427 * You must remove (unlink) the PPD file when you are finished with 428 * it. The PPD filename is stored in a static location that will be 429 * overwritten on the next call to @link cupsGetPPD@, @link cupsGetPPD2@, 430 * or @link cupsGetServerPPD@. 431 * 432 * @since CUPS 1.3/macOS 10.5@ 433 */ 434 435 char * /* O - Name of PPD file or @code NULL@ on error */ 436 cupsGetServerPPD(http_t *http, /* I - Connection to server or @code CUPS_HTTP_DEFAULT@ */ 437 const char *name) /* I - Name of PPD file ("ppd-name") */ 438 { 439 int fd; /* PPD file descriptor */ 440 ipp_t *request; /* IPP request */ 441 _ppd_globals_t *pg = _ppdGlobals(); 442 /* Pointer to library globals */ 443 444 445 /* 446 * Range check input... 447 */ 448 449 if (!name) 450 { 451 _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("No PPD name"), 1); 452 453 return (NULL); 454 } 455 456 if (!http) 457 if ((http = _cupsConnect()) == NULL) 458 return (NULL); 459 460 /* 461 * Get a temp file... 462 */ 463 464 if ((fd = cupsTempFd(pg->ppd_filename, sizeof(pg->ppd_filename))) < 0) 465 { 466 /* 467 * Can't open file; close the server connection and return NULL... 468 */ 469 470 _cupsSetError(IPP_STATUS_ERROR_INTERNAL, NULL, 0); 471 472 return (NULL); 473 } 474 475 /* 476 * Get the PPD file... 477 */ 478 479 request = ippNewRequest(IPP_OP_CUPS_GET_PPD); 480 ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "ppd-name", NULL, 481 name); 482 483 ippDelete(cupsDoIORequest(http, request, "/", -1, fd)); 484 485 close(fd); 486 487 if (cupsLastError() != IPP_STATUS_OK) 488 { 489 unlink(pg->ppd_filename); 490 return (NULL); 491 } 492 else 493 return (pg->ppd_filename); 494 } 495 496 497 /* 498 * 'cups_get_printer_uri()' - Get the printer-uri-supported attribute for the 499 * first printer in a class. 500 */ 501 502 static int /* O - 1 on success, 0 on failure */ 503 cups_get_printer_uri( 504 http_t *http, /* I - Connection to server */ 505 const char *name, /* I - Name of printer or class */ 506 char *host, /* I - Hostname buffer */ 507 int hostsize, /* I - Size of hostname buffer */ 508 int *port, /* O - Port number */ 509 char *resource, /* I - Resource buffer */ 510 int resourcesize, /* I - Size of resource buffer */ 511 int depth) /* I - Depth of query */ 512 { 513 int i; /* Looping var */ 514 int http_port; /* Port number */ 515 http_t *http2; /* Alternate HTTP connection */ 516 ipp_t *request, /* IPP request */ 517 *response; /* IPP response */ 518 ipp_attribute_t *attr; /* Current attribute */ 519 char uri[HTTP_MAX_URI], /* printer-uri attribute */ 520 scheme[HTTP_MAX_URI], /* Scheme name */ 521 username[HTTP_MAX_URI], /* Username:password */ 522 classname[255], /* Temporary class name */ 523 http_hostname[HTTP_MAX_HOST]; 524 /* Hostname associated with connection */ 525 static const char * const requested_attrs[] = 526 { /* Requested attributes */ 527 "device-uri", 528 "member-uris", 529 "printer-uri-supported", 530 "printer-type" 531 }; 532 533 534 DEBUG_printf(("4cups_get_printer_uri(http=%p, name=\"%s\", host=%p, hostsize=%d, resource=%p, resourcesize=%d, depth=%d)", http, name, host, hostsize, resource, resourcesize, depth)); 535 536 /* 537 * Setup the printer URI... 538 */ 539 540 if (httpAssembleURIf(HTTP_URI_CODING_ALL, uri, sizeof(uri), "ipp", NULL, "localhost", 0, "/printers/%s", name) < HTTP_URI_STATUS_OK) 541 { 542 _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("Unable to create printer-uri"), 1); 543 544 *host = '\0'; 545 *resource = '\0'; 546 547 return (0); 548 } 549 550 DEBUG_printf(("5cups_get_printer_uri: printer-uri=\"%s\"", uri)); 551 552 /* 553 * Get the hostname and port number we are connected to... 554 */ 555 556 httpGetHostname(http, http_hostname, sizeof(http_hostname)); 557 http_port = httpAddrPort(http->hostaddr); 558 559 DEBUG_printf(("5cups_get_printer_uri: http_hostname=\"%s\"", http_hostname)); 560 561 /* 562 * Build an IPP_GET_PRINTER_ATTRIBUTES request, which requires the following 563 * attributes: 564 * 565 * attributes-charset 566 * attributes-natural-language 567 * printer-uri 568 * requested-attributes 569 */ 570 571 request = ippNewRequest(IPP_OP_GET_PRINTER_ATTRIBUTES); 572 573 ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, uri); 574 575 ippAddStrings(request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD, "requested-attributes", sizeof(requested_attrs) / sizeof(requested_attrs[0]), NULL, requested_attrs); 576 577 /* 578 * Do the request and get back a response... 579 */ 580 581 snprintf(resource, (size_t)resourcesize, "/printers/%s", name); 582 583 if ((response = cupsDoRequest(http, request, resource)) != NULL) 584 { 585 const char *device_uri = NULL; /* device-uri value */ 586 587 if ((attr = ippFindAttribute(response, "device-uri", IPP_TAG_URI)) != NULL) 588 { 589 device_uri = attr->values[0].string.text; 590 DEBUG_printf(("5cups_get_printer_uri: device-uri=\"%s\"", device_uri)); 591 } 592 593 if (device_uri && 594 (((!strncmp(device_uri, "ipp://", 6) || !strncmp(device_uri, "ipps://", 7)) && 595 (strstr(device_uri, "/printers/") != NULL || strstr(device_uri, "/classes/") != NULL)) || 596 ((strstr(device_uri, "._ipp.") != NULL || strstr(device_uri, "._ipps.") != NULL) && 597 !strcmp(device_uri + strlen(device_uri) - 5, "/cups")))) 598 { 599 /* 600 * Statically-configured shared printer. 601 */ 602 603 httpSeparateURI(HTTP_URI_CODING_ALL, _httpResolveURI(device_uri, uri, sizeof(uri), _HTTP_RESOLVE_DEFAULT, NULL, NULL), scheme, sizeof(scheme), username, sizeof(username), host, hostsize, port, resource, resourcesize); 604 ippDelete(response); 605 606 DEBUG_printf(("5cups_get_printer_uri: Resolved to host=\"%s\", port=%d, resource=\"%s\"", host, *port, resource)); 607 return (1); 608 } 609 else if ((attr = ippFindAttribute(response, "member-uris", IPP_TAG_URI)) != NULL) 610 { 611 /* 612 * Get the first actual printer name in the class... 613 */ 614 615 DEBUG_printf(("5cups_get_printer_uri: Got member-uris with %d values.", ippGetCount(attr))); 616 617 for (i = 0; i < attr->num_values; i ++) 618 { 619 DEBUG_printf(("5cups_get_printer_uri: member-uris[%d]=\"%s\"", i, ippGetString(attr, i, NULL))); 620 621 httpSeparateURI(HTTP_URI_CODING_ALL, attr->values[i].string.text, scheme, sizeof(scheme), username, sizeof(username), host, hostsize, port, resource, resourcesize); 622 if (!strncmp(resource, "/printers/", 10)) 623 { 624 /* 625 * Found a printer! 626 */ 627 628 ippDelete(response); 629 630 DEBUG_printf(("5cups_get_printer_uri: Found printer member with host=\"%s\", port=%d, resource=\"%s\"", host, *port, resource)); 631 return (1); 632 } 633 } 634 635 /* 636 * No printers in this class - try recursively looking for a printer, 637 * but not more than 3 levels deep... 638 */ 639 640 if (depth < 3) 641 { 642 for (i = 0; i < attr->num_values; i ++) 643 { 644 httpSeparateURI(HTTP_URI_CODING_ALL, attr->values[i].string.text, 645 scheme, sizeof(scheme), username, sizeof(username), 646 host, hostsize, port, resource, resourcesize); 647 if (!strncmp(resource, "/classes/", 9)) 648 { 649 /* 650 * Found a class! Connect to the right server... 651 */ 652 653 if (!_cups_strcasecmp(http_hostname, host) && *port == http_port) 654 http2 = http; 655 else if ((http2 = httpConnect2(host, *port, NULL, AF_UNSPEC, cupsEncryption(), 1, 30000, NULL)) == NULL) 656 { 657 DEBUG_puts("8cups_get_printer_uri: Unable to connect to server"); 658 659 continue; 660 } 661 662 /* 663 * Look up printers on that server... 664 */ 665 666 strlcpy(classname, resource + 9, sizeof(classname)); 667 668 cups_get_printer_uri(http2, classname, host, hostsize, port, 669 resource, resourcesize, depth + 1); 670 671 /* 672 * Close the connection as needed... 673 */ 674 675 if (http2 != http) 676 httpClose(http2); 677 678 if (*host) 679 return (1); 680 } 681 } 682 } 683 } 684 else if ((attr = ippFindAttribute(response, "printer-uri-supported", IPP_TAG_URI)) != NULL) 685 { 686 httpSeparateURI(HTTP_URI_CODING_ALL, _httpResolveURI(attr->values[0].string.text, uri, sizeof(uri), _HTTP_RESOLVE_DEFAULT, NULL, NULL), scheme, sizeof(scheme), username, sizeof(username), host, hostsize, port, resource, resourcesize); 687 ippDelete(response); 688 689 DEBUG_printf(("5cups_get_printer_uri: Resolved to host=\"%s\", port=%d, resource=\"%s\"", host, *port, resource)); 690 691 if (!strncmp(resource, "/classes/", 9)) 692 { 693 _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("No printer-uri found for class"), 1); 694 695 *host = '\0'; 696 *resource = '\0'; 697 698 DEBUG_puts("5cups_get_printer_uri: Not returning class."); 699 return (0); 700 } 701 702 return (1); 703 } 704 705 ippDelete(response); 706 } 707 708 if (cupsLastError() != IPP_STATUS_ERROR_NOT_FOUND) 709 _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("No printer-uri found"), 1); 710 711 *host = '\0'; 712 *resource = '\0'; 713 714 DEBUG_puts("5cups_get_printer_uri: Printer URI not found."); 715 return (0); 716 } 717