1"""
2This runtime module contains everything about running
3Python and QtScript scripts inside Scribus.
4
5Look at run_filename for details.
6"""
7import os
8import hashlib
9from ConfigParser import ConfigParser
10
11import sip
12from PyQt4.QtCore import QThread, QObject, QVariant
13from PyQt4.QtGui import qApp,  QMessageBox
14from PyQt4.QtScript import QScriptEngine, QScriptValue
15
16from safe_eval import checkCode
17import permitdlg
18
19import __main__
20
21from inspect import getargspec
22
23class RuntimeConfig(ConfigParser):
24
25    # I cannot use Scripter.preferences because a safe script could
26    # mark other scripts as safe (=allowed) although they use import and
27    # other (possible) dangerous stuff..
28    # Perhaps I will find a better solution later.
29
30    def __init__(self):
31        ConfigParser.__init__(self)
32        # XXX better use ScPaths->...
33        path = os.path.expanduser("~/.scribus/scripter")
34        if not os.path.exists(path):
35            os.makedirs(path)
36        self.filename = os.path.join(path, "runtime.cfg")
37        self.read([self.filename])
38
39
40    def save(self):
41        fp = open(self.filename, "w")
42        self.write(fp)
43        fp.close()
44
45
46    def set(self, section, key, value):
47        if not self.has_section(section):
48            self.add_section(section)
49        ConfigParser.set(self, section, key, value)
50        self.save()
51
52
53    def getbool(self, section, key):
54        value = self.get(section, key).strip().lower()
55        if value and value in ["true", "on", "yes", "1"]:
56            return True
57        elif value and value in ["false", "off", "no", "0"]:
58            return False
59        else:
60            raise ValueError, "Invalid boolean value %r" % value
61
62
63runtime_config = RuntimeConfig()
64
65extension_namespace = __main__.__dict__
66
67
68qts_engine = None
69
70# XXX share namespaces of Python and QtScript
71
72class QtSRuntimeError(Exception):
73    pass
74
75
76def qts_func_decorator(func):
77    def wrapper(context, engine):
78        args = []
79        (fargs, fvarargs, fvarkw, fdefaults) = getargspec(func)
80        if len(fargs) and fargs[0] == "self":
81            args.append(context.thisObject())
82        for i in xrange(context.argumentCount()):
83            args.append(context.argument(i))
84        try:
85            result = func(*args)
86        except Exception, e:
87            # XXX correct behaviour?
88            # http://lists.trolltech.com/qt-interest/2007-06/thread00892-0.html
89            return context.throwValue(QScriptValue(engine, str(e)))
90        if result:
91            return QScriptValue(engine, result)
92        else:
93            return QScriptValue()
94    return wrapper
95
96
97@qts_func_decorator
98def alert(msg_qsv):
99    msg = msg_qsv.toString()
100    QMessageBox.information(Scripter.dialogs.mainWindow.qt, "Alert", msg)
101
102
103def update_qs_namespace(engine, ns):
104    go = engine.globalObject()
105    for name, value in ns.items():
106        if isinstance(value, QObject):
107            value = engine.newQObject(value)
108        elif callable(value):
109            value = engine.newFunction(value)
110        #elif not isinstance(value, QScriptValue):
111        #    value = QScriptValue(engine, value)
112        go.setProperty(name, value)
113
114
115def newQScriptEngine():
116    engine = QScriptEngine()
117    update_qs_namespace(engine,
118       {
119          "Application": qApp,
120          "Scripter": Scripter.qt,
121          "alert": alert
122       })
123    return engine
124
125
126def run_qtscript(filename, subroutine=None, extension=False):
127    global qts_engine
128    if not extension:
129        engine = newQScriptEngine()
130    else:
131        engine = qts_engine = qts_engine or newQScriptEngine()
132    code = open(filename).read()
133    engine.clearExceptions()
134    result = engine.evaluate(code)
135    engine.collectGarbage()
136    if not engine.hasUncaughtException() and subroutine:
137        sub = engine.globalObject().property(subroutine)
138        sub.call()
139    if engine.hasUncaughtException():
140        bt = engine.uncaughtExceptionBacktrace()
141        raise QtSRuntimeError("%s\nTraceback:\%s" % (
142              str(engine.uncaughtException().toString()),
143              "\n".join(["  %s" % l for l in list(bt)])))
144
145
146def hash_source(filename, source=None):
147    # I gueses sha256 is safe enough without collisions?
148    source = source or open(filename).read()
149    return "%s:%s:%s" % (
150        os.path.basename(filename), len(filename), hashlib.sha256(source).hexdigest())
151
152
153def check_python(filename):
154    filename = os.path.abspath(os.path.expanduser(filename))
155    path = os.path.dirname(filename)
156    # Allow files from global autoload folder by default.
157    # XXX Good idea?
158    if path == os.path.join(Scripter.path, "autoload"):
159        return True
160    code = open(filename).read()
161    h = hash_source(filename, code)
162    if runtime_config.has_option("permissions", h):
163        return runtime_config.getbool("permissions", h)
164
165    problems = checkCode(code)
166    if problems and len(problems) == 1 and isinstance(problems[0], SyntaxError):
167        return True # let's ignore it and let excepthook hande the error later
168    elif problems:
169        ok = permitdlg.ask(filename, problems)
170        if ok == -2: # deny and remember
171            runtime_config.set("permissions", h, False)
172            return False
173        elif ok == 2: # deny
174            return False
175        elif ok == -1: # allow and remember
176            runtime_config.set("permissions", h, True)
177        elif ok == 1: # allow but now remember
178            pass
179        else:
180            raise ValueError, "Inknown return code for permission dialog: %r" % ok
181    return True
182
183
184def run_python(filename, subroutine=None, extension=False):
185    if not extension:
186        namespace = {
187        "__name__": "__scribus__",
188        "__file__": filename
189        }
190    else:
191        namespace = extension_namespace
192    if not check_python(filename):
193        return
194    execfile(filename, namespace)
195    if subroutine:
196        sub = namespace[subroutine]
197        sub()
198    if not extension:
199        del namespace
200
201
202threads = []
203
204class RunThread(QThread):
205
206
207    def __init__(self, func, *args):
208        QThread.__init__(self, Scripter.qt)
209        self.func = func
210        self.args = args
211
212
213    def run(self):
214        threads.append(self)
215        self.func(*self.args)
216        threads.remove(self)
217
218
219def run_background(func, *args):
220    thread = RunThread(func, *args)
221    thread.start()
222    # XXX: connect done signal with cleanup?
223    return thread
224
225
226
227def mark_keep():
228    """
229    mark every child of Scripter.collector to keep
230    """
231    for child in Scripter.collector.children():
232        if hasattr(child, "qt"): child = child.qt
233        child.setProperty("keep", QVariant(True))
234
235
236
237def cleanup():
238    """
239    delete every child which is not marked as keep
240    """
241    for child in Scripter.collector.children():
242        if hasattr(child, "qt"): child = child.qt
243        v = child.property("keep")
244        if v and v.toBool() == True:
245            #print "Keeping", child
246            continue
247        print "* deleting collected", child
248        sip.delete(child)
249
250
251
252def run_filename(filename, subroutine=None, extension=False, background=False):
253    """
254    Call this function to run a script and nothing else.
255    It will do everything for you, including garbage collection
256    for QtScript (very simple implementation, see mark_keep and cleanup).
257    Running as extension uses the __main__ namespace and does not
258    delete objects after execution.
259    Running in background as a thread is not much tested and
260    should only be used for non-GUI scripts.
261    """
262    mark_keep()
263    if background:
264        run_func = run_background
265    else:
266        run_func = lambda func, *args: func(*args)
267    if filename.endswith((".sqts", ".qts", ".sjs", ".js")):
268        run_func(run_qtscript, filename, subroutine, extension)
269    else:
270        run_func(run_python, filename, subroutine, extension)
271    if not background and not extension:
272        # XXX: make sure this is called if an exception occures...
273        cleanup()
274