From cbbd367c00a29acbc5d30f351d4a0cba8afd9e67 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 28 Dec 2010 11:48:31 +0200 Subject: [PATCH 01/41] base_crypt: write query with %s bzr revid: p_christ@hol.gr-20101228094831-ci5zz12y37miby0g --- addons/base_crypt/crypt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/base_crypt/crypt.py b/addons/base_crypt/crypt.py index 2d29704aeb0..839ebafb498 100755 --- a/addons/base_crypt/crypt.py +++ b/addons/base_crypt/crypt.py @@ -178,7 +178,7 @@ def check(db, uid, passwd): return True cr = pooler.get_db(db).cursor() if passwd not in _salt_cache: - cr.execute( 'select login from res_users where id=%d', (uid,) ) + cr.execute( 'select login from res_users where id=%s', (uid,) ) stored_login = cr.fetchone() if stored_login: stored_login = stored_login[0] @@ -186,7 +186,7 @@ def check(db, uid, passwd): if not login(db,stored_login,passwd): return False salt = _salt_cache[passwd] - cr.execute(' select count(*) from res_users where id=%d and password=%s', (int(uid), encrypt_md5( passwd, salt )) ) + cr.execute(' select count(*) from res_users where id=%s and password=%s', (int(uid), encrypt_md5( passwd, salt )) ) res = cr.fetchone()[0] cr.close() if not bool(res): From 85f7655cfe1a7de1dc1991c4b61a1916a643d242 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 28 Dec 2010 11:48:31 +0200 Subject: [PATCH 02/41] Base_crypt: only use latin letters, not intl ones. bzr revid: p_christ@hol.gr-20101228094831-dzypbxuut66t0cnc --- addons/base_crypt/crypt.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/addons/base_crypt/crypt.py b/addons/base_crypt/crypt.py index 839ebafb498..b52206e80a8 100755 --- a/addons/base_crypt/crypt.py +++ b/addons/base_crypt/crypt.py @@ -37,7 +37,7 @@ # USA. from random import seed, sample -from string import letters, digits +from string import ascii_letters, digits from osv import fields,osv import pooler import tools @@ -46,7 +46,7 @@ from service import security magic_md5 = '$1$' -def gen_salt( length=8, symbols=letters + digits ): +def gen_salt( length=8, symbols=ascii_letters + digits ): seed() return ''.join( sample( symbols, length ) ) @@ -66,6 +66,7 @@ def gen_salt( length=8, symbols=letters + digits ): # * # * Poul-Henning Kamp +#TODO: py>=2.6: from hashlib import md5 import md5 def encrypt_md5( raw_pw, salt, magic=magic_md5 ): From 6846791c64da3d42cdf1a976059df8ebf42ef855 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 28 Dec 2010 11:48:31 +0200 Subject: [PATCH 03/41] Base_crypt: adapt to security fix of server bzr revid: p_christ@hol.gr-20101228094831-z6c334w8b15v2xfq --- addons/base_crypt/crypt.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/base_crypt/crypt.py b/addons/base_crypt/crypt.py index b52206e80a8..8510f336dee 100755 --- a/addons/base_crypt/crypt.py +++ b/addons/base_crypt/crypt.py @@ -174,8 +174,8 @@ def login(db, login, password): # else: # raise Exception('AccessDenied') -def check(db, uid, passwd): - if security._uid_cache.has_key( uid ) and (security._uid_cache[uid]==passwd): +def check_creds(db, uid, passwd): + if security._uid_cache.get(db, {}).get(uid) and security._uid_cache.get(db, {}).get(uid) == passwd: return True cr = pooler.get_db(db).cursor() if passwd not in _salt_cache: @@ -191,7 +191,7 @@ def check(db, uid, passwd): res = cr.fetchone()[0] cr.close() if not bool(res): - raise Exception('AccessDenied') + raise security.ExceptionNoTb('AccessDenied') if res: security._uid_cache[uid] = passwd return bool(res) @@ -204,14 +204,14 @@ def access(db, uid, passwd, sec_level, ids): res = cr.fetchone() cr.close() if not res: - raise Exception('Bad username or password') + raise security.ExceptionNoTb('Bad username or password') return res[0] # check if module is installed or not security.login=login #security.check_super=check_super security.access=access -security.check=check +security.check_creds=check_creds class users(osv.osv): _name="res.users" From 04a4cf4a321ee59054ee6a6973fb6774bfc18218 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 28 Dec 2010 11:48:31 +0200 Subject: [PATCH 04/41] Base_crypt: fix password check, again. bzr revid: p_christ@hol.gr-20101228094831-c8hvqdl34ircxhr3 --- addons/base_crypt/crypt.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/addons/base_crypt/crypt.py b/addons/base_crypt/crypt.py index 8510f336dee..455885534aa 100755 --- a/addons/base_crypt/crypt.py +++ b/addons/base_crypt/crypt.py @@ -174,9 +174,11 @@ def login(db, login, password): # else: # raise Exception('AccessDenied') -def check_creds(db, uid, passwd): - if security._uid_cache.get(db, {}).get(uid) and security._uid_cache.get(db, {}).get(uid) == passwd: +def check(db, uid, passwd): + cached_pass = security._uid_cache.get(db, {}).get(uid) + if (cached_pass is not None) and cached_pass == passwd: return True + cr = pooler.get_db(db).cursor() if passwd not in _salt_cache: cr.execute( 'select login from res_users where id=%s', (uid,) ) @@ -185,7 +187,7 @@ def check_creds(db, uid, passwd): stored_login = stored_login[0] if not login(db,stored_login,passwd): - return False + return security.ExceptionNoTb('AccessDenied') salt = _salt_cache[passwd] cr.execute(' select count(*) from res_users where id=%s and password=%s', (int(uid), encrypt_md5( passwd, salt )) ) res = cr.fetchone()[0] @@ -211,7 +213,7 @@ def access(db, uid, passwd, sec_level, ids): security.login=login #security.check_super=check_super security.access=access -security.check_creds=check_creds +security.check=check class users(osv.osv): _name="res.users" From a5c654f0989ec9a5481a2bd794fc85a06877edb1 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 28 Dec 2010 11:48:31 +0200 Subject: [PATCH 05/41] Base crypt: fix, again. We must raise Exc. not return. bzr revid: p_christ@hol.gr-20101228094831-a543jrneb9znpvnz --- addons/base_crypt/crypt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/base_crypt/crypt.py b/addons/base_crypt/crypt.py index 455885534aa..46c8e2f8695 100755 --- a/addons/base_crypt/crypt.py +++ b/addons/base_crypt/crypt.py @@ -187,7 +187,7 @@ def check(db, uid, passwd): stored_login = stored_login[0] if not login(db,stored_login,passwd): - return security.ExceptionNoTb('AccessDenied') + raise security.ExceptionNoTb('AccessDenied') salt = _salt_cache[passwd] cr.execute(' select count(*) from res_users where id=%s and password=%s', (int(uid), encrypt_md5( passwd, salt )) ) res = cr.fetchone()[0] From 78588c19c051603c13219f899039aba7c19d2bc6 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 28 Dec 2010 11:48:32 +0200 Subject: [PATCH 06/41] Cleanup all el translations, move from el_GR. bzr revid: p_christ@hol.gr-20101228094832-oi357v8ef0bo9fzz --- addons/base_crypt/i18n/el.po | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 addons/base_crypt/i18n/el.po diff --git a/addons/base_crypt/i18n/el.po b/addons/base_crypt/i18n/el.po new file mode 100644 index 00000000000..b9aaac39501 --- /dev/null +++ b/addons/base_crypt/i18n/el.po @@ -0,0 +1,27 @@ +# Translation of OpenERP Server. +# This file contains the translation of the following modules: +# * base_crypt +# +# Copyright (C) 2008,2009 P. Christeas +# <> <>, 2009. +msgid "" +msgstr "" +"Project-Id-Version: OpenERP Server 5.0.0\n" +"Report-Msgid-Bugs-To: support@openerp.com\n" +"POT-Creation-Date: 2009-05-30 15:14:08+0000\n" +"PO-Revision-Date: 2009-03-27 14:30+0200\n" +"Last-Translator: <> <>\n" +"Language-Team: <>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#~ msgid "Module for password encryption." +#~ msgstr "Άρθρωμα για κρυπτογράφηση κωδικών." + +#~ msgid "Invalid XML for View Architecture!" +#~ msgstr "Άκυρο XML για αρχιτεκτονική προβολής!" + +#~ msgid "Password" +#~ msgstr "Κωδικός" From bb5680dcfde29ba864974525d00edb18b1626a03 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 28 Dec 2010 11:52:10 +0200 Subject: [PATCH 07/41] Base crypt: adapt to new server API Now, login() and check() lie in res.users class. bzr revid: p_christ@hol.gr-20101228095210-k0ih0a2bibgloc7v --- addons/base_crypt/__terp__.py | 1 - addons/base_crypt/crypt.py | 166 ++++++++++++++++------------------ 2 files changed, 79 insertions(+), 88 deletions(-) diff --git a/addons/base_crypt/__terp__.py b/addons/base_crypt/__terp__.py index 400a277ca81..7356ab4eb76 100755 --- a/addons/base_crypt/__terp__.py +++ b/addons/base_crypt/__terp__.py @@ -29,7 +29,6 @@ "init_xml" : [], "demo_xml" : [], "update_xml" : [ - "base_update.xml", ], "active": False, "installable": True, diff --git a/addons/base_crypt/crypt.py b/addons/base_crypt/crypt.py index 46c8e2f8695..a8c80aa1b2c 100755 --- a/addons/base_crypt/crypt.py +++ b/addons/base_crypt/crypt.py @@ -126,46 +126,6 @@ def encrypt_md5( raw_pw, salt, magic=magic_md5 ): _salt_cache = {} -def login(db, login, password): - cr = pooler.get_db(db).cursor() - cr.execute( 'select password from res_users where login=%s', (login.encode( 'utf-8' ),) ) - stored_pw = cr.fetchone() - - if stored_pw: - stored_pw = stored_pw[0] - else: - # Return early if no one has a login name like that. - return False - - # Calculate a new password ('updated_pw') from 'stored_pw' if the - # latter isn't encrypted yet. Use that to update the database entry. - # Also update the 'stored_pw' to reflect the change. - - if stored_pw[0:3] != magic_md5: - updated_pw = encrypt_md5( stored_pw, gen_salt() ) - cr.execute( 'update res_users set password=%s where login=%s', (updated_pw.encode( 'utf-8' ), login.encode( 'utf-8' ),) ) - cr.commit() - - cr.execute( 'select password from res_users where login=%s', (login.encode( 'utf-8' ),) ) - stored_pw = cr.fetchone()[0] - - # Calculate an encrypted password from the user-provided - # password ('encrypted_pw'). - - salt = _salt_cache[password] = stored_pw[3:11] - encrypted_pw = encrypt_md5( password, salt ) - - # Retrieve a user id from the database, factoring in an encrypted - # password. - - cr.execute('select id from res_users where login=%s and password=%s and active', (login.encode('utf-8'), encrypted_pw.encode('utf-8'))) - res = cr.fetchone() - cr.close() - - if res: - return res[0] - else: - return False #def check_super(passwd): # salt = _salt_cache[passwd] @@ -174,52 +134,89 @@ def login(db, login, password): # else: # raise Exception('AccessDenied') -def check(db, uid, passwd): - cached_pass = security._uid_cache.get(db, {}).get(uid) - if (cached_pass is not None) and cached_pass == passwd: - return True - - cr = pooler.get_db(db).cursor() - if passwd not in _salt_cache: - cr.execute( 'select login from res_users where id=%s', (uid,) ) - stored_login = cr.fetchone() - if stored_login: - stored_login = stored_login[0] - - if not login(db,stored_login,passwd): - raise security.ExceptionNoTb('AccessDenied') - salt = _salt_cache[passwd] - cr.execute(' select count(*) from res_users where id=%s and password=%s', (int(uid), encrypt_md5( passwd, salt )) ) - res = cr.fetchone()[0] - cr.close() - if not bool(res): - raise security.ExceptionNoTb('AccessDenied') - if res: - security._uid_cache[uid] = passwd - return bool(res) - - -def access(db, uid, passwd, sec_level, ids): - cr = pooler.get_db(db).cursor() - salt = _salt_cache[passwd] - cr.execute('select id from res_users where id=%s and password=%s', (uid, encrypt_md5( passwd, salt )) ) - res = cr.fetchone() - cr.close() - if not res: - raise security.ExceptionNoTb('Bad username or password') - return res[0] - -# check if module is installed or not -security.login=login -#security.check_super=check_super -security.access=access -security.check=check class users(osv.osv): _name="res.users" _inherit="res.users" - # agi - 022108 - # Add handlers for 'input_pw' field. + + def login(self, db, login, password): + if not password: + return False + + cr = pooler.get_db(db).cursor() + cr.execute( 'SELECT password FROM res_users WHERE login=%s', (login.encode( 'utf-8' ),) ) + stored_pw = cr.fetchone() + + if stored_pw: + stored_pw = stored_pw[0] + else: + # Return early if no one has a login name like that. + return False + + # Calculate a new password ('updated_pw') from 'stored_pw' if the + # latter isn't encrypted yet. Use that to update the database entry. + # Also update the 'stored_pw' to reflect the change. + + if stored_pw[0:3] != magic_md5: + updated_pw = encrypt_md5( stored_pw, gen_salt() ) + cr.execute( 'UPDATE res_users SET password=%s WHERE login=%s', (updated_pw.encode( 'utf-8' ), login.encode( 'utf-8' ),) ) + cr.commit() + + cr.execute( 'SELECT password FROM res_users WHERE login=%s', (login.encode( 'utf-8' ),) ) + stored_pw = cr.fetchone()[0] + + # Calculate an encrypted password from the user-provided + # password ('encrypted_pw'). + + salt = _salt_cache[password] = stored_pw[3:11] + encrypted_pw = encrypt_md5( password, salt ) + + # Retrieve a user id from the database, factoring in an encrypted + # password. + + cr.execute('select id from res_users where login=%s and password=%s and active', (login.encode('utf-8'), encrypted_pw.encode('utf-8'))) + res = cr.fetchone() + cr.close() + + if res: + return res[0] + else: + return False + + def check(self, db, uid, passwd): + cached_pass = self._uid_cache.get(db, {}).get(uid) + if (cached_pass is not None) and cached_pass == passwd: + return True + + cr = pooler.get_db(db).cursor() + if passwd not in _salt_cache: + cr.execute( 'SELECT login FROM res_users WHERE id=%s', (uid,) ) + stored_login = cr.fetchone() + if stored_login: + stored_login = stored_login[0] + + if not self.login(db,stored_login,passwd): + raise security.ExceptionNoTb('AccessDenied') + salt = _salt_cache[passwd] + cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s', (int(uid), encrypt_md5( passwd, salt )) ) + res = cr.fetchone()[0] + cr.close() + if not bool(res): + raise security.ExceptionNoTb('AccessDenied') + if res: + self._uid_cache[uid] = passwd + return bool(res) + + + def access(self, db, uid, passwd, sec_level, ids): + cr = pooler.get_db(db).cursor() + salt = _salt_cache[passwd] + cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, encrypt_md5( passwd, salt )) ) + res = cr.fetchone() + cr.close() + if not res: + raise security.ExceptionNoTb('Bad username or password') + return res[0] def set_pw( self, cr, uid, id, name, value, args, context ): if not value: @@ -233,9 +230,4 @@ class users(osv.osv): res[id] = '' return res - # Continuing to original code. - - _columns = { - 'input_pw': fields.function( get_pw, fnct_inv=set_pw, type='char', method=True, size=20, string='Password', invisible=True), - } users() \ No newline at end of file From 72ad853a208dccfdbe3ce398196a6ff87507db8d Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 28 Dec 2010 11:52:10 +0200 Subject: [PATCH 09/41] Base_crypt: fix the login algorithm. bzr revid: p_christ@hol.gr-20101228095210-9tynwynx8cn2pgs2 --- addons/base_crypt/crypt.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/addons/base_crypt/crypt.py b/addons/base_crypt/crypt.py index a8c80aa1b2c..c71d0216794 100755 --- a/addons/base_crypt/crypt.py +++ b/addons/base_crypt/crypt.py @@ -140,8 +140,18 @@ class users(osv.osv): _inherit="res.users" def login(self, db, login, password): - if not password: - return False + try: + return self._login(db, login, password) + except Exception, e: + import logging + logging.getLogger('netsvc').exception('Could not authenticate') + return Exception('Access Denied') + + def _login(self, db, login, password): + if not password: + return False + if db is False: + raise RuntimeError("Cannot authenticate to False db!") cr = pooler.get_db(db).cursor() cr.execute( 'SELECT password FROM res_users WHERE login=%s', (login.encode( 'utf-8' ),) ) From b102ade21f740d008611203cd38a28a83e28c866 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 28 Dec 2010 11:52:10 +0200 Subject: [PATCH 10/41] base_crypt: Fix login(), close the cursor. bzr revid: p_christ@hol.gr-20101228095210-utld4macqsodsbx4 --- addons/base_crypt/crypt.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/addons/base_crypt/crypt.py b/addons/base_crypt/crypt.py index c71d0216794..7283711209f 100755 --- a/addons/base_crypt/crypt.py +++ b/addons/base_crypt/crypt.py @@ -140,20 +140,21 @@ class users(osv.osv): _inherit="res.users" def login(self, db, login, password): - try: - return self._login(db, login, password) - except Exception, e: - import logging - logging.getLogger('netsvc').exception('Could not authenticate') - return Exception('Access Denied') - - def _login(self, db, login, password): if not password: return False if db is False: raise RuntimeError("Cannot authenticate to False db!") + cr = None + try: + cr = pooler.get_db(db).cursor() + return self._login(cr, db, login, password) + except Exception, e: + if cr: cr.close() + import logging + logging.getLogger('netsvc').exception('Could not authenticate') + return Exception('Access Denied') - cr = pooler.get_db(db).cursor() + def _login(self, cr, db, login, password): cr.execute( 'SELECT password FROM res_users WHERE login=%s', (login.encode( 'utf-8' ),) ) stored_pw = cr.fetchone() From 774983d16d3afe3b4e22ce0f3cde3c34c66b7dec Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 28 Dec 2010 12:41:00 +0200 Subject: [PATCH 11/41] base_crypt: small changes, rearrange, to help merging with vmt's branch bzr revid: p_christ@hol.gr-20101228104100-0cl0veaor9iv8q5n --- .../{__terp__.py => __openerp__.py} | 5 +- addons/base_crypt/base_update.xml | 29 ------------ addons/base_crypt/crypt.py | 47 ++++++++----------- 3 files changed, 20 insertions(+), 61 deletions(-) rename addons/base_crypt/{__terp__.py => __openerp__.py} (94%) delete mode 100755 addons/base_crypt/base_update.xml diff --git a/addons/base_crypt/__terp__.py b/addons/base_crypt/__openerp__.py similarity index 94% rename from addons/base_crypt/__terp__.py rename to addons/base_crypt/__openerp__.py index 7356ab4eb76..fcc5fa3c60d 100755 --- a/addons/base_crypt/__terp__.py +++ b/addons/base_crypt/__openerp__.py @@ -26,10 +26,7 @@ "category" : "Generic Modules/Base", "description": "Module for password encryption.", "depends" : ["base"], - "init_xml" : [], - "demo_xml" : [], - "update_xml" : [ - ], + "data" : [], "active": False, "installable": True, } diff --git a/addons/base_crypt/base_update.xml b/addons/base_crypt/base_update.xml deleted file mode 100755 index 66e4eaeb8c7..00000000000 --- a/addons/base_crypt/base_update.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - res.users.form.modif.inherit - res.users - form - - - - - - - - - - res.users.form.inherit1 - res.users - form - - - - - - - - - diff --git a/addons/base_crypt/crypt.py b/addons/base_crypt/crypt.py index 7283711209f..20899b3013c 100755 --- a/addons/base_crypt/crypt.py +++ b/addons/base_crypt/crypt.py @@ -124,21 +124,22 @@ def encrypt_md5( raw_pw, salt, magic=magic_md5 ): return magic + salt + '$' + rearranged -_salt_cache = {} - - -#def check_super(passwd): -# salt = _salt_cache[passwd] -# if encrypt_md5( passwd, salt ) == tools.config['admin_passwd']: -# return True -# else: -# raise Exception('AccessDenied') - - class users(osv.osv): _name="res.users" _inherit="res.users" + def set_pw(self, cr, uid, id, name, value, args, context): + if not value: + raise osv.except_osv(_('Error'), _("Please specify the password !")) + self.write( cr, uid, id, { 'password' : encrypt_md5( value, gen_salt() ) } ) + del value + + def get_pw( self, cr, uid, ids, name, args, context ): + res = {} + for id in ids: + res[id] = '' + return res + def login(self, db, login, password): if not password: return False @@ -155,9 +156,10 @@ class users(osv.osv): return Exception('Access Denied') def _login(self, cr, db, login, password): - cr.execute( 'SELECT password FROM res_users WHERE login=%s', (login.encode( 'utf-8' ),) ) - stored_pw = cr.fetchone() - + cr.execute( 'SELECT password FROM res_users WHERE login=%s', + (login.encode('utf-8'),)) + stored_pw = id = cr.fetchone() + if stored_pw: stored_pw = stored_pw[0] else: @@ -177,7 +179,7 @@ class users(osv.osv): stored_pw = cr.fetchone()[0] # Calculate an encrypted password from the user-provided - # password ('encrypted_pw'). + # password. salt = _salt_cache[password] = stored_pw[3:11] encrypted_pw = encrypt_md5( password, salt ) @@ -209,7 +211,8 @@ class users(osv.osv): if not self.login(db,stored_login,passwd): raise security.ExceptionNoTb('AccessDenied') salt = _salt_cache[passwd] - cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s', (int(uid), encrypt_md5( passwd, salt )) ) + cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s', + (int(uid), encrypt_md5( passwd, salt )) ) res = cr.fetchone()[0] cr.close() if not bool(res): @@ -229,16 +232,4 @@ class users(osv.osv): raise security.ExceptionNoTb('Bad username or password') return res[0] - def set_pw( self, cr, uid, id, name, value, args, context ): - if not value: - raise osv.except_osv(_('Error'), _("Please specify the password !")) - self.write( cr, uid, id, { 'password' : encrypt_md5( value, gen_salt() ) } ) - del value - - def get_pw( self, cr, uid, ids, name, args, context ): - res = {} - for id in ids: - res[id] = '' - return res - users() \ No newline at end of file From 6fa81844d271c9e8e1fc032cdf3d9a694427f0e8 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 28 Dec 2010 15:26:37 +0200 Subject: [PATCH 12/41] base_crypt: improve. close cursors, use hashlib, update login date bzr revid: p_christ@hol.gr-20101228132637-oe86bgxujv17tiz2 --- addons/base_crypt/crypt.py | 79 ++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/addons/base_crypt/crypt.py b/addons/base_crypt/crypt.py index 510f4470d20..ae5f3a3fc55 100644 --- a/addons/base_crypt/crypt.py +++ b/addons/base_crypt/crypt.py @@ -66,11 +66,14 @@ def gen_salt( length=8, symbols=ascii_letters + digits ): # * Poul-Henning Kamp #TODO: py>=2.6: from hashlib import md5 -import md5 +import hashlib def encrypt_md5( raw_pw, salt, magic=magic_md5 ): - hash = md5.new( raw_pw + magic + salt ) - stretch = md5.new( raw_pw + salt + raw_pw).digest() + hash = hashlib.md5() + hash.update( raw_pw + magic + salt ) + st = hashlib.md5() + st.update( raw_pw + salt + raw_pw) + stretch = st.digest() for i in range( 0, len( raw_pw ) ): hash.update( stretch[i % 16] ) @@ -87,7 +90,7 @@ def encrypt_md5( raw_pw, salt, magic=magic_md5 ): saltedmd5 = hash.digest() for i in range( 1000 ): - hash = md5.new() + hash = hashlib.md5() if i & 1: hash.update( raw_pw ) @@ -174,10 +177,12 @@ class users(osv.osv): cr = pooler.get_db(db).cursor() return self._login(cr, db, login, password) except Exception: - if cr: cr.close() import logging logging.getLogger('netsvc').exception('Could not authenticate') return Exception('Access Denied') + finally: + if cr is not None: + cr.close() def _login(self, cr, db, login, password): cr.execute( 'SELECT password, id FROM res_users WHERE login=%s', @@ -190,6 +195,10 @@ class users(osv.osv): return False stored_pw = self.maybe_encrypt(cr, stored_pw, id) + + if not stored_pw: + # means couldn't encrypt or user is not active! + return False # Calculate an encrypted password from the user-provided # password. @@ -198,10 +207,11 @@ class users(osv.osv): encrypted_pw = encrypt_md5(password, salt) # Check if the encrypted password matches against the one in the db. - cr.execute('select id from res_users where id=%s and password=%s and active', + cr.execute('UPDATE res_users SET date=now() ' \ + 'WHERE id=%s AND password=%s AND active RETURNING id', (int(id), encrypted_pw.encode('utf-8'))) res = cr.fetchone() - cr.close() + cr.commit() if res: return res[0] @@ -220,20 +230,26 @@ class users(osv.osv): return True cr = pooler.get_db(db).cursor() - if uid not in self._salt_cache.get(db, {}): - cr.execute( 'SELECT login FROM res_users WHERE id=%s', (uid,) ) - stored_login = cr.fetchone() - if stored_login: - stored_login = stored_login[0] - - if not self.login(db,stored_login,passwd): - raise security.ExceptionNoTb('AccessDenied') + try: + if uid not in self._salt_cache.get(db, {}): + # If we don't have cache, we have to repeat the procedure + # through the login function. + cr.execute( 'SELECT login FROM res_users WHERE id=%s', (uid,) ) + stored_login = cr.fetchone() + if stored_login: + stored_login = stored_login[0] + + res = self._login(cr, db, stored_login, passwd) + if not res: + raise security.ExceptionNoTb('AccessDenied') + else: + salt = self._salt_cache[db][uid] + cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s', + (int(uid), encrypt_md5(passwd, salt))) + res = cr.fetchone()[0] + finally: + cr.close() - salt = self._salt_cache[db][uid] - cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s', - (int(uid), encrypt_md5(passwd, salt))) - res = cr.fetchone()[0] - cr.close() if not bool(res): raise security.ExceptionNoTb('AccessDenied') @@ -246,18 +262,23 @@ class users(osv.osv): return bool(res) def maybe_encrypt(self, cr, pw, id): - # If the password 'pw' is not encrypted, then encrypt all passwords - # in the db. Returns the (possibly newly) encrypted password for 'id'. + """ Return the password 'pw', making sure it is encrypted. + + If the password 'pw' is not encrypted, then encrypt all active passwords + in the db. Returns the (possibly newly) encrypted password for 'id'. + """ - if pw[0:len(magic_md5)] != magic_md5: - cr.execute('select id, password from res_users') + if not pw.startswith(magic_md5): + encrypted_res = False + cr.execute("SELECT id, password FROM res_users " \ + "WHERE active=true AND password NOT LIKE '$%'") + # Note that we skip all passwords like $.., in anticipation for + # more than md5 magic prefixes. res = cr.fetchall() for i, p in res: - encrypted = p - if p[0:len(magic_md5)] != magic_md5: - encrypted = encrypt_md5(p, gen_salt()) - cr.execute('update res_users set password=%s where id=%s', - (encrypted.encode('utf-8'), int(i))) + encrypted = encrypt_md5(p, gen_salt()) + cr.execute('UPDATE res_users SET password=%s where id=%s', + (encrypted, i)) if i == id: encrypted_res = encrypted cr.commit() From 545f5421c3f80d6dd5816006ce2155d599e23cae Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 28 Dec 2010 15:28:13 +0200 Subject: [PATCH 13/41] base_setup: adapt to base_crypt mangling the passwords When base_crypt is installed, base_setup should not list the hashed passwords. bzr revid: p_christ@hol.gr-20101228132813-ur2ezdevecdjwr75 --- addons/base_setup/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/addons/base_setup/__init__.py b/addons/base_setup/__init__.py index 85d01779842..9ed5ec84535 100644 --- a/addons/base_setup/__init__.py +++ b/addons/base_setup/__init__.py @@ -45,9 +45,15 @@ class base_setup_config_choice(osv.osv_memory): def get_users(self, cr, uid, context=None): user_obj = self.pool.get('res.users') user_ids = user_obj.search(cr, uid, []) - users = user_obj.browse(cr, uid, user_ids, context=context) - user_str = '\n'.join(map(lambda x: ' - %s :\n\t\tLogin : %s \n\t\tPassword : %s' % (x.name, x.login, x.password), users)) - return _('The following users have been installed : \n')+ user_str + user_list = [] + user_tmpl_nopass = _(' - %s :\n\t\tLogin : %s') + user_tmpl_pass = _(' - %s :\n\t\tLogin : %s \n\t\tPassword : %s') + for user in user_obj.browse(cr, uid, user_ids, context=context): + if user.password and not user.password.startswith('$'): + user_list.append(user_tmpl_pass % (user.name, user.login, user.password)) + else: + user_list.append(user_tmpl_nopass % (user.name, user.login)) + return _('The following users have been installed : \n')+ '\n'.join(user_list) _columns = { 'installed_users':fields.text('Installed Users', readonly=True), From d807742f50b70fbac0ec3a7675d2f2efc4c040fc Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 28 Dec 2010 18:58:14 +0200 Subject: [PATCH 14/41] doc ftp,webdav: fallback to passwd=login when using base_crypt At the test scripts for these modules, we need to know the password for the user that's making the tests. If we detect that passwords are hashed by base_crypt, don't use them and attempt to assume that login=passwd. bzr revid: p_christ@hol.gr-20101228165814-sfbyxy9ngryyfaea --- addons/document_ftp/test_easyftp.py | 7 ++++++- addons/document_webdav/test_davclient.py | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/addons/document_ftp/test_easyftp.py b/addons/document_ftp/test_easyftp.py index 1f7324b821e..fbc142c1267 100644 --- a/addons/document_ftp/test_easyftp.py +++ b/addons/document_ftp/test_easyftp.py @@ -37,7 +37,12 @@ def get_plain_ftp(timeout=10.0): def get_ftp_login(cr, uid, ormobj): ftp = get_plain_ftp() user = ormobj.pool.get('res.users').read(cr, uid, uid) - ftp.login(user.get('login',''), user.get('password','')) + passwd = user.get('password','') + if passwd.startswith("$1$"): + # md5 by base crypt. We cannot decode, wild guess + # that passwd = login + passwd = user.get('login', '') + ftp.login(user.get('login',''), passwd) ftp.cwd("/" + cr.dbname) return ftp diff --git a/addons/document_webdav/test_davclient.py b/addons/document_webdav/test_davclient.py index ac40d1a3d30..28455dc52a4 100644 --- a/addons/document_webdav/test_davclient.py +++ b/addons/document_webdav/test_davclient.py @@ -364,6 +364,10 @@ class DAVClient(object): assert res, "uid %s not found" % uid self.user = res[0]['login'] self.passwd = res[0]['password'] + if self.passwd.startswith('$1$'): + # md5 by base crypt. We cannot decode, wild guess + # that passwd = login + self.passwd = self.user return True def set_useragent(self, uastr): From 73af237c8b40dd6f57b54273f731576713a877ba Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Thu, 6 Jan 2011 13:14:00 +0200 Subject: [PATCH 15/41] pyPdf: upgrade from upstream git: 4abdca42a7d8a4 bzr revid: p_christ@hol.gr-20110106111400-hqw1nu5wx1mict4t --- bin/report/pyPdf/__init__.py | 1 - bin/report/pyPdf/filters.py | 505 +++++++++++++------------ bin/report/pyPdf/generic.py | 35 +- bin/report/pyPdf/pdf.py | 418 ++++++++++++++++++-- bin/report/pyPdf/utils.py | 233 ++++++------ bin/report/pyPdf/xmp.py | 711 +++++++++++++++++------------------ 6 files changed, 1136 insertions(+), 767 deletions(-) diff --git a/bin/report/pyPdf/__init__.py b/bin/report/pyPdf/__init__.py index 28360a93847..af02553da69 100644 --- a/bin/report/pyPdf/__init__.py +++ b/bin/report/pyPdf/__init__.py @@ -1,3 +1,2 @@ -# -*- coding: utf-8 -*- from pdf import PdfFileReader, PdfFileWriter __all__ = ["pdf"] diff --git a/bin/report/pyPdf/filters.py b/bin/report/pyPdf/filters.py index 04bb7d9df55..7fe10fb4819 100644 --- a/bin/report/pyPdf/filters.py +++ b/bin/report/pyPdf/filters.py @@ -1,253 +1,252 @@ -# -*- coding: utf-8 -*- -# vim: sw=4:expandtab:foldmethod=marker -# -# Copyright (c) 2006, Mathieu Fenniak -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * The name of the author may not be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - - -""" -Implementation of stream filters for PDF. -""" -__author__ = "Mathieu Fenniak" -__author_email__ = "biziqe@mathieu.fenniak.net" - -from utils import PdfReadError -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - -try: - import zlib - def decompress(data): - return zlib.decompress(data) - def compress(data): - return zlib.compress(data) -except ImportError: - # Unable to import zlib. Attempt to use the System.IO.Compression - # library from the .NET framework. (IronPython only) - import System - from System import IO, Collections, Array - def _string_to_bytearr(buf): - retval = Array.CreateInstance(System.Byte, len(buf)) - for i in range(len(buf)): - retval[i] = ord(buf[i]) - return retval - def _bytearr_to_string(bytes): - retval = "" - for i in range(bytes.Length): - retval += chr(bytes[i]) - return retval - def _read_bytes(stream): - ms = IO.MemoryStream() - buf = Array.CreateInstance(System.Byte, 2048) - while True: - bytes = stream.Read(buf, 0, buf.Length) - if bytes == 0: - break - else: - ms.Write(buf, 0, bytes) - retval = ms.ToArray() - ms.Close() - return retval - def decompress(data): - bytes = _string_to_bytearr(data) - ms = IO.MemoryStream() - ms.Write(bytes, 0, bytes.Length) - ms.Position = 0 # fseek 0 - gz = IO.Compression.DeflateStream(ms, IO.Compression.CompressionMode.Decompress) - bytes = _read_bytes(gz) - retval = _bytearr_to_string(bytes) - gz.Close() - return retval - def compress(data): - bytes = _string_to_bytearr(data) - ms = IO.MemoryStream() - gz = IO.Compression.DeflateStream(ms, IO.Compression.CompressionMode.Compress, True) - gz.Write(bytes, 0, bytes.Length) - gz.Close() - ms.Position = 0 # fseek 0 - bytes = ms.ToArray() - retval = _bytearr_to_string(bytes) - ms.Close() - return retval - - -class FlateDecode(object): - def decode(data, decodeParms): - data = decompress(data) - predictor = 1 - if decodeParms: - predictor = decodeParms.get("/Predictor", 1) - # predictor 1 == no predictor - if predictor != 1: - columns = decodeParms["/Columns"] - # PNG prediction: - if predictor >= 10 and predictor <= 15: - output = StringIO() - # PNG prediction can vary from row to row - rowlength = columns + 1 - assert len(data) % rowlength == 0 - prev_rowdata = (0,) * rowlength - for row in xrange(len(data) / rowlength): - rowdata = [ord(x) for x in data[(row*rowlength):((row+1)*rowlength)]] - filterByte = rowdata[0] - if filterByte == 0: - pass - elif filterByte == 1: - for i in range(2, rowlength): - rowdata[i] = (rowdata[i] + rowdata[i-1]) % 256 - elif filterByte == 2: - for i in range(1, rowlength): - rowdata[i] = (rowdata[i] + prev_rowdata[i]) % 256 - else: - # unsupported PNG filter - raise PdfReadError("Unsupported PNG filter %r" % filterByte) - prev_rowdata = rowdata - output.write(''.join([chr(x) for x in rowdata[1:]])) - data = output.getvalue() - else: - # unsupported predictor - raise PdfReadError("Unsupported flatedecode predictor %r" % predictor) - return data - decode = staticmethod(decode) - - def encode(data): - return compress(data) - encode = staticmethod(encode) - -class ASCIIHexDecode(object): - def decode(data, decodeParms=None): - retval = "" - char = "" - x = 0 - while True: - c = data[x] - if c == ">": - break - elif c.isspace(): - x += 1 - continue - char += c - if len(char) == 2: - retval += chr(int(char, base=16)) - char = "" - x += 1 - assert char == "" - return retval - decode = staticmethod(decode) - -class ASCII85Decode(object): - def decode(data, decodeParms=None): - retval = "" - group = [] - x = 0 - hitEod = False - # remove all whitespace from data - data = [y for y in data if not (y in ' \n\r\t')] - while not hitEod: - c = data[x] - if len(retval) == 0 and c == "<" and data[x+1] == "~": - x += 2 - continue - #elif c.isspace(): - # x += 1 - # continue - elif c == 'z': - assert len(group) == 0 - retval += '\x00\x00\x00\x00' - continue - elif c == "~" and data[x+1] == ">": - if len(group) != 0: - # cannot have a final group of just 1 char - assert len(group) > 1 - cnt = len(group) - 1 - group += [ 85, 85, 85 ] - hitEod = cnt - else: - break - else: - c = ord(c) - 33 - assert c >= 0 and c < 85 - group += [ c ] - if len(group) >= 5: - b = group[0] * (85**4) + \ - group[1] * (85**3) + \ - group[2] * (85**2) + \ - group[3] * 85 + \ - group[4] - assert b < (2**32 - 1) - c4 = chr((b >> 0) % 256) - c3 = chr((b >> 8) % 256) - c2 = chr((b >> 16) % 256) - c1 = chr(b >> 24) - retval += (c1 + c2 + c3 + c4) - if hitEod: - retval = retval[:-4+hitEod] - group = [] - x += 1 - return retval - decode = staticmethod(decode) - -def decodeStreamData(stream): - from generic import NameObject - filters = stream.get("/Filter", ()) - if len(filters) and not isinstance(filters[0], NameObject): - # we have a single filter instance - filters = (filters,) - data = stream._data - for filterType in filters: - if filterType == "/FlateDecode": - data = FlateDecode.decode(data, stream.get("/DecodeParms")) - elif filterType == "/ASCIIHexDecode": - data = ASCIIHexDecode.decode(data) - elif filterType == "/ASCII85Decode": - data = ASCII85Decode.decode(data) - elif filterType == "/Crypt": - decodeParams = stream.get("/DecodeParams", {}) - if "/Name" not in decodeParams and "/Type" not in decodeParams: - pass - else: - raise NotImplementedError("/Crypt filter with /Name or /Type not supported yet") - else: - # unsupported filter - raise NotImplementedError("unsupported filter %s" % filterType) - return data - -if __name__ == "__main__": - assert "abc" == ASCIIHexDecode.decode('61\n626\n3>') - - ascii85Test = """ - <~9jqo^BlbD-BleB1DJ+*+F(f,q/0JhKFCj@.4Gp$d7F!,L7@<6@)/0JDEF@3BB/F*&OCAfu2/AKY - i(DIb:@FD,*)+C]U=@3BN#EcYf8ATD3s@q?d$AftVqCh[NqF-FD5W8ARlolDIa - l(DIduD.RTpAKYo'+CT/5+Cei#DII?(E,9)oF*2M7/c~> - """ - ascii85_originalText="Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure." - assert ASCII85Decode.decode(ascii85Test) == ascii85_originalText - +# vim: sw=4:expandtab:foldmethod=marker +# +# Copyright (c) 2006, Mathieu Fenniak +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +""" +Implementation of stream filters for PDF. +""" +__author__ = "Mathieu Fenniak" +__author_email__ = "biziqe@mathieu.fenniak.net" + +from utils import PdfReadError +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +try: + import zlib + def decompress(data): + return zlib.decompress(data) + def compress(data): + return zlib.compress(data) +except ImportError: + # Unable to import zlib. Attempt to use the System.IO.Compression + # library from the .NET framework. (IronPython only) + import System + from System import IO, Collections, Array + def _string_to_bytearr(buf): + retval = Array.CreateInstance(System.Byte, len(buf)) + for i in range(len(buf)): + retval[i] = ord(buf[i]) + return retval + def _bytearr_to_string(bytes): + retval = "" + for i in range(bytes.Length): + retval += chr(bytes[i]) + return retval + def _read_bytes(stream): + ms = IO.MemoryStream() + buf = Array.CreateInstance(System.Byte, 2048) + while True: + bytes = stream.Read(buf, 0, buf.Length) + if bytes == 0: + break + else: + ms.Write(buf, 0, bytes) + retval = ms.ToArray() + ms.Close() + return retval + def decompress(data): + bytes = _string_to_bytearr(data) + ms = IO.MemoryStream() + ms.Write(bytes, 0, bytes.Length) + ms.Position = 0 # fseek 0 + gz = IO.Compression.DeflateStream(ms, IO.Compression.CompressionMode.Decompress) + bytes = _read_bytes(gz) + retval = _bytearr_to_string(bytes) + gz.Close() + return retval + def compress(data): + bytes = _string_to_bytearr(data) + ms = IO.MemoryStream() + gz = IO.Compression.DeflateStream(ms, IO.Compression.CompressionMode.Compress, True) + gz.Write(bytes, 0, bytes.Length) + gz.Close() + ms.Position = 0 # fseek 0 + bytes = ms.ToArray() + retval = _bytearr_to_string(bytes) + ms.Close() + return retval + + +class FlateDecode(object): + def decode(data, decodeParms): + data = decompress(data) + predictor = 1 + if decodeParms: + predictor = decodeParms.get("/Predictor", 1) + # predictor 1 == no predictor + if predictor != 1: + columns = decodeParms["/Columns"] + # PNG prediction: + if predictor >= 10 and predictor <= 15: + output = StringIO() + # PNG prediction can vary from row to row + rowlength = columns + 1 + assert len(data) % rowlength == 0 + prev_rowdata = (0,) * rowlength + for row in xrange(len(data) / rowlength): + rowdata = [ord(x) for x in data[(row*rowlength):((row+1)*rowlength)]] + filterByte = rowdata[0] + if filterByte == 0: + pass + elif filterByte == 1: + for i in range(2, rowlength): + rowdata[i] = (rowdata[i] + rowdata[i-1]) % 256 + elif filterByte == 2: + for i in range(1, rowlength): + rowdata[i] = (rowdata[i] + prev_rowdata[i]) % 256 + else: + # unsupported PNG filter + raise PdfReadError("Unsupported PNG filter %r" % filterByte) + prev_rowdata = rowdata + output.write(''.join([chr(x) for x in rowdata[1:]])) + data = output.getvalue() + else: + # unsupported predictor + raise PdfReadError("Unsupported flatedecode predictor %r" % predictor) + return data + decode = staticmethod(decode) + + def encode(data): + return compress(data) + encode = staticmethod(encode) + +class ASCIIHexDecode(object): + def decode(data, decodeParms=None): + retval = "" + char = "" + x = 0 + while True: + c = data[x] + if c == ">": + break + elif c.isspace(): + x += 1 + continue + char += c + if len(char) == 2: + retval += chr(int(char, base=16)) + char = "" + x += 1 + assert char == "" + return retval + decode = staticmethod(decode) + +class ASCII85Decode(object): + def decode(data, decodeParms=None): + retval = "" + group = [] + x = 0 + hitEod = False + # remove all whitespace from data + data = [y for y in data if not (y in ' \n\r\t')] + while not hitEod: + c = data[x] + if len(retval) == 0 and c == "<" and data[x+1] == "~": + x += 2 + continue + #elif c.isspace(): + # x += 1 + # continue + elif c == 'z': + assert len(group) == 0 + retval += '\x00\x00\x00\x00' + continue + elif c == "~" and data[x+1] == ">": + if len(group) != 0: + # cannot have a final group of just 1 char + assert len(group) > 1 + cnt = len(group) - 1 + group += [ 85, 85, 85 ] + hitEod = cnt + else: + break + else: + c = ord(c) - 33 + assert c >= 0 and c < 85 + group += [ c ] + if len(group) >= 5: + b = group[0] * (85**4) + \ + group[1] * (85**3) + \ + group[2] * (85**2) + \ + group[3] * 85 + \ + group[4] + assert b < (2**32 - 1) + c4 = chr((b >> 0) % 256) + c3 = chr((b >> 8) % 256) + c2 = chr((b >> 16) % 256) + c1 = chr(b >> 24) + retval += (c1 + c2 + c3 + c4) + if hitEod: + retval = retval[:-4+hitEod] + group = [] + x += 1 + return retval + decode = staticmethod(decode) + +def decodeStreamData(stream): + from generic import NameObject + filters = stream.get("/Filter", ()) + if len(filters) and not isinstance(filters[0], NameObject): + # we have a single filter instance + filters = (filters,) + data = stream._data + for filterType in filters: + if filterType == "/FlateDecode": + data = FlateDecode.decode(data, stream.get("/DecodeParms")) + elif filterType == "/ASCIIHexDecode": + data = ASCIIHexDecode.decode(data) + elif filterType == "/ASCII85Decode": + data = ASCII85Decode.decode(data) + elif filterType == "/Crypt": + decodeParams = stream.get("/DecodeParams", {}) + if "/Name" not in decodeParams and "/Type" not in decodeParams: + pass + else: + raise NotImplementedError("/Crypt filter with /Name or /Type not supported yet") + else: + # unsupported filter + raise NotImplementedError("unsupported filter %s" % filterType) + return data + +if __name__ == "__main__": + assert "abc" == ASCIIHexDecode.decode('61\n626\n3>') + + ascii85Test = """ + <~9jqo^BlbD-BleB1DJ+*+F(f,q/0JhKFCj@.4Gp$d7F!,L7@<6@)/0JDEF@3BB/F*&OCAfu2/AKY + i(DIb:@FD,*)+C]U=@3BN#EcYf8ATD3s@q?d$AftVqCh[NqF-FD5W8ARlolDIa + l(DIduD.RTpAKYo'+CT/5+Cei#DII?(E,9)oF*2M7/c~> + """ + ascii85_originalText="Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure." + assert ASCII85Decode.decode(ascii85Test) == ascii85_originalText + diff --git a/bin/report/pyPdf/generic.py b/bin/report/pyPdf/generic.py index f216732a85d..aaf503180f8 100644 --- a/bin/report/pyPdf/generic.py +++ b/bin/report/pyPdf/generic.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # vim: sw=4:expandtab:foldmethod=marker # # Copyright (c) 2006, Mathieu Fenniak @@ -207,15 +206,18 @@ class FloatObject(decimal.Decimal, PdfObject): def __new__(cls, value="0", context=None): return decimal.Decimal.__new__(cls, str(value), context) def __repr__(self): - return str(self) + if self == self.to_integral(): + return str(self.quantize(decimal.Decimal(1))) + else: + # XXX: this adds useless extraneous zeros. + return "%.5f" % self def writeToStream(self, stream, encryption_key): - stream.write(str(self)) + stream.write(repr(self)) class NumberObject(int, PdfObject): def __init__(self, value): - int.__init__(self) - self = value + int.__init__(value) def writeToStream(self, stream, encryption_key): stream.write(repr(self)) @@ -301,7 +303,7 @@ def readStringFromStream(stream): elif tok == "t": tok = "\t" elif tok == "b": - tok == "\b" + tok = "\b" elif tok == "f": tok = "\f" elif tok == "(": @@ -311,7 +313,17 @@ def readStringFromStream(stream): elif tok == "\\": tok = "\\" elif tok.isdigit(): - tok += stream.read(2) + # "The number ddd may consist of one, two, or three + # octal digits; high-order overflow shall be ignored. + # Three octal digits shall be used, with leading zeros + # as needed, if the next character of the string is also + # a digit." (PDF reference 7.3.4.2, p 16) + for i in range(2): + ntok = stream.read(1) + if ntok.isdigit(): + tok += ntok + else: + break tok = chr(int(tok, base=8)) elif tok in "\n\r": # This case is hit when a backslash followed by a line @@ -405,8 +417,7 @@ class NameObject(str, PdfObject): delimiterCharacters = "(", ")", "<", ">", "[", "]", "{", "}", "/", "%" def __init__(self, data): - str.__init__(self) - self = data + str.__init__(data) def writeToStream(self, stream, encryption_key): stream.write(self) @@ -710,6 +721,12 @@ class RectangleObject(ArrayObject): def setUpperRight(self, value): self[2], self[3] = [self.ensureIsNumber(x) for x in value] + def getWidth(self): + return self.getUpperRight_x() - self.getLowerLeft_x() + + def getHeight(self): + return self.getUpperRight_y() - self.getLowerLeft_x() + lowerLeft = property(getLowerLeft, setLowerLeft, None, None) lowerRight = property(getLowerRight, setLowerRight, None, None) upperLeft = property(getUpperLeft, setUpperLeft, None, None) diff --git a/bin/report/pyPdf/pdf.py b/bin/report/pyPdf/pdf.py index 9bc56cf8b05..bf60d0157dc 100644 --- a/bin/report/pyPdf/pdf.py +++ b/bin/report/pyPdf/pdf.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# # vim: sw=4:expandtab:foldmethod=marker # # Copyright (c) 2006, Mathieu Fenniak @@ -39,7 +40,9 @@ It may be a solid base for future PDF file work in Python. __author__ = "Mathieu Fenniak" __author_email__ = "biziqe@mathieu.fenniak.net" +import math import struct +from sys import version_info try: from cStringIO import StringIO except ImportError: @@ -51,6 +54,14 @@ import warnings from generic import * from utils import readNonWhitespace, readUntilWhitespace, ConvertFunctionsToVirtualList +if version_info < ( 2, 4 ): + from sets import ImmutableSet as frozenset + +if version_info < ( 2, 5 ): + from md5 import md5 +else: + from hashlib import md5 + ## # This class supports writing PDF files out, given pages produced by another # class (typically {@link #PdfFileReader PdfFileReader}). @@ -92,6 +103,21 @@ class PdfFileWriter(object): raise ValueError("pdf must be self") return self._objects[ido.idnum - 1] + ## + # Common method for inserting or adding a page to this PDF file. + # + # @param page The page to add to the document. This argument should be + # an instance of {@link #PageObject PageObject}. + # @param action The function which will insert the page in the dictionnary. + # Takes: page list, page to add. + def _addPage(self, page, action): + assert page["/Type"] == "/Page" + page[NameObject("/Parent")] = self._pages + page = self._addObject(page) + pages = self.getObject(self._pages) + action(pages["/Kids"], page) + pages[NameObject("/Count")] = NumberObject(pages["/Count"] + 1) + ## # Adds a page to this PDF file. The page is usually acquired from a # {@link #PdfFileReader PdfFileReader} instance. @@ -101,12 +127,64 @@ class PdfFileWriter(object): # @param page The page to add to the document. This argument should be # an instance of {@link #PageObject PageObject}. def addPage(self, page): - assert page["/Type"] == "/Page" - page[NameObject("/Parent")] = self._pages - page = self._addObject(page) + self._addPage(page, list.append) + + ## + # Insert a page in this PDF file. The page is usually acquired from a + # {@link #PdfFileReader PdfFileReader} instance. + # + # @param page The page to add to the document. This argument should be + # an instance of {@link #PageObject PageObject}. + # @param index Position at which the page will be inserted. + def insertPage(self, page, index=0): + self._addPage(page, lambda l, p: l.insert(index, p)) + + ## + # Retrieves a page by number from this PDF file. + # @return Returns a {@link #PageObject PageObject} instance. + def getPage(self, pageNumber): pages = self.getObject(self._pages) - pages["/Kids"].append(page) - pages[NameObject("/Count")] = NumberObject(pages["/Count"] + 1) + # XXX: crude hack + return pages["/Kids"][pageNumber].getObject() + + ## + # Return the number of pages. + # @return The number of pages. + def getNumPages(self): + pages = self.getObject(self._pages) + return int(pages[NameObject("/Count")]) + + ## + # Append a blank page to this PDF file and returns it. If no page size + # is specified, use the size of the last page; throw + # PageSizeNotDefinedError if it doesn't exist. + # @param width The width of the new page expressed in default user + # space units. + # @param height The height of the new page expressed in default user + # space units. + def addBlankPage(self, width=None, height=None): + page = PageObject.createBlankPage(self, width, height) + self.addPage(page) + return page + + ## + # Insert a blank page to this PDF file and returns it. If no page size + # is specified, use the size of the page in the given index; throw + # PageSizeNotDefinedError if it doesn't exist. + # @param width The width of the new page expressed in default user + # space units. + # @param height The height of the new page expressed in default user + # space units. + # @param index Position to add the page. + def insertBlankPage(self, width=None, height=None, index=0): + if width is None or height is None and \ + (self.getNumPages() - 1) >= index: + oldpage = self.getPage(index) + width = oldpage.mediaBox.getWidth() + height = oldpage.mediaBox.getHeight() + page = PageObject.createBlankPage(self, width, height) + self.insertPage(page, index) + return page ## # Encrypt this PDF file with the PDF Standard encryption handler. @@ -119,7 +197,7 @@ class PdfFileWriter(object): # encryption. When false, 40bit encryption will be used. By default, this # flag is on. def encrypt(self, user_pwd, owner_pwd = None, use_128bit = True): - import md5, time, random + import time, random if owner_pwd == None: owner_pwd = user_pwd if use_128bit: @@ -133,8 +211,8 @@ class PdfFileWriter(object): # permit everything: P = -1 O = ByteStringObject(_alg33(owner_pwd, user_pwd, rev, keylen)) - ID_1 = md5.new(repr(time.time())).digest() - ID_2 = md5.new(repr(random.random())).digest() + ID_1 = md5(repr(time.time())).digest() + ID_2 = md5(repr(random.random())).digest() self._ID = ArrayObject((ByteStringObject(ID_1), ByteStringObject(ID_2))) if rev == 2: U, key = _alg34(user_pwd, O, P, ID_1) @@ -160,9 +238,28 @@ class PdfFileWriter(object): # @param stream An object to write the file to. The object must support # the write method, and the tell method, similar to a file object. def write(self, stream): - import struct, md5 + import struct externalReferenceMap = {} + + # PDF objects sometimes have circular references to their /Page objects + # inside their object tree (for example, annotations). Those will be + # indirect references to objects that we've recreated in this PDF. To + # address this problem, PageObject's store their original object + # reference number, and we add it to the external reference map before + # we sweep for indirect references. This forces self-page-referencing + # trees to reference the correct new object location, rather than + # copying in a new copy of the page object. + for objIndex in xrange(len(self._objects)): + obj = self._objects[objIndex] + if isinstance(obj, PageObject) and obj.indirectRef != None: + data = obj.indirectRef + if not externalReferenceMap.has_key(data.pdf): + externalReferenceMap[data.pdf] = {} + if not externalReferenceMap[data.pdf].has_key(data.generation): + externalReferenceMap[data.pdf][data.generation] = {} + externalReferenceMap[data.pdf][data.generation][data.idnum] = IndirectObject(objIndex + 1, 0, self) + self.stack = [] self._sweepIndirectReferences(externalReferenceMap, self._root) del self.stack @@ -181,7 +278,7 @@ class PdfFileWriter(object): pack2 = struct.pack(" 0: + lastpage = pdf.getPage(pdf.getNumPages() - 1) + width = lastpage.mediaBox.getWidth() + height = lastpage.mediaBox.getHeight() + else: + raise utils.PageSizeNotDefinedError() + page.__setitem__(NameObject('/MediaBox'), + RectangleObject([0, 0, width, height])) + + return page + createBlankPage = staticmethod(createBlankPage) ## # Rotates a page clockwise by increments of 90 degrees. @@ -931,7 +1066,7 @@ class PageObject(DictionaryObject): renameRes[key] = newname newRes[newname] = page2Res[key] elif not newRes.has_key(key): - newRes[key] = page2Res[key] + newRes[key] = page2Res.raw_get(key) return newRes, renameRes _mergeResources = staticmethod(_mergeResources) @@ -957,6 +1092,26 @@ class PageObject(DictionaryObject): return stream _pushPopGS = staticmethod(_pushPopGS) + def _addTransformationMatrix(contents, pdf, ctm): + # adds transformation matrix at the beginning of the given + # contents stream. + a, b, c, d, e, f = ctm + contents = ContentStream(contents, pdf) + contents.operations.insert(0, [[FloatObject(a), FloatObject(b), + FloatObject(c), FloatObject(d), FloatObject(e), + FloatObject(f)], " cm"]) + return contents + _addTransformationMatrix = staticmethod(_addTransformationMatrix) + + ## + # Returns the /Contents object, or None if it doesn't exist. + # /Contents is optionnal, as described in PDF Reference 7.7.3.3 + def getContents(self): + if self.has_key("/Contents"): + return self["/Contents"].getObject() + else: + return None + ## # Merges the content streams of two pages into one. Resource references # (i.e. fonts) are maintained from both pages. The mediabox/cropbox/etc @@ -968,7 +1123,23 @@ class PageObject(DictionaryObject): # @param page2 An instance of {@link #PageObject PageObject} to be merged # into this one. def mergePage(self, page2): + self._mergePage(page2) + ## + # Actually merges the content streams of two pages into one. Resource + # references (i.e. fonts) are maintained from both pages. The + # mediabox/cropbox/etc of this page are not altered. The parameter page's + # content stream will be added to the end of this page's content stream, + # meaning that it will be drawn after, or "on top" of this page. + # + # @param page2 An instance of {@link #PageObject PageObject} to be merged + # into this one. + # @param page2transformation A fuction which applies a transformation to + # the content stream of page2. Takes: page2 + # contents stream. Must return: new contents + # stream. If omitted, the content stream will + # not be modified. + def _mergePage(self, page2, page2transformation=None): # First we work on merging the resource dictionaries. This allows us # to find out what symbols in the content streams we might need to # rename. @@ -978,7 +1149,7 @@ class PageObject(DictionaryObject): originalResources = self["/Resources"].getObject() page2Resources = page2["/Resources"].getObject() - for res in "/ExtGState", "/Font", "/XObject", "/ColorSpace", "/Pattern", "/Shading": + for res in "/ExtGState", "/Font", "/XObject", "/ColorSpace", "/Pattern", "/Shading", "/Properties": new, newrename = PageObject._mergeResources(originalResources, page2Resources, res) if new: newResources[NameObject(res)] = new @@ -993,17 +1164,191 @@ class PageObject(DictionaryObject): newContentArray = ArrayObject() - originalContent = self["/Contents"].getObject() - newContentArray.append(PageObject._pushPopGS(originalContent, self.pdf)) + originalContent = self.getContents() + if originalContent is not None: + newContentArray.append(PageObject._pushPopGS( + originalContent, self.pdf)) - page2Content = page2['/Contents'].getObject() - page2Content = PageObject._contentStreamRename(page2Content, rename, self.pdf) - page2Content = PageObject._pushPopGS(page2Content, self.pdf) - newContentArray.append(page2Content) + page2Content = page2.getContents() + if page2Content is not None: + if page2transformation is not None: + page2Content = page2transformation(page2Content) + page2Content = PageObject._contentStreamRename( + page2Content, rename, self.pdf) + page2Content = PageObject._pushPopGS(page2Content, self.pdf) + newContentArray.append(page2Content) self[NameObject('/Contents')] = ContentStream(newContentArray, self.pdf) self[NameObject('/Resources')] = newResources + ## + # This is similar to mergePage, but a transformation matrix is + # applied to the merged stream. + # + # @param page2 An instance of {@link #PageObject PageObject} to be merged. + # @param ctm A 6 elements tuple containing the operands of the + # transformation matrix + def mergeTransformedPage(self, page2, ctm): + self._mergePage(page2, lambda page2Content: + PageObject._addTransformationMatrix(page2Content, page2.pdf, ctm)) + + ## + # This is similar to mergePage, but the stream to be merged is scaled + # by appling a transformation matrix. + # + # @param page2 An instance of {@link #PageObject PageObject} to be merged. + # @param factor The scaling factor + def mergeScaledPage(self, page2, factor): + # CTM to scale : [ sx 0 0 sy 0 0 ] + return self.mergeTransformedPage(page2, [factor, 0, + 0, factor, + 0, 0]) + + ## + # This is similar to mergePage, but the stream to be merged is rotated + # by appling a transformation matrix. + # + # @param page2 An instance of {@link #PageObject PageObject} to be merged. + # @param rotation The angle of the rotation, in degrees + def mergeRotatedPage(self, page2, rotation): + rotation = math.radians(rotation) + return self.mergeTransformedPage(page2, + [math.cos(rotation), math.sin(rotation), + -math.sin(rotation), math.cos(rotation), + 0, 0]) + + ## + # This is similar to mergePage, but the stream to be merged is translated + # by appling a transformation matrix. + # + # @param page2 An instance of {@link #PageObject PageObject} to be merged. + # @param tx The translation on X axis + # @param tx The translation on Y axis + def mergeTranslatedPage(self, page2, tx, ty): + return self.mergeTransformedPage(page2, [1, 0, + 0, 1, + tx, ty]) + + ## + # This is similar to mergePage, but the stream to be merged is rotated + # and scaled by appling a transformation matrix. + # + # @param page2 An instance of {@link #PageObject PageObject} to be merged. + # @param rotation The angle of the rotation, in degrees + # @param factor The scaling factor + def mergeRotatedScaledPage(self, page2, rotation, scale): + rotation = math.radians(rotation) + rotating = [[math.cos(rotation), math.sin(rotation),0], + [-math.sin(rotation),math.cos(rotation), 0], + [0, 0, 1]] + scaling = [[scale,0, 0], + [0, scale,0], + [0, 0, 1]] + ctm = utils.matrixMultiply(rotating, scaling) + + return self.mergeTransformedPage(page2, + [ctm[0][0], ctm[0][1], + ctm[1][0], ctm[1][1], + ctm[2][0], ctm[2][1]]) + + ## + # This is similar to mergePage, but the stream to be merged is translated + # and scaled by appling a transformation matrix. + # + # @param page2 An instance of {@link #PageObject PageObject} to be merged. + # @param scale The scaling factor + # @param tx The translation on X axis + # @param tx The translation on Y axis + def mergeScaledTranslatedPage(self, page2, scale, tx, ty): + translation = [[1, 0, 0], + [0, 1, 0], + [tx,ty,1]] + scaling = [[scale,0, 0], + [0, scale,0], + [0, 0, 1]] + ctm = utils.matrixMultiply(scaling, translation) + + return self.mergeTransformedPage(page2, [ctm[0][0], ctm[0][1], + ctm[1][0], ctm[1][1], + ctm[2][0], ctm[2][1]]) + + ## + # This is similar to mergePage, but the stream to be merged is translated, + # rotated and scaled by appling a transformation matrix. + # + # @param page2 An instance of {@link #PageObject PageObject} to be merged. + # @param tx The translation on X axis + # @param ty The translation on Y axis + # @param rotation The angle of the rotation, in degrees + # @param scale The scaling factor + def mergeRotatedScaledTranslatedPage(self, page2, rotation, scale, tx, ty): + translation = [[1, 0, 0], + [0, 1, 0], + [tx,ty,1]] + rotation = math.radians(rotation) + rotating = [[math.cos(rotation), math.sin(rotation),0], + [-math.sin(rotation),math.cos(rotation), 0], + [0, 0, 1]] + scaling = [[scale,0, 0], + [0, scale,0], + [0, 0, 1]] + ctm = utils.matrixMultiply(rotating, scaling) + ctm = utils.matrixMultiply(ctm, translation) + + return self.mergeTransformedPage(page2, [ctm[0][0], ctm[0][1], + ctm[1][0], ctm[1][1], + ctm[2][0], ctm[2][1]]) + + ## + # Applys a transformation matrix the page. + # + # @param ctm A 6 elements tuple containing the operands of the + # transformation matrix + def addTransformation(self, ctm): + originalContent = self.getContents() + if originalContent is not None: + newContent = PageObject._addTransformationMatrix( + originalContent, self.pdf, ctm) + newContent = PageObject._pushPopGS(newContent, self.pdf) + self[NameObject('/Contents')] = newContent + + ## + # Scales a page by the given factors by appling a transformation + # matrix to its content and updating the page size. + # + # @param sx The scaling factor on horizontal axis + # @param sy The scaling factor on vertical axis + def scale(self, sx, sy): + self.addTransformation([sx, 0, + 0, sy, + 0, 0]) + self.mediaBox = RectangleObject([ + float(self.mediaBox.getLowerLeft_x()) * sx, + float(self.mediaBox.getLowerLeft_y()) * sy, + float(self.mediaBox.getUpperRight_x()) * sx, + float(self.mediaBox.getUpperRight_y()) * sy]) + + ## + # Scales a page by the given factor by appling a transformation + # matrix to its content and updating the page size. + # + # @param factor The scaling factor + def scaleBy(self, factor): + self.scale(factor, factor) + + ## + # Scales a page to the specified dimentions by appling a + # transformation matrix to its content and updating the page size. + # + # @param width The new width + # @param height The new heigth + def scaleTo(self, width, height): + sx = width / (self.mediaBox.getUpperRight_x() - + self.mediaBox.getLowerLeft_x ()) + sy = height / (self.mediaBox.getUpperRight_y() - + self.mediaBox.getLowerLeft_x ()) + self.scale(sx, sy) + ## # Compresses the size of this page by joining all content streams and # applying a FlateDecode filter. @@ -1012,10 +1357,11 @@ class PageObject(DictionaryObject): # However, it is possible that this function will perform no action if # content stream compression becomes "automatic" for some reason. def compressContentStreams(self): - content = self["/Contents"].getObject() - if not isinstance(content, ContentStream): - content = ContentStream(content, self.pdf) - self[NameObject("/Contents")] = content.flateEncode() + content = self.getContents() + if content is not None: + if not isinstance(content, ContentStream): + content = ContentStream(content, self.pdf) + self[NameObject("/Contents")] = content.flateEncode() ## # Locate all text drawing commands, in the order they are provided in the @@ -1369,8 +1715,8 @@ def _alg32(password, rev, keylen, owner_entry, p_entry, id1_entry, metadata_encr password = (password + _encryption_padding)[:32] # 2. Initialize the MD5 hash function and pass the result of step 1 as # input to this function. - import md5, struct - m = md5.new(password) + import struct + m = md5(password) # 3. Pass the value of the encryption dictionary's /O entry to the MD5 hash # function. m.update(owner_entry) @@ -1394,7 +1740,7 @@ def _alg32(password, rev, keylen, owner_entry, p_entry, id1_entry, metadata_encr # /Length entry. if rev >= 3: for i in range(50): - md5_hash = md5.new(md5_hash[:keylen]).digest() + md5_hash = md5(md5_hash[:keylen]).digest() # 9. Set the encryption key to the first n bytes of the output from the # final MD5 hash, where n is always 5 for revision 2 but, for revision 3 or # greater, depends on the value of the encryption dictionary's /Length @@ -1436,14 +1782,13 @@ def _alg33_1(password, rev, keylen): password = (password + _encryption_padding)[:32] # 2. Initialize the MD5 hash function and pass the result of step 1 as # input to this function. - import md5 - m = md5.new(password) + m = md5(password) # 3. (Revision 3 or greater) Do the following 50 times: Take the output # from the previous MD5 hash and pass it as input into a new MD5 hash. md5_hash = m.digest() if rev >= 3: for i in range(50): - md5_hash = md5.new(md5_hash).digest() + md5_hash = md5(md5_hash).digest() # 4. Create an RC4 encryption key using the first n bytes of the output # from the final MD5 hash, where n is always 5 for revision 2 but, for # revision 3 or greater, depends on the value of the encryption @@ -1473,8 +1818,7 @@ def _alg35(password, rev, keylen, owner_entry, p_entry, id1_entry, metadata_encr key = _alg32(password, rev, keylen, owner_entry, p_entry, id1_entry) # 2. Initialize the MD5 hash function and pass the 32-byte padding string # shown in step 1 of Algorithm 3.2 as input to this function. - import md5 - m = md5.new() + m = md5() m.update(_encryption_padding) # 3. Pass the first element of the file's file identifier array (the value # of the ID entry in the document's trailer dictionary; see Table 3.13 on diff --git a/bin/report/pyPdf/utils.py b/bin/report/pyPdf/utils.py index 2a778dd0c05..7c1d472d0b7 100644 --- a/bin/report/pyPdf/utils.py +++ b/bin/report/pyPdf/utils.py @@ -1,111 +1,122 @@ -# -*- coding: utf-8 -*- -# vim: sw=4:expandtab:foldmethod=marker -# -# Copyright (c) 2006, Mathieu Fenniak -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * The name of the author may not be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - - -""" -Utility functions for PDF library. -""" -__author__ = "Mathieu Fenniak" -__author_email__ = "biziqe@mathieu.fenniak.net" - -#ENABLE_PSYCO = False -#if ENABLE_PSYCO: -# try: -# import psyco -# except ImportError: -# ENABLE_PSYCO = False -# -#if not ENABLE_PSYCO: -# class psyco: -# def proxy(func): -# return func -# proxy = staticmethod(proxy) - -def readUntilWhitespace(stream, maxchars=None): - txt = "" - while True: - tok = stream.read(1) - if tok.isspace() or not tok: - break - txt += tok - if len(txt) == maxchars: - break - return txt - -def readNonWhitespace(stream): - tok = ' ' - while tok == '\n' or tok == '\r' or tok == ' ' or tok == '\t': - tok = stream.read(1) - return tok - -class ConvertFunctionsToVirtualList(object): - def __init__(self, lengthFunction, getFunction): - self.lengthFunction = lengthFunction - self.getFunction = getFunction - - def __len__(self): - return self.lengthFunction() - - def __getitem__(self, index): - if not isinstance(index, int): - raise TypeError, "sequence indices must be integers" - len_self = len(self) - if index < 0: - # support negative indexes - index = len_self + index - if index < 0 or index >= len_self: - raise IndexError, "sequence index out of range" - return self.getFunction(index) - -def RC4_encrypt(key, plaintext): - S = [i for i in range(256)] - j = 0 - for i in range(256): - j = (j + S[i] + ord(key[i % len(key)])) % 256 - S[i], S[j] = S[j], S[i] - i, j = 0, 0 - retval = "" - for x in range(len(plaintext)): - i = (i + 1) % 256 - j = (j + S[i]) % 256 - S[i], S[j] = S[j], S[i] - t = S[(S[i] + S[j]) % 256] - retval += chr(ord(plaintext[x]) ^ t) - return retval - -class PdfReadError(Exception): - pass - -if __name__ == "__main__": - # test RC4 - out = RC4_encrypt("Key", "Plaintext") - print repr(out) - pt = RC4_encrypt("Key", out) - print repr(pt) +# vim: sw=4:expandtab:foldmethod=marker +# +# Copyright (c) 2006, Mathieu Fenniak +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +""" +Utility functions for PDF library. +""" +__author__ = "Mathieu Fenniak" +__author_email__ = "biziqe@mathieu.fenniak.net" + +#ENABLE_PSYCO = False +#if ENABLE_PSYCO: +# try: +# import psyco +# except ImportError: +# ENABLE_PSYCO = False +# +#if not ENABLE_PSYCO: +# class psyco: +# def proxy(func): +# return func +# proxy = staticmethod(proxy) + +def readUntilWhitespace(stream, maxchars=None): + txt = "" + while True: + tok = stream.read(1) + if tok.isspace() or not tok: + break + txt += tok + if len(txt) == maxchars: + break + return txt + +def readNonWhitespace(stream): + tok = ' ' + while tok == '\n' or tok == '\r' or tok == ' ' or tok == '\t': + tok = stream.read(1) + return tok + +class ConvertFunctionsToVirtualList(object): + def __init__(self, lengthFunction, getFunction): + self.lengthFunction = lengthFunction + self.getFunction = getFunction + + def __len__(self): + return self.lengthFunction() + + def __getitem__(self, index): + if not isinstance(index, int): + raise TypeError, "sequence indices must be integers" + len_self = len(self) + if index < 0: + # support negative indexes + index = len_self + index + if index < 0 or index >= len_self: + raise IndexError, "sequence index out of range" + return self.getFunction(index) + +def RC4_encrypt(key, plaintext): + S = [i for i in range(256)] + j = 0 + for i in range(256): + j = (j + S[i] + ord(key[i % len(key)])) % 256 + S[i], S[j] = S[j], S[i] + i, j = 0, 0 + retval = "" + for x in range(len(plaintext)): + i = (i + 1) % 256 + j = (j + S[i]) % 256 + S[i], S[j] = S[j], S[i] + t = S[(S[i] + S[j]) % 256] + retval += chr(ord(plaintext[x]) ^ t) + return retval + +def matrixMultiply(a, b): + return [[sum([float(i)*float(j) + for i, j in zip(row, col)] + ) for col in zip(*b)] + for row in a] + +class PyPdfError(Exception): + pass + +class PdfReadError(PyPdfError): + pass + +class PageSizeNotDefinedError(PyPdfError): + pass + +if __name__ == "__main__": + # test RC4 + out = RC4_encrypt("Key", "Plaintext") + print repr(out) + pt = RC4_encrypt("Key", out) + print repr(pt) diff --git a/bin/report/pyPdf/xmp.py b/bin/report/pyPdf/xmp.py index 5018b7825ed..b070df9093e 100644 --- a/bin/report/pyPdf/xmp.py +++ b/bin/report/pyPdf/xmp.py @@ -1,356 +1,355 @@ -# -*- coding: utf-8 -*- -import re -import datetime -import decimal -from generic import PdfObject -from xml.dom import getDOMImplementation -from xml.dom.minidom import parseString - -RDF_NAMESPACE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" -DC_NAMESPACE = "http://purl.org/dc/elements/1.1/" -XMP_NAMESPACE = "http://ns.adobe.com/xap/1.0/" -PDF_NAMESPACE = "http://ns.adobe.com/pdf/1.3/" -XMPMM_NAMESPACE = "http://ns.adobe.com/xap/1.0/mm/" - -# What is the PDFX namespace, you might ask? I might ask that too. It's -# a completely undocumented namespace used to place "custom metadata" -# properties, which are arbitrary metadata properties with no semantic or -# documented meaning. Elements in the namespace are key/value-style storage, -# where the element name is the key and the content is the value. The keys -# are transformed into valid XML identifiers by substituting an invalid -# identifier character with \u2182 followed by the unicode hex ID of the -# original character. A key like "my car" is therefore "my\u21820020car". -# -# \u2182, in case you're wondering, is the unicode character -# \u{ROMAN NUMERAL TEN THOUSAND}, a straightforward and obvious choice for -# escaping characters. -# -# Intentional users of the pdfx namespace should be shot on sight. A -# custom data schema and sensical XML elements could be used instead, as is -# suggested by Adobe's own documentation on XMP (under "Extensibility of -# Schemas"). -# -# Information presented here on the /pdfx/ schema is a result of limited -# reverse engineering, and does not constitute a full specification. -PDFX_NAMESPACE = "http://ns.adobe.com/pdfx/1.3/" - -iso8601 = re.compile(""" - (?P[0-9]{4}) - (- - (?P[0-9]{2}) - (- - (?P[0-9]+) - (T - (?P[0-9]{2}): - (?P[0-9]{2}) - (:(?P[0-9]{2}(.[0-9]+)?))? - (?PZ|[-+][0-9]{2}:[0-9]{2}) - )? - )? - )? - """, re.VERBOSE) - -## -# An object that represents Adobe XMP metadata. -class XmpInformation(PdfObject): - - def __init__(self, stream): - self.stream = stream - docRoot = parseString(self.stream.getData()) - self.rdfRoot = docRoot.getElementsByTagNameNS(RDF_NAMESPACE, "RDF")[0] - self.cache = {} - - def writeToStream(self, stream, encryption_key): - self.stream.writeToStream(stream, encryption_key) - - def getElement(self, aboutUri, namespace, name): - for desc in self.rdfRoot.getElementsByTagNameNS(RDF_NAMESPACE, "Description"): - if desc.getAttributeNS(RDF_NAMESPACE, "about") == aboutUri: - attr = desc.getAttributeNodeNS(namespace, name) - if attr != None: - yield attr - for element in desc.getElementsByTagNameNS(namespace, name): - yield element - - def getNodesInNamespace(self, aboutUri, namespace): - for desc in self.rdfRoot.getElementsByTagNameNS(RDF_NAMESPACE, "Description"): - if desc.getAttributeNS(RDF_NAMESPACE, "about") == aboutUri: - for i in range(desc.attributes.length): - attr = desc.attributes.item(i) - if attr.namespaceURI == namespace: - yield attr - for child in desc.childNodes: - if child.namespaceURI == namespace: - yield child - - def _getText(self, element): - text = "" - for child in element.childNodes: - if child.nodeType == child.TEXT_NODE: - text += child.data - return text - - def _converter_string(value): - return value - - def _converter_date(value): - m = iso8601.match(value) - year = int(m.group("year")) - month = int(m.group("month") or "1") - day = int(m.group("day") or "1") - hour = int(m.group("hour") or "0") - minute = int(m.group("minute") or "0") - second = decimal.Decimal(m.group("second") or "0") - seconds = second.to_integral(decimal.ROUND_FLOOR) - milliseconds = (second - seconds) * 1000000 - tzd = m.group("tzd") or "Z" - dt = datetime.datetime(year, month, day, hour, minute, seconds, milliseconds) - if tzd != "Z": - tzd_hours, tzd_minutes = [int(x) for x in tzd.split(":")] - tzd_hours *= -1 - if tzd_hours < 0: - tzd_minutes *= -1 - dt = dt + datetime.timedelta(hours=tzd_hours, minutes=tzd_minutes) - return dt - _test_converter_date = staticmethod(_converter_date) - - def _getter_bag(namespace, name, converter): - def get(self): - cached = self.cache.get(namespace, {}).get(name) - if cached: - return cached - retval = [] - for element in self.getElement("", namespace, name): - bags = element.getElementsByTagNameNS(RDF_NAMESPACE, "Bag") - if len(bags): - for bag in bags: - for item in bag.getElementsByTagNameNS(RDF_NAMESPACE, "li"): - value = self._getText(item) - value = converter(value) - retval.append(value) - ns_cache = self.cache.setdefault(namespace, {}) - ns_cache[name] = retval - return retval - return get - - def _getter_seq(namespace, name, converter): - def get(self): - cached = self.cache.get(namespace, {}).get(name) - if cached: - return cached - retval = [] - for element in self.getElement("", namespace, name): - seqs = element.getElementsByTagNameNS(RDF_NAMESPACE, "Seq") - if len(seqs): - for seq in seqs: - for item in seq.getElementsByTagNameNS(RDF_NAMESPACE, "li"): - value = self._getText(item) - value = converter(value) - retval.append(value) - else: - value = converter(self._getText(element)) - retval.append(value) - ns_cache = self.cache.setdefault(namespace, {}) - ns_cache[name] = retval - return retval - return get - - def _getter_langalt(namespace, name, converter): - def get(self): - cached = self.cache.get(namespace, {}).get(name) - if cached: - return cached - retval = {} - for element in self.getElement("", namespace, name): - alts = element.getElementsByTagNameNS(RDF_NAMESPACE, "Alt") - if len(alts): - for alt in alts: - for item in alt.getElementsByTagNameNS(RDF_NAMESPACE, "li"): - value = self._getText(item) - value = converter(value) - retval[item.getAttribute("xml:lang")] = value - else: - retval["x-default"] = converter(self._getText(element)) - ns_cache = self.cache.setdefault(namespace, {}) - ns_cache[name] = retval - return retval - return get - - def _getter_single(namespace, name, converter): - def get(self): - cached = self.cache.get(namespace, {}).get(name) - if cached: - return cached - value = None - for element in self.getElement("", namespace, name): - if element.nodeType == element.ATTRIBUTE_NODE: - value = element.nodeValue - else: - value = self._getText(element) - break - if value != None: - value = converter(value) - ns_cache = self.cache.setdefault(namespace, {}) - ns_cache[name] = value - return value - return get - - ## - # Contributors to the resource (other than the authors). An unsorted - # array of names. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - dc_contributor = property(_getter_bag(DC_NAMESPACE, "contributor", _converter_string)) - - ## - # Text describing the extent or scope of the resource. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - dc_coverage = property(_getter_single(DC_NAMESPACE, "coverage", _converter_string)) - - ## - # A sorted array of names of the authors of the resource, listed in order - # of precedence. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - dc_creator = property(_getter_seq(DC_NAMESPACE, "creator", _converter_string)) - - ## - # A sorted array of dates (datetime.datetime instances) of signifigance to - # the resource. The dates and times are in UTC. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - dc_date = property(_getter_seq(DC_NAMESPACE, "date", _converter_date)) - - ## - # A language-keyed dictionary of textual descriptions of the content of the - # resource. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - dc_description = property(_getter_langalt(DC_NAMESPACE, "description", _converter_string)) - - ## - # The mime-type of the resource. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - dc_format = property(_getter_single(DC_NAMESPACE, "format", _converter_string)) - - ## - # Unique identifier of the resource. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - dc_identifier = property(_getter_single(DC_NAMESPACE, "identifier", _converter_string)) - - ## - # An unordered array specifying the languages used in the resource. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - dc_language = property(_getter_bag(DC_NAMESPACE, "language", _converter_string)) - - ## - # An unordered array of publisher names. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - dc_publisher = property(_getter_bag(DC_NAMESPACE, "publisher", _converter_string)) - - ## - # An unordered array of text descriptions of relationships to other - # documents. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - dc_relation = property(_getter_bag(DC_NAMESPACE, "relation", _converter_string)) - - ## - # A language-keyed dictionary of textual descriptions of the rights the - # user has to this resource. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - dc_rights = property(_getter_langalt(DC_NAMESPACE, "rights", _converter_string)) - - ## - # Unique identifier of the work from which this resource was derived. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - dc_source = property(_getter_single(DC_NAMESPACE, "source", _converter_string)) - - ## - # An unordered array of descriptive phrases or keywrods that specify the - # topic of the content of the resource. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - dc_subject = property(_getter_bag(DC_NAMESPACE, "subject", _converter_string)) - - ## - # A language-keyed dictionary of the title of the resource. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - dc_title = property(_getter_langalt(DC_NAMESPACE, "title", _converter_string)) - - ## - # An unordered array of textual descriptions of the document type. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - dc_type = property(_getter_bag(DC_NAMESPACE, "type", _converter_string)) - - ## - # An unformatted text string representing document keywords. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - pdf_keywords = property(_getter_single(PDF_NAMESPACE, "Keywords", _converter_string)) - - ## - # The PDF file version, for example 1.0, 1.3. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - pdf_pdfversion = property(_getter_single(PDF_NAMESPACE, "PDFVersion", _converter_string)) - - ## - # The name of the tool that created the PDF document. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - pdf_producer = property(_getter_single(PDF_NAMESPACE, "Producer", _converter_string)) - - ## - # The date and time the resource was originally created. The date and - # time are returned as a UTC datetime.datetime object. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - xmp_createDate = property(_getter_single(XMP_NAMESPACE, "CreateDate", _converter_date)) - - ## - # The date and time the resource was last modified. The date and time - # are returned as a UTC datetime.datetime object. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - xmp_modifyDate = property(_getter_single(XMP_NAMESPACE, "ModifyDate", _converter_date)) - - ## - # The date and time that any metadata for this resource was last - # changed. The date and time are returned as a UTC datetime.datetime - # object. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - xmp_metadataDate = property(_getter_single(XMP_NAMESPACE, "MetadataDate", _converter_date)) - - ## - # The name of the first known tool used to create the resource. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - xmp_creatorTool = property(_getter_single(XMP_NAMESPACE, "CreatorTool", _converter_string)) - - ## - # The common identifier for all versions and renditions of this resource. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - xmpmm_documentId = property(_getter_single(XMPMM_NAMESPACE, "DocumentID", _converter_string)) - - ## - # An identifier for a specific incarnation of a document, updated each - # time a file is saved. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - xmpmm_instanceId = property(_getter_single(XMPMM_NAMESPACE, "InstanceID", _converter_string)) - - def custom_properties(self): - if not hasattr(self, "_custom_properties"): - self._custom_properties = {} - for node in self.getNodesInNamespace("", PDFX_NAMESPACE): - key = node.localName - while True: - # see documentation about PDFX_NAMESPACE earlier in file - idx = key.find(u"\u2182") - if idx == -1: - break - key = key[:idx] + chr(int(key[idx+1:idx+5], base=16)) + key[idx+5:] - if node.nodeType == node.ATTRIBUTE_NODE: - value = node.nodeValue - else: - value = self._getText(node) - self._custom_properties[key] = value - return self._custom_properties - - ## - # Retrieves custom metadata properties defined in the undocumented pdfx - # metadata schema. - #

Stability: Added in v1.12, will exist for all future v1.x releases. - # @return Returns a dictionary of key/value items for custom metadata - # properties. - custom_properties = property(custom_properties) - - +import re +import datetime +import decimal +from generic import PdfObject +from xml.dom import getDOMImplementation +from xml.dom.minidom import parseString + +RDF_NAMESPACE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +DC_NAMESPACE = "http://purl.org/dc/elements/1.1/" +XMP_NAMESPACE = "http://ns.adobe.com/xap/1.0/" +PDF_NAMESPACE = "http://ns.adobe.com/pdf/1.3/" +XMPMM_NAMESPACE = "http://ns.adobe.com/xap/1.0/mm/" + +# What is the PDFX namespace, you might ask? I might ask that too. It's +# a completely undocumented namespace used to place "custom metadata" +# properties, which are arbitrary metadata properties with no semantic or +# documented meaning. Elements in the namespace are key/value-style storage, +# where the element name is the key and the content is the value. The keys +# are transformed into valid XML identifiers by substituting an invalid +# identifier character with \u2182 followed by the unicode hex ID of the +# original character. A key like "my car" is therefore "my\u21820020car". +# +# \u2182, in case you're wondering, is the unicode character +# \u{ROMAN NUMERAL TEN THOUSAND}, a straightforward and obvious choice for +# escaping characters. +# +# Intentional users of the pdfx namespace should be shot on sight. A +# custom data schema and sensical XML elements could be used instead, as is +# suggested by Adobe's own documentation on XMP (under "Extensibility of +# Schemas"). +# +# Information presented here on the /pdfx/ schema is a result of limited +# reverse engineering, and does not constitute a full specification. +PDFX_NAMESPACE = "http://ns.adobe.com/pdfx/1.3/" + +iso8601 = re.compile(""" + (?P[0-9]{4}) + (- + (?P[0-9]{2}) + (- + (?P[0-9]+) + (T + (?P[0-9]{2}): + (?P[0-9]{2}) + (:(?P[0-9]{2}(.[0-9]+)?))? + (?PZ|[-+][0-9]{2}:[0-9]{2}) + )? + )? + )? + """, re.VERBOSE) + +## +# An object that represents Adobe XMP metadata. +class XmpInformation(PdfObject): + + def __init__(self, stream): + self.stream = stream + docRoot = parseString(self.stream.getData()) + self.rdfRoot = docRoot.getElementsByTagNameNS(RDF_NAMESPACE, "RDF")[0] + self.cache = {} + + def writeToStream(self, stream, encryption_key): + self.stream.writeToStream(stream, encryption_key) + + def getElement(self, aboutUri, namespace, name): + for desc in self.rdfRoot.getElementsByTagNameNS(RDF_NAMESPACE, "Description"): + if desc.getAttributeNS(RDF_NAMESPACE, "about") == aboutUri: + attr = desc.getAttributeNodeNS(namespace, name) + if attr != None: + yield attr + for element in desc.getElementsByTagNameNS(namespace, name): + yield element + + def getNodesInNamespace(self, aboutUri, namespace): + for desc in self.rdfRoot.getElementsByTagNameNS(RDF_NAMESPACE, "Description"): + if desc.getAttributeNS(RDF_NAMESPACE, "about") == aboutUri: + for i in range(desc.attributes.length): + attr = desc.attributes.item(i) + if attr.namespaceURI == namespace: + yield attr + for child in desc.childNodes: + if child.namespaceURI == namespace: + yield child + + def _getText(self, element): + text = "" + for child in element.childNodes: + if child.nodeType == child.TEXT_NODE: + text += child.data + return text + + def _converter_string(value): + return value + + def _converter_date(value): + m = iso8601.match(value) + year = int(m.group("year")) + month = int(m.group("month") or "1") + day = int(m.group("day") or "1") + hour = int(m.group("hour") or "0") + minute = int(m.group("minute") or "0") + second = decimal.Decimal(m.group("second") or "0") + seconds = second.to_integral(decimal.ROUND_FLOOR) + milliseconds = (second - seconds) * 1000000 + tzd = m.group("tzd") or "Z" + dt = datetime.datetime(year, month, day, hour, minute, seconds, milliseconds) + if tzd != "Z": + tzd_hours, tzd_minutes = [int(x) for x in tzd.split(":")] + tzd_hours *= -1 + if tzd_hours < 0: + tzd_minutes *= -1 + dt = dt + datetime.timedelta(hours=tzd_hours, minutes=tzd_minutes) + return dt + _test_converter_date = staticmethod(_converter_date) + + def _getter_bag(namespace, name, converter): + def get(self): + cached = self.cache.get(namespace, {}).get(name) + if cached: + return cached + retval = [] + for element in self.getElement("", namespace, name): + bags = element.getElementsByTagNameNS(RDF_NAMESPACE, "Bag") + if len(bags): + for bag in bags: + for item in bag.getElementsByTagNameNS(RDF_NAMESPACE, "li"): + value = self._getText(item) + value = converter(value) + retval.append(value) + ns_cache = self.cache.setdefault(namespace, {}) + ns_cache[name] = retval + return retval + return get + + def _getter_seq(namespace, name, converter): + def get(self): + cached = self.cache.get(namespace, {}).get(name) + if cached: + return cached + retval = [] + for element in self.getElement("", namespace, name): + seqs = element.getElementsByTagNameNS(RDF_NAMESPACE, "Seq") + if len(seqs): + for seq in seqs: + for item in seq.getElementsByTagNameNS(RDF_NAMESPACE, "li"): + value = self._getText(item) + value = converter(value) + retval.append(value) + else: + value = converter(self._getText(element)) + retval.append(value) + ns_cache = self.cache.setdefault(namespace, {}) + ns_cache[name] = retval + return retval + return get + + def _getter_langalt(namespace, name, converter): + def get(self): + cached = self.cache.get(namespace, {}).get(name) + if cached: + return cached + retval = {} + for element in self.getElement("", namespace, name): + alts = element.getElementsByTagNameNS(RDF_NAMESPACE, "Alt") + if len(alts): + for alt in alts: + for item in alt.getElementsByTagNameNS(RDF_NAMESPACE, "li"): + value = self._getText(item) + value = converter(value) + retval[item.getAttribute("xml:lang")] = value + else: + retval["x-default"] = converter(self._getText(element)) + ns_cache = self.cache.setdefault(namespace, {}) + ns_cache[name] = retval + return retval + return get + + def _getter_single(namespace, name, converter): + def get(self): + cached = self.cache.get(namespace, {}).get(name) + if cached: + return cached + value = None + for element in self.getElement("", namespace, name): + if element.nodeType == element.ATTRIBUTE_NODE: + value = element.nodeValue + else: + value = self._getText(element) + break + if value != None: + value = converter(value) + ns_cache = self.cache.setdefault(namespace, {}) + ns_cache[name] = value + return value + return get + + ## + # Contributors to the resource (other than the authors). An unsorted + # array of names. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + dc_contributor = property(_getter_bag(DC_NAMESPACE, "contributor", _converter_string)) + + ## + # Text describing the extent or scope of the resource. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + dc_coverage = property(_getter_single(DC_NAMESPACE, "coverage", _converter_string)) + + ## + # A sorted array of names of the authors of the resource, listed in order + # of precedence. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + dc_creator = property(_getter_seq(DC_NAMESPACE, "creator", _converter_string)) + + ## + # A sorted array of dates (datetime.datetime instances) of signifigance to + # the resource. The dates and times are in UTC. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + dc_date = property(_getter_seq(DC_NAMESPACE, "date", _converter_date)) + + ## + # A language-keyed dictionary of textual descriptions of the content of the + # resource. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + dc_description = property(_getter_langalt(DC_NAMESPACE, "description", _converter_string)) + + ## + # The mime-type of the resource. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + dc_format = property(_getter_single(DC_NAMESPACE, "format", _converter_string)) + + ## + # Unique identifier of the resource. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + dc_identifier = property(_getter_single(DC_NAMESPACE, "identifier", _converter_string)) + + ## + # An unordered array specifying the languages used in the resource. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + dc_language = property(_getter_bag(DC_NAMESPACE, "language", _converter_string)) + + ## + # An unordered array of publisher names. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + dc_publisher = property(_getter_bag(DC_NAMESPACE, "publisher", _converter_string)) + + ## + # An unordered array of text descriptions of relationships to other + # documents. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + dc_relation = property(_getter_bag(DC_NAMESPACE, "relation", _converter_string)) + + ## + # A language-keyed dictionary of textual descriptions of the rights the + # user has to this resource. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + dc_rights = property(_getter_langalt(DC_NAMESPACE, "rights", _converter_string)) + + ## + # Unique identifier of the work from which this resource was derived. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + dc_source = property(_getter_single(DC_NAMESPACE, "source", _converter_string)) + + ## + # An unordered array of descriptive phrases or keywrods that specify the + # topic of the content of the resource. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + dc_subject = property(_getter_bag(DC_NAMESPACE, "subject", _converter_string)) + + ## + # A language-keyed dictionary of the title of the resource. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + dc_title = property(_getter_langalt(DC_NAMESPACE, "title", _converter_string)) + + ## + # An unordered array of textual descriptions of the document type. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + dc_type = property(_getter_bag(DC_NAMESPACE, "type", _converter_string)) + + ## + # An unformatted text string representing document keywords. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + pdf_keywords = property(_getter_single(PDF_NAMESPACE, "Keywords", _converter_string)) + + ## + # The PDF file version, for example 1.0, 1.3. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + pdf_pdfversion = property(_getter_single(PDF_NAMESPACE, "PDFVersion", _converter_string)) + + ## + # The name of the tool that created the PDF document. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + pdf_producer = property(_getter_single(PDF_NAMESPACE, "Producer", _converter_string)) + + ## + # The date and time the resource was originally created. The date and + # time are returned as a UTC datetime.datetime object. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + xmp_createDate = property(_getter_single(XMP_NAMESPACE, "CreateDate", _converter_date)) + + ## + # The date and time the resource was last modified. The date and time + # are returned as a UTC datetime.datetime object. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + xmp_modifyDate = property(_getter_single(XMP_NAMESPACE, "ModifyDate", _converter_date)) + + ## + # The date and time that any metadata for this resource was last + # changed. The date and time are returned as a UTC datetime.datetime + # object. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + xmp_metadataDate = property(_getter_single(XMP_NAMESPACE, "MetadataDate", _converter_date)) + + ## + # The name of the first known tool used to create the resource. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + xmp_creatorTool = property(_getter_single(XMP_NAMESPACE, "CreatorTool", _converter_string)) + + ## + # The common identifier for all versions and renditions of this resource. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + xmpmm_documentId = property(_getter_single(XMPMM_NAMESPACE, "DocumentID", _converter_string)) + + ## + # An identifier for a specific incarnation of a document, updated each + # time a file is saved. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + xmpmm_instanceId = property(_getter_single(XMPMM_NAMESPACE, "InstanceID", _converter_string)) + + def custom_properties(self): + if not hasattr(self, "_custom_properties"): + self._custom_properties = {} + for node in self.getNodesInNamespace("", PDFX_NAMESPACE): + key = node.localName + while True: + # see documentation about PDFX_NAMESPACE earlier in file + idx = key.find(u"\u2182") + if idx == -1: + break + key = key[:idx] + chr(int(key[idx+1:idx+5], base=16)) + key[idx+5:] + if node.nodeType == node.ATTRIBUTE_NODE: + value = node.nodeValue + else: + value = self._getText(node) + self._custom_properties[key] = value + return self._custom_properties + + ## + # Retrieves custom metadata properties defined in the undocumented pdfx + # metadata schema. + #

Stability: Added in v1.12, will exist for all future v1.x releases. + # @return Returns a dictionary of key/value items for custom metadata + # properties. + custom_properties = property(custom_properties) + + From 8ce6a08a7bf02dda963041cd68660a97baa4766c Mon Sep 17 00:00:00 2001 From: Yogesh Sakhreliya Date: Mon, 10 Jan 2011 15:14:13 +0530 Subject: [PATCH 16/41] [FIX] Fields.function : Correct accessors passed to fields of type integer and integer_big bzr revid: ysa@tinyerp.com-20110110094413-3640rl39pj1r9v1t --- bin/osv/fields.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bin/osv/fields.py b/bin/osv/fields.py index 6f720ea4669..a6ab3a97072 100644 --- a/bin/osv/fields.py +++ b/bin/osv/fields.py @@ -762,6 +762,11 @@ class function(_column): self._symbol_f = boolean._symbol_f self._symbol_set = boolean._symbol_set + if type in ['integer','integer_big']: + self._symbol_c = integer._symbol_c + self._symbol_f = integer._symbol_f + self._symbol_set = integer._symbol_set + def digits_change(self, cr): if self.digits_compute: t = self.digits_compute(cr) From 480026aa89572869911bd6ebae6d24eb52a3d5df Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 11 Jan 2011 12:23:16 +0100 Subject: [PATCH 17/41] [I18N] base: updated translation templates after latest changes bzr revid: odo@openerp.com-20110111112316-p10xvk290eknyyz6 --- bin/addons/base/i18n/base.pot | 444 +++++++++++++++++++++------------- 1 file changed, 270 insertions(+), 174 deletions(-) diff --git a/bin/addons/base/i18n/base.pot b/bin/addons/base/i18n/base.pot index a5507d852a8..9484a376151 100644 --- a/bin/addons/base/i18n/base.pot +++ b/bin/addons/base/i18n/base.pot @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: OpenERP Server 6.0.0-rc2\n" "Report-Msgid-Bugs-To: support@openerp.com\n" -"POT-Creation-Date: 2011-01-03 16:57:13+0000\n" -"PO-Revision-Date: 2011-01-03 16:57:13+0000\n" +"POT-Creation-Date: 2011-01-11 11:14:51+0000\n" +"PO-Revision-Date: 2011-01-11 11:14:51+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -108,11 +108,16 @@ msgid "Created Views" msgstr "" #. module: base -#: code:addons/base/ir/ir_model.py:339 +#: code:addons/base/ir/ir_model.py:485 #, python-format msgid "You can not write in this document (%s) ! Be sure your user belongs to one of these groups: %s." msgstr "" +#. module: base +#: help:ir.model.fields,domain:0 +msgid "The optional domain to restrict possible values for relationship fields, specified as a Python expression defining a list of triplets. For example: [('color','=','red')]" +msgstr "" + #. module: base #: field:res.partner,ref:0 msgid "Reference" @@ -124,8 +129,15 @@ msgid "Target Window" msgstr "" #. module: base -#: model:res.country,name:base.kr -msgid "South Korea" +#: code:addons/base/res/res_user.py:507 +#, python-format +msgid "Warning!" +msgstr "" + +#. module: base +#: code:addons/base/ir/ir_model.py:304 +#, python-format +msgid "Properties of base fields cannot be altered in this manner! Please modify them through Python code, preferably through a custom addon!" msgstr "" #. module: base @@ -331,7 +343,7 @@ msgid "Netherlands Antilles" msgstr "" #. module: base -#: code:addons/base/res/res_user.py:373 +#: code:addons/base/res/res_user.py:389 #, python-format msgid "You can not remove the admin user as it is used internally for resources created by OpenERP (updates, module installation, ...)" msgstr "" @@ -367,6 +379,11 @@ msgstr "" msgid "This ISO code is the name of po files to use for translations" msgstr "" +#. module: base +#: view:base.module.upgrade:0 +msgid "Your system will be updated." +msgstr "" + #. module: base #: field:ir.actions.todo,note:0 #: selection:ir.property,type:0 @@ -433,7 +450,7 @@ msgid "Miscellaneous Suppliers" msgstr "" #. module: base -#: code:addons/base/ir/ir_model.py:216 +#: code:addons/base/ir/ir_model.py:255 #, python-format msgid "Custom fields must have a name that starts with 'x_' !" msgstr "" @@ -758,6 +775,12 @@ msgstr "" msgid "Human Resources Dashboard" msgstr "" +#. module: base +#: code:addons/base/res/res_user.py:507 +#, python-format +msgid "Setting empty passwords is not allowed for security reasons!" +msgstr "" + #. module: base #: selection:ir.actions.server,state:0 #: selection:workflow.activity,kind:0 @@ -774,6 +797,11 @@ msgstr "" msgid "Cayman Islands" msgstr "" +#. module: base +#: model:res.country,name:base.kr +msgid "South Korea" +msgstr "" + #. module: base #: model:ir.actions.act_window,name:base.action_workflow_transition_form #: model:ir.ui.menu,name:base.menu_workflow_transition @@ -874,6 +902,12 @@ msgstr "" msgid "Marshall Islands" msgstr "" +#. module: base +#: code:addons/base/ir/ir_model.py:328 +#, python-format +msgid "Changing the model of a field is forbidden!" +msgstr "" + #. module: base #: model:res.country,name:base.ht msgid "Haiti" @@ -898,6 +932,12 @@ msgstr "" msgid "2. Group-specific rules are combined together with a logical AND operator" msgstr "" +#. module: base +#: code:addons/base/res/res_user.py:206 +#, python-format +msgid "Operation Canceled" +msgstr "" + #. module: base #: help:base.language.export,lang:0 msgid "To export a new language, do not select a language." @@ -1023,13 +1063,19 @@ msgstr "" msgid "Reports" msgstr "" +#. module: base +#: help:ir.actions.act_window.view,multi:0 +#: help:ir.actions.report.xml,multi:0 +msgid "If set to true, the action will not be displayed on the right toolbar of a form view." +msgstr "" + #. module: base #: field:workflow,on_create:0 msgid "On Create" msgstr "" #. module: base -#: code:addons/base/ir/ir_model.py:461 +#: code:addons/base/ir/ir_model.py:607 #, python-format msgid "'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id" msgstr "" @@ -1067,8 +1113,10 @@ msgid "Wizard Info" msgstr "" #. module: base -#: model:res.country,name:base.km -msgid "Comoros" +#: view:base.language.export:0 +#: model:ir.actions.act_window,name:base.action_wizard_lang_export +#: model:ir.ui.menu,name:base.menu_wizard_lang_export +msgid "Export Translation" msgstr "" #. module: base @@ -1122,11 +1170,6 @@ msgstr "" msgid "Day: %(day)s" msgstr "" -#. module: base -#: model:ir.actions.act_window,help:base.open_module_tree -msgid "List all certified modules available to configure your OpenERP. Modules that are installed are flagged as such. You can search for a specific module using the name or the description of the module. You do not need to install modules one by one, you can install many at the same time by clicking on the schedule button in this list. Then, apply the schedule upgrade from Action menu once for all the ones you have scheduled for installation." -msgstr "" - #. module: base #: model:res.country,name:base.mv msgid "Maldives" @@ -1230,7 +1273,7 @@ msgid "Formula" msgstr "" #. module: base -#: code:addons/base/res/res_user.py:373 +#: code:addons/base/res/res_user.py:389 #, python-format msgid "Can not remove root user!" msgstr "" @@ -1242,7 +1285,7 @@ msgstr "" #. module: base #: code:addons/base/res/res_user.py:51 -#: code:addons/base/res/res_user.py:397 +#: code:addons/base/res/res_user.py:413 #, python-format msgid "%s (copy)" msgstr "" @@ -1400,11 +1443,6 @@ msgstr "" msgid "Example: GLOBAL_RULE_1 AND GLOBAL_RULE_2 AND ( (GROUP_A_RULE_1 AND GROUP_A_RULE_2) OR (GROUP_B_RULE_1 AND GROUP_B_RULE_2) )" msgstr "" -#. module: base -#: field:change.user.password,new_password:0 -msgid "New Password" -msgstr "" - #. module: base #: model:ir.actions.act_window,help:base.action_ui_view msgid "Views allows you to personalize each view of OpenERP. You can add new fields, move fields, rename them or delete the ones that you do not need." @@ -1500,6 +1538,11 @@ msgstr "" msgid "Groups (no group = global)" msgstr "" +#. module: base +#: model:res.country,name:base.fo +msgid "Faroe Islands" +msgstr "" + #. module: base #: selection:res.config.users,view:0 #: selection:res.config.view,view:0 @@ -1533,7 +1576,7 @@ msgid "Madagascar" msgstr "" #. module: base -#: code:addons/base/ir/ir_model.py:94 +#: code:addons/base/ir/ir_model.py:96 #, python-format msgid "The Object name must start with x_ and not contain any special character !" msgstr "" @@ -1716,6 +1759,12 @@ msgid "Messages" msgstr "" #. module: base +#: code:addons/base/ir/ir_model.py:303 +#: code:addons/base/ir/ir_model.py:317 +#: code:addons/base/ir/ir_model.py:319 +#: code:addons/base/ir/ir_model.py:321 +#: code:addons/base/ir/ir_model.py:328 +#: code:addons/base/ir/ir_model.py:331 #: code:addons/base/module/wizard/base_update_translations.py:38 #, python-format msgid "Error!" @@ -1772,6 +1821,11 @@ msgstr "" msgid "Korean (KR) / 한국어 (KR)" msgstr "" +#. module: base +#: help:ir.model.fields,model:0 +msgid "The technical name of the model this field belongs to" +msgstr "" + #. module: base #: field:ir.actions.server,action_id:0 #: selection:ir.actions.server,state:0 @@ -1809,6 +1863,11 @@ msgstr "" msgid "Cuba" msgstr "" +#. module: base +#: model:ir.model,name:base.model_res_partner_event +msgid "res.partner.event" +msgstr "" + #. module: base #: model:res.widget,title:base.facebook_widget msgid "Facebook" @@ -2007,6 +2066,11 @@ msgstr "" msgid "workflow.activity" msgstr "" +#. module: base +#: help:ir.ui.view_sc,res_id:0 +msgid "Reference of the target resource, whose model/table depends on the 'Resource Name' field." +msgstr "" + #. module: base #: field:ir.model.fields,select_level:0 msgid "Searchable" @@ -2091,6 +2155,7 @@ msgstr "" #: view:ir.module.module:0 #: field:ir.module.module.dependency,module_id:0 #: report:ir.module.reference.graph:0 +#: field:ir.translation,module:0 msgid "Module" msgstr "" @@ -2159,7 +2224,7 @@ msgid "Mayotte" msgstr "" #. module: base -#: code:addons/base/ir/ir_actions.py:593 +#: code:addons/base/ir/ir_actions.py:597 #, python-format msgid "Please specify an action to launch !" msgstr "" @@ -2307,11 +2372,6 @@ msgstr "" msgid "%x - Appropriate date representation." msgstr "" -#. module: base -#: model:ir.model,name:base.model_change_user_password -msgid "change.user.password" -msgstr "" - #. module: base #: view:res.lang:0 msgid "%d - Day of the month [01,31]." @@ -2625,11 +2685,6 @@ msgstr "" msgid "Trigger Object" msgstr "" -#. module: base -#: help:change.user.password,confirm_password:0 -msgid "Enter the new password again for confirmation." -msgstr "" - #. module: base #: view:res.users:0 msgid "Current Activity" @@ -2668,8 +2723,8 @@ msgid "Sequence Type" msgstr "" #. module: base -#: selection:base.language.install,lang:0 -msgid "Hindi / हिंदी" +#: view:ir.ui.view.custom:0 +msgid "Customized Architecture" msgstr "" #. module: base @@ -2694,6 +2749,7 @@ msgstr "" #. module: base #: field:ir.actions.server,srcmodel_id:0 +#: field:ir.model.fields,model_id:0 msgid "Model" msgstr "" @@ -2798,8 +2854,8 @@ msgid "You try to remove a module that is installed or will be installed" msgstr "" #. module: base -#: help:ir.values,key2:0 -msgid "The kind of action or button in the client side that will trigger the action." +#: view:base.module.upgrade:0 +msgid "The selected modules have been updated / installed !" msgstr "" #. module: base @@ -2820,6 +2876,11 @@ msgstr "" msgid "Workflows" msgstr "" +#. module: base +#: field:ir.translation,xml_id:0 +msgid "XML Id" +msgstr "" + #. module: base #: model:ir.actions.act_window,name:base.action_config_user_form msgid "Create Users" @@ -2858,7 +2919,7 @@ msgid "Lesotho" msgstr "" #. module: base -#: code:addons/base/ir/ir_model.py:112 +#: code:addons/base/ir/ir_model.py:114 #, python-format msgid "You can not remove the model '%s' !" msgstr "" @@ -2956,7 +3017,7 @@ msgid "API ID" msgstr "" #. module: base -#: code:addons/base/ir/ir_model.py:340 +#: code:addons/base/ir/ir_model.py:486 #, python-format msgid "You can not create this document (%s) ! Be sure your user belongs to one of these groups: %s." msgstr "" @@ -3080,11 +3141,6 @@ msgstr "" msgid "System update completed" msgstr "" -#. module: base -#: field:ir.model.fields,selection:0 -msgid "Field Selection" -msgstr "" - #. module: base #: selection:res.request,state:0 msgid "draft" @@ -3122,9 +3178,9 @@ msgid "Apply For Delete" msgstr "" #. module: base -#: help:ir.actions.act_window.view,multi:0 -#: help:ir.actions.report.xml,multi:0 -msgid "If set to true, the action will not be displayed on the right toolbar of a form view." +#: code:addons/base/ir/ir_model.py:319 +#, python-format +msgid "Cannot rename column to %s, because that column already exists!" msgstr "" #. module: base @@ -3300,6 +3356,12 @@ msgstr "" msgid "Montserrat" msgstr "" +#. module: base +#: code:addons/base/ir/ir_model.py:205 +#, python-format +msgid "The Selection Options expression is not a valid Pythonic expression.Please provide an expression in the [('key','Label'), ...] format." +msgstr "" + #. module: base #: model:ir.ui.menu,name:base.menu_translation_app msgid "Application Terms" @@ -3337,8 +3399,8 @@ msgid "Starter Partner" msgstr "" #. module: base -#: view:base.module.upgrade:0 -msgid "Your system will be updated." +#: help:ir.model.fields,relation_field:0 +msgid "For one2many fields, the field on the target model that implement the opposite many2one relationship" msgstr "" #. module: base @@ -3507,7 +3569,7 @@ msgid "Flow Start" msgstr "" #. module: base -#: code:addons/__init__.py:827 +#: code:addons/__init__.py:834 #, python-format msgid "module base cannot be loaded! (hint: verify addons-path)" msgstr "" @@ -3539,9 +3601,9 @@ msgid "Guadeloupe (French)" msgstr "" #. module: base -#: code:addons/base/res/res_lang.py:86 -#: code:addons/base/res/res_lang.py:88 -#: code:addons/base/res/res_lang.py:90 +#: code:addons/base/res/res_lang.py:157 +#: code:addons/base/res/res_lang.py:159 +#: code:addons/base/res/res_lang.py:161 #, python-format msgid "User Error" msgstr "" @@ -3603,6 +3665,11 @@ msgstr "" msgid "Partner Addresses" msgstr "" +#. module: base +#: help:ir.model.fields,translate:0 +msgid "Whether values for this field can be translated (enables the translation mechanism for that field)" +msgstr "" + #. module: base #: view:res.lang:0 msgid "%S - Seconds [00,61]." @@ -3764,11 +3831,6 @@ msgstr "" msgid "Menus" msgstr "" -#. module: base -#: view:base.module.upgrade:0 -msgid "The selected modules have been updated / installed !" -msgstr "" - #. module: base #: selection:base.language.install,lang:0 msgid "Serbian (Latin) / srpski" @@ -3955,6 +4017,11 @@ msgstr "" msgid "Provide the field name where the record id is stored after the create operations. If it is empty, you can not track the new record." msgstr "" +#. module: base +#: help:ir.model.fields,relation:0 +msgid "For relationship fields, the technical name of the target model" +msgstr "" + #. module: base #: selection:base.language.install,lang:0 msgid "Indonesian / Bahasa Indonesia" @@ -4006,6 +4073,11 @@ msgstr "" msgid "Serial Key" msgstr "" +#. module: base +#: selection:res.request,priority:0 +msgid "Low" +msgstr "" + #. module: base #: model:ir.ui.menu,name:base.menu_audit msgid "Audit" @@ -4036,11 +4108,6 @@ msgstr "" msgid "Create Access" msgstr "" -#. module: base -#: field:change.user.password,current_password:0 -msgid "Current Password" -msgstr "" - #. module: base #: field:res.partner.address,state_id:0 msgid "Fed. State" @@ -4233,6 +4300,12 @@ msgstr "" msgid "The configuration wizards are used to help you configure a new instance of OpenERP. They are launched during the installation of new modules, but you can choose to restart some wizards manually from this menu." msgstr "" +#. module: base +#: code:addons/base/res/res_user.py:206 +#, python-format +msgid "Please use the change password wizard (in User Preferences or User menu) to change your own password." +msgstr "" + #. module: base #: code:addons/orm.py:1350 #, python-format @@ -4489,7 +4562,7 @@ msgid "Change My Preferences" msgstr "" #. module: base -#: code:addons/base/ir/ir_actions.py:160 +#: code:addons/base/ir/ir_actions.py:164 #, python-format msgid "Invalid model name in the action definition." msgstr "" @@ -4678,8 +4751,8 @@ msgid "ir.ui.menu" msgstr "" #. module: base -#: view:partner.wizard.ean.check:0 -msgid "Ignore" +#: model:res.country,name:base.us +msgid "United States" msgstr "" #. module: base @@ -4705,7 +4778,7 @@ msgid "ir.server.object.lines" msgstr "" #. module: base -#: code:addons/base/module/module.py:530 +#: code:addons/base/module/module.py:531 #, python-format msgid "Module %s: Invalid Quality Certificate" msgstr "" @@ -4736,8 +4809,9 @@ msgid "Nigeria" msgstr "" #. module: base -#: model:ir.model,name:base.model_res_partner_event -msgid "res.partner.event" +#: code:addons/base/ir/ir_model.py:250 +#, python-format +msgid "For selection fields, the Selection Options must be given!" msgstr "" #. module: base @@ -4760,12 +4834,6 @@ msgstr "" msgid "Values for Event Type" msgstr "" -#. module: base -#: code:addons/base/res/res_user.py:605 -#, python-format -msgid "The current password does not match, please double-check it." -msgstr "" - #. module: base #: selection:ir.model.fields,select_level:0 msgid "Always Searchable" @@ -4878,6 +4946,13 @@ msgstr "" msgid "If you have groups, the visibility of this menu will be based on these groups. If this field is empty, OpenERP will compute visibility based on the related object's read access." msgstr "" +#. module: base +#: model:ir.actions.act_window,name:base.action_ui_view_custom +#: model:ir.ui.menu,name:base.menu_action_ui_view_custom +#: view:ir.ui.view.custom:0 +msgid "Customized Views" +msgstr "" + #. module: base #: view:partner.sms.send:0 msgid "Bulk SMS send" @@ -4900,7 +4975,7 @@ msgid "Unable to upgrade module \"%s\" because an external dependency is not met msgstr "" #. module: base -#: code:addons/base/res/res_user.py:241 +#: code:addons/base/res/res_user.py:257 #, python-format msgid "Please keep in mind that documents currently displayed may not be relevant after switching to another company. If you have unsaved changes, please make sure to save and close all forms before switching to a different company. (You can click on Cancel in the User Preferences now)" msgstr "" @@ -5073,6 +5148,12 @@ msgstr "" msgid "Reunion (French)" msgstr "" +#. module: base +#: code:addons/base/ir/ir_model.py:321 +#, python-format +msgid "New column name must still start with x_ , because it is a custom field!" +msgstr "" + #. module: base #: view:ir.model.access:0 #: view:ir.rule:0 @@ -5080,11 +5161,6 @@ msgstr "" msgid "Global" msgstr "" -#. module: base -#: view:change.user.password:0 -msgid "You must logout and login again after changing your password." -msgstr "" - #. module: base #: model:res.country,name:base.mp msgid "Northern Mariana Islands" @@ -5096,7 +5172,7 @@ msgid "Solomon Islands" msgstr "" #. module: base -#: code:addons/base/ir/ir_model.py:344 +#: code:addons/base/ir/ir_model.py:490 #: code:addons/orm.py:1897 #: code:addons/orm.py:2972 #: code:addons/orm.py:3165 @@ -5112,7 +5188,7 @@ msgid "Waiting" msgstr "" #. module: base -#: code:addons/__init__.py:827 +#: code:addons/__init__.py:834 #, python-format msgid "Could not load base module" msgstr "" @@ -5166,8 +5242,8 @@ msgid "Module Category" msgstr "" #. module: base -#: model:res.country,name:base.us -msgid "United States" +#: view:partner.wizard.ean.check:0 +msgid "Ignore" msgstr "" #. module: base @@ -5317,13 +5393,13 @@ msgid "res.widget" msgstr "" #. module: base -#: code:addons/base/ir/ir_model.py:219 +#: code:addons/base/ir/ir_model.py:258 #, python-format msgid "Model %s does not exist!" msgstr "" #. module: base -#: code:addons/base/res/res_lang.py:88 +#: code:addons/base/res/res_lang.py:159 #, python-format msgid "You cannot delete the language which is User's Preferred Language !" msgstr "" @@ -5358,7 +5434,6 @@ msgstr "" #: view:base.module.update:0 #: view:base.module.upgrade:0 #: view:base.update.translations:0 -#: view:change.user.password:0 #: view:partner.clear.ids:0 #: view:partner.sms.send:0 #: view:partner.wizard.spam:0 @@ -5377,6 +5452,11 @@ msgstr "" msgid "Neutral Zone" msgstr "" +#. module: base +#: selection:base.language.install,lang:0 +msgid "Hindi / हिंदी" +msgstr "" + #. module: base #: view:ir.model:0 msgid "Custom" @@ -5387,11 +5467,6 @@ msgstr "" msgid "Current" msgstr "" -#. module: base -#: help:change.user.password,new_password:0 -msgid "Enter the new password." -msgstr "" - #. module: base #: model:res.partner.category,name:base.res_partner_category_9 msgid "Components Supplier" @@ -5502,7 +5577,7 @@ msgid "Select the object on which the action will work (read, write, create)." msgstr "" #. module: base -#: code:addons/base/ir/ir_actions.py:625 +#: code:addons/base/ir/ir_actions.py:629 #, python-format msgid "Please specify server option --email-from !" msgstr "" @@ -5658,11 +5733,6 @@ msgstr "" msgid "Sequence Codes" msgstr "" -#. module: base -#: field:change.user.password,confirm_password:0 -msgid "Confirm Password" -msgstr "" - #. module: base #: selection:base.language.install,lang:0 msgid "Spanish (CO) / Español (CO)" @@ -5698,6 +5768,12 @@ msgstr "" msgid "res.log" msgstr "" +#. module: base +#: help:ir.translation,module:0 +#: help:ir.translation,xml_id:0 +msgid "Maps to the ir_model_data for which this translation is provided." +msgstr "" + #. module: base #: view:workflow.activity:0 #: field:workflow.activity,flow_stop:0 @@ -5716,8 +5792,6 @@ msgstr "" #. module: base #: code:addons/base/module/wizard/base_module_import.py:67 -#: code:addons/base/res/res_user.py:594 -#: code:addons/base/res/res_user.py:605 #, python-format msgid "Error !" msgstr "" @@ -5826,12 +5900,6 @@ msgstr "" msgid "Pitcairn Island" msgstr "" -#. module: base -#: code:addons/base/res/res_user.py:594 -#, python-format -msgid "The new and confirmation passwords do not match, please double-check them." -msgstr "" - #. module: base #: view:base.module.upgrade:0 msgid "We suggest to reload the menu tab to see the new menus (Ctrl+T then Ctrl+R)." @@ -5912,11 +5980,6 @@ msgstr "" msgid "Done" msgstr "" -#. module: base -#: view:change.user.password:0 -msgid "Change" -msgstr "" - #. module: base #: model:res.partner.title,name:base.res_partner_title_miss msgid "Miss" @@ -6003,7 +6066,7 @@ msgid "To browse official translations, you can start with these links:" msgstr "" #. module: base -#: code:addons/base/ir/ir_model.py:338 +#: code:addons/base/ir/ir_model.py:484 #, python-format msgid "You can not read this document (%s) ! Be sure your user belongs to one of these groups: %s." msgstr "" @@ -6102,6 +6165,11 @@ msgid "No rate found \n" "at the date: %s" msgstr "" +#. module: base +#: model:ir.actions.act_window,help:base.action_ui_view_custom +msgid "Customized views are used when users reorganize the content of their dashboard views (via web client)" +msgstr "" + #. module: base #: field:ir.model,name:0 #: field:ir.model.fields,model:0 @@ -6132,6 +6200,11 @@ msgstr "" msgid "Icon" msgstr "" +#. module: base +#: help:ir.model.fields,model_id:0 +msgid "The model this field belongs to" +msgstr "" + #. module: base #: model:res.country,name:base.mq msgid "Martinique (French)" @@ -6177,7 +6250,7 @@ msgid "Samoa" msgstr "" #. module: base -#: code:addons/base/res/res_lang.py:90 +#: code:addons/base/res/res_lang.py:161 #, python-format msgid "You cannot delete the language which is Active !\n" "Please de-activate the language first." @@ -6195,8 +6268,8 @@ msgid "Child IDs" msgstr "" #. module: base -#: code:addons/base/ir/ir_actions.py:709 -#: code:addons/base/ir/ir_actions.py:712 +#: code:addons/base/ir/ir_actions.py:713 +#: code:addons/base/ir/ir_actions.py:716 #, python-format msgid "Problem in configuration `Record Id` in Server Action!" msgstr "" @@ -6498,8 +6571,8 @@ msgid "Grenada" msgstr "" #. module: base -#: view:ir.actions.server:0 -msgid "Trigger Configuration" +#: model:res.country,name:base.wf +msgid "Wallis and Futuna Islands" msgstr "" #. module: base @@ -6536,7 +6609,7 @@ msgid "ir.wizard.screen" msgstr "" #. module: base -#: code:addons/base/ir/ir_model.py:189 +#: code:addons/base/ir/ir_model.py:223 #, python-format msgid "Size of the field can never be less than 1 !" msgstr "" @@ -6684,10 +6757,8 @@ msgid "Manufacturing" msgstr "" #. module: base -#: view:base.language.export:0 -#: model:ir.actions.act_window,name:base.action_wizard_lang_export -#: model:ir.ui.menu,name:base.menu_wizard_lang_export -msgid "Export Translation" +#: model:res.country,name:base.km +msgid "Comoros" msgstr "" #. module: base @@ -6702,6 +6773,11 @@ msgstr "" msgid "Cancel Install" msgstr "" +#. module: base +#: field:ir.model.fields,selection:0 +msgid "Selection Options" +msgstr "" + #. module: base #: field:res.partner.category,parent_right:0 msgid "Right parent" @@ -6718,7 +6794,7 @@ msgid "Copy Object" msgstr "" #. module: base -#: code:addons/base/res/res_user.py:551 +#: code:addons/base/res/res_user.py:581 #, python-format msgid "Group(s) cannot be deleted, because some user(s) still belong to them: %s !" msgstr "" @@ -6804,13 +6880,19 @@ msgid "Number of time the function is called,\n" "a negative number indicates no limit" msgstr "" +#. module: base +#: code:addons/base/ir/ir_model.py:331 +#, python-format +msgid "Changing the type of a column is not yet supported. Please drop it and create it again!" +msgstr "" + #. module: base #: field:ir.ui.view_sc,user_id:0 msgid "User Ref." msgstr "" #. module: base -#: code:addons/base/res/res_user.py:550 +#: code:addons/base/res/res_user.py:580 #, python-format msgid "Warning !" msgstr "" @@ -6956,7 +7038,7 @@ msgid "workflow.triggers" msgstr "" #. module: base -#: code:addons/base/ir/ir_model.py:60 +#: code:addons/base/ir/ir_model.py:62 #, python-format msgid "Invalid search criterions" msgstr "" @@ -6991,13 +7073,6 @@ msgstr "" msgid "Selection" msgstr "" -#. module: base -#: view:change.user.password:0 -#: model:ir.actions.act_window,name:base.action_view_change_password_form -#: view:res.users:0 -msgid "Change Password" -msgstr "" - #. module: base #: field:res.company,rml_header1:0 msgid "Report Header" @@ -7133,6 +7208,12 @@ msgstr "" msgid "Norwegian Bokmål / Norsk bokmål" msgstr "" +#. module: base +#: help:res.config.users,new_password:0 +#: help:res.users,new_password:0 +msgid "Only specify a value if you want to change the user password. This user will have to logout and login again!" +msgstr "" + #. module: base #: model:res.partner.title,name:base.res_partner_title_madam msgid "Madam" @@ -7153,6 +7234,12 @@ msgstr "" msgid "Binary File or external URL" msgstr "" +#. module: base +#: field:res.config.users,new_password:0 +#: field:res.users,new_password:0 +msgid "Change password" +msgstr "" + #. module: base #: model:res.country,name:base.nl msgid "Netherlands" @@ -7178,6 +7265,11 @@ msgstr "" msgid "Occitan (FR, post 1500) / Occitan" msgstr "" +#. module: base +#: model:ir.actions.act_window,help:base.open_module_tree +msgid "You can install new modules in order to activate new features, menu, reports or data in your OpenERP instance. To install some modules, click on the button \"Schedule for Installation\" from the form view, then click on \"Apply Scheduled Upgrades\" to migrate your system." +msgstr "" + #. module: base #: model:ir.ui.menu,name:base.menu_emails #: model:ir.ui.menu,name:base.menu_mail_gateway @@ -7242,11 +7334,6 @@ msgstr "" msgid "Croatian / hrvatski jezik" msgstr "" -#. module: base -#: help:change.user.password,current_password:0 -msgid "Enter your current password." -msgstr "" - #. module: base #: field:base.language.install,overwrite:0 msgid "Overwrite Existing Terms" @@ -7263,8 +7350,8 @@ msgid "The code of the country must be unique !" msgstr "" #. module: base -#: model:ir.ui.menu,name:base.menu_project_management_time_tracking -msgid "Time Tracking" +#: selection:ir.module.module.dependency,state:0 +msgid "Uninstallable" msgstr "" #. module: base @@ -7306,8 +7393,8 @@ msgid "Menu Action" msgstr "" #. module: base -#: model:res.country,name:base.fo -msgid "Faroe Islands" +#: help:ir.model.fields,selection:0 +msgid "List of options for a selection field, specified as a Python expression defining a list of (key, label) pairs. For example: [('blue','Blue'),('yellow','Yellow')]" msgstr "" #. module: base @@ -7429,6 +7516,12 @@ msgstr "" msgid "Name of the method to be called on the object when this scheduler is executed." msgstr "" +#. module: base +#: code:addons/base/ir/ir_model.py:219 +#, python-format +msgid "The Selection Options expression is must be in the [('key','Label'), ...] format!" +msgstr "" + #. module: base #: view:ir.actions.report.xml:0 msgid "Miscellaneous" @@ -7440,7 +7533,7 @@ msgid "China" msgstr "" #. module: base -#: code:addons/base/res/res_user.py:486 +#: code:addons/base/res/res_user.py:516 #, python-format msgid "--\n" "%(name)s %(email)s\n" @@ -7524,7 +7617,6 @@ msgid "ltd" msgstr "" #. module: base -#: field:ir.model.fields,model_id:0 #: field:ir.values,res_id:0 #: field:res.log,res_id:0 msgid "Object ID" @@ -7632,7 +7724,7 @@ msgid "Account No." msgstr "" #. module: base -#: code:addons/base/res/res_lang.py:86 +#: code:addons/base/res/res_lang.py:157 #, python-format msgid "Base Language 'en_US' can not be deleted !" msgstr "" @@ -7731,7 +7823,7 @@ msgid "Auto-Refresh" msgstr "" #. module: base -#: code:addons/base/ir/ir_model.py:60 +#: code:addons/base/ir/ir_model.py:62 #, python-format msgid "The osv_memory field can only be compared with = and != operator." msgstr "" @@ -7741,11 +7833,6 @@ msgstr "" msgid "Diagram" msgstr "" -#. module: base -#: model:res.country,name:base.wf -msgid "Wallis and Futuna Islands" -msgstr "" - #. module: base #: help:multi_company.default,name:0 msgid "Name it to easily find a record" @@ -7803,14 +7890,17 @@ msgid "Turkmenistan" msgstr "" #. module: base -#: code:addons/base/ir/ir_actions.py:593 -#: code:addons/base/ir/ir_actions.py:625 -#: code:addons/base/ir/ir_actions.py:709 -#: code:addons/base/ir/ir_actions.py:712 -#: code:addons/base/ir/ir_model.py:112 -#: code:addons/base/ir/ir_model.py:197 -#: code:addons/base/ir/ir_model.py:216 -#: code:addons/base/ir/ir_model.py:219 +#: code:addons/base/ir/ir_actions.py:597 +#: code:addons/base/ir/ir_actions.py:629 +#: code:addons/base/ir/ir_actions.py:713 +#: code:addons/base/ir/ir_actions.py:716 +#: code:addons/base/ir/ir_model.py:114 +#: code:addons/base/ir/ir_model.py:204 +#: code:addons/base/ir/ir_model.py:218 +#: code:addons/base/ir/ir_model.py:232 +#: code:addons/base/ir/ir_model.py:250 +#: code:addons/base/ir/ir_model.py:255 +#: code:addons/base/ir/ir_model.py:258 #: code:addons/base/module/module.py:215 #: code:addons/base/module/module.py:258 #: code:addons/base/module/module.py:262 @@ -7819,7 +7909,7 @@ msgstr "" #: code:addons/base/module/module.py:321 #: code:addons/base/module/module.py:336 #: code:addons/base/module/module.py:429 -#: code:addons/base/module/module.py:530 +#: code:addons/base/module/module.py:531 #: code:addons/base/publisher_warranty/publisher_warranty.py:163 #: code:addons/base/publisher_warranty/publisher_warranty.py:281 #: code:addons/base/res/res_currency.py:100 @@ -7867,6 +7957,11 @@ msgstr "" msgid "Danish / Dansk" msgstr "" +#. module: base +#: selection:ir.model.fields,select_level:0 +msgid "Advanced Search (deprecated)" +msgstr "" + #. module: base #: model:res.country,name:base.cx msgid "Christmas Island" @@ -7877,11 +7972,6 @@ msgstr "" msgid "Other Actions Configuration" msgstr "" -#. module: base -#: selection:ir.module.module.dependency,state:0 -msgid "Uninstallable" -msgstr "" - #. module: base #: view:res.config.installer:0 msgid "Install Modules" @@ -8066,12 +8156,12 @@ msgid "Luxembourg" msgstr "" #. module: base -#: selection:res.request,priority:0 -msgid "Low" +#: help:ir.values,key2:0 +msgid "The kind of action or button in the client side that will trigger the action." msgstr "" #. module: base -#: code:addons/base/ir/ir_ui_menu.py:305 +#: code:addons/base/ir/ir_ui_menu.py:285 #, python-format msgid "Error ! You can not create recursive Menu." msgstr "" @@ -8233,7 +8323,7 @@ msgid "View Auto-Load" msgstr "" #. module: base -#: code:addons/base/ir/ir_model.py:197 +#: code:addons/base/ir/ir_model.py:232 #, python-format msgid "You cannot remove the field '%s' !" msgstr "" @@ -8272,7 +8362,7 @@ msgid "Supported file formats: *.csv (Comma-separated values) or *.po (GetText P msgstr "" #. module: base -#: code:addons/base/ir/ir_model.py:341 +#: code:addons/base/ir/ir_model.py:487 #, python-format msgid "You can not delete this document (%s) ! Be sure your user belongs to one of these groups: %s." msgstr "" @@ -8424,8 +8514,8 @@ msgid "Czech / Čeština" msgstr "" #. module: base -#: selection:ir.model.fields,select_level:0 -msgid "Advanced Search" +#: view:ir.actions.server:0 +msgid "Trigger Configuration" msgstr "" #. module: base @@ -8541,6 +8631,12 @@ msgstr "" msgid "Portrait" msgstr "" +#. module: base +#: code:addons/base/ir/ir_model.py:317 +#, python-format +msgid "Can only rename one column at a time!" +msgstr "" + #. module: base #: selection:ir.translation,type:0 msgid "Wizard Button" @@ -8708,7 +8804,7 @@ msgid "Account Owner" msgstr "" #. module: base -#: code:addons/base/res/res_user.py:240 +#: code:addons/base/res/res_user.py:256 #, python-format msgid "Company Switch Warning" msgstr "" From c017828b3faa734fbb2e3d5c3caa6b11e8b32e0a Mon Sep 17 00:00:00 2001 From: "Ujjvala Collins (OpenERP)" Date: Wed, 12 Jan 2011 10:15:25 +0530 Subject: [PATCH 18/41] [FIX] crm: Corrected field string on history tab of partner for phonecalls. lp bug: https://launchpad.net/bugs/701556 fixed bzr revid: uco@tinyerp.com-20110112044525-i0uv6e0wzvqqu8om --- addons/crm/res_partner_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/crm/res_partner_view.xml b/addons/crm/res_partner_view.xml index cbe3ec94c20..9df185ae831 100644 --- a/addons/crm/res_partner_view.xml +++ b/addons/crm/res_partner_view.xml @@ -54,7 +54,7 @@ - +