root / pkipplib / trunk / pkipplib / pkipplib.py @ 3562

Revision 3562, 30.6 kB (checked in by jerome, 11 years ago)

Changed copyright years.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id Revision
RevLine 
[3]1#! /usr/bin/env python
[3437]2# -*- coding: utf-8 -*-
[3]3#
[14]4# pkipplib : IPP and CUPS support for Python
[3]5#
[3562]6# (c) 2003-2013 Jerome Alet <alet@librelogiciel.com>
[40]7# This program is free software: you can redistribute it and/or modify
[3]8# it under the terms of the GNU General Public License as published by
[40]9# the Free Software Foundation, either version 3 of the License, or
[3]10# (at your option) any later version.
[3437]11#
[3]12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
[3437]16#
[3]17# You should have received a copy of the GNU General Public License
[40]18# along with this program.  If not, see <http://www.gnu.org/licenses/>.
[3]19#
20# $Id$
21#
22#
23
24import sys
[25]25import os
[3]26import urllib2
[21]27import socket
[3]28from struct import pack, unpack
29
30IPP_VERSION = "1.1"     # default version number
31
32IPP_PORT = 631
33
34IPP_MAX_NAME = 256
35IPP_MAX_VALUES = 8
36
37IPP_TAG_ZERO = 0x00
38IPP_TAG_OPERATION = 0x01
39IPP_TAG_JOB = 0x02
40IPP_TAG_END = 0x03
41IPP_TAG_PRINTER = 0x04
42IPP_TAG_UNSUPPORTED_GROUP = 0x05
43IPP_TAG_SUBSCRIPTION = 0x06
44IPP_TAG_EVENT_NOTIFICATION = 0x07
45IPP_TAG_UNSUPPORTED_VALUE = 0x10
46IPP_TAG_DEFAULT = 0x11
47IPP_TAG_UNKNOWN = 0x12
48IPP_TAG_NOVALUE = 0x13
49IPP_TAG_NOTSETTABLE = 0x15
50IPP_TAG_DELETEATTR = 0x16
51IPP_TAG_ADMINDEFINE = 0x17
52IPP_TAG_INTEGER = 0x21
53IPP_TAG_BOOLEAN = 0x22
54IPP_TAG_ENUM = 0x23
55IPP_TAG_STRING = 0x30
56IPP_TAG_DATE = 0x31
57IPP_TAG_RESOLUTION = 0x32
58IPP_TAG_RANGE = 0x33
59IPP_TAG_BEGIN_COLLECTION = 0x34
60IPP_TAG_TEXTLANG = 0x35
61IPP_TAG_NAMELANG = 0x36
62IPP_TAG_END_COLLECTION = 0x37
63IPP_TAG_TEXT = 0x41
64IPP_TAG_NAME = 0x42
65IPP_TAG_KEYWORD = 0x44
66IPP_TAG_URI = 0x45
67IPP_TAG_URISCHEME = 0x46
68IPP_TAG_CHARSET = 0x47
69IPP_TAG_LANGUAGE = 0x48
70IPP_TAG_MIMETYPE = 0x49
71IPP_TAG_MEMBERNAME = 0x4a
72IPP_TAG_MASK = 0x7fffffff
73IPP_TAG_COPY = -0x7fffffff-1
74
75IPP_RES_PER_INCH = 3
76IPP_RES_PER_CM = 4
77
78IPP_FINISHINGS_NONE = 3
79IPP_FINISHINGS_STAPLE = 4
80IPP_FINISHINGS_PUNCH = 5
81IPP_FINISHINGS_COVER = 6
82IPP_FINISHINGS_BIND = 7
83IPP_FINISHINGS_SADDLE_STITCH = 8
84IPP_FINISHINGS_EDGE_STITCH = 9
85IPP_FINISHINGS_FOLD = 10
86IPP_FINISHINGS_TRIM = 11
87IPP_FINISHINGS_BALE = 12
88IPP_FINISHINGS_BOOKLET_MAKER = 13
89IPP_FINISHINGS_JOB_OFFSET = 14
90IPP_FINISHINGS_STAPLE_TOP_LEFT = 20
91IPP_FINISHINGS_STAPLE_BOTTOM_LEFT = 21
92IPP_FINISHINGS_STAPLE_TOP_RIGHT = 22
93IPP_FINISHINGS_STAPLE_BOTTOM_RIGHT = 23
94IPP_FINISHINGS_EDGE_STITCH_LEFT = 24
95IPP_FINISHINGS_EDGE_STITCH_TOP = 25
96IPP_FINISHINGS_EDGE_STITCH_RIGHT = 26
97IPP_FINISHINGS_EDGE_STITCH_BOTTOM = 27
98IPP_FINISHINGS_STAPLE_DUAL_LEFT = 28
99IPP_FINISHINGS_STAPLE_DUAL_TOP = 29
100IPP_FINISHINGS_STAPLE_DUAL_RIGHT = 30
101IPP_FINISHINGS_STAPLE_DUAL_BOTTOM = 31
102IPP_FINISHINGS_BIND_LEFT = 50
103IPP_FINISHINGS_BIND_TOP = 51
104IPP_FINISHINGS_BIND_RIGHT = 52
105IPP_FINISHINGS_BIND_BOTTO = 53
106
107IPP_PORTRAIT = 3
108IPP_LANDSCAPE = 4
109IPP_REVERSE_LANDSCAPE = 5
110IPP_REVERSE_PORTRAIT = 6
111
112IPP_QUALITY_DRAFT = 3
113IPP_QUALITY_NORMAL = 4
114IPP_QUALITY_HIGH = 5
115
116IPP_JOB_PENDING = 3
117IPP_JOB_HELD = 4
118IPP_JOB_PROCESSING = 5
119IPP_JOB_STOPPED = 6
120IPP_JOB_CANCELLED = 7
121IPP_JOB_ABORTED = 8
122IPP_JOB_COMPLETE = 9
123
124IPP_PRINTER_IDLE = 3
125IPP_PRINTER_PROCESSING = 4
126IPP_PRINTER_STOPPED = 5
127
128IPP_ERROR = -1
129IPP_IDLE = 0
130IPP_HEADER = 1
131IPP_ATTRIBUTE = 2
132IPP_DATA = 3
133
134IPP_PRINT_JOB = 0x0002
135IPP_PRINT_URI = 0x0003
136IPP_VALIDATE_JOB = 0x0004
137IPP_CREATE_JOB = 0x0005
138IPP_SEND_DOCUMENT = 0x0006
139IPP_SEND_URI = 0x0007
140IPP_CANCEL_JOB = 0x0008
141IPP_GET_JOB_ATTRIBUTES = 0x0009
142IPP_GET_JOBS = 0x000a
143IPP_GET_PRINTER_ATTRIBUTES = 0x000b
144IPP_HOLD_JOB = 0x000c
145IPP_RELEASE_JOB = 0x000d
146IPP_RESTART_JOB = 0x000e
147IPP_PAUSE_PRINTER = 0x0010
148IPP_RESUME_PRINTER = 0x0011
149IPP_PURGE_JOBS = 0x0012
150IPP_SET_PRINTER_ATTRIBUTES = 0x0013
151IPP_SET_JOB_ATTRIBUTES = 0x0014
152IPP_GET_PRINTER_SUPPORTED_VALUES = 0x0015
153IPP_CREATE_PRINTER_SUBSCRIPTION = 0x0016
154IPP_CREATE_JOB_SUBSCRIPTION = 0x0017
155IPP_GET_SUBSCRIPTION_ATTRIBUTES = 0x0018
156IPP_GET_SUBSCRIPTIONS = 0x0019
157IPP_RENEW_SUBSCRIPTION = 0x001a
158IPP_CANCEL_SUBSCRIPTION = 0x001b
159IPP_GET_NOTIFICATIONS = 0x001c
160IPP_SEND_NOTIFICATIONS = 0x001d
161IPP_GET_PRINT_SUPPORT_FILES = 0x0021
162IPP_ENABLE_PRINTER = 0x0022
163IPP_DISABLE_PRINTER = 0x0023
164IPP_PAUSE_PRINTER_AFTER_CURRENT_JOB = 0x0024
165IPP_HOLD_NEW_JOBS = 0x0025
166IPP_RELEASE_HELD_NEW_JOBS = 0x0026
167IPP_DEACTIVATE_PRINTER = 0x0027
168IPP_ACTIVATE_PRINTER = 0x0028
169IPP_RESTART_PRINTER = 0x0029
170IPP_SHUTDOWN_PRINTER = 0x002a
171IPP_STARTUP_PRINTER = 0x002b
172IPP_REPROCESS_JOB = 0x002c
173IPP_CANCEL_CURRENT_JOB = 0x002d
174IPP_SUSPEND_CURRENT_JOB = 0x002e
175IPP_RESUME_JOB = 0x002f
176IPP_PROMOTE_JOB = 0x0030
177IPP_SCHEDULE_JOB_AFTER = 0x0031
178IPP_PRIVATE = 0x4000
179CUPS_GET_DEFAULT = 0x4001
180CUPS_GET_PRINTERS = 0x4002
181CUPS_ADD_PRINTER = 0x4003
182CUPS_DELETE_PRINTER = 0x4004
183CUPS_GET_CLASSES = 0x4005
184CUPS_ADD_CLASS = 0x4006
185CUPS_DELETE_CLASS = 0x4007
186CUPS_ACCEPT_JOBS = 0x4008
187CUPS_REJECT_JOBS = 0x4009
188CUPS_SET_DEFAULT = 0x400a
189CUPS_GET_DEVICES = 0x400b
190CUPS_GET_PPDS = 0x400c
191CUPS_MOVE_JOB = 0x400d
192CUPS_AUTHENTICATE_JOB = 0x400e
[39]193CUPS_GET_PPD = 0x400f
194CUPS_GET_DOCUMENT = 0x4027
[3]195
196IPP_OK = 0x0000
197IPP_OK_SUBST = 0x0001
198IPP_OK_CONFLICT = 0x0002
199IPP_OK_IGNORED_SUBSCRIPTIONS = 0x0003
200IPP_OK_IGNORED_NOTIFICATIONS = 0x0004
201IPP_OK_TOO_MANY_EVENTS = 0x0005
202IPP_OK_BUT_CANCEL_SUBSCRIPTION = 0x0006
203IPP_REDIRECTION_OTHER_SITE = 0x0300
204IPP_BAD_REQUEST = 0x0400
205IPP_FORBIDDEN = 0x0401
206IPP_NOT_AUTHENTICATED = 0x0402
207IPP_NOT_AUTHORIZED = 0x0403
208IPP_NOT_POSSIBLE = 0x0404
209IPP_TIMEOUT = 0x0405
210IPP_NOT_FOUND = 0x0406
211IPP_GONE = 0x0407
212IPP_REQUEST_ENTITY = 0x0408
213IPP_REQUEST_VALUE = 0x0409
214IPP_DOCUMENT_FORMAT = 0x040a
215IPP_ATTRIBUTES = 0x040b
216IPP_URI_SCHEME = 0x040c
217IPP_CHARSET = 0x040d
218IPP_CONFLICT = 0x040e
219IPP_COMPRESSION_NOT_SUPPORTED = 0x040f
220IPP_COMPRESSION_ERROR = 0x0410
221IPP_DOCUMENT_FORMAT_ERROR = 0x0411
222IPP_DOCUMENT_ACCESS_ERROR = 0x0412
223IPP_ATTRIBUTES_NOT_SETTABLE = 0x0413
224IPP_IGNORED_ALL_SUBSCRIPTIONS = 0x0414
225IPP_TOO_MANY_SUBSCRIPTIONS = 0x0415
226IPP_IGNORED_ALL_NOTIFICATIONS = 0x0416
227IPP_PRINT_SUPPORT_FILE_NOT_FOUND = 0x0417
228
229IPP_INTERNAL_ERROR = 0x0500
230IPP_OPERATION_NOT_SUPPORTED = 0x0501
231IPP_SERVICE_UNAVAILABLE = 0x0502
232IPP_VERSION_NOT_SUPPORTED = 0x0503
233IPP_DEVICE_ERROR = 0x0504
234IPP_TEMPORARY_ERROR = 0x0505
235IPP_NOT_ACCEPTING = 0x0506
236IPP_PRINTER_BUSY = 0x0507
237IPP_ERROR_JOB_CANCELLED = 0x0508
238IPP_MULTIPLE_JOBS_NOT_SUPPORTED = 0x0509
239IPP_PRINTER_IS_DEACTIVATED = 0x50a
[3437]240
[26]241CUPS_PRINTER_LOCAL = 0x0000
242CUPS_PRINTER_CLASS = 0x0001
243CUPS_PRINTER_REMOTE = 0x0002
244CUPS_PRINTER_BW = 0x0004
245CUPS_PRINTER_COLOR = 0x0008
246CUPS_PRINTER_DUPLEX = 0x0010
247CUPS_PRINTER_STAPLE = 0x0020
248CUPS_PRINTER_COPIES = 0x0040
249CUPS_PRINTER_COLLATE = 0x0080
250CUPS_PRINTER_PUNCH = 0x0100
251CUPS_PRINTER_COVER = 0x0200
252CUPS_PRINTER_BIND = 0x0400
253CUPS_PRINTER_SORT = 0x0800
254CUPS_PRINTER_SMALL = 0x1000
255CUPS_PRINTER_MEDIUM = 0x2000
256CUPS_PRINTER_LARGE = 0x4000
257CUPS_PRINTER_VARIABLE = 0x8000
258CUPS_PRINTER_IMPLICIT = 0x1000
259CUPS_PRINTER_DEFAULT = 0x2000
260CUPS_PRINTER_FAX = 0x4000
261CUPS_PRINTER_REJECTING = 0x8000
262CUPS_PRINTER_DELETE = 0x1000
263CUPS_PRINTER_NOT_SHARED = 0x2000
264CUPS_PRINTER_AUTHENTICATED = 0x4000
265CUPS_PRINTER_COMMANDS = 0x8000
266CUPS_PRINTER_OPTIONS = 0xe6ff
[3437]267
268
[3]269class IPPError(Exception) :
270    """An exception for IPP related stuff."""
271    def __init__(self, message = ""):
272        self.message = message
273        Exception.__init__(self, message)
274    def __repr__(self):
275        return self.message
276    __str__ = __repr__
277
[7]278class FakeAttribute :
279    """Fakes an IPPRequest attribute to simplify usage syntax."""
280    def __init__(self, request, name) :
281        """Initializes the fake attribute."""
282        self.request = request
283        self.name = name
[3437]284
[7]285    def __setitem__(self, key, value) :
286        """Appends the value to the real attribute."""
287        attributeslist = getattr(self.request, "_%s_attributes" % self.name)
288        for i in range(len(attributeslist)) :
289            attribute = attributeslist[i]
290            for j in range(len(attribute)) :
291                (attrname, attrvalue) = attribute[j]
292                if attrname == key :
293                    attribute[j][1].append(value)
294                    return
[3437]295            attribute.append((key, [value]))
296
[7]297    def __getitem__(self, key) :
298        """Returns an attribute's value."""
[26]299        answer = []
[7]300        attributeslist = getattr(self.request, "_%s_attributes" % self.name)
301        for i in range(len(attributeslist)) :
302            attribute = attributeslist[i]
303            for j in range(len(attribute)) :
304                (attrname, attrvalue) = attribute[j]
305                if attrname == key :
[26]306                    answer.extend(attrvalue)
307        if answer :
308            return answer
[3437]309        raise KeyError, key
310
[3]311class IPPRequest :
312    """A class for IPP requests."""
313    attributes_types = ("operation", "job", "printer", "unsupported", \
314                                     "subscription", "event_notification")
[3437]315    def __init__(self, data="", version=IPP_VERSION,
[3]316                                operation_id=None, \
[7]317                                request_id=None, \
318                                debug=False) :
[3]319        """Initializes an IPP Message object.
[3437]320
[3]321           Parameters :
[3437]322
[3]323             data : the complete IPP Message's content.
324             debug : a boolean value to output debug info on stderr.
325        """
326        self.debug = debug
327        self._data = data
[7]328        self.parsed = False
[3437]329
[3]330        # Initializes message
[3437]331        self.setVersion(version)
[3]332        self.setOperationId(operation_id)
333        self.setRequestId(request_id)
334        self.data = ""
[3437]335
[3]336        for attrtype in self.attributes_types :
[7]337            setattr(self, "_%s_attributes" % attrtype, [[]])
[3437]338
339        # Initialize tags
[3]340        self.tags = [ None ] * 256 # by default all tags reserved
[3437]341
[3]342        # Delimiter tags
343        self.tags[0x01] = "operation-attributes-tag"
344        self.tags[0x02] = "job-attributes-tag"
345        self.tags[0x03] = "end-of-attributes-tag"
346        self.tags[0x04] = "printer-attributes-tag"
347        self.tags[0x05] = "unsupported-attributes-tag"
348        self.tags[0x06] = "subscription-attributes-tag"
[27]349        self.tags[0x07] = "event_notification-attributes-tag"
[3437]350
[3]351        # out of band values
352        self.tags[0x10] = "unsupported"
353        self.tags[0x11] = "reserved-for-future-default"
354        self.tags[0x12] = "unknown"
355        self.tags[0x13] = "no-value"
356        self.tags[0x15] = "not-settable"
357        self.tags[0x16] = "delete-attribute"
358        self.tags[0x17] = "admin-define"
[3437]359
[3]360        # integer values
361        self.tags[0x20] = "generic-integer"
362        self.tags[0x21] = "integer"
363        self.tags[0x22] = "boolean"
364        self.tags[0x23] = "enum"
[3437]365
[3]366        # octetString
367        self.tags[0x30] = "octetString-with-an-unspecified-format"
368        self.tags[0x31] = "dateTime"
369        self.tags[0x32] = "resolution"
370        self.tags[0x33] = "rangeOfInteger"
371        self.tags[0x34] = "begCollection" # TODO : find sample files for testing
372        self.tags[0x35] = "textWithLanguage"
373        self.tags[0x36] = "nameWithLanguage"
374        self.tags[0x37] = "endCollection"
[3437]375
[3]376        # character strings
377        self.tags[0x40] = "generic-character-string"
378        self.tags[0x41] = "textWithoutLanguage"
379        self.tags[0x42] = "nameWithoutLanguage"
380        self.tags[0x44] = "keyword"
381        self.tags[0x45] = "uri"
382        self.tags[0x46] = "uriScheme"
383        self.tags[0x47] = "charset"
384        self.tags[0x48] = "naturalLanguage"
385        self.tags[0x49] = "mimeMediaType"
386        self.tags[0x4a] = "memberAttrName"
[3437]387
[3]388        # Reverse mapping to generate IPP messages
389        self.tagvalues = {}
390        for i in range(len(self.tags)) :
391            value = self.tags[i]
392            if value is not None :
393                self.tagvalues[value] = i
[3437]394
395    def __getattr__(self, name) :
[7]396        """Fakes attribute access."""
397        if name in self.attributes_types :
398            return FakeAttribute(self, name)
399        else :
400            raise AttributeError, name
[3437]401
402    def __str__(self) :
[3]403        """Returns the parsed IPP message in a readable form."""
404        if not self.parsed :
405            return ""
406        mybuffer = []
407        mybuffer.append("IPP version : %s.%s" % self.version)
408        mybuffer.append("IPP operation Id : 0x%04x" % self.operation_id)
409        mybuffer.append("IPP request Id : 0x%08x" % self.request_id)
410        for attrtype in self.attributes_types :
[7]411            for attribute in getattr(self, "_%s_attributes" % attrtype) :
[3]412                if attribute :
413                    mybuffer.append("%s attributes :" % attrtype.title())
414                for (name, value) in attribute :
415                    mybuffer.append("  %s : %s" % (name, value))
[3437]416        if self.data :
[3]417            mybuffer.append("IPP datas : %s" % repr(self.data))
418        return "\n".join(mybuffer)
[3437]419
420    def logDebug(self, msg) :
[3]421        """Prints a debug message."""
422        if self.debug :
423            sys.stderr.write("%s\n" % msg)
424            sys.stderr.flush()
[3437]425
[3]426    def setVersion(self, version) :
427        """Sets the request's operation id."""
428        if version is not None :
429            try :
430                self.version = [int(p) for p in version.split(".")]
431            except AttributeError :
432                if len(version) == 2 : # 2-tuple
433                    self.version = version
[3437]434                else :
[3]435                    try :
436                        self.version = [int(p) for p in str(float(version)).split(".")]
437                    except :
438                        self.version = [int(p) for p in IPP_VERSION.split(".")]
[3437]439
440    def setOperationId(self, opid) :
[3]441        """Sets the request's operation id."""
442        self.operation_id = opid
[3437]443
444    def setRequestId(self, reqid) :
[3]445        """Sets the request's request id."""
446        self.request_id = reqid
[3437]447
448    def dump(self) :
[3]449        """Generates an IPP Message.
[3437]450
[3]451           Returns the message as a string of text.
[3437]452        """
[3]453        mybuffer = []
[7]454        if None not in (self.version, self.operation_id) :
[3]455            mybuffer.append(chr(self.version[0]) + chr(self.version[1]))
456            mybuffer.append(pack(">H", self.operation_id))
[25]457            mybuffer.append(pack(">I", self.request_id or 1))
[3]458            for attrtype in self.attributes_types :
[7]459                for attribute in getattr(self, "_%s_attributes" % attrtype) :
[3]460                    if attribute :
461                        mybuffer.append(chr(self.tagvalues["%s-attributes-tag" % attrtype]))
462                    for (attrname, value) in attribute :
463                        nameprinted = 0
464                        for (vtype, val) in value :
465                            mybuffer.append(chr(self.tagvalues[vtype]))
466                            if not nameprinted :
467                                mybuffer.append(pack(">H", len(attrname)))
468                                mybuffer.append(attrname)
469                                nameprinted = 1
[3437]470                            else :
[3]471                                mybuffer.append(pack(">H", 0))
472                            if vtype in ("integer", "enum") :
473                                mybuffer.append(pack(">H", 4))
474                                mybuffer.append(pack(">I", val))
475                            elif vtype == "boolean" :
476                                mybuffer.append(pack(">H", 1))
477                                mybuffer.append(chr(val))
[3437]478                            else :
[3]479                                mybuffer.append(pack(">H", len(val)))
480                                mybuffer.append(val)
481            mybuffer.append(chr(self.tagvalues["end-of-attributes-tag"]))
[3437]482        mybuffer.append(self.data)
[3]483        return "".join(mybuffer)
[3437]484
[3]485    def parse(self) :
486        """Parses an IPP Request.
[3437]487
[3]488           NB : Only a subset of RFC2910 is implemented.
489        """
490        self._curname = None
491        self._curattributes = None
[3437]492
[3]493        try :
[41]494            self.setVersion((ord(self._data[0]), ord(self._data[1])))
495            self.setOperationId(unpack(">H", self._data[2:4])[0])
496            self.setRequestId(unpack(">I", self._data[4:8])[0])
497            self.position = 8
498            endofattributes = self.tagvalues["end-of-attributes-tag"]
499            maxdelimiter = self.tagvalues["event_notification-attributes-tag"]
500            nulloffset = lambda : 0
[3]501            tag = ord(self._data[self.position])
502            while tag != endofattributes :
503                self.position += 1
504                name = self.tags[tag]
505                if name is not None :
506                    func = getattr(self, name.replace("-", "_"), nulloffset)
507                    self.position += func()
508                    if ord(self._data[self.position]) > maxdelimiter :
509                        self.position -= 1
510                        continue
[3437]511                oldtag = tag
[3]512                tag = ord(self._data[self.position])
513                if tag == oldtag :
514                    self._curattributes.append([])
515        except IndexError :
516            raise IPPError, "Unexpected end of IPP message."
[3437]517
518        self.data = self._data[self.position+1:]
[7]519        self.parsed = True
[3437]520
521    def parseTag(self) :
[3]522        """Extracts information from an IPP tag."""
523        pos = self.position
524        tagtype = self.tags[ord(self._data[pos])]
525        pos += 1
526        posend = pos2 = pos + 2
527        namelength = unpack(">H", self._data[pos:pos2])[0]
528        if not namelength :
529            name = self._curname
[3437]530        else :
[3]531            posend += namelength
532            self._curname = name = self._data[pos2:posend]
533        pos2 = posend + 2
534        valuelength = unpack(">H", self._data[posend:pos2])[0]
535        posend = pos2 + valuelength
536        value = self._data[pos2:posend]
537        if tagtype in ("integer", "enum") :
538            value = unpack(">I", value)[0]
[3437]539        elif tagtype == "boolean" :
[3]540            value = ord(value)
[3437]541        try :
[3]542            (oldname, oldval) = self._curattributes[-1][-1]
543            if oldname == name :
544                oldval.append((tagtype, value))
[3437]545            else :
[3]546                raise IndexError
[3437]547        except IndexError :
[3]548            self._curattributes[-1].append((name, [(tagtype, value)]))
549        self.logDebug("%s(%s) : %s" % (name, tagtype, value))
550        return posend - self.position
[3437]551
552    def operation_attributes_tag(self) :
[3]553        """Indicates that the parser enters into an operation-attributes-tag group."""
[7]554        self._curattributes = self._operation_attributes
[3]555        return self.parseTag()
[3437]556
557    def job_attributes_tag(self) :
[3]558        """Indicates that the parser enters into a job-attributes-tag group."""
[7]559        self._curattributes = self._job_attributes
[3]560        return self.parseTag()
[3437]561
562    def printer_attributes_tag(self) :
[3]563        """Indicates that the parser enters into a printer-attributes-tag group."""
[7]564        self._curattributes = self._printer_attributes
[3]565        return self.parseTag()
[3437]566
567    def unsupported_attributes_tag(self) :
[3]568        """Indicates that the parser enters into an unsupported-attributes-tag group."""
[7]569        self._curattributes = self._unsupported_attributes
[3]570        return self.parseTag()
[3437]571
572    def subscription_attributes_tag(self) :
[3]573        """Indicates that the parser enters into a subscription-attributes-tag group."""
[7]574        self._curattributes = self._subscription_attributes
[3]575        return self.parseTag()
[3437]576
577    def event_notification_attributes_tag(self) :
[3]578        """Indicates that the parser enters into an event-notification-attributes-tag group."""
[7]579        self._curattributes = self._event_notification_attributes
[3]580        return self.parseTag()
[3437]581
582
[7]583class CUPS :
584    """A class for a CUPS instance."""
[44]585    def __init__(self, url=None, username=None, password=None, charset="utf-8", language="en-US", debug=False) :
[7]586        """Initializes the CUPS instance."""
[25]587        if url is not None :
588            self.url = url.replace("ipp://", "http://")
589            if self.url.endswith("/") :
590                self.url = self.url[:-1]
[3437]591        else :
[25]592            self.url = self.getDefaultURL()
[3437]593
[43]594        # If no username or password, use the ones set by CUPS, if any
595        self.username = username or os.environ.get("AUTH_USERNAME")
596        self.password = password or os.environ.get("AUTH_PASSWORD")
[3437]597
[7]598        self.charset = charset
599        self.language = language
[25]600        self.debug = debug
[23]601        self.lastError = None
602        self.lastErrorMessage = None
[25]603        self.requestId = None
[3437]604
605    def getDefaultURL(self) :
[25]606        """Builds a default URL."""
607        # TODO : encryption methods.
[43]608        server = os.environ.get("CUPS_SERVER", "localhost")
609        port = os.environ.get("IPP_PORT", "631")
[25]610        if server.startswith("/") :
611            # it seems it's a unix domain socket.
612            # we can't handle this right now, so we use the default instead.
[43]613            return "http://localhost:%s" % port
614            #return "socket:%s" % server # TODO : make this work !
[3437]615        else :
[25]616            return "http://%s:%s" % (server, port)
[3437]617
[9]618    def identifierToURI(self, service, ident) :
619        """Transforms an identifier into a particular URI depending on requested service."""
620        return "%s/%s/%s" % (self.url.replace("http://", "ipp://"),
621                             service,
622                             ident)
[3437]623
624    def nextRequestId(self) :
[25]625        """Increments the current request id and returns the new value."""
626        try :
627            self.requestId += 1
[3437]628        except TypeError :
[25]629            self.requestId = 1
630        return self.requestId
[3437]631
[7]632    def newRequest(self, operationid=None) :
633        """Generates a new empty request."""
634        if operationid is not None :
635            req = IPPRequest(operation_id=operationid, \
[25]636                             request_id=self.nextRequestId(), \
637                             debug=self.debug)
[7]638            req.operation["attributes-charset"] = ("charset", self.charset)
639            req.operation["attributes-natural-language"] = ("naturalLanguage", self.language)
[44]640            if self.username :
641                req.operation["requesting-user-name"] = ("nameWithoutLanguage", self.username)
[7]642            return req
[3437]643
[26]644    def doRequest(self, req, url=None) :
[25]645        """Sends a request to the CUPS server.
646           returns a new IPPRequest object, containing the parsed answer.
[3437]647        """
[41]648        url = url or self.url
[42]649        connection = urllib2.Request(url=url, \
[25]650                             data=req.dump())
[42]651        connection.add_header("Content-Type", "application/ipp")
[3468]652        proxyhandler = urllib2.ProxyHandler({})
[25]653        if self.username :
654            pwmanager = urllib2.HTTPPasswordMgrWithDefaultRealm()
655            pwmanager.add_password(None, \
[42]656                                   "%s%s" % (connection.get_host(), \
657                                             connection.get_selector()), \
[25]658                                   self.username, \
659                                   self.password or "")
[3437]660            authhandler = urllib2.HTTPBasicAuthHandler(pwmanager)
[3468]661            opener = urllib2.build_opener(proxyhandler, authhandler)
[3437]662        else : # TODO : also do this in the 'if' part
[3468]663            if not url.startswith("socket:") :
664                opener = urllib2.build_opener(proxyhandler)
665            else :
[41]666                class SocketHandler(urllib2.HTTPHandler) :
667                    """A class to handle IPP connections over an Unix domain socket."""
668                    def socket_open(self, req) :
669                        """Opens an Unix domain socket for IPP."""
670                        req.host = "localhost"
671                        req.geturl = lambda : req.get_full_url()[7:]
672                        req.info = lambda : "Unix domain socket %s" % req.geturl()
673                        req.get_selector = req.geturl
674                        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
675                        s.connect(req.get_selector())
676                        s.settimeout(5.0)
677                        sys.stderr.write("Opened [%s]\n" % req.get_selector())
[42]678                        return s.makefile(mode="r+b")
[3437]679
[3468]680                opener = urllib2.build_opener(proxyhandler, SocketHandler())
681
682        urllib2.install_opener(opener)
[3437]683        self.lastError = None
[25]684        self.lastErrorMessage = None
[3437]685        try :
[42]686            response = urllib2.urlopen(connection)
[3437]687        except (urllib2.URLError, urllib2.HTTPError, socket.error), error :
[25]688            self.lastError = error
689            self.lastErrorMessage = str(error)
690            return None
[3437]691        else :
[41]692            bytes = []
693            try :
694                try :
695                    while True :
[42]696                        try :
697                            byte = response.read(1)
[3437]698                        except AttributeError :
[42]699                            byte = response.recv(1)
[41]700                        if not byte :
701                            break
[3437]702                        else :
[41]703                            bytes.append(byte)
[42]704                except socket.error :
705                    sys.stderr.write("socket error\n")
[41]706                    pass
707            finally :
708                datas = "".join(bytes)
[42]709            if datas :
710                ippresponse = IPPRequest(datas)
711                ippresponse.parse()
712                return ippresponse
713            else :
714                return None
[3437]715
716    def getPPD(self, queuename) :
[26]717        """Retrieves the PPD for a particular queuename."""
718        req = self.newRequest(IPP_GET_PRINTER_ATTRIBUTES)
719        req.operation["printer-uri"] = ("uri", self.identifierToURI("printers", queuename))
720        for attrib in ("printer-uri-supported", "printer-type", "member-uris") :
721            req.operation["requested-attributes"] = ("nameWithoutLanguage", attrib)
722        return self.doRequest(req)  # TODO : get the PPD from the actual print server
[3437]723
[7]724    def getDefault(self) :
725        """Retrieves CUPS' default printer."""
[23]726        return self.doRequest(self.newRequest(CUPS_GET_DEFAULT))
[3437]727
728    def getJobAttributes(self, jobid) :
[7]729        """Retrieves a print job's attributes."""
730        req = self.newRequest(IPP_GET_JOB_ATTRIBUTES)
[9]731        req.operation["job-uri"] = ("uri", self.identifierToURI("jobs", jobid))
[23]732        return self.doRequest(req)
[3437]733
734    def getPrinters(self) :
[26]735        """Returns the list of print queues names."""
736        req = self.newRequest(CUPS_GET_PRINTERS)
737        req.operation["requested-attributes"] = ("keyword", "printer-name")
738        req.operation["printer-type"] = ("enum", 0)
739        req.operation["printer-type-mask"] = ("enum", CUPS_PRINTER_CLASS)
740        return [printer[1] for printer in self.doRequest(req).printer["printer-name"]]
[3437]741
742    def getDevices(self) :
[26]743        """Returns a list of devices as (deviceclass, deviceinfo, devicemakeandmodel, deviceuri) tuples."""
744        answer = self.doRequest(self.newRequest(CUPS_GET_DEVICES))
745        return zip([d[1] for d in answer.printer["device-class"]], \
746                   [d[1] for d in answer.printer["device-info"]], \
747                   [d[1] for d in answer.printer["device-make-and-model"]], \
748                   [d[1] for d in answer.printer["device-uri"]])
[3437]749
750    def getPPDs(self) :
[26]751        """Returns a list of PPDs as (ppdnaturallanguage, ppdmake, ppdmakeandmodel, ppdname) tuples."""
752        answer = self.doRequest(self.newRequest(CUPS_GET_PPDS))
753        return zip([d[1] for d in answer.printer["ppd-natural-language"]], \
754                   [d[1] for d in answer.printer["ppd-make"]], \
755                   [d[1] for d in answer.printer["ppd-make-and-model"]], \
756                   [d[1] for d in answer.printer["ppd-name"]])
[3437]757
[28]758    def createSubscription(self, uri, events=["all"],
759                                      userdata=None,
760                                      recipient=None,
761                                      pullmethod=None,
762                                      charset=None,
763                                      naturallanguage=None,
764                                      leaseduration=None,
765                                      timeinterval=None,
766                                      jobid=None) :
[35]767        """Creates a job, printer or server subscription.
[3437]768
[35]769           uri : the subscription's uri, e.g. ipp://server
770           events : a list of events to subscribe to, e.g. ["printer-added", "printer-deleted"]
771           recipient : the notifier's uri
772           pullmethod : the pull method to use
773           charset : the charset to use when sending notifications
774           naturallanguage : the language to use when sending notifications
775           leaseduration : the duration of the lease in seconds
776           timeinterval : the interval of time during notifications
777           jobid : the optional job id in case of a job subscription
[3437]778        """
[30]779        if jobid is not None :
780            opid = IPP_CREATE_JOB_SUBSCRIPTION
781            uritype = "job-uri"
782        else :
783            opid = IPP_CREATE_PRINTER_SUBSCRIPTION
784            uritype = "printer-uri"
785        req = self.newRequest(opid)
786        req.operation[uritype] = ("uri", uri)
[28]787        for event in events :
788            req.subscription["notify-events"] = ("keyword", event)
[3437]789        if userdata is not None :
[28]790            req.subscription["notify-user-data"] = ("octetString-with-an-unspecified-format", userdata)
[3437]791        if recipient is not None :
[3504]792            req.subscription["notify-recipient-uri"] = ("uri", recipient)
[28]793        if pullmethod is not None :
794            req.subscription["notify-pull-method"] = ("keyword", pullmethod)
795        if charset is not None :
[44]796            req.subscription["notify-charset"] = ("charset", charset or self.charset)
[28]797        if naturallanguage is not None :
[44]798            req.subscription["notify-natural-language"] = ("naturalLanguage", naturallanguage or self.language)
[28]799        if leaseduration is not None :
800            req.subscription["notify-lease-duration"] = ("integer", leaseduration)
801        if timeinterval is not None :
802            req.subscription["notify-time-interval"] = ("integer", timeinterval)
803        if jobid is not None :
804            req.subscription["notify-job-id"] = ("integer", jobid)
805        return self.doRequest(req)
[3437]806
807    def cancelSubscription(self, uri, subscriptionid, jobid=None) :
[35]808        """Cancels a subscription.
[3437]809
[35]810           uri : the subscription's uri.
811           subscriptionid : the subscription's id.
812           jobid : the optional job's id.
813        """
[30]814        req = self.newRequest(IPP_CANCEL_SUBSCRIPTION)
815        if jobid is not None :
816            uritype = "job-uri"
817        else :
818            uritype = "printer-uri"
819        req.operation[uritype] = ("uri", uri)
820        req.event_notification["notify-subscription-id"] = ("integer", subscriptionid)
821        return self.doRequest(req)
[3437]822
823if __name__ == "__main__" :
[3]824    if (len(sys.argv) < 2) or (sys.argv[1] == "--debug") :
[23]825        print "usage : python pkipplib.py /var/spool/cups/c00005 [--debug] (for example)\n"
[3437]826    else :
[3]827        infile = open(sys.argv[1], "rb")
[25]828        filedata = infile.read()
[3]829        infile.close()
[3437]830
[25]831        msg = IPPRequest(filedata, debug=(sys.argv[-1]=="--debug"))
832        msg.parse()
833        msg2 = IPPRequest(msg.dump())
834        msg2.parse()
835        filedata2 = msg2.dump()
[3437]836
[25]837        if filedata == filedata2 :
[3]838            print "Test OK : parsing original and parsing the output of the dump produce the same dump !"
[25]839            print str(msg)
[3437]840        else :
[3]841            print "Test Failed !"
[25]842            print str(msg)
[3]843            print
[25]844            print str(msg2)
Note: See TracBrowser for help on using the browser.