1483 lines
53 KiB
Python
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]))
|