1 /*
2  *  Plugins.cpp - plugin manager
3 
4 	Copyright (C) 2009 and beyond by Gregory Smith
5 	and the "Aleph One" developers.
6 
7 	This program is free software; you can redistribute it and/or modify
8 	it under the terms of the GNU General Public License as published by
9 	the Free Software Foundation; either version 3 of the License, or
10 	(at your option) any later version.
11 
12 	This program is distributed in the hope that it will be useful,
13 	but WITHOUT ANY WARRANTY; without even the implied warranty of
14 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 	GNU General Public License for more details.
16 
17 	This license is contained in the file "COPYING",
18 	which is included with this source code; it is available online at
19 	http://www.gnu.org/licenses/gpl.html
20 
21 */
22 
23 #include "cseries.h"
24 #include "Plugins.h"
25 
26 #include <algorithm>
27 
28 #include "alephversion.h"
29 #include "FileHandler.h"
30 #include "game_errors.h"
31 #include "Logging.h"
32 #include "preferences.h"
33 #include "InfoTree.h"
34 #include "XML_ParseTreeRoot.h"
35 #include "Scenario.h"
36 
37 #ifdef HAVE_ZZIP
38 #include <zzip/lib.h>
39 #endif
40 
41 #include <boost/algorithm/string/predicate.hpp>
42 
43 namespace algo = boost::algorithm;
44 
45 class PluginLoader {
46 public:
PluginLoader()47 	PluginLoader() { }
~PluginLoader()48 	~PluginLoader() { }
49 
50 	bool ParsePlugin(FileSpecifier& file);
51 	bool ParseDirectory(FileSpecifier& dir);
52 };
53 
compatible() const54 bool Plugin::compatible() const {
55 	if (required_version.size() > 0 && A1_DATE_VERSION < required_version)
56 		return false;
57 
58 	if (required_scenarios.size() == 0)
59 		return true;
60 	std::string scenName = Scenario::instance()->GetName();
61 	std::string scenID = Scenario::instance()->GetID();
62 	std::string scenVers = Scenario::instance()->GetVersion();
63 	for (std::vector<ScenarioInfo>::const_iterator it = required_scenarios.begin(); it != required_scenarios.end(); ++it)
64 	{
65 		if ((it->name.empty() || it->name == scenName) &&
66 		    (it->scenario_id.empty() || it->scenario_id == scenID) &&
67 		    (it->version.empty() || it->version == scenVers))
68 			return true;
69 	}
70 	return false;
71 }
allowed() const72 bool Plugin::allowed() const {
73 	if (stats_lua.empty() || network_preferences->allow_stats)
74 		return true;
75 
76 	return false;
77 }
valid() const78 bool Plugin::valid() const {
79 	if (!enabled)
80 		return false;
81 
82 	if (!environment_preferences->use_solo_lua &&
83 		Plugins::instance()->mode() == Plugins::kMode_Solo)
84 		return !overridden_solo;
85 
86 	return !overridden;
87 }
88 
instance()89 Plugins* Plugins::instance() {
90 	static Plugins* m_instance = nullptr;
91 	if (!m_instance) {
92 		m_instance = new Plugins;
93 	}
94 
95 	return m_instance;
96 }
97 
disable(const std::string & path)98 void Plugins::disable(const std::string& path) {
99 	for (std::vector<Plugin>::iterator it = m_plugins.begin(); it != m_plugins.end(); ++it) {
100 		if (it->directory == path) {
101 			it->enabled = false;
102 			m_validated = false;
103 			return;
104 		}
105 	}
106 }
107 
load_mmls(const Plugin & plugin)108 static void load_mmls(const Plugin& plugin)
109 {
110 	ScopedSearchPath ssp(plugin.directory);
111 	for (std::vector<std::string>::const_iterator it = plugin.mmls.begin(); it != plugin.mmls.end(); ++it)
112 	{
113 		FileSpecifier file;
114 		if (file.SetNameWithPath(it->c_str()))
115 		{
116 			ParseMMLFromFile(file);
117 		}
118 		else
119 		{
120 			logWarning("%s Plugin: %s not found; ignoring", plugin.name.c_str(), it->c_str());
121 		}
122 	}
123 }
124 
load_mml()125 void Plugins::load_mml() {
126 	validate();
127 
128 	for (std::vector<Plugin>::iterator it = m_plugins.begin(); it != m_plugins.end(); ++it)
129 	{
130 		if (it->valid())
131 		{
132 			load_mmls(*it);
133 		}
134 	}
135 }
136 
137 void load_shapes_patch(SDL_RWops* p, bool override_replacements);
138 
load_shapes_patches(bool is_opengl)139 void Plugins::load_shapes_patches(bool is_opengl)
140 {
141 	validate();
142 	for (std::vector<Plugin>::iterator it = m_plugins.begin(); it != m_plugins.end(); ++it)
143 	{
144 		if (it->valid())
145 		{
146 			ScopedSearchPath ssp(it->directory);
147 
148 			for (std::vector<ShapesPatch>::iterator shapes_patch = it->shapes_patches.begin(); shapes_patch != it->shapes_patches.end(); ++shapes_patch)
149 			{
150 				if (is_opengl || !shapes_patch->requires_opengl)
151 				{
152 					FileSpecifier file;
153 					if (file.SetNameWithPath(shapes_patch->path.c_str()))
154 					{
155 						OpenedFile ofile;
156 						if (file.Open(ofile))
157 						{
158 							load_shapes_patch(ofile.GetRWops(), false);
159 						}
160 
161 					}
162 					else
163 					{
164 						logWarning("%s Plugin: %s not found; ignoring", it->name.c_str(), shapes_patch->path.c_str());
165 					}
166 				}
167 			}
168 		}
169 	}
170 }
171 
find_hud_lua()172 const Plugin* Plugins::find_hud_lua()
173 {
174 	validate();
175 	std::vector<Plugin>::const_reverse_iterator rend = m_plugins.rend();
176 	for (std::vector<Plugin>::const_reverse_iterator rit = m_plugins.rbegin(); rit != rend; ++rit)
177 	{
178 		if (rit->hud_lua.size() && rit->valid())
179 		{
180 			return &(*rit);
181 		}
182 	}
183 
184 	return 0;
185 }
186 
find_solo_lua()187 const Plugin* Plugins::find_solo_lua()
188 {
189 	validate();
190 	std::vector<Plugin>::const_reverse_iterator rend = m_plugins.rend();
191 	for (std::vector<Plugin>::const_reverse_iterator rit = m_plugins.rbegin(); rit != rend; ++rit)
192 	{
193 		if (rit->solo_lua.size() && rit->valid())
194 		{
195 			return &(*rit);
196 		}
197 	}
198 
199 	return 0;
200 }
201 
find_stats_lua()202 const Plugin* Plugins::find_stats_lua()
203 {
204 	validate();
205 	std::vector<Plugin>::const_reverse_iterator rend = m_plugins.rend();
206 	for (std::vector<Plugin>::const_reverse_iterator rit = m_plugins.rbegin(); rit != rend; ++rit)
207 	{
208 		if (rit->stats_lua.size() && rit->valid())
209 		{
210 			return &(*rit);
211 		}
212 	}
213 
214 	return 0;
215 }
216 
find_theme()217 const Plugin* Plugins::find_theme()
218 {
219 	validate();
220 	std::vector<Plugin>::const_reverse_iterator rend = m_plugins.rend();
221 	for (std::vector<Plugin>::const_reverse_iterator rit = m_plugins.rbegin(); rit != rend; ++rit)
222 	{
223 		if (rit->theme.size() && rit->valid())
224 		{
225 			return &(*rit);
226 		}
227 	}
228 
229 	return 0;
230 }
231 
plugin_file_exists(const Plugin & Data,std::string Path)232 static bool plugin_file_exists(const Plugin& Data, std::string Path)
233 {
234 	FileSpecifier f = Data.directory + Path;
235 	return f.Exists();
236 }
237 
ParsePlugin(FileSpecifier & file_name)238 bool PluginLoader::ParsePlugin(FileSpecifier& file_name)
239 {
240 	OpenedFile file;
241 	if (file_name.Open(file))
242 	{
243 		int32 data_size;
244 		file.GetLength(data_size);
245 		std::vector<char> file_data;
246 		file_data.resize(data_size);
247 
248 		if (file.Read(data_size, &file_data[0]))
249 		{
250 			DirectorySpecifier current_plugin_directory;
251 			file_name.ToDirectory(current_plugin_directory);
252 
253 			char name[256];
254 			current_plugin_directory.GetName(name);
255 
256 			std::istringstream strm(std::string(file_data.begin(), file_data.end()));
257 			try {
258 				InfoTree root = InfoTree::load_xml(strm).get_child("plugin");
259 
260 				Plugin Data = Plugin();
261 				Data.directory = current_plugin_directory;
262 				Data.enabled = true;
263 
264 				root.read_attr("name", Data.name);
265 				root.read_attr("version", Data.version);
266 				root.read_attr("description", Data.description);
267 				root.read_attr("minimum_version", Data.required_version);
268 
269 				if (root.read_attr("hud_lua", Data.hud_lua) &&
270 					!plugin_file_exists(Data, Data.hud_lua))
271 					Data.hud_lua = "";
272 
273 				if (root.read_attr("solo_lua", Data.solo_lua) &&
274 					!plugin_file_exists(Data, Data.solo_lua))
275 					Data.solo_lua = "";
276 
277 				if (root.read_attr("stats_lua", Data.stats_lua) &&
278 					!plugin_file_exists(Data, Data.stats_lua))
279 					Data.stats_lua = "";
280 
281 				if (root.read_attr("theme_dir", Data.theme) &&
282 					!plugin_file_exists(Data, Data.theme + "/theme2.mml"))
283 					Data.theme = "";
284 
285 				BOOST_FOREACH(InfoTree tree, root.children_named("mml"))
286 				{
287 					std::string mml_path;
288 					if (tree.read_attr("file", mml_path) &&
289 						plugin_file_exists(Data, mml_path))
290 						Data.mmls.push_back(mml_path);
291 				}
292 
293 				BOOST_FOREACH(InfoTree tree, root.children_named("shapes_patch"))
294 				{
295 					ShapesPatch patch;
296 					tree.read_attr("file", patch.path);
297 					tree.read_attr("requires_opengl", patch.requires_opengl);
298 					if (plugin_file_exists(Data, patch.path))
299 						Data.shapes_patches.push_back(patch);
300 				}
301 
302 				BOOST_FOREACH(InfoTree tree, root.children_named("scenario"))
303 				{
304 					ScenarioInfo info;
305 					tree.read_attr("name", info.name);
306 					if (info.name.size() > 31)
307 						info.name.erase(31);
308 
309 					tree.read_attr("id", info.scenario_id);
310 					if (info.scenario_id.size() > 23)
311 						info.scenario_id.erase(23);
312 
313 					tree.read_attr("version", info.version);
314 					if (info.version.size() > 7)
315 						info.version.erase(7);
316 
317 					if (info.name.size() || info.scenario_id.size())
318 						Data.required_scenarios.push_back(info);
319 				}
320 
321 				if (Data.name.length()) {
322 					std::sort(Data.mmls.begin(), Data.mmls.end());
323 					if (Data.theme.size()) {
324 						Data.hud_lua = "";
325 						Data.solo_lua = "";
326 						Data.shapes_patches.clear();
327 					}
328 					Plugins::instance()->add(Data);
329 				}
330 
331 			} catch (InfoTree::parse_error e) {
332 				logError("There were parsing errors in %s Plugin.xml: %s", name, e.what());
333 			} catch (InfoTree::path_error e) {
334 				logError("There were parsing errors in %s Plugin.xml: %s", name, e.what());
335 			} catch (InfoTree::data_error e) {
336 				logError("There were parsing errors in %s Plugin.xml: %s", name, e.what());
337 			} catch (InfoTree::unexpected_error e) {
338 				logError("There were parsing errors in %s Plugin.xml: %s", name, e.what());
339 			}
340 		}
341 
342 		return true;
343 	}
344 	return false;
345 }
346 
ParseDirectory(FileSpecifier & dir)347 bool PluginLoader::ParseDirectory(FileSpecifier& dir)
348 {
349 	std::vector<dir_entry> de;
350 	if (!dir.ReadDirectory(de))
351 		return false;
352 
353 	for (std::vector<dir_entry>::const_iterator it = de.begin(); it != de.end(); ++it) {
354 		FileSpecifier file = dir + it->name;
355 		if (it->name == "Plugin.xml")
356 		{
357 			ParsePlugin(file);
358 		}
359 		else if (it->is_directory && it->name[0] != '.')
360 		{
361 			ParseDirectory(file);
362 		}
363 #ifdef HAVE_ZZIP
364 		else if (algo::ends_with(it->name, ".zip") || algo::ends_with(it->name, ".ZIP"))
365 		{
366 			// search it for a Plugin.xml file
367 			ZZIP_DIR* zzipdir = zzip_dir_open(file.GetPath(), 0);
368 			if (zzipdir)
369 			{
370 				ZZIP_DIRENT dirent;
371 				while (zzip_dir_read(zzipdir, &dirent))
372 				{
373 					if (strcmp(dirent.d_name, "Plugin.xml") == 0 || algo::ends_with(dirent.d_name, "/Plugin.xml"))
374 					{
375 						std::string archive = file.GetPath();
376 						FileSpecifier file_name = FileSpecifier(archive.substr(0, archive.find_last_of('.'))) + dirent.d_name;
377 						ParsePlugin(file_name);
378 					}
379 				}
380 				zzip_dir_close(zzipdir);
381 			}
382 		}
383 #endif
384 	}
385 
386 	return true;
387 }
388 
389 extern std::vector<DirectorySpecifier> data_search_path;
390 
enumerate()391 void Plugins::enumerate() {
392 
393 	logContext("parsing plugins");
394 	PluginLoader loader;
395 
396 	for (std::vector<DirectorySpecifier>::const_iterator it = data_search_path.begin(); it != data_search_path.end(); ++it) {
397 		DirectorySpecifier path = *it + "Plugins";
398 		loader.ParseDirectory(path);
399 	}
400 	std::sort(m_plugins.begin(), m_plugins.end());
401 	clear_game_error();
402 	m_validated = false;
403 }
404 
405 // enforce all-or-nothing loading of plugins which contain
406 // an "only one-at-a-time" item, like a Lua script or theme
validate()407 void Plugins::validate()
408 {
409 	if (m_validated)
410 		return;
411 	m_validated = true;
412 
413 	// determine active plugins including solo Lua
414 	bool found_solo_lua = false;
415 	bool found_hud_lua = false;
416 	bool found_stats_lua = false;
417 	bool found_theme = false;
418 	for (std::vector<Plugin>::reverse_iterator rit = m_plugins.rbegin(); rit != m_plugins.rend(); ++rit)
419 	{
420 		rit->overridden_solo = false;
421 		if (!rit->enabled || !rit->compatible() || !rit->allowed() ||
422 			(found_solo_lua && rit->solo_lua.size()) ||
423 			(found_hud_lua && rit->hud_lua.size()) ||
424 			(found_stats_lua && rit->stats_lua.size()) ||
425 			(found_theme && rit->theme.size()))
426 		{
427 			rit->overridden_solo = true;
428 			continue;
429 		}
430 
431 		if (rit->solo_lua.size())
432 			found_solo_lua = true;
433 		if (rit->hud_lua.size())
434 			found_hud_lua = true;
435 		if (rit->stats_lua.size())
436 			found_stats_lua = true;
437 		if (rit->theme.size())
438 			found_theme = true;
439 	}
440 
441 	// determine active plugins excluding solo Lua
442 	found_hud_lua = false;
443 	found_stats_lua = false;
444 	found_theme = false;
445 	for (std::vector<Plugin>::reverse_iterator rit = m_plugins.rbegin(); rit != m_plugins.rend(); ++rit)
446 	{
447 		rit->overridden = false;
448 		if (!rit->enabled || !rit->compatible() || !rit->allowed() ||
449 			(rit->solo_lua.size()) ||
450 			(found_hud_lua && rit->hud_lua.size()) ||
451 			(found_stats_lua && rit->stats_lua.size()) ||
452 			(found_theme && rit->theme.size()))
453 		{
454 			rit->overridden = true;
455 			continue;
456 		}
457 
458 		if (rit->hud_lua.size())
459 			found_hud_lua = true;
460 		if (rit->stats_lua.size())
461 			found_stats_lua = true;
462 		if (rit->theme.size())
463 			found_theme = true;
464 	}
465 }
466