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