move drlaunch in drlaunch
[drlaunch] / drlaunch / src / xdg / DesktopEntry.py
1 """
2 Complete implementation of the XDG Desktop Entry Specification Version 0.9.4
3 http://standards.freedesktop.org/desktop-entry-spec/
4
5 Not supported:
6 - Encoding: Legacy Mixed
7 - Does not check exec parameters
8 - Does not check URL's
9 - Does not completly validate deprecated/kde items
10 - Does not completly check categories
11 """
12
13 from xdg.IniFile import *
14 from xdg.BaseDirectory import *
15 import os.path
16
17 class DesktopEntry(IniFile):
18     "Class to parse and validate DesktopEntries"
19
20     defaultGroup = 'Desktop Entry'
21
22     def __init__(self, filename=None):
23         self.content = dict()
24         if filename and os.path.exists(filename):
25             self.parse(filename)
26         elif filename:
27             self.new(filename)
28
29     def __str__(self):
30         return self.getName()
31
32     def parse(self, file):
33         IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"])
34
35     # start standard keys
36     def getType(self):
37         return self.get('Type')
38     """ @deprecated, use getVersionString instead """
39     def getVersion(self):
40         return self.get('Version', type="numeric")
41     def getVersionString(self):
42         return self.get('Version')
43     def getName(self):
44         return self.get('Name', locale=True)
45     def getGenericName(self):
46         return self.get('GenericName', locale=True)
47     def getNoDisplay(self):
48         return self.get('NoDisplay', type="boolean")
49     def getComment(self):
50         return self.get('Comment', locale=True)
51     def getIcon(self):
52         return self.get('Icon', locale=True)
53     def getHidden(self):
54         return self.get('Hidden', type="boolean")
55     def getOnlyShowIn(self):
56         return self.get('OnlyShowIn', list=True)
57     def getNotShowIn(self):
58         return self.get('NotShowIn', list=True)
59     def getTryExec(self):
60         return self.get('TryExec')
61     def getExec(self):
62         return self.get('Exec')
63     def getPath(self):
64         return self.get('Path')
65     def getTerminal(self):
66         return self.get('Terminal', type="boolean")
67     """ @deprecated, use getMimeTypes instead """
68     def getMimeType(self):
69         return self.get('MimeType', list=True, type="regex")
70     def getMimeTypes(self):
71         return self.get('MimeType', list=True)
72     def getCategories(self):
73         return self.get('Categories', list=True)
74     def getStartupNotify(self):
75         return self.get('StartupNotify', type="boolean")
76     def getStartupWMClass(self):
77         return self.get('StartupWMClass')
78     def getURL(self):
79         return self.get('URL')
80     # end standard keys
81
82     # start kde keys
83     def getServiceTypes(self):
84         return self.get('ServiceTypes', list=True)
85     def getDocPath(self):
86         return self.get('DocPath')
87     def getKeywords(self):
88         return self.get('Keywords', list=True, locale=True)
89     def getInitialPreference(self):
90         return self.get('InitialPreference')
91     def getDev(self):
92         return self.get('Dev')
93     def getFSType(self):
94         return self.get('FSType')
95     def getMountPoint(self):
96         return self.get('MountPoint')
97     def getReadonly(self):
98         return self.get('ReadOnly', type="boolean")
99     def getUnmountIcon(self):
100         return self.get('UnmountIcon', locale=True)
101     # end kde keys
102
103     # start deprecated keys
104     def getMiniIcon(self):
105         return self.get('MiniIcon', locale=True)
106     def getTerminalOptions(self):
107         return self.get('TerminalOptions')
108     def getDefaultApp(self):
109         return self.get('DefaultApp')
110     def getProtocols(self):
111         return self.get('Protocols', list=True)
112     def getExtensions(self):
113         return self.get('Extensions', list=True)
114     def getBinaryPattern(self):
115         return self.get('BinaryPattern')
116     def getMapNotify(self):
117         return self.get('MapNotify')
118     def getEncoding(self):
119         return self.get('Encoding')
120     def getSwallowTitle(self):
121         return self.get('SwallowTitle', locale=True)
122     def getSwallowExec(self):
123         return self.get('SwallowExec')
124     def getSortOrder(self): 
125         return self.get('SortOrder', list=True)
126     def getFilePattern(self):
127         return self.get('FilePattern', type="regex")
128     def getActions(self):
129         return self.get('Actions', list=True)
130     # end deprecated keys
131
132     # desktop entry edit stuff
133     def new(self, filename):
134         if os.path.splitext(filename)[1] == ".desktop":
135             type = "Application"
136         elif os.path.splitext(filename)[1] == ".directory":
137             type = "Directory"
138         else:
139             raise ParsingError("Unknown extension", filename)
140
141         self.content = dict()
142         self.addGroup(self.defaultGroup)
143         self.set("Type", type)
144         self.filename = filename
145     # end desktop entry edit stuff
146
147     # validation stuff
148     def checkExtras(self):
149         # header
150         if self.defaultGroup == "KDE Desktop Entry":
151             self.warnings.append('[KDE Desktop Entry]-Header is deprecated')
152
153         # file extension
154         if self.fileExtension == ".kdelnk":
155             self.warnings.append("File extension .kdelnk is deprecated")
156         elif self.fileExtension != ".desktop" and self.fileExtension != ".directory":
157             self.warnings.append('Unknown File extension')
158
159         # Type
160         try:
161             self.type = self.content[self.defaultGroup]["Type"]
162         except KeyError:
163             self.errors.append("Key 'Type' is missing")
164
165         # Name
166         try:
167             self.name = self.content[self.defaultGroup]["Name"]
168         except KeyError:
169             self.errors.append("Key 'Name' is missing")
170
171     def checkGroup(self, group):
172         # check if group header is valid
173         if not (group == self.defaultGroup \
174         or re.match("^\Desktop Action [a-zA-Z]+\$", group) \
175         or (re.match("^\X-", group) and group.decode("utf-8", "ignore").encode("ascii", 'ignore') == group)):
176             self.errors.append("Invalid Group name: %s" % group)
177         else:
178             #OnlyShowIn and NotShowIn
179             if self.content[group].has_key("OnlyShowIn") and self.content[group].has_key("NotShowIn"):
180                 self.errors.append("Group may either have OnlyShowIn or NotShowIn, but not both")
181
182     def checkKey(self, key, value, group):
183         # standard keys     
184         if key == "Type":
185             if value == "ServiceType" or value == "Service" or value == "FSDevice":
186                 self.warnings.append("Type=%s is a KDE extension" % key)
187             elif value == "MimeType":
188                 self.warnings.append("Type=MimeType is deprecated")
189             elif not (value == "Application" or value == "Link" or value == "Directory"):
190                 self.errors.append("Value of key 'Type' must be Application, Link or Directory, but is '%s'" % value)
191
192             if self.fileExtension == ".directory" and not value == "Directory":
193                 self.warnings.append("File extension is .directory, but Type is '%s'" % value)
194             elif self.fileExtension == ".desktop" and value == "Directory":
195                 self.warnings.append("Files with Type=Directory should have the extension .directory")
196
197             if value == "Application":
198                 if not self.content[group].has_key("Exec"):
199                     self.warnings.append("Type=Application needs 'Exec' key")
200             if value == "Link":
201                 if not self.content[group].has_key("URL"):
202                     self.warnings.append("Type=Application needs 'Exec' key")
203
204         elif key == "Version":
205             self.checkValue(key, value)
206
207         elif re.match("^Name"+xdg.Locale.regex+"$", key):
208             pass # locale string
209
210         elif re.match("^GenericName"+xdg.Locale.regex+"$", key):
211             pass # locale string
212
213         elif key == "NoDisplay":
214             self.checkValue(key, value, type="boolean")
215
216         elif re.match("^Comment"+xdg.Locale.regex+"$", key):
217             pass # locale string
218
219         elif re.match("^Icon"+xdg.Locale.regex+"$", key):
220             self.checkValue(key, value)
221
222         elif key == "Hidden":
223             self.checkValue(key, value, type="boolean")
224
225         elif key == "OnlyShowIn":
226             self.checkValue(key, value, list=True)
227             self.checkOnlyShowIn(value)
228
229         elif key == "NotShowIn":
230             self.checkValue(key, value, list=True)
231             self.checkOnlyShowIn(value)
232
233         elif key == "TryExec":
234             self.checkValue(key, value)
235             self.checkType(key, "Application")
236
237         elif key == "Exec":
238             self.checkValue(key, value)
239             self.checkType(key, "Application")
240
241         elif key == "Path":
242             self.checkValue(key, value)
243             self.checkType(key, "Application")
244
245         elif key == "Terminal":
246             self.checkValue(key, value, type="boolean")
247             self.checkType(key, "Application")
248
249         elif key == "MimeType":
250             self.checkValue(key, value, list=True)
251             self.checkType(key, "Application")
252
253         elif key == "Categories":
254             self.checkValue(key, value)
255             self.checkType(key, "Application")
256             self.checkCategorie(value)
257
258         elif key == "StartupNotify":
259             self.checkValue(key, value, type="boolean")
260             self.checkType(key, "Application")
261
262         elif key == "StartupWMClass":
263             self.checkType(key, "Application")
264
265         elif key == "URL":
266             self.checkValue(key, value)
267             self.checkType(key, "URL")
268
269         # kde extensions
270         elif key == "ServiceTypes":
271             self.checkValue(key, value, list=True)
272             self.warnings.append("Key '%s' is a KDE extension" % key)
273
274         elif key == "DocPath":
275             self.checkValue(key, value)
276             self.warnings.append("Key '%s' is a KDE extension" % key)
277
278         elif re.match("^Keywords"+xdg.Locale.regex+"$", key):
279             self.checkValue(key, value, list=True)
280             self.warnings.append("Key '%s' is a KDE extension" % key)
281
282         elif key == "InitialPreference":
283             self.checkValue(key, value, type="numeric")
284             self.warnings.append("Key '%s' is a KDE extension" % key)
285
286         elif key == "Dev":
287             self.checkValue(key, value)
288             self.checkType(key, "FSDevice")
289             self.warnings.append("Key '%s' is a KDE extension" % key)
290
291         elif key == "FSType":
292             self.checkValue(key, value)
293             self.checkType(key, "FSDevice")
294             self.warnings.append("Key '%s' is a KDE extension" % key)
295
296         elif key == "MountPoint":
297             self.checkValue(key, value)
298             self.checkType(key, "FSDevice")
299             self.warnings.append("Key '%s' is a KDE extension" % key)
300
301         elif key == "ReadOnly":
302             self.checkValue(key, value, type="boolean")
303             self.checkType(key, "FSDevice")
304             self.warnings.append("Key '%s' is a KDE extension" % key)
305
306         elif re.match("^UnmountIcon"+xdg.Locale.regex+"$", key):
307             self.checkValue(key, value)
308             self.checkType(key, "FSDevice")
309             self.warnings.append("Key '%s' is a KDE extension" % key)
310
311         # deprecated keys
312         elif key == "Encoding":
313             self.checkValue(key, value)
314             self.warnings.append("Key '%s' is deprecated" % key)
315
316         elif re.match("^MiniIcon"+xdg.Locale.regex+"$", key):
317             self.checkValue(key, value)
318             self.warnings.append("Key '%s' is deprecated" % key)
319
320         elif key == "TerminalOptions":
321             self.checkValue(key, value)
322             self.warnings.append("Key '%s' is deprecated" % key)
323
324         elif key == "DefaultApp":
325             self.checkValue(key, value)
326             self.warnings.append("Key '%s' is deprecated" % key)
327
328         elif key == "Protocols":
329             self.checkValue(key, value, list=True)
330             self.warnings.append("Key '%s' is deprecated" % key)
331
332         elif key == "Extensions":
333             self.checkValue(key, value, list=True)
334             self.warnings.append("Key '%s' is deprecated" % key)
335
336         elif key == "BinaryPattern":
337             self.checkValue(key, value)
338             self.warnings.append("Key '%s' is deprecated" % key)
339
340         elif key == "MapNotify":
341             self.checkValue(key, value)
342             self.warnings.append("Key '%s' is deprecated" % key)
343
344         elif re.match("^SwallowTitle"+xdg.Locale.regex+"$", key):
345             self.warnings.append("Key '%s' is deprecated" % key)
346
347         elif key == "SwallowExec":
348             self.checkValue(key, value)
349             self.warnings.append("Key '%s' is deprecated" % key)
350
351         elif key == "FilePattern":
352             self.checkValue(key, value, type="regex", list=True)
353             self.warnings.append("Key '%s' is deprecated" % key)
354
355         elif key == "SortOrder":
356             self.checkValue(key, value, list=True)
357             self.warnings.append("Key '%s' is deprecated" % key)
358
359         elif key == "Actions":
360             self.checkValue(key, value, list=True)
361             self.warnings.append("Key '%s' is deprecated" % key)
362
363         # "X-" extensions
364         elif re.match("^X-[a-zA-Z0-9-]+", key):
365             pass
366
367         else:
368             self.errors.append("Invalid key: %s" % key)
369
370     def checkType(self, key, type):
371         if not self.getType() == type:
372             self.errors.append("Key '%s' only allowed in Type=%s" % (key, type))
373
374     def checkOnlyShowIn(self, value):
375         values = self.getList(value)
376         valid = ["GNOME", "KDE", "ROX", "XFCE", "Old", "LXDE"]
377         for item in values:
378             if item not in valid and item[0:2] != "X-":
379                 self.errors.append("'%s' is not a registered OnlyShowIn value" % item);
380
381     def checkCategorie(self, value):
382         values = self.getList(value)
383
384         main = ["AudioVideo", "Audio", "Video", "Development", "Education", "Game", "Graphics", "Network", "Office", "Settings", "System", "Utility"]
385         hasmain = False
386         for item in values:
387             if item in main:
388                 hasmain = True
389         if hasmain == False:
390             self.errors.append("Missing main category")
391
392         additional = ["Building", "Debugger", "IDE", "GUIDesigner", "Profiling", "RevisionControl", "Translation", "Calendar", "ContactManagement", "Database", "Dictionary", "Chart", "Email", "Finance", "FlowChart", "PDA", "ProjectManagement", "Presentation", "Spreadsheet", "WordProcessor", "2DGraphics", "VectorGraphics", "3DGraphics", "RasterGraphics", "Scanning", "OCR", "Photography", "Publishing", "Viewer", "TextTools", "DesktopSettings", "HardwareSettings", "Printing", "PackageManager", "Dialup", "InstantMessaging", "Chat", "IRCClient", "FileTransfer", "HamRadio", "News", "P2P", "RemoteAccess", "Telephony", "TelephonyTools", "VideoConference", "WebBrowser", "WebDevelopment", "Midi", "Mixer", "Sequencer", "Tuner", "TV", "AudioVideoEditing", "Player", "Recorder", "DiscBurning", "ActionGame", "AdventureGame", "ArcadeGame", "BoardGame", "BlocksGame", "CardGame", "KidsGame", "LogicGame", "RolePlaying", "Simulation", "SportsGame", "StrategyGame", "Art", "Construction", "Music", "Languages", "Science", "ArtificialIntelligence", "Astronomy", "Biology", "Chemistry", "ComputerScience", "DataVisualization", "Economy", "Electricity", "Geography", "Geology", "Geoscience", "History", "ImageProcessing", "Literature", "Math", "NumericalAnalysis", "MedicalSoftware", "Physics", "Robotics", "Sports", "ParallelComputing", "Amusement", "Archiving", "Compression", "Electronics", "Emulator", "Engineering", "FileTools", "FileManager", "TerminalEmulator", "Filesystem", "Monitor", "Security", "Accessibility", "Calculator", "Clock", "TextEditor", "Documentation", "Core", "KDE", "GNOME", "GTK", "Qt", "Motif", "Java", "ConsoleOnly", "Screensaver", "TrayIcon", "Applet", "Shell"]
393         
394         for item in values:
395             if item not in additional + main and item[0:2] != "X-":
396                 self.errors.append("'%s' is not a registered Category" % item);
397