1# 2# Gramps - a GTK+/GNOME based genealogy program 3# 4# Copyright (C) 2009 B. Malengier 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program; if not, write to the Free Software 18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19# 20 21""" 22General utility functions useful for the generic plugin system 23""" 24 25#------------------------------------------------------------------------- 26# 27# Standard Python modules 28# 29#------------------------------------------------------------------------- 30import sys 31import os 32import datetime 33from io import StringIO, BytesIO 34 35#------------------------------------------------------------------------- 36# 37# set up logging 38# 39#------------------------------------------------------------------------- 40import logging 41LOG = logging.getLogger(".gen.plug") 42 43#------------------------------------------------------------------------- 44# 45# Gramps modules 46# 47#------------------------------------------------------------------------- 48from ._pluginreg import make_environment 49from ..const import USER_PLUGINS 50from ...version import VERSION_TUPLE 51from . import BasePluginManager 52from ..utils.configmanager import safe_eval 53from ..config import config 54from ..constfunc import mac 55from ..const import GRAMPS_LOCALE as glocale 56_ = glocale.translation.sgettext 57 58#------------------------------------------------------------------------- 59# 60# Local utility functions for gen.plug 61# 62#------------------------------------------------------------------------- 63def version_str_to_tup(sversion, positions): 64 """ 65 Given a string version and positions count, returns a tuple of 66 integers. 67 68 >>> version_str_to_tup("1.02.9", 2) 69 (1, 2) 70 """ 71 try: 72 tup = tuple(map(int, sversion.split("."))) 73 tup += (0,) * (positions - len(tup)) 74 except: 75 tup = (0,) * positions 76 return tup[:positions] 77 78class newplugin: 79 """ 80 Fake newplugin. 81 """ 82 def __init__(self): 83 globals()["register_results"].append({}) 84 def __setattr__(self, attr, value): 85 globals()["register_results"][-1][attr] = value 86 87def register(ptype, **kwargs): 88 """ 89 Fake registration. Side-effect sets register_results to kwargs. 90 """ 91 retval = {"ptype": ptype} 92 retval.update(kwargs) 93 # Get the results back to calling function 94 if "register_results" in globals(): 95 globals()["register_results"].append(retval) 96 else: 97 globals()["register_results"] = [retval] 98 99class Zipfile: 100 """ 101 Class to duplicate the methods of tarfile.TarFile, for Python 2.5. 102 """ 103 def __init__(self, buffer): 104 import zipfile 105 self.buffer = buffer 106 self.zip_obj = zipfile.ZipFile(buffer) 107 108 def extractall(self, path, members=None): 109 """ 110 Extract all of the files in the zip into path. 111 """ 112 names = self.zip_obj.namelist() 113 for name in self.get_paths(names): 114 fullname = os.path.join(path, name) 115 if not os.path.exists(fullname): 116 os.mkdir(fullname) 117 for name in self.get_files(names): 118 fullname = os.path.join(path, name) 119 outfile = file(fullname, 'wb') 120 outfile.write(self.zip_obj.read(name)) 121 outfile.close() 122 123 def extractfile(self, name): 124 """ 125 Extract a name from the zip file. 126 127 >>> Zipfile(buffer).extractfile("Dir/dile.py").read() 128 <Contents> 129 """ 130 class ExtractFile: 131 def __init__(self, zip_obj, name): 132 self.zip_obj = zip_obj 133 self.name = name 134 def read(self): 135 data = self.zip_obj.read(self.name) 136 del self.zip_obj 137 return data 138 return ExtractFile(self.zip_obj, name) 139 140 def close(self): 141 """ 142 Close the zip object. 143 """ 144 self.zip_obj.close() 145 146 def getnames(self): 147 """ 148 Get the files and directories of the zipfile. 149 """ 150 return self.zip_obj.namelist() 151 152 def get_paths(self, items): 153 """ 154 Get the directories from the items. 155 """ 156 return (name for name in items if self.is_path(name) and not self.is_file(name)) 157 158 def get_files(self, items): 159 """ 160 Get the files from the items. 161 """ 162 return (name for name in items if self.is_file(name)) 163 164 def is_path(self, name): 165 """ 166 Is the name a path? 167 """ 168 return os.path.split(name)[0] 169 170 def is_file(self, name): 171 """ 172 Is the name a directory? 173 """ 174 return os.path.split(name)[1] 175 176def urlopen_maybe_no_check_cert(URL): 177 """ 178 Similar to urllib.request.urlopen, but disables certificate 179 verification on Mac. 180 """ 181 context = None 182 from urllib.request import urlopen 183 if mac(): 184 from ssl import create_default_context, CERT_NONE 185 context = create_default_context() 186 context.check_hostname = False 187 context.verify_mode = CERT_NONE 188 timeout = 10 # seconds 189 fp = None 190 try: 191 fp = urlopen(URL, timeout=timeout, context=context) 192 except TypeError: 193 fp = urlopen(URL, timeout=timeout) 194 return fp 195 196def available_updates(): 197 whattypes = config.get('behavior.check-for-addon-update-types') 198 199 LOG.debug("Checking for updated addons...") 200 langs = glocale.get_language_list() 201 langs.append("en") 202 # now we have a list of languages to try: 203 fp = None 204 for lang in langs: 205 URL = ("%s/listings/addons-%s.txt" % 206 (config.get("behavior.addons-url"), lang)) 207 LOG.debug(" trying: %s" % URL) 208 try: 209 fp = urlopen_maybe_no_check_cert(URL) 210 except: 211 try: 212 URL = ("%s/listings/addons-%s.txt" % 213 (config.get("behavior.addons-url"), lang[:2])) 214 fp = urlopen_maybe_no_check_cert(URL) 215 except Exception as err: # some error 216 LOG.warning("Failed to open addon metadata for {lang} {url}: {err}". 217 format(lang=lang, url=URL, err=err)) 218 fp = None 219 if fp and fp.getcode() == 200: # ok 220 break 221 222 pmgr = BasePluginManager.get_instance() 223 addon_update_list = [] 224 if fp and fp.getcode() == 200: 225 lines = list(fp.readlines()) 226 count = 0 227 for line in lines: 228 line = line.decode('utf-8') 229 try: 230 plugin_dict = safe_eval(line) 231 if type(plugin_dict) != type({}): 232 raise TypeError("Line with addon metadata is not " 233 "a dictionary") 234 except: 235 LOG.warning("Skipped a line in the addon listing: " + 236 str(line)) 237 continue 238 id = plugin_dict["i"] 239 plugin = pmgr.get_plugin(id) 240 if plugin: 241 LOG.debug("Comparing %s > %s" % 242 (version_str_to_tup(plugin_dict["v"], 3), 243 version_str_to_tup(plugin.version, 3))) 244 if (version_str_to_tup(plugin_dict["v"], 3) > 245 version_str_to_tup(plugin.version, 3)): 246 LOG.debug(" Downloading '%s'..." % plugin_dict["z"]) 247 if "update" in whattypes: 248 if (not config.get('behavior.do-not-show-previously-seen-addon-updates') or 249 plugin_dict["i"] not in config.get('behavior.previously-seen-addon-updates')): 250 addon_update_list.append((_("Updated"), 251 "%s/download/%s" % 252 (config.get("behavior.addons-url"), 253 plugin_dict["z"]), 254 plugin_dict)) 255 else: 256 LOG.debug(" '%s' is ok" % plugin_dict["n"]) 257 else: 258 LOG.debug(" '%s' is not installed" % plugin_dict["n"]) 259 if "new" in whattypes: 260 if (not config.get('behavior.do-not-show-previously-seen-addon-updates') or 261 plugin_dict["i"] not in config.get('behavior.previously-seen-addon-updates')): 262 addon_update_list.append((_("updates|New"), 263 "%s/download/%s" % 264 (config.get("behavior.addons-url"), 265 plugin_dict["z"]), 266 plugin_dict)) 267 config.set("behavior.last-check-for-addon-updates", 268 datetime.date.today().strftime("%Y/%m/%d")) 269 count += 1 270 if fp: 271 fp.close() 272 else: 273 LOG.debug("Checking Addons Failed") 274 LOG.debug("Done checking!") 275 276 return addon_update_list 277 278def load_addon_file(path, callback=None): 279 """ 280 Load an addon from a particular path (from URL or file system). 281 """ 282 from urllib.request import urlopen 283 import tarfile 284 if (path.startswith("http://") or 285 path.startswith("https://") or 286 path.startswith("ftp://")): 287 try: 288 fp = urlopen_maybe_no_check_cert(path) 289 except: 290 if callback: 291 callback(_("Unable to open '%s'") % path) 292 return False 293 else: 294 try: 295 fp = open(path) 296 except: 297 if callback: 298 callback(_("Unable to open '%s'") % path) 299 return False 300 try: 301 content = fp.read() 302 buffer = BytesIO(content) 303 except: 304 if callback: 305 callback(_("Error in reading '%s'") % path) 306 return False 307 fp.close() 308 # file_obj is either Zipfile or TarFile 309 if path.endswith(".zip") or path.endswith(".ZIP"): 310 file_obj = Zipfile(buffer) 311 elif path.endswith(".tar.gz") or path.endswith(".tgz"): 312 try: 313 file_obj = tarfile.open(None, fileobj=buffer) 314 except: 315 if callback: 316 callback(_("Error: cannot open '%s'") % path) 317 return False 318 else: 319 if callback: 320 callback(_("Error: unknown file type: '%s'") % path) 321 return False 322 # First, see what versions we have/are getting: 323 good_gpr = set() 324 for gpr_file in [name for name in file_obj.getnames() if name.endswith(".gpr.py")]: 325 if callback: 326 callback((_("Examining '%s'...") % gpr_file) + "\n") 327 contents = file_obj.extractfile(gpr_file).read() 328 # Put a fake register and _ function in environment: 329 env = make_environment(register=register, 330 newplugin=newplugin, 331 _=lambda text: text) 332 # clear out the result variable: 333 globals()["register_results"] = [] 334 # evaluate the contents: 335 try: 336 exec(contents, env) 337 except Exception as exp: 338 if callback: 339 msg = _("Error in '%s' file: cannot load.") % gpr_file 340 callback(" " + msg + "\n" + str(exp)) 341 continue 342 # There can be multiple addons per gpr file: 343 for results in globals()["register_results"]: 344 gramps_target_version = results.get("gramps_target_version", None) 345 id = results.get("id", None) 346 if gramps_target_version: 347 vtup = version_str_to_tup(gramps_target_version, 2) 348 # Is it for the right version of gramps? 349 if vtup == VERSION_TUPLE[0:2]: 350 # If this version is not installed, or > installed, install it 351 good_gpr.add(gpr_file) 352 if callback: 353 callback(" " + (_("'%s' is for this version of Gramps.") % id) + "\n") 354 else: 355 # If the plugin is for another version; inform and do nothing 356 if callback: 357 callback(" " + (_("'%s' is NOT for this version of Gramps.") % id) + "\n") 358 callback(" " + (_("It is for version %(v1)d.%(v2)d") % { 359 'v1': vtup[0], 360 'v2': vtup[1]} 361 + "\n")) 362 continue 363 else: 364 # another register function doesn't have gramps_target_version 365 if gpr_file in good_gpr: 366 s.remove(gpr_file) 367 if callback: 368 callback(" " + (_("Error: missing gramps_target_version in '%s'...") % gpr_file) + "\n") 369 registered_count = 0 370 if len(good_gpr) > 0: 371 # Now, install the ok ones 372 try: 373 file_obj.extractall(USER_PLUGINS) 374 except OSError: 375 if callback: 376 callback("OSError installing '%s', skipped!" % path) 377 file_obj.close() 378 return False 379 if callback: 380 callback((_("Installing '%s'...") % path) + "\n") 381 gpr_files = set([os.path.split(os.path.join(USER_PLUGINS, name))[0] 382 for name in good_gpr]) 383 for gpr_file in gpr_files: 384 if callback: 385 callback(" " + (_("Registered '%s'") % gpr_file) + "\n") 386 registered_count += 1 387 file_obj.close() 388 if registered_count: 389 return True 390 else: 391 return False 392 393#------------------------------------------------------------------------- 394# 395# OpenFileOrStdout class 396# 397#------------------------------------------------------------------------- 398class OpenFileOrStdout: 399 """Context manager to open a file or stdout for writing.""" 400 def __init__(self, filename, encoding=None, errors=None, newline=None): 401 self.filename = filename 402 self.filehandle = None 403 self.encoding = encoding 404 self.errors = errors 405 self.newline = newline 406 407 def __enter__(self): 408 if self.filename == '-': 409 self.filehandle = sys.stdout 410 else: 411 self.filehandle = open(self.filename, 'w', encoding=self.encoding, 412 errors=self.errors, newline=self.newline) 413 return self.filehandle 414 415 def __exit__(self, exc_type, exc_value, traceback): 416 if self.filehandle and self.filename != '-': 417 self.filehandle.close() 418 return False 419 420#------------------------------------------------------------------------- 421# 422# OpenFileOrStdin class 423# 424#------------------------------------------------------------------------- 425class OpenFileOrStdin: 426 """Context manager to open a file or stdin for reading.""" 427 def __init__(self, filename, add_mode='', encoding=None): 428 self.filename = filename 429 self.mode = 'r%s' % add_mode 430 self.filehandle = None 431 self.encoding = encoding 432 433 def __enter__(self): 434 if self.filename == '-': 435 self.filehandle = sys.stdin 436 elif self.encoding: 437 self.filehandle = open(self.filename, self.mode, encoding=self.encoding) 438 else: 439 self.filehandle = open(self.filename, self.mode) 440 return self.filehandle 441 442 def __exit__(self, exc_type, exc_value, traceback): 443 if self.filename != '-': 444 self.filehandle.close() 445 return False 446