trisquel-icecat/icecat/debian/apport/source_firefox.py.in

1483 lines
53 KiB
Python

'''firefox apport hook
/usr/share/apport/package-hooks/firefox.py
Copyright (c) 2007: Hilario J. Montoliu <hmontoliu@gmail.com>
(c) 2011: Chris Coulson <chris.coulson@canonical.com>
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version. See http://www.gnu.org/copyleft/gpl.html for
the full text of the license.
'''
import os
import os.path
import sys
import fcntl
import subprocess
import struct
from subprocess import Popen
try:
from configparser import ConfigParser
except ImportError:
from ConfigParser import ConfigParser
import sqlite3
import tempfile
import re
import apport.packaging
from apport.hookutils import *
from glob import glob
import zipfile
import stat
import functools
if sys.version_info[0] < 3:
import codecs
DISTRO_ADDONS = [
'@MOZ_PKG_NAME@',
'xul-ext-ubufox'
]
class PrefParseError(Exception):
def __init__(self, msg, filename, linenum):
super(PrefParseError, self).__init__(msg)
self.msg = msg
self.filename = filename
self.linenum = linenum
def __str__(self):
return self.msg + ' @ ' + self.filename + ':' + str(self.linenum)
class PluginRegParseError(Exception):
def __init__(self, msg, linenum):
super(PluginRegParseError, self).__init__(msg)
self.msg = msg
self.linenum = linenum
def __str__(self):
return self.msg + ' @ line ' + str(self.linenum)
class ExtensionTypeNotRecognised(Exception):
def __init__(self, ext_type, ext_id):
super(ExtensionTypeNotRecognised, self).__init__(ext_type, ext_id)
self.ext_type = ext_type
self.ext_id = ext_id
def __str__(self):
return "Extension type not recognised: %s (ID: %s)" % (self.ext_type, self.ext_id)
class VersionCompareFailed(Exception):
def __init__(self, a, b, e):
if a == None:
a = ''
if b == None:
b = ''
super(VersionCompareFailed, self).__init__(a, b, e)
self.a = a
self.b = b
self.e = e
def __str__(self):
return "Failed to compare versions A = %s, B = %s (%s)" % (self.a, self.b, str(self.e))
def _open(filename, mode):
if sys.version_info[0] < 3:
return codecs.open(filename, mode, 'utf-8')
return open(filename, mode)
def mkstemp_copy(path):
'''Make a copy of a file to a temporary file, and return the path'''
(outfd, outpath) = tempfile.mkstemp()
outfile = os.fdopen(outfd, 'wb')
infile = open(path, 'rb')
total = 0
while True:
data = infile.read(4096)
total += len(data)
outfile.write(data)
infile.seek(total)
outfile.seek(total)
if len(data) < 4096: break
return outpath
def anonymize_path(path, profiledir = None):
if profiledir != None and path == os.path.join(profiledir, 'prefs.js'):
return 'prefs.js'
elif profiledir != None and path == os.path.join(profiledir, 'user.js'):
return 'user.js'
elif profiledir != None and path.startswith(profiledir):
return os.path.join('[Profile]', os.path.relpath(path, profiledir))
elif path.startswith(os.environ['HOME']):
return os.path.join('[HomeDir]', os.path.relpath(path, os.environ['HOME']))
else:
return path
class CompatINIParser(ConfigParser):
def __init__(self, path):
ConfigParser.__init__(self)
self.read(os.path.join(path, "compatibility.ini"))
@property
def last_version(self):
if not self.has_section("Compatibility") or not self.has_option("Compatibility", "LastVersion"):
return None
return re.sub(r'([^_]*)(.*)', r'\1', self.get("Compatibility", "LastVersion"))
@property
def last_buildid(self):
if not self.has_section("Compatibility") or not self.has_option("Compatibility", "LastVersion"):
return None
return re.sub(r'([^_]*)_([^/]*)/(.*)', r'\2', self.get("Compatibility", "LastVersion"))
class AppINIParser(ConfigParser):
def __init__(self, path):
ConfigParser.__init__(self)
self.read(os.path.join(path, "application.ini"))
@property
def buildid(self):
if not self.has_section('App') or not self.has_option('App', 'BuildID'):
return None
return self.get('App', 'BuildID')
@property
def appid(self):
if not self.has_section('App') or not self.has_option('App', 'ID'):
return None
return self.get('App', 'ID')
class ExtensionINIParser(ConfigParser):
def __init__(self, path):
ConfigParser.__init__(self)
self.read(os.path.join(path, "extensions.ini"))
self._extensions = []
if self.has_section('ExtensionDirs'):
items = self.items('ExtensionDirs')
for item in items:
self._extensions.append(item[1])
def __getitem__(self, key):
if key > len(self) - 1:
raise IndexError
return self._extensions[key]
def __iter__(self):
class ExtensionINIParserIter:
def __init__(self, parser):
self.parser = parser
self.index = 0
def __next__(self):
if self.index == len(self.parser):
raise StopIteration
res = self.parser[self.index]
self.index += 1
return res
def next(self):
return self.__next__()
return ExtensionINIParserIter(self)
def __len__(self):
return len(self._extensions)
def compare_versions(a, b):
'''Compare 2 version numbers, returns -1 for a<b, 0 for a=b and 1 for a>b
This is basically just a python reimplementation of nsVersionComparator'''
class VersionPart:
def __init__(self):
self.numA = 0
self.strB = None
self.numC = 0
self.extraD = None
def parse_version(part):
res = VersionPart()
if part == None or part == '':
return (part, res)
spl = part.split('.')
if part == '*' and len(part) == 1:
try:
res.numA = sys.maxint
except:
res.numA = sys.maxsize # python3
res.strB = ""
else:
res.numA = int(re.sub(r'([0-9]*)(.*)', r'\1', spl[0]))
res.strB = re.sub(r'([0-9]*)(.*)', r'\2', spl[0])
if res.strB == '':
res.strB = None
if res.strB != None:
if res.strB[0] == '+':
res.numA += 1
res.strB = "pre"
else:
tmp = res.strB
res.strB = re.sub(r'([^0-9+-]*)([0-9]*)(.*)', r'\1', tmp)
strC = re.sub(r'([^0-9+-]*)([0-9]*)(.*)', r'\2', tmp)
if strC != '':
res.numC = int(strC)
res.extraD = re.sub(r'([^0-9+-]*)([0-9]*)(.*)', r'\3', tmp)
if res.extraD == '':
res.extraD = None
return (re.sub(r'([^\.]*)\.*(.*)', r'\2', part), res)
def strcmp(stra, strb):
if stra == None and strb == None:
return 0
elif stra == None and strb != None:
return 1
elif stra != None and strb == None:
return -1
if stra < strb:
return -1
elif stra > strb:
return 1
else:
return 0
def do_compare(apart, bpart):
if apart.numA < bpart.numA:
return -1
elif apart.numA > bpart.numA:
return 1
res = strcmp(apart.strB, bpart.strB)
if res != 0:
return res
if apart.numC < bpart.numC:
return -1
elif apart.numC > bpart.numC:
return 1
return strcmp(apart.extraD, bpart.extraD)
try:
saved_a = a
saved_b = b
while a or b:
(a, va) = parse_version(a)
(b, vb) = parse_version(b)
res = do_compare(va, vb)
if res != 0:
break
except Exception as e:
raise VersionCompareFailed(saved_a, saved_b, e)
return res
class Plugin(object):
def __init__(self):
self.lib = None
self.path = None
self.desc = None
self._package = None
self._checked_package = False
def dump(self):
if self.path.startswith(os.path.join(os.environ['HOME'], '.mozilla', '@MOZ_APP_NAME@')):
location = "[Profile]"
else:
location = os.path.dirname(self.path)
pkgname = ' (%s)' % self.package if self.package != None else ''
return ("%s - %s%s" % (self.desc, os.path.join(location, self.lib), pkgname))
@property
def package(self):
if self._checked_package == False:
self._package = apport.packaging.get_file_package(self.path)
self._checked_package = True
return self._package
class PluginRegistry:
STATE_PENDING = 0
STATE_START = 1
STATE_PROCESSING_1 = 2
STATE_PROCESSING_2 = 3
STATE_PROCESSING_3 = 4
STATE_FINISHED = 5
def __init__(self, path):
self.plugins = []
self._state = PluginRegistry.STATE_PENDING
self._current_plugin = None
self._profile_path = path
self.error = None
fd = None
try:
fd = _open(os.path.join(path, 'pluginreg.dat'), 'r')
try:
skip = 0
linenum = 1
for line in fd.readlines():
if skip == 0:
skip = self._parseline(line, linenum)
if skip == -1:
break
else:
skip -= 1
linenum += 1
if skip > 0:
raise PluginRegParseError("Unexpected EOF", linenum)
except Exception as e:
self.error = str(e)
except:
pass
finally:
if fd != None:
fd.close()
def _parseline(self, line, linenum):
line = line.strip()
if line != '' and line[0] == '[' and self._state != PluginRegistry.STATE_START and self._state != PluginRegistry.STATE_PENDING:
raise PluginRegParseError('Unexpected section header', linenum)
if self._state == PluginRegistry.STATE_PENDING:
if line == '[PLUGINS]':
self._state += 1
return 0
elif self._state == PluginRegistry.STATE_START:
if line == '':
return 0
if line[0] == '[':
self._state = PluginRegistry.STATE_FINISHED
return -1
self._current_plugin = Plugin()
self._current_plugin.lib = line.split(':')[0]
self._state += 1
return 0
elif self._state == PluginRegistry.STATE_PROCESSING_1:
path = line.split(':')[0]
if path[0] != '/':
raise PluginRegParseError("Invalid path", linenum)
self._current_plugin.path = anonymize_path(path, self._profile_path)
self._state += 1
return 3
elif self._state == PluginRegistry.STATE_PROCESSING_2:
self._current_plugin.desc = line.split(':')[0]
self._state += 1
return 0
elif self._state == PluginRegistry.STATE_PROCESSING_3:
self.plugins.append(self._current_plugin)
self._state = PluginRegistry.STATE_START
return int(line.strip())
else:
return -1
def __getitem__(self, key):
if key > len(self) - 1:
raise IndexError
return self.plugins[key]
def __iter__(self):
class PluginRegistryIter:
def __init__(self, registry):
self.registry = registry
self.index = 0
def __next__(self):
if self.index == len(self.registry):
raise StopIteration
ret = self.registry[self.index]
self.index += 1
return ret
def next(self):
return self.__next__()
return PluginRegistryIter(self)
def __len__(self):
return len(self.plugins)
class Prefs:
'''Class which represents a pref file. Handles all of the parsing, and can be accessed
like a normal python dictionary'''
PREF_WHITELIST = [
r'accessibility\.*',
r'browser\.fixup\.*',
r'browser\.history_expire_*',
r'browser\.link\.open_newwindow',
r'browser\.mousewheel\.*',
r'browser\.places\.*',
r'browser\.startup\.homepage',
r'browser\.tabs\.*',
r'browser\.zoom\.*',
r'dom\.*',
r'extensions\.autoDisableScopes',
r'extensions\.checkCompatibility\.*',
r'extensions\.enabledScopes',
r'extensions\.lastAppVersion',
r'extensions\.minCompatibleAppVersion',
r'extensions\.minCompatiblePlatformVersion',
r'extensions\.strictCompatibility',
r'font\.*',
r'general\.skins\.*',
r'general\.useragent\.*',
r'gfx\.*',
r'html5\.*',
r'mozilla\.widget\.render\-mode',
r'layers\.*',
r'javascript\.*',
r'keyword\.*',
r'layout\.css\.dpi',
r'network\.*',
r'places\.*',
r'plugin\.*',
r'plugins\.*',
r'print\.*',
r'privacy\.*',
r'security\.*',
r'webgl\.*'
]
PREF_BLACKLIST = [
r'^network.*proxy\.*',
r'.*print_to_filename$',
r'print\.tmp\.',
r'print\.printer_*',
r'printer_*'
]
STATE_READY = 0
STATE_COMMENT_MAYBE_START = 1
STATE_COMMENT_BLOCK = 2
STATE_COMMENT_BLOCK_MAYBE_END = 3
STATE_PARSE_UNTIL_OPEN_PAREN = 4
STATE_PARSE_UNTIL_NAME = 5
STATE_PARSE_UNTIL_COMMA = 6
STATE_PARSE_UNTIL_VALUE = 7
STATE_PARSE_UNTIL_CLOSE_PAREN = 8
STATE_PARSE_UNTIL_SEMICOLON = 9
STATE_PARSE_STRING = 10
STATE_PARSE_ESC_SEQ = 11
STATE_PARSE_INT = 12
STATE_SKIP = 13
STATE_PARSE_UNTIL_EOL = 14
def __init__(self, profile_path, extra_paths=None, whitelist=None, blacklist=None):
self.whitelist = whitelist if whitelist != None else Prefs.PREF_WHITELIST
self.blacklist = blacklist if blacklist != None else Prefs.PREF_BLACKLIST
self.prefs = {}
self.pref_sources = []
self.errors = {}
self._profile_path = profile_path
# Read all preferences. Note that we hide preferences that are considered
# default (ie, all of those set by the Firefox package or bundled addons,
# unless any of the pref files have been modified by the user).
# The load order is *very important*
if profile_path != None:
locations = [
"/@MOZ_LIBDIR@/omni.ja:greprefs.js",
"/@MOZ_LIBDIR@/omni.ja:defaults/pref/*.js",
"/@MOZ_LIBDIR@/defaults/pref/*.js",
"/@MOZ_LIBDIR@/defaults/pref/unix.js",
"/@MOZ_LIBDIR@/omni.ja:defaults/preferences/*.js"
"/@MOZ_LIBDIR@/defaults/preferences/*.js"
]
append_dirs = [ 'defaults/preferences/*.js' ]
if os.path.isdir('/@MOZ_LIBDIR@/distribution/bundles'):
bundles = os.listdir('/@MOZ_LIBDIR@/distribution/bundles')
bundles.sort(reverse=True)
for d in append_dirs:
for bundle in bundles:
path = os.path.join('/@MOZ_LIBDIR@/distribution/bundles', bundle)
if path.endswith('.xpi'):
locations.append(path + ':' + d)
elif os.path.isdir(path):
locations.append(os.path.join(path, d))
locations.append(os.path.join(profile_path, "prefs.js"))
locations.append(os.path.join(profile_path, "user.js"))
extensions = ExtensionINIParser(profile_path)
for extension in extensions:
if extension.endswith('.xpi'):
locations.append(extension + ':defaults/preferences/*.(J|j)(S|s)')
elif os.path.isdir(extension):
locations.append(os.path.join(extension, 'defaults/preferences/*.js'))
locations.append(os.path.join(profile_path, 'preferences/*.js'))
else: locations = []
if extra_paths != None:
for extra in extra_paths:
locations.append(extra)
for location in locations:
m = re.match(r'^([^:]*):?(.*)', location)
if m.group(2) == '':
files = glob(location)
files.sort(reverse=True)
for f in files:
self._parse_file(f)
else:
self._parse_jar(m.group(1), m.group(2))
def _should_ignore_file(self, filename):
realpath = os.path.realpath(filename)
package = apport.packaging.get_file_package(realpath)
if package and apport.packaging.is_distro_package(package) and \
package in DISTRO_ADDONS and \
realpath[1:] not in apport.packaging.get_modified_files(package):
return True
return False
def _parse_file(self, filename):
f = None
self._state = Prefs.STATE_READY
try:
f = _open(filename, 'r')
try:
linenum = 1
state = None
for line in f.readlines():
state = self._parseline(line, filename, linenum, state)
linenum += 1
except Exception as e:
self.errors[filename] = str(e)
except:
pass
finally:
if f != None:
f.close()
if filename not in self.errors \
and not self._should_ignore_file(filename):
self.pref_sources.append(filename)
def _parse_jar(self, jar, match):
jarfile = None
try:
jarfile = zipfile.ZipFile(jar)
entries = jarfile.namelist()
entries.sort(reverse=True)
for entry in entries:
if re.match(r'^' + match + '$', entry):
source = '%s:%s' % (jar, entry)
try:
f = jarfile.open(entry, 'r')
linenum = 1
state = None
for line in f.readlines():
state = self._parseline(line.decode('utf-8'),
source, linenum, state)
linenum += 1
except Exception as e:
self.errors[source] = str(e)
finally:
if source not in self.errors \
and not self._should_ignore_file(jar):
self.pref_sources.append(source)
except:
pass
finally:
if jarfile != None:
jarfile.close()
def _maybe_add_pref(self, key, value, source, default, locked):
class Pref(object):
def __init__(self, profile_path):
self._default = None
self._value = None
self._default_source = None
self._value_source = None
self.locked = False
self._profile_path = profile_path
@property
def value(self):
if self._value != None:
return self._value
return self._default
@property
def source(self):
if self._value != None:
return self._value_source
return self._default_source
@property
def anon_source(self):
if self._value != None:
return anonymize_path(self._value_source, self._profile_path)
return anonymize_path(self._default_source, self._profile_path)
def set_value(self, value, source, default, locked):
if self.locked == True:
return
if default == True:
self._default = value
self._default_source = source
else:
self._value = value
self._value_source = source
self.locked = locked
for match in self.blacklist:
if re.match(match, key):
return
for match in self.whitelist:
if re.match(match, key):
if key not in self.prefs:
self.prefs[key] = Pref(self._profile_path)
self.prefs[key].set_value(value, source, default, locked)
def _parseline(self, line, source, linenum, old_state):
# XXX: I pity the poor soul who ever needs to change anything inside this function
class PrefParseState(object):
def __init__(self):
self.state = Prefs.STATE_READY
def _reset(self):
self.next_state = Prefs.STATE_READY
self.default = False
self.locked = False
self.name = None
self.value = None
self.tmp = None
self.skip = None
self.quote = None
def _get_state(self):
return self._state
def _set_state(self, state):
self._state = state
if state == Prefs.STATE_READY:
self._reset()
state = property(_get_state, _set_state)
state = old_state
if state == None:
state = PrefParseState()
index = 0
for c in line:
if state.state == Prefs.STATE_READY:
if c == '/':
state.state = Prefs.STATE_COMMENT_MAYBE_START
elif c == '#':
state.state = Prefs.STATE_PARSE_UNTIL_EOL
elif line.startswith('pref', index):
state.default == True
state.next_state = Prefs.STATE_PARSE_UNTIL_OPEN_PAREN
state.state = Prefs.STATE_SKIP
state.skip = 3
elif line.startswith('user_pref', index):
state.next_state = Prefs.STATE_PARSE_UNTIL_OPEN_PAREN
state.state = Prefs.STATE_SKIP
state.skip = 8
elif line.startswith('lockPref', index):
state.default = True
state.locked = True
state.next_state = Prefs.STATE_PARSE_UNTIL_OPEN_PAREN
state.state = Prefs.STATE_SKIP
state.skip = 7
elif not c.isspace():
raise PrefParseError("Unexpected character '%s' before pref" % c,
anonymize_path(source, self._profile_path),
linenum)
elif state.state == Prefs.STATE_SKIP:
state.skip -= 1
if state.skip == 0:
state.state = state.next_state
state.next_state = Prefs.STATE_READY
elif state.state == Prefs.STATE_COMMENT_MAYBE_START:
if c == '*':
state.state = Prefs.STATE_COMMENT_BLOCK
elif c == '/':
state.state = Prefs.STATE_PARSE_UNTIL_EOL
else:
raise PrefParseError("Unexpected '/'",
anonymize_path(source, self._profile_path),
linenum)
elif state.state == Prefs.STATE_PARSE_UNTIL_EOL:
pass
elif state.state == Prefs.STATE_COMMENT_BLOCK:
if c == '*':
state.state = Prefs.STATE_COMMENT_BLOCK_MAYBE_END
elif state.state == Prefs.STATE_COMMENT_BLOCK_MAYBE_END:
if c == '/':
state.state = state.next_state
state.next_state = Prefs.STATE_READY
else:
state.state = Prefs.STATE_COMMENT_BLOCK
elif state.state == Prefs.STATE_PARSE_UNTIL_OPEN_PAREN:
if c == '(':
state.state = Prefs.STATE_PARSE_UNTIL_NAME
elif c == '/':
state.next_state = state.state
state.state = Prefs.STATE_COMMENT_MAYBE_START
elif not c.isspace():
raise PrefParseError("Unexpected character '%s' before open parenthesis" % c,
anonymize_path(source, self._profile_path),
linenum)
elif state.state == Prefs.STATE_PARSE_UNTIL_NAME:
if c == '"' or c == '\'':
state.tmp = ''
state.quote = c
state.state = Prefs.STATE_PARSE_STRING
state.next_state = Prefs.STATE_PARSE_UNTIL_COMMA
elif c == '/':
state.next_state = state.state
state.state = Prefs.STATE_COMMENT_MAYBE_START
elif not c.isspace():
raise PrefParseError("Unexpected character '%s' before pref name" % c,
anonymize_path(source, self._profile_path),
linenum)
elif state.state == Prefs.STATE_PARSE_STRING:
if c == '\\':
state.state = Prefs.STATE_PARSE_ESC_SEQ
elif c == state.quote:
state.state = state.next_state
state.next_state = Prefs.STATE_READY
else:
state.tmp += c
elif state.state == Prefs.STATE_PARSE_ESC_SEQ:
# XXX: We don't handle UTF16 / hex here
if c == 'n':
c = '\n'
elif c == 'r':
c = '\r'
state.tmp += c
state.state = Prefs.STATE_PARSE_STRING
elif state.state == Prefs.STATE_PARSE_UNTIL_COMMA:
if state.tmp != None:
state.name = state.tmp
state.tmp = None
if c == ',':
state.state = Prefs.STATE_PARSE_UNTIL_VALUE
elif c == '/':
state.next_state = state.state
state.state = Prefs.STATE_COMMENT_MAYBE_START
elif not c.isspace():
raise PrefParseError("Unexpected character '%s' before comma" % c,
anonymize_path(source, self._profile_path),
linenum)
elif state.state == Prefs.STATE_PARSE_UNTIL_VALUE:
if c == '"' or c == '\'':
state.tmp = ''
state.quote = c
state.state = Prefs.STATE_PARSE_STRING
state.next_state = Prefs.STATE_PARSE_UNTIL_CLOSE_PAREN
elif line.startswith('true', index):
state.tmp = True
state.next_state = Prefs.STATE_PARSE_UNTIL_CLOSE_PAREN
state.state = Prefs.STATE_SKIP
state.skip = 3
elif line.startswith('false', index):
state.tmp = False
state.next_state = Prefs.STATE_PARSE_UNTIL_CLOSE_PAREN
state.state = Prefs.STATE_SKIP
state.skip = 4
elif (c >= '0' and c <= '9') or c == '+' or c == '-':
state.tmp = c
state.state = Prefs.STATE_PARSE_INT
elif c == '/':
state.next_state = state
state.state = Prefs.STATE_COMMENT_MAYBE_START
elif not c.isspace():
raise PrefParseError("Unexpected character '%s' before value" % c,
anonymize_path(source, self._profile_path),
linenum)
elif state.state == Prefs.STATE_PARSE_INT:
if c >= '0' and c <= '9':
state.tmp += c
elif c == ')':
state.value = int(state.tmp)
state.tmp = None
state.state = Prefs.STATE_PARSE_UNTIL_SEMICOLON
elif c.isspace():
state.tmp = int(state.tmp)
state.state = Prefs.STATE_PARSE_UNTIL_CLOSE_PAREN
elif c == '/':
state.tmp = int(state.tmp)
state.next_state = Prefs.STATE_PARSE_UNTIL_CLOSE_PAREN
state.state = Prefs.STATE_COMMENT_MAYBE_START
else:
raise PrefParseError("Unexpected character '%s' whilst parsing int" % c,
anonymize_path(source, self._profile_path),
linenum)
elif state.state == Prefs.STATE_PARSE_UNTIL_CLOSE_PAREN:
if state.tmp != None:
state.value = state.tmp
state.tmp = None
if c == ')':
state.state = Prefs.STATE_PARSE_UNTIL_SEMICOLON
elif c == '/':
state.next_state = state.state
state.state = Prefs.STATE_COMMENT_MAYBE_START
elif not c.isspace():
raise PrefParseError("Unexpected character '%s' before close parenthesis" % c,
anonymize_path(source, self._profile_path),
linenum)
elif state.state == Prefs.STATE_PARSE_UNTIL_SEMICOLON:
if c == ';':
self._maybe_add_pref(state.name, state.value, source,
state.default, state.locked)
state.state = Prefs.STATE_READY
elif c == '/':
state.next_state = state.state
state.state = Prefs.STATE_COMMENT_MAYBE_START
elif not c.isspace():
raise PrefParseError("Unexpected character '%s' before semicolon" % c,
anonymize_path(source, self._profile_path),
linenum)
index += 1
if state.state == Prefs.STATE_PARSE_UNTIL_EOL:
state.state = Prefs.STATE_READY
return state
def __getitem__(self, key):
res = self.prefs[key]
if res.source in self.pref_sources:
return res
raise KeyError
def __iter__(self):
class PrefsIter:
def __init__(self, prefs):
self.index = 0
self.keys = []
for k in prefs.prefs.keys():
try:
test = prefs[k]
self.keys.append(k)
except:
pass
self.keys.sort()
def __next__(self):
if self.index == len(self.keys):
raise StopIteration
res = self.keys[self.index]
self.index += 1
return res
def next(self):
return self.__next__()
return PrefsIter(self)
def __len__(self):
i = 0
for k in self:
i += 1
return i
class Extension:
'''Small class representing an extension'''
def __init__(self, ext_id, location, ver, ext_type, active, desc, min_appver,
max_appver, cur_appver, visible, userDisabled, appDisabled,
softDisabled, foreign, hasBinary, strictCompat, appStrictCompat):
self.ext_id = ext_id;
self.location = location
self.ver = ver
self.ext_type = ext_type
self.active = active
self.desc = desc
self.min_appver = min_appver
self.max_appver = max_appver
self.cur_appver = cur_appver
self.visible = visible
self.userDisabled = userDisabled
self.appDisabled = appDisabled
self.softDisabled = softDisabled
self.foreign = foreign
self.hasBinary = hasBinary
self.strictCompat = strictCompat
self.appStrictCompat = appStrictCompat
def dump(self):
active = "Yes" if self.active == True else "No"
foreign = "Yes" if self.foreign == True else "No"
visible = "Yes" if self.visible == True else "No"
hasBinary = "Yes" if self.hasBinary == True else "No"
strictCompat = "Yes" if self.strictCompat == True else "No"
if self.active == True:
disabled_reason = ""
elif self.softDisabled == True:
disabled_reason = "(Soft-blocked)"
elif self.appDisabled == True:
disabled_reason = "(Application disabled)"
elif self.userDisabled == True:
disabled_reason = "(User disabled)"
else:
disabled_reason = "(Reason unknown)"
return ("%s - ID=%s, Version=%s, minVersion=%s, maxVersion=%s, Location=%s, " +
"Type=%s, Foreign=%s, Visible=%s, BinaryComponents=%s, StrictCompat=%s, " +
"Active=%s %s") % \
(self.desc, self.ext_id, self.ver, self.min_appver, self.max_appver,
self.location, self.ext_type, foreign, visible, hasBinary,
strictCompat, active, disabled_reason)
@property
def active_but_incompatible(self):
return self.active and (self.cur_appver != None and \
(compare_versions(self.cur_appver, self.min_appver) == -1 or \
compare_versions(self.cur_appver, self.max_appver) == 1) and \
(self.hasBinary or self.strictCompat or self.appStrictCompat))
class Profile:
'''Container to represent a profile'''
def __init__(self, id, name, path, is_default, appini):
self.extensions = {}
self.locales = {}
self.themes = {}
self.id = id
self.name = name
self.path = path
self.default = is_default
self.appini = appini
self.prefs = Prefs(path)
self.plugins = PluginRegistry(path)
try:
self._populate_extensions()
except:
self.extensions = None
def _populate_extensions(self):
# We copy the db as it's locked whilst Firefox is open. This is still racy
# though, as it could be modified during the copy, leaving us with a corrupt
# DB. Can we detect this somehow?
tmp_db = mkstemp_copy(os.path.join(self.path, "extensions.sqlite"))
conn = sqlite3.connect(tmp_db)
def get_extension_from_row(row):
moz_id = row[0]
ext_id = row[1]
location = row[2]
ext_ver = row[3]
ext_type = row[4]
visible = True if row[6] == 1 else False
active = True if row[7] == 1 else False
userDisabled = True if row[8] == 1 else False
appDisabled = True if row[9] == 1 else False
softDisabled = True if row[10] == 1 else False
foreign = True if row[11] == 1 else False
hasBinary = True if row[12] == 1 else False
strictCompat = True if row[13] == 1 else False
cur = conn.cursor()
cur.execute("select name from locale where id=:id", { "id": row[5] })
desc = cur.fetchone()[0]
cur = conn.cursor()
cur.execute("select minVersion, maxVersion from targetApplication where addon_internal_id=:id and (id=:appid or id=:tkid)", \
{ "id": row[0], "appid": self.appini.appid, "tkid": "toolkit@mozilla.org" })
(min_ver, max_ver) = cur.fetchone()
appStrictCompat = 'extensions.strictCompatibility' in self.prefs and \
self.prefs['extensions.strictCompatibility'].value == 'true'
return Extension(ext_id, location, ext_ver, ext_type, active, desc,
min_ver, max_ver, self.last_version, visible,
userDisabled, appDisabled, softDisabled, foreign,
hasBinary, strictCompat, appStrictCompat)
cur = conn.cursor()
cur.execute("select internal_id, id, location, version, type, defaultLocale, " + \
"visible, active, userDisabled, appDisabled, softDisabled, " + \
"isForeignInstall, hasBinaryComponents, strictCompatibility from addon")
rows = cur.fetchall()
for row in rows:
extension = get_extension_from_row(row)
if extension.ext_type == "extension":
storage_array = self.extensions
elif extension.ext_type == "locale":
storage_array = self.locales
elif extension.ext_type == "theme":
storage_array = self.themes
else:
raise ExtensionTypeNotRecognised(extension.type, extension.ext_id)
if not extension.location in storage_array:
storage_array[extension.location] = []
storage_array[extension.location].append(extension)
os.remove(tmp_db)
def _do_dump(self, storage_array):
if self.extensions == None:
return "extensions.sqlite corrupt or missing"
ret = ""
for location in storage_array:
ret += "Location: " + location + "\n\n"
for extension in storage_array[location]:
prefix = " (Inactive) " if not extension.active else ""
ret += '\t%s%s\n' % (prefix, extension.dump())
ret += "\n\n\n"
return ret
@property
def running(self):
if not hasattr(self, '_running'):
# We detect if this profile is running or not by trying to lock the lockfile
# If we can't lock it, then Firefox is running
fd = os.open(os.path.join(self.path, ".parentlock"), os.O_WRONLY|os.O_CREAT|os.O_TRUNC, 0o666)
lock = struct.pack("hhqqi", 1, 0, 0, 0, 0)
try:
fcntl.fcntl(fd, fcntl.F_SETLK, lock)
self._running = False
# If we acquired the lock, ensure that we unlock again immediately
lock = struct.pack("hhqqi", 2, 0, 0, 0, 0)
fcntl.fcntl(fd, fcntl.F_SETLK, lock)
except:
self._running = True
return self._running
def dump_extensions(self):
return self._do_dump(self.extensions)
def dump_locales(self):
return self._do_dump(self.locales)
def dump_themes(self):
return self._do_dump(self.themes)
def dump_prefs(self):
ret = ''
for pref in self.prefs:
if type(self.prefs[pref].value) == int:
value = str(self.prefs[pref].value)
elif type(self.prefs[pref].value) == bool:
value = 'true' if self.prefs[pref].value == True else 'false'
else:
value = "\"%s\"" % self.prefs[pref].value
ret += pref + ': ' + value + ' (' + self.prefs[pref].anon_source + ')\n'
return ret
def dump_pref_sources(self):
ret = ''
for source in self.prefs.pref_sources:
ret += anonymize_path(source, self.path) + '\n'
return ret
def dump_pref_errors(self):
ret = ''
for source in self.prefs.errors:
ret += self.prefs.errors[source] + '\n'
return ret
def dump_plugins(self):
if self.plugins.error != None:
return "pluginreg.dat exists but isn't parseable. %s" % self.plugins.error
ret = ''
for plugin in self.plugins:
ret += plugin.dump() + '\n'
return ret
def get_plugin_packages(self, pkglist):
if self.plugins.error != None:
return None
for plugin in self.plugins:
if plugin.package != None and plugin.package not in pkglist:
pkglist.append(plugin.package)
@property
def current(self):
return True if self.appini.buildid == self.last_buildid or self.appini.buildid == None else False
@property
def has_active_but_incompatible_extensions(self):
if self.last_version == None or self.extensions == None:
return False
for storage_array in self.extensions, self.locales, self.themes:
for location in storage_array:
for extension in storage_array[location]:
if extension.active_but_incompatible:
return True
return False
def dump_active_but_incompatible_extensions(self):
if self.last_version == None or self.extensions == None:
return "Unavailable (corrupt or non-existant compatibility.ini or extensions.sqlite)"
res = ''
for storage_array in self.extensions, self.locales, self.themes:
for location in storage_array:
for extension in storage_array[location]:
if extension.active_but_incompatible:
res += extension.desc + " - " + extension.ext_id + "\n"
return res
def dump_files_with_broken_permissions(self):
broken = []
blacklist = [
r'^lock$'
]
for dirpath, dirnames, filenames in os.walk(self.path):
def check_path(path):
relpath = os.path.relpath(path, self.path)
for i in blacklist:
if re.match(i, relpath):
return
flags = os.R_OK | os.W_OK
if os.path.isdir(path):
flags |= os.X_OK
if not os.access(path, flags):
broken.append(relpath)
check_path(dirpath)
for name in filenames:
check_path(os.path.join(dirpath, name))
uid = os.getuid()
broken.sort()
broken_txt = ''
for file in broken:
fstat = os.stat(os.path.join(self.path, file))
summary = "%#o" % (fstat.st_mode & (stat.S_IRWXU | stat.S_IRWXG |
stat.S_IRWXO))
if fstat.st_uid != uid:
summary += ', wrong owner'
broken_txt += file + ' (' + summary + ')\n'
return broken_txt
@property
def has_forced_layers_acceleration(self):
if "layers.acceleration.force-enabled" in self.prefs and self.prefs["layers.acceleration.force-enabled"].value == "true":
return True
return False
@property
def compatini(self):
if not hasattr(self, '_compatini'):
self._compatini = CompatINIParser(self.path)
return self._compatini
@property
def last_version(self):
return self.compatini.last_version
@property
def last_buildid(self):
return self.compatini.last_buildid
@property
def addon_compat_check_disabled(self):
if self.last_version == None:
return False
is_nightly = re.sub(r'^[^\.]+\.[0-9]+([a-z0-9]*).*', r'\1', self.last_version) == 'a1'
if is_nightly == True:
pref = "extensions.checkCompatibility.nightly"
else:
pref = "extensions.checkCompatibility.%s" % re.sub(r'(^[^\.]+\.[0-9]+[a-z]*).*', r'\1', self.last_version)
return pref in self.prefs and self.prefs[pref].value == 'false'
class Profiles:
'''Small class to build an array of profiles from a profile.ini.
Can be accessed like a normal array'''
def __init__(self, ini_file, appini):
self.profiles = []
parser = ConfigParser()
parser.read(ini_file)
profile_folder = os.path.dirname(ini_file)
for section in parser.sections():
if section == "General": continue
if not parser.has_option(section, "Path"): continue
path = parser.get(section, "Path")
name = parser.get(section, "Name")
is_default = True if parser.has_option(section, "Default") and parser.getint(section, "Default") == 1 else False
self.profiles.append(Profile(section, name, os.path.join(profile_folder, path), is_default, appini))
# No "Default" entry when there is one profile
if len(self) == 1: self[0].default = True
def __getitem__(self, key):
if key > len(self) - 1:
raise IndexError
return self.profiles[key]
def __iter__(self):
class ProfilesIter:
def __init__(self, profiles):
self.profiles = profiles
self.index = 0
def __next__(self):
if self.index == len(self.profiles):
raise StopIteration
res = self.profiles[self.index]
self.index += 1
return res
def next(self):
return self.__next__()
return ProfilesIter(self)
def __len__(self):
return len(self.profiles)
def dump_profile_summaries(self):
res = ''
for profile in self:
running = " (In use)" if profile.running == True else ""
default = " (Default)" if profile.default else ""
outdated = " (Out of date)" if not profile.current else ""
res += "%s%s - LastVersion=%s/%s%s%s\n" % (profile.id, default, profile.last_version, profile.last_buildid, running, outdated)
return res
def recent_kernlog(pattern):
'''Extract recent messages from kern.log or message which match a regex.
pattern should be a "re" object. '''
lines = ''
if os.path.exists('/var/log/kern.log'):
file = '/var/log/kern.log'
elif os.path.exists('/var/log/messages'):
file = '/var/log/messages'
else:
return lines
for line in open(file):
if pattern.search(line):
lines += line
return lines
def recent_auditlog(pattern):
'''Extract recent messages from kern.log or message which match a regex.
pattern should be a "re" object. '''
lines = ''
if os.path.exists('/var/log/audit/audit.log'):
file = '/var/log/audit/audit.log'
else:
return lines
for line in open(file):
if pattern.search(line):
lines += line
return lines
def add_info(report, ui):
'''Entry point for apport'''
def populate_item(key, data):
if data != None and data.strip() != '':
report[key] = data
def append_tag(tag):
tags = report.get('Tags', '')
if tags:
tags += ' '
report['Tags'] = tags + tag
ddproc = Popen(['dpkg-divert', '--truename', '/usr/bin/@MOZ_APP_NAME@'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
truename = ddproc.communicate()
if ddproc.returncode == 0 and truename[0].strip() != '/usr/bin/@MOZ_APP_NAME@':
ddproc = Popen(['dpkg-divert', '--listpackage', '/usr/bin/@MOZ_APP_NAME@'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
diverter = ddproc.communicate()
report['UnreportableReason'] = "/usr/bin/@MOZ_APP_NAME@ has been diverted by a third party package (%s)" % diverter[0].strip()
return
conf_dir = os.path.join(os.environ["HOME"], ".mozilla", "@MOZ_APP_NAME@")
appini = AppINIParser('/@MOZ_LIBDIR@')
populate_item("BuildID", appini.buildid)
profiles = Profiles(os.path.join(conf_dir, "profiles.ini"), appini)
populate_item("Profiles", profiles.dump_profile_summaries())
if len(profiles) == 0: report["NoProfiles"] = 'True'
for profile in profiles:
if profile.running and not profile.current:
report["UnreportableReason"] = "Firefox has been upgraded since you started it. Please restart all instances of Firefox and try again"
return
seen_default = False
running_incompatible_addons = False
forced_layers_accel = False
addon_compat_check_disabled = False
for profile in profiles:
if profile.default and not seen_default and len(profiles) > 1:
prefix = 'DefaultProfile'
seen_default = True
elif len(profiles) > 1:
prefix = profile.id
else:
prefix = ''
populate_item(prefix + "Extensions", profile.dump_extensions())
populate_item(prefix + "Locales", profile.dump_locales())
populate_item(prefix + "Themes", profile.dump_themes())
populate_item(prefix + "Plugins", profile.dump_plugins())
populate_item(prefix + "IncompatibleExtensions", profile.dump_active_but_incompatible_extensions())
populate_item(prefix + "Prefs", profile.dump_prefs())
populate_item(prefix + "PrefSources", profile.dump_pref_sources())
populate_item(prefix + "PrefErrors", profile.dump_pref_errors())
populate_item(prefix + "BrokenPermissions", profile.dump_files_with_broken_permissions())
if (profile.current or profile.default) and profile.has_active_but_incompatible_extensions:
running_incompatible_addons = True
if (profile.current or profile.default) and profile.has_forced_layers_acceleration:
forced_layers_accel = True
if (profile.current or profile.default) and profile.addon_compat_check_disabled:
addon_compat_check_disabled = True
crash_reports = []
report_to_mtime = {}
most_recent_report = None
most_recent_mtime = 0
for crash in glob(os.path.join(conf_dir, 'Crash Reports', 'submitted', '*.txt')):
id = re.sub(r'\.txt$', '', os.path.basename(crash))
report_to_mtime[id] = os.stat(crash).st_mtime
crash_reports.append(id)
if most_recent_report == None or report_to_mtime[id] > most_recent_mtime:
most_recent_report = id
most_recent_mtime = report_to_mtime[id]
def crashes_sort(a, b):
if report_to_mtime[b] > report_to_mtime[a]:
return 1
elif report_to_mtime[b] < report_to_mtime[a]:
return -1
else:
return 0
# Put the most recent first
crash_reports.sort(key=functools.cmp_to_key(crashes_sort))
crash_reports_str = ''
i = 0
for crash in crash_reports:
crash_reports_str += crash + '\n'
i += 1
if i == 15: break
populate_item('SubmittedCrashIDs', crash_reports_str)
populate_item('MostRecentCrashID', most_recent_report)
plugin_packages = []
for profile in profiles:
profile.get_plugin_packages(plugin_packages)
if len(plugin_packages) > 0: attach_related_packages(report, plugin_packages)
report["RunningIncompatibleAddons"] = 'True' if running_incompatible_addons == True else 'False'
report["ForcedLayersAccel"] = 'True' if forced_layers_accel == True else 'False'
report["AddonCompatCheckDisabled"] = 'True' if addon_compat_check_disabled == True else 'False'
if '@MOZ_APP_NAME@' == 'firefox-trunk':
report["Channel"] = 'nightly'
append_tag('nightly-channel')
if report["SourcePackage"] == 'firefox-trunk':
report["SourcePackage"] = 'firefox'
else:
channelpref = Prefs(None, ['/@MOZ_LIBDIR@/defaults/pref/channel-prefs.js'], whitelist = [ r'app\.update\.channel' ])
if "app.update.channel" in channelpref:
report["Channel"] = channelpref["app.update.channel"].value
append_tag(channelpref["app.update.channel"].value + '-channel')
else:
report["Channel"] = 'Unavailable'
if os.path.exists('/sys/bus/pci'):
report['Lspci'] = command_output(['lspci','-vvnn'])
attach_alsa(report)
attach_network(report)
# Get apparmor stuff if the profile isn't disabled. copied from
# source_apparmor.py until apport runs hooks via attach_related_packages
apparmor_disable_dir = "/etc/apparmor.d/disable"
add_apparmor = True
if os.path.isdir(apparmor_disable_dir):
for f in os.listdir(apparmor_disable_dir):
if f.startswith("usr.bin.@MOZ_PKG_NAME@"):
add_apparmor = False
break
if add_apparmor:
attach_related_packages(report, ['apparmor', 'libapparmor1',
'libapparmor-perl', 'apparmor-utils', 'auditd', 'libaudit0'])
attach_file(report, '/proc/version_signature', 'ProcVersionSignature')
attach_file(report, '/proc/cmdline', 'ProcCmdline')
sec_re = re.compile('audit\(|apparmor|selinux|security', re.IGNORECASE)
report['KernLog'] = recent_kernlog(sec_re)
if os.path.exists("/var/log/audit"):
# this needs to be run as root
report['AuditLog'] = recent_auditlog(sec_re)
if __name__ == "__main__":
import apport
from apport import packaging
D = {}
D['Package'] = '@MOZ_PKG_NAME@'
D['SourcePackage'] = '@MOZ_PKG_NAME@'
add_info(D, None)
for KEY in D.keys():
print('''-------------------%s: ------------------\n%s''' % (KEY, D[KEY]))