1 /*
2 This is part of TeXworks, an environment for working with TeX documents
3 Copyright (C) 2009-2013 Jonathan Kew, Stefan Löffler, Charlie Sharpsteen
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18 For links to further information, or to contact the authors,
19 see <http://www.tug.org/texworks/>.
20 */
21
22 #include "TWScript.h"
23 #include "TWScriptAPI.h"
24 #include "ConfigurableApp.h"
25 #include "DefaultPrefs.h"
26
27 #include <QTextStream>
28 #include <QMetaObject>
29 #include <QMetaMethod>
30 #include <QApplication>
31 #include <QTextCodec>
32 #include <QDir>
33
TWScript(QObject * plugin,const QString & fileName)34 TWScript::TWScript(QObject * plugin, const QString& fileName)
35 : m_Plugin(plugin), m_Filename(fileName), m_Type(ScriptUnknown), m_Enabled(true), m_FileSize(0)
36 {
37 m_Codec = QTextCodec::codecForName("UTF-8");
38 if (!m_Codec)
39 m_Codec = QTextCodec::codecForLocale();
40 }
41
run(QObject * context,QVariant & result)42 bool TWScript::run(QObject *context, QVariant& result)
43 {
44 TWScriptAPI tw(this, qApp, context, result);
45 return execute(&tw);
46 }
47
hasChanged() const48 bool TWScript::hasChanged() const
49 {
50 QFileInfo fi(m_Filename);
51 return (fi.size() != m_FileSize || fi.lastModified() != m_LastModified);
52 }
53
doParseHeader(const QString & beginComment,const QString & endComment,const QString & Comment,bool skipEmpty)54 bool TWScript::doParseHeader(const QString& beginComment, const QString& endComment,
55 const QString& Comment, bool skipEmpty /* = true */)
56 {
57 QFile file(m_Filename);
58 QStringList lines;
59 QString line;
60 bool codecChanged = true;
61 bool success = false;
62 QTextCodec* codec;
63
64 if (!file.exists() || !file.open(QIODevice::ReadOnly))
65 return false;
66
67 m_Codec = QTextCodec::codecForName("UTF-8");
68 if (!m_Codec)
69 m_Codec = QTextCodec::codecForLocale();
70
71 while (codecChanged) {
72 codec = m_Codec;
73 file.seek(0);
74 lines = codec->toUnicode(file.readAll()).split(QRegExp("\r\n|[\n\r]"));
75
76 // skip any empty lines
77 if (skipEmpty) {
78 while (!lines.isEmpty() && lines.first().isEmpty())
79 lines.removeFirst();
80 }
81 if (lines.isEmpty())
82 break;
83
84 // is this a valid TW script?
85 line = lines.takeFirst();
86 if (!beginComment.isEmpty()) {
87 if (!line.startsWith(beginComment))
88 break;
89 line = line.mid(beginComment.size()).trimmed();
90 }
91 else if (!Comment.isEmpty()) {
92 if (!line.startsWith(Comment))
93 break;
94 line = line.mid(Comment.size()).trimmed();
95 }
96 if (!line.startsWith("TeXworksScript"))
97 break;
98
99 // scan to find the extent of the header lines
100 QStringList::iterator i;
101 for (i = lines.begin(); i != lines.end(); ++i) {
102 // have we reached the end?
103 if (skipEmpty && i->isEmpty()) {
104 i = lines.erase(i);
105 --i;
106 continue;
107 }
108 if (!endComment.isEmpty()) {
109 if (i->startsWith(endComment))
110 break;
111 }
112 if (!i->startsWith(Comment))
113 break;
114 *i = i->mid(Comment.size()).trimmed();
115 }
116 lines.erase(i, lines.end());
117
118 codecChanged = false;
119 switch (doParseHeader(lines)) {
120 case ParseHeader_OK:
121 success = true;
122 break;
123 case ParseHeader_Failed:
124 success = false;
125 break;
126 case ParseHeader_CodecChanged:
127 codecChanged = true;
128 break;
129 }
130 }
131
132 file.close();
133 return success;
134 }
135
doParseHeader(const QStringList & lines)136 TWScript::ParseHeaderResult TWScript::doParseHeader(const QStringList & lines)
137 {
138 QString line, key, value;
139 QFileInfo fi(m_Filename);
140
141 m_FileSize = fi.size();
142 m_LastModified = fi.lastModified();
143
144 foreach (line, lines) {
145 key = line.section(':', 0, 0).trimmed();
146 value = line.section(':', 1).trimmed();
147
148 if (key == "Title") m_Title = value;
149 else if (key == "Description") m_Description = value;
150 else if (key == "Author") m_Author = value;
151 else if (key == "Version") m_Version = value;
152 else if (key == "Script-Type") {
153 if (value == "hook") m_Type = ScriptHook;
154 else if (value == "standalone") m_Type = ScriptStandalone;
155 else m_Type = ScriptUnknown;
156 }
157 else if (key == "Hook") m_Hook = value;
158 else if (key == "Context") m_Context = value;
159 else if (key == "Shortcut") m_KeySequence = QKeySequence(value);
160 else if (key == "Encoding") {
161 QTextCodec * codec = QTextCodec::codecForName(value.toUtf8());
162 if (codec) {
163 if (!m_Codec || codec->name() != m_Codec->name()) {
164 m_Codec = codec;
165 return ParseHeader_CodecChanged;
166 }
167 }
168 }
169 }
170
171 if (m_Type != ScriptUnknown && !m_Title.isEmpty())
172 return ParseHeader_OK;
173 return ParseHeader_Failed;
174 }
175
176 /*static*/
doGetProperty(const QObject * obj,const QString & name,QVariant & value)177 TWScript::PropertyResult TWScript::doGetProperty(const QObject * obj, const QString& name, QVariant & value)
178 {
179 int iProp, i;
180 QMetaProperty prop;
181
182 if (!obj || !(obj->metaObject()))
183 return Property_Invalid;
184
185 // Get the parameters
186 iProp = obj->metaObject()->indexOfProperty(qPrintable(name));
187
188 // if we didn't find a property maybe it's a method
189 if (iProp < 0) {
190 for (i = 0; i < obj->metaObject()->methodCount(); ++i) {
191 #if QT_VERSION >= 0x050000
192 if (QString(obj->metaObject()->method(i).methodSignature()).startsWith(name + "("))
193 return Property_Method;
194 #else
195 if (QString(obj->metaObject()->method(i).signature()).startsWith(name + "("))
196 return Property_Method;
197 #endif
198 }
199 return Property_DoesNotExist;
200 }
201
202 prop = obj->metaObject()->property(iProp);
203
204 // If we can't get the property's value, abort
205 if (!prop.isReadable())
206 return Property_NotReadable;
207
208 value = prop.read(obj);
209 return Property_OK;
210 }
211
212 /*static*/
doSetProperty(QObject * obj,const QString & name,const QVariant & value)213 TWScript::PropertyResult TWScript::doSetProperty(QObject * obj, const QString& name, const QVariant & value)
214 {
215 int iProp;
216 QMetaProperty prop;
217
218 if (!obj || !(obj->metaObject()))
219 return Property_Invalid;
220
221 iProp = obj->metaObject()->indexOfProperty(qPrintable(name));
222
223 // if we didn't find the property abort
224 if (iProp < 0)
225 return Property_DoesNotExist;
226
227 prop = obj->metaObject()->property(iProp);
228
229 // If we can't set the property's value, abort
230 if (!prop.isWritable())
231 return Property_NotWritable;
232
233 prop.write(obj, value);
234 return Property_OK;
235 }
236
237 /*static*/
doCallMethod(QObject * obj,const QString & name,QVariantList & arguments,QVariant & result)238 TWScript::MethodResult TWScript::doCallMethod(QObject * obj, const QString& name,
239 QVariantList & arguments, QVariant & result)
240 {
241 const QMetaObject * mo;
242 bool methodExists = false;
243 QList<QGenericArgument> genericArgs;
244 int type, typeOfArg, i, j;
245 QString typeName;
246 char * strTypeName;
247 QMetaMethod mm;
248 QGenericReturnArgument retValArg;
249 void * retValBuffer = NULL;
250 TWScript::MethodResult status;
251 void * myNullPtr = NULL;
252
253 if (!obj || !(obj->metaObject()))
254 return Method_Invalid;
255
256 mo = obj->metaObject();
257
258 for (i = 0; i < mo->methodCount(); ++i) {
259 mm = mo->method(i);
260 // Check for the method name
261 #if QT_VERSION >= 0x050000
262 if (!QString(mm.methodSignature()).startsWith(name + "("))
263 continue;
264 #else
265 if (!QString(mm.signature()).startsWith(name + "("))
266 continue;
267 #endif
268 // we can only call public methods
269 if (mm.access() != QMetaMethod::Public)
270 continue;
271
272 methodExists = true;
273
274 // we need the correct number of arguments
275 if (mm.parameterTypes().count() != arguments.count())
276 continue;
277
278 // Check if the given arguments are compatible with those taken by the
279 // method
280 for (j = 0; j < arguments.count(); ++j) {
281 // QVariant can be passed as-is
282 if (mm.parameterTypes()[j] == "QVariant")
283 continue;
284
285 type = QMetaType::type(mm.parameterTypes()[j]);
286 typeOfArg = (int)arguments[j].type();
287 if (typeOfArg == (int)type)
288 continue;
289 if (arguments[j].canConvert((QVariant::Type)type))
290 continue;
291 // allow invalid===NULL for pointers
292 #if QT_VERSION >= 0x050000
293 if (typeOfArg == QVariant::Invalid && type == QMetaType::QObjectStar)
294 continue;
295 #else
296 if (typeOfArg == QVariant::Invalid && (type == QMetaType::QObjectStar || type == QMetaType::QWidgetStar))
297 continue;
298 // QObject* and QWidget* may be convertible
299 if (typeOfArg == QMetaType::QWidgetStar && type == QMetaType::QObjectStar)
300 continue;
301 if (typeOfArg == QMetaType::QObjectStar && type == QMetaType::QWidgetStar && (arguments[j].value<QObject*>() == NULL || qobject_cast<QWidget*>(arguments[j].value<QObject*>())))
302 continue;
303 #endif
304 break;
305 }
306 if (j < arguments.count())
307 continue;
308
309 // Convert the arguments into QGenericArgument structures
310 for (j = 0; j < arguments.count() && j < 10; ++j) {
311 typeName = mm.parameterTypes()[j];
312 type = QMetaType::type(qPrintable(typeName));
313 typeOfArg = (int)arguments[j].type();
314
315 // allocate type name on the heap so it survives the method call
316 strTypeName = new char[typeName.size() + 1];
317 strcpy(strTypeName, qPrintable(typeName));
318
319 if (typeName == "QVariant") {
320 genericArgs.append(QGenericArgument(strTypeName, &arguments[j]));
321 continue;
322 }
323 if (arguments[j].canConvert((QVariant::Type)type))
324 arguments[j].convert((QVariant::Type)type);
325 #if QT_VERSION >= 0x050000
326 else if (typeOfArg == QVariant::Invalid && type == QMetaType::QObjectStar) {
327 genericArgs.append(QGenericArgument(strTypeName, &myNullPtr));
328 continue;
329 }
330 #else
331 else if (typeOfArg == QVariant::Invalid && (type == QMetaType::QObjectStar || type == QMetaType::QWidgetStar)) {
332 genericArgs.append(QGenericArgument(strTypeName, &myNullPtr));
333 continue;
334 }
335 else if (typeOfArg == QMetaType::QWidgetStar && type == QMetaType::QObjectStar)
336 arguments[j] = QVariant::fromValue(qobject_cast<QObject*>(arguments[j].value<QWidget*>()));
337 else if (typeOfArg == QMetaType::QObjectStar && type == QMetaType::QWidgetStar && (arguments[j].value<QObject*>() == NULL || qobject_cast<QWidget*>(arguments[j].value<QObject*>())))
338 arguments[j] = QVariant::fromValue(qobject_cast<QWidget*>(arguments[j].value<QObject*>()));
339 #endif
340 // \TODO handle failure during conversion
341 else { }
342
343 // Note: This line is a hack!
344 // QVariant::data() is undocumented; QGenericArgument should not be
345 // called directly; if this ever causes problems, think of another
346 // (better) way to do this
347 genericArgs.append(QGenericArgument(strTypeName, arguments[j].data()));
348 }
349 // Fill up the list so we get the 10 values we need later on
350 for (; j < 10; ++j)
351 genericArgs.append(QGenericArgument());
352
353 typeName = mm.typeName();
354 if (typeName.isEmpty()) {
355 // no return type
356 retValArg = QGenericReturnArgument();
357 }
358 else if (typeName == "QVariant") {
359 // QMetaType can't construct QVariant objects
360 retValArg = Q_RETURN_ARG(QVariant, result);
361 }
362 else {
363 // Note: These two lines are a hack!
364 // QGenericReturnArgument should not be constructed directly; if
365 // this ever causes problems, think of another (better) way to do this
366 #if QT_VERSION >= 0x050000
367 retValBuffer = QMetaType::create(QMetaType::type(mm.typeName()));
368 #else
369 retValBuffer = QMetaType::construct(QMetaType::type(mm.typeName()));
370 #endif
371 retValArg = QGenericReturnArgument(mm.typeName(), retValBuffer);
372 }
373
374 if (mo->invokeMethod(obj, qPrintable(name),
375 Qt::DirectConnection,
376 retValArg,
377 genericArgs[0],
378 genericArgs[1],
379 genericArgs[2],
380 genericArgs[3],
381 genericArgs[4],
382 genericArgs[5],
383 genericArgs[6],
384 genericArgs[7],
385 genericArgs[8],
386 genericArgs[9])
387 ) {
388 if (retValBuffer)
389 result = QVariant(QMetaType::type(mm.typeName()), retValBuffer);
390 else if (typeName == "QVariant")
391 ; // don't do anything here; the return valus is already in result
392 else
393 result = QVariant();
394 status = Method_OK;
395 }
396 else
397 status = Method_Failed;
398
399 if (retValBuffer)
400 QMetaType::destroy(QMetaType::type(mm.typeName()), retValBuffer);
401
402 for (j = 0; j < arguments.count() && j < 10; ++j) {
403 // we pushed the data on the heap, we need to remove it from there
404 delete[] genericArgs[j].name();
405 }
406
407 return status;
408 }
409
410 if (methodExists)
411 return Method_WrongArgs;
412 return Method_DoesNotExist;
413 }
414
setGlobal(const QString & key,const QVariant & val)415 void TWScript::setGlobal(const QString& key, const QVariant& val)
416 {
417 QVariant v = val;
418
419 if (key.isEmpty())
420 return;
421
422 // For objects on the heap make sure we are notified when their lifetimes
423 // end so that we can remove them from our hash accordingly
424 switch ((QMetaType::Type)val.type()) {
425 case QMetaType::QObjectStar:
426 connect(v.value<QObject*>(), SIGNAL(destroyed(QObject*)), this, SLOT(globalDestroyed(QObject*)));
427 break;
428 #if QT_VERSION < 0x050000
429 case QMetaType::QWidgetStar:
430 connect((QWidget*)v.data(), SIGNAL(destroyed(QObject*)), this, SLOT(globalDestroyed(QObject*)));
431 break;
432 #endif
433 default: break;
434 }
435 m_globals[key] = v;
436 }
437
globalDestroyed(QObject * obj)438 void TWScript::globalDestroyed(QObject * obj)
439 {
440 QHash<QString, QVariant>::iterator i = m_globals.begin();
441
442 while (i != m_globals.end()) {
443 switch ((QMetaType::Type)i.value().type()) {
444 case QMetaType::QObjectStar:
445 if (i.value().value<QObject*>() == obj)
446 i = m_globals.erase(i);
447 else
448 ++i;
449 break;
450 #if QT_VERSION < 0x050000
451 case QMetaType::QWidgetStar:
452 if (i.value().value<QWidget*>() == obj)
453 i = m_globals.erase(i);
454 else
455 ++i;
456 break;
457 #endif
458 default:
459 ++i;
460 break;
461 }
462 }
463 }
464
465
mayExecuteSystemCommand(const QString & cmd,QObject * context) const466 bool TWScript::mayExecuteSystemCommand(const QString& cmd, QObject * context) const
467 {
468 Q_UNUSED(cmd)
469 Q_UNUSED(context)
470
471 // cmd may be a true command line, or a single file/directory to run or open
472 QSETTINGS_OBJECT(settings);
473 return settings.value("allowSystemCommands", false).toBool();
474 }
475
mayWriteFile(const QString & filename,QObject * context) const476 bool TWScript::mayWriteFile(const QString& filename, QObject * context) const
477 {
478 Q_UNUSED(filename)
479 Q_UNUSED(context)
480
481 QSETTINGS_OBJECT(settings);
482 return settings.value("allowScriptFileWriting", false).toBool();
483 }
484
mayReadFile(const QString & filename,QObject * context) const485 bool TWScript::mayReadFile(const QString& filename, QObject * context) const
486 {
487 QSETTINGS_OBJECT(settings);
488 QDir scriptDir(QFileInfo(m_Filename).absoluteDir());
489 QVariant targetFile;
490 QDir targetDir;
491
492 if (settings.value("allowScriptFileReading", kDefault_AllowScriptFileReading).toBool())
493 return true;
494
495 // even if global reading is disallowed, some exceptions may apply
496 QFileInfo fi(QDir::cleanPath(filename));
497
498 // reading in subdirectories of the script file's directory is always allowed
499 if (!scriptDir.relativeFilePath(fi.absolutePath()).startsWith(".."))
500 return true;
501
502 if (context) {
503 // reading subdirectories of the current file is always allowed
504 targetFile = context->property("fileName");
505 if (targetFile.isValid() && !targetFile.toString().isEmpty()) {
506 targetDir = QFileInfo(targetFile.toString()).absoluteDir();
507 if (!targetDir.relativeFilePath(fi.absolutePath()).startsWith(".."))
508 return true;
509 }
510 // reading subdirectories of the root file is always allowed
511 targetFile = context->property("rootFileName");
512 if (targetFile.isValid() && !targetFile.toString().isEmpty()) {
513 targetDir = QFileInfo(targetFile.toString()).absoluteDir();
514 if (!targetDir.relativeFilePath(fi.absolutePath()).startsWith(".."))
515 return true;
516 }
517 }
518
519 return false;
520 }
521
522