1// Aseprite
2// Copyright (C) 2020-2022 Igara Studio S.A.
3// Copyright (C) 2017-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/extensions.h"
13
14#include "app/app.h"
15#include "app/app_menus.h"
16#include "app/commands/command.h"
17#include "app/commands/commands.h"
18#include "app/console.h"
19#include "app/ini_file.h"
20#include "app/load_matrix.h"
21#include "app/pref/preferences.h"
22#include "app/resource_finder.h"
23#include "base/exception.h"
24#include "base/file_content.h"
25#include "base/file_handle.h"
26#include "base/fs.h"
27#include "base/fstream_path.h"
28#include "render/dithering_matrix.h"
29
30#ifdef ENABLE_SCRIPTING
31 #include "app/script/engine.h"
32 #include "app/script/luacpp.h"
33#endif
34
35#include "archive.h"
36#include "archive_entry.h"
37#include "json11.hpp"
38
39#include <fstream>
40#include <queue>
41#include <sstream>
42#include <string>
43
44#include "base/log.h"
45
46namespace app {
47
48namespace {
49
50const char* kPackageJson = "package.json";
51const char* kInfoJson = "__info.json";
52const char* kPrefLua = "__pref.lua";
53const char* kAsepriteDefaultThemeExtensionName = "aseprite-theme";
54
55class ReadArchive {
56public:
57 ReadArchive(const std::string& filename)
58 : m_arch(nullptr), m_open(false) {
59 m_arch = archive_read_new();
60 archive_read_support_format_zip(m_arch);
61
62 m_file = base::open_file(filename, "rb");
63 if (!m_file)
64 throw base::Exception("Error loading file %s",
65 filename.c_str());
66
67 int err;
68 if ((err = archive_read_open_FILE(m_arch, m_file.get())))
69 throw base::Exception("Error uncompressing extension\n%s (%d)",
70 archive_error_string(m_arch), err);
71
72 m_open = true;
73 }
74
75 ~ReadArchive() {
76 if (m_arch) {
77 if (m_open)
78 archive_read_close(m_arch);
79 archive_read_free(m_arch);
80 }
81 }
82
83 archive_entry* readEntry() {
84 archive_entry* entry;
85 int err = archive_read_next_header(m_arch, &entry);
86
87 if (err == ARCHIVE_EOF)
88 return nullptr;
89
90 if (err != ARCHIVE_OK)
91 throw base::Exception("Error uncompressing extension\n%s",
92 archive_error_string(m_arch));
93
94 return entry;
95 }
96
97 int copyDataTo(archive* out) {
98 const void* buf;
99 size_t size;
100 int64_t offset;
101 for (;;) {
102 int err = archive_read_data_block(m_arch, &buf, &size, &offset);
103 if (err == ARCHIVE_EOF)
104 break;
105 if (err != ARCHIVE_OK)
106 return err;
107
108 err = archive_write_data_block(out, buf, size, offset);
109 if (err != ARCHIVE_OK) {
110 throw base::Exception("Error writing data blocks\n%s (%d)",
111 archive_error_string(out), err);
112 return err;
113 }
114 }
115 return ARCHIVE_OK;
116 }
117
118 int copyDataTo(std::ostream& dst) {
119 const void* buf;
120 size_t size;
121 int64_t offset;
122 for (;;) {
123 int err = archive_read_data_block(m_arch, &buf, &size, &offset);
124 if (err == ARCHIVE_EOF)
125 break;
126 if (err != ARCHIVE_OK)
127 return err;
128 dst.write((const char*)buf, size);
129 }
130 return ARCHIVE_OK;
131 }
132
133private:
134 base::FileHandle m_file;
135 archive* m_arch;
136 bool m_open;
137};
138
139class WriteArchive {
140public:
141 WriteArchive()
142 : m_arch(nullptr)
143 , m_open(false) {
144 m_arch = archive_write_disk_new();
145 m_open = true;
146 }
147
148 ~WriteArchive() {
149 if (m_arch) {
150 if (m_open)
151 archive_write_close(m_arch);
152 archive_write_free(m_arch);
153 }
154 }
155
156 void writeEntry(ReadArchive& in, archive_entry* entry) {
157 int err = archive_write_header(m_arch, entry);
158 if (err != ARCHIVE_OK)
159 throw base::Exception("Error writing file into disk\n%s (%d)",
160 archive_error_string(m_arch), err);
161
162 in.copyDataTo(m_arch);
163 err = archive_write_finish_entry(m_arch);
164 if (err != ARCHIVE_OK)
165 throw base::Exception("Error saving the last part of a file entry in disk\n%s (%d)",
166 archive_error_string(m_arch), err);
167 }
168
169private:
170 archive* m_arch;
171 bool m_open;
172};
173
174void read_json_file(const std::string& path, json11::Json& json)
175{
176 std::string jsonText, line;
177 std::ifstream in(FSTREAM_PATH(path), std::ifstream::binary);
178 while (std::getline(in, line)) {
179 jsonText += line;
180 jsonText.push_back('\n');
181 }
182 std::string err;
183 json = json11::Json::parse(jsonText, err);
184 if (!err.empty())
185 throw base::Exception("Error parsing JSON file: %s\n",
186 err.c_str());
187}
188
189void write_json_file(const std::string& path, const json11::Json& json)
190{
191 std::string text;
192 json.dump(text);
193 std::ofstream out(FSTREAM_PATH(path), std::ifstream::binary);
194 out.write(text.c_str(), text.size());
195}
196
197} // anonymous namespace
198
199//////////////////////////////////////////////////////////////////////
200// Extension
201
202Extension::DitheringMatrixInfo::DitheringMatrixInfo()
203{
204}
205
206Extension::DitheringMatrixInfo::DitheringMatrixInfo(const std::string& path,
207 const std::string& name)
208 : m_path(path)
209 , m_name(name)
210{
211}
212
213const render::DitheringMatrix& Extension::DitheringMatrixInfo::matrix() const
214{
215 if (!m_loaded) {
216 m_loaded = true;
217 load_dithering_matrix_from_sprite(m_path, m_matrix);
218 }
219 return m_matrix;
220}
221
222Extension::Extension(const std::string& path,
223 const std::string& name,
224 const std::string& version,
225 const std::string& displayName,
226 const bool isEnabled,
227 const bool isBuiltinExtension)
228 : m_path(path)
229 , m_name(name)
230 , m_version(version)
231 , m_displayName(displayName)
232 , m_category(Category::None)
233 , m_isEnabled(isEnabled)
234 , m_isInstalled(true)
235 , m_isBuiltinExtension(isBuiltinExtension)
236{
237#ifdef ENABLE_SCRIPTING
238 m_plugin.pluginRef = LUA_REFNIL;
239#endif
240}
241
242Extension::~Extension()
243{
244}
245
246void Extension::executeInitActions()
247{
248#ifdef ENABLE_SCRIPTING
249 if (isEnabled() && hasScripts())
250 initScripts();
251#endif
252}
253
254void Extension::executeExitActions()
255{
256#ifdef ENABLE_SCRIPTING
257 if (isEnabled() && hasScripts())
258 exitScripts();
259#endif // ENABLE_SCRIPTING
260}
261
262void Extension::addKeys(const std::string& id, const std::string& path)
263{
264 m_keys[id] = path;
265 updateCategory(Category::Keys);
266}
267
268void Extension::addLanguage(const std::string& id, const std::string& path)
269{
270 m_languages[id] = path;
271 updateCategory(Category::Languages);
272}
273
274void Extension::addTheme(const std::string& id, const std::string& path, const std::string& variant)
275{
276 m_themes[id] = ThemeInfo(path, variant);
277 updateCategory(Category::Themes);
278}
279
280void Extension::addPalette(const std::string& id, const std::string& path)
281{
282 m_palettes[id] = path;
283 updateCategory(Category::Palettes);
284}
285
286void Extension::addDitheringMatrix(const std::string& id,
287 const std::string& path,
288 const std::string& name)
289{
290 DitheringMatrixInfo info(path, name);
291 m_ditheringMatrices[id] = std::move(info);
292 updateCategory(Category::DitheringMatrices);
293}
294
295#ifdef ENABLE_SCRIPTING
296
297void Extension::addCommand(const std::string& id)
298{
299 PluginItem item;
300 item.type = PluginItem::Command;
301 item.id = id;
302 m_plugin.items.push_back(item);
303}
304
305void Extension::removeCommand(const std::string& id)
306{
307 for (auto it=m_plugin.items.begin(); it != m_plugin.items.end(); ) {
308 if (it->type == PluginItem::Command &&
309 it->id == id) {
310 it = m_plugin.items.erase(it);
311 }
312 else {
313 ++it;
314 }
315 }
316}
317
318#endif
319
320bool Extension::canBeDisabled() const
321{
322 return (m_isEnabled &&
323 //!isCurrentTheme() &&
324 !isDefaultTheme()); // Default theme cannot be disabled or uninstalled
325}
326
327bool Extension::canBeUninstalled() const
328{
329 return (!m_isBuiltinExtension &&
330 // We can uninstall the current theme (e.g. to upgrade it)
331 //!isCurrentTheme() &&
332 !isDefaultTheme());
333}
334
335void Extension::enable(const bool state)
336{
337 if (m_isEnabled != state) {
338 set_config_bool("extensions", m_name.c_str(), state);
339 flush_config_file();
340
341 m_isEnabled = state;
342 }
343
344#ifdef ENABLE_SCRIPTING
345 if (hasScripts()) {
346 if (m_isEnabled) {
347 initScripts();
348 }
349 else {
350 exitScripts();
351 }
352 }
353#endif // ENABLE_SCRIPTING
354}
355
356void Extension::uninstall(const DeletePluginPref delPref)
357{
358 if (!m_isInstalled)
359 return;
360
361 ASSERT(canBeUninstalled());
362 if (!canBeUninstalled())
363 return;
364
365 TRACE("EXT: Uninstall extension '%s' from '%s'...\n",
366 m_name.c_str(), m_path.c_str());
367
368 // Execute exit actions of scripts
369 executeExitActions();
370
371 // Remove all files inside the extension path
372 uninstallFiles(m_path, delPref);
373
374 m_isEnabled = false;
375 m_isInstalled = false;
376}
377
378void Extension::uninstallFiles(const std::string& path,
379 const DeletePluginPref delPref)
380{
381#if 1 // Read the list of files to be uninstalled from __info.json file
382
383 std::string infoFn = base::join_path(path, kInfoJson);
384 if (!base::is_file(infoFn))
385 throw base::Exception("Cannot remove extension, '%s' file doesn't exist",
386 infoFn.c_str());
387
388 json11::Json json;
389 read_json_file(infoFn, json);
390
391 base::paths installedDirs;
392
393 for (const auto& value : json["installedFiles"].array_items()) {
394 std::string fn = base::join_path(path, value.string_value());
395 if (base::is_file(fn)) {
396 TRACE("EXT: Deleting file '%s'\n", fn.c_str());
397 base::delete_file(fn);
398 }
399 else if (base::is_directory(fn)) {
400 installedDirs.push_back(fn);
401 }
402 }
403
404 // Delete __pref.lua file (only if specified, e.g. if the user is
405 // updating the extension, the preferences should be kept).
406 bool hasPrefFile = false;
407 {
408 std::string fn = base::join_path(path, kPrefLua);
409 if (base::is_file(fn)) {
410 if (delPref == DeletePluginPref::kYes)
411 base::delete_file(fn);
412 else
413 hasPrefFile = true;
414 }
415 }
416
417 std::sort(installedDirs.begin(),
418 installedDirs.end(),
419 [](const std::string& a,
420 const std::string& b) {
421 return b.size() < a.size();
422 });
423
424 for (const auto& dir : installedDirs) {
425 TRACE("EXT: Deleting directory '%s'\n", dir.c_str());
426 try {
427 base::remove_directory(dir);
428 }
429 catch (const std::exception& ex) {
430 LOG(ERROR, "RECO: Extension subdirectory cannot be removed, it's not empty.\n"
431 " Error: %s\n", ex.what());
432 }
433 }
434
435 // Delete __info.json file if it does exist (e.g. maybe the
436 // "installedFiles" list included the __info.json so the file was
437 // already deleted, this can happen if the .json file was modified
438 // by hand/the user)
439 if (base::is_file(infoFn)) {
440 TRACE("EXT: Deleting file '%s'\n", infoFn.c_str());
441 base::delete_file(infoFn);
442 }
443
444 TRACE("EXT: Deleting extension directory '%s'\n", path.c_str());
445 if (!hasPrefFile) {
446 try {
447 base::remove_directory(path);
448 }
449 catch (const std::exception& ex) {
450 LOG(ERROR, "RECO: Extension directory cannot be removed, it's not empty.\n"
451 " Error: %s\n", ex.what());
452 }
453 }
454
455#else // The following code delete the whole "path",
456 // we prefer the __info.json approach.
457
458 for (auto& item : base::list_files(path)) {
459 std::string fn = base::join_path(path, item);
460 if (base::is_file(fn)) {
461 TRACE("EXT: Deleting file '%s'\n", fn.c_str());
462 base::delete_file(fn);
463 }
464 else if (base::is_directory(fn)) {
465 uninstallFiles(fn, deleteUserPref);
466 }
467 }
468
469 TRACE("EXT: Deleting directory '%s'\n", path.c_str());
470 base::remove_directory(path);
471
472#endif
473}
474
475bool Extension::isCurrentTheme() const
476{
477 auto it = m_themes.find(Preferences::instance().theme.selected());
478 return (it != m_themes.end());
479}
480
481bool Extension::isDefaultTheme() const
482{
483 return (name() == kAsepriteDefaultThemeExtensionName);
484}
485
486void Extension::updateCategory(const Category newCategory)
487{
488 if (m_category == Category::None ||
489 m_category == Category::Keys) {
490 m_category = newCategory;
491 }
492 else if (m_category != newCategory)
493 m_category = Category::Multiple;
494}
495
496#ifdef ENABLE_SCRIPTING
497
498// TODO move this to app/script/tableutils.h
499static void serialize_table(lua_State* L, int idx, std::string& result)
500{
501 bool first = true;
502
503 result.push_back('{');
504
505 idx = lua_absindex(L, idx);
506 lua_pushnil(L);
507 while (lua_next(L, idx) != 0) {
508 if (first) {
509 first = false;
510 }
511 else {
512 result.push_back(',');
513 }
514
515 // Save key
516 if (lua_type(L, -2) == LUA_TSTRING) {
517 if (const char* k = lua_tostring(L, -2)) {
518 result += k;
519 result.push_back('=');
520 }
521 }
522
523 // Save value
524 switch (lua_type(L, -1)) {
525 case LUA_TNIL:
526 default:
527 result += "nil";
528 break;
529 case LUA_TBOOLEAN:
530 if (lua_toboolean(L, -1))
531 result += "true";
532 else
533 result += "false";
534 break;
535 case LUA_TNUMBER:
536 result += lua_tostring(L, -1);
537 break;
538 case LUA_TSTRING:
539 result.push_back('\"');
540 if (const char* p = lua_tostring(L, -1)) {
541 for (; *p; ++p) {
542 switch (*p) {
543 case '\"':
544 result.push_back('\\');
545 result.push_back('\"');
546 break;
547 case '\\':
548 result.push_back('\\');
549 result.push_back('\\');
550 break;
551 case '\t':
552 result.push_back('\\');
553 result.push_back('t');
554 break;
555 case '\r':
556 result.push_back('\\');
557 result.push_back('n');
558 break;
559 case '\n':
560 result.push_back('\\');
561 result.push_back('n');
562 break;
563 default:
564 result.push_back(*p);
565 break;
566 }
567 }
568 }
569 result.push_back('\"');
570 break;
571 case LUA_TTABLE:
572 serialize_table(L, -1, result);
573 break;
574 }
575 lua_pop(L, 1);
576 }
577
578 result.push_back('}');
579}
580
581Extension::ScriptItem::ScriptItem(const std::string& fn)
582 : fn(fn)
583 , exitFunctionRef(LUA_REFNIL)
584{
585}
586
587void Extension::initScripts()
588{
589 script::Engine* engine = App::instance()->scriptEngine();
590 lua_State* L = engine->luaState();
591
592 // Put a new "plugin" object for init()/exit() functions
593 script::push_plugin(L, this);
594 m_plugin.pluginRef = luaL_ref(L, LUA_REGISTRYINDEX);
595
596 // Read plugin.preferences value
597 {
598 std::string fn = base::join_path(m_path, kPrefLua);
599 if (base::is_file(fn)) {
600 lua_rawgeti(L, LUA_REGISTRYINDEX, m_plugin.pluginRef);
601 if (luaL_loadfile(L, fn.c_str()) == LUA_OK) {
602 if (lua_pcall(L, 0, 1, 0) == LUA_OK) {
603 lua_setfield(L, -2, "preferences");
604 }
605 else {
606 const char* s = lua_tostring(L, -1);
607 if (s) {
608 Console().printf("%s\n", s);
609 }
610 }
611 lua_pop(L, 1);
612 }
613 else {
614 lua_pop(L, 1);
615 }
616 }
617 }
618
619 for (auto& script : m_plugin.scripts) {
620 // Reset global init()/exit() functions
621 engine->evalCode("init=nil exit=nil");
622
623 // Eval the code of the script (it should define an init() and an exit() function)
624 engine->evalFile(script.fn);
625
626 if (lua_getglobal(L, "exit") == LUA_TFUNCTION) {
627 // Save a reference to the exit() function of this script
628 script.exitFunctionRef = luaL_ref(L, LUA_REGISTRYINDEX);
629 }
630 else {
631 lua_pop(L, 1);
632 }
633
634 // Call the init() function of thi sscript with a Plugin object as first parameter
635 if (lua_getglobal(L, "init") == LUA_TFUNCTION) {
636 // Call init(plugin)
637 lua_rawgeti(L, LUA_REGISTRYINDEX, m_plugin.pluginRef);
638 lua_pcall(L, 1, 1, 0);
639 lua_pop(L, 1);
640 }
641 else {
642 lua_pop(L, 1);
643 }
644 }
645}
646
647void Extension::exitScripts()
648{
649 script::Engine* engine = App::instance()->scriptEngine();
650 lua_State* L = engine->luaState();
651
652 // Call the exit() function of each script
653 for (auto& script : m_plugin.scripts) {
654 if (script.exitFunctionRef != LUA_REFNIL) {
655 // Get the exit() function, the "plugin" object, and call exit(plugin)
656 lua_rawgeti(L, LUA_REGISTRYINDEX, script.exitFunctionRef);
657 lua_rawgeti(L, LUA_REGISTRYINDEX, m_plugin.pluginRef);
658 lua_pcall(L, 1, 0, 0);
659
660 luaL_unref(L, LUA_REGISTRYINDEX, script.exitFunctionRef);
661 script.exitFunctionRef = LUA_REFNIL;
662 }
663 }
664
665 // Save the plugin preferences object
666 if (m_plugin.pluginRef != LUA_REFNIL) {
667 lua_rawgeti(L, LUA_REGISTRYINDEX, m_plugin.pluginRef);
668 lua_getfield(L, -1, "preferences");
669
670 lua_pushnil(L); // Push a nil key, to ask for the first element of the table
671 bool hasPreferences = (lua_next(L, -2) != 0);
672 if (hasPreferences)
673 lua_pop(L, 2); // Remove the value and the key
674
675 if (hasPreferences) {
676 std::string result = "return ";
677 serialize_table(L, -1, result);
678 base::write_file_content(
679 base::join_path(m_path, kPrefLua),
680 (const uint8_t*)result.c_str(), result.size());
681 }
682 lua_pop(L, 2); // Pop preferences table and plugin
683
684 luaL_unref(L, LUA_REGISTRYINDEX, m_plugin.pluginRef);
685 m_plugin.pluginRef = LUA_REFNIL;
686 }
687
688 // Remove plugin items automatically
689 for (const auto& item : m_plugin.items) {
690 switch (item.type) {
691 case PluginItem::Command: {
692 auto cmds = Commands::instance();
693 auto cmd = cmds->byId(item.id.c_str());
694 ASSERT(cmd);
695
696 if (cmd) {
697#ifdef ENABLE_UI
698 // TODO use a signal
699 AppMenus::instance()->removeMenuItemFromGroup(cmd);
700#endif // ENABLE_UI
701
702 cmds->remove(cmd);
703
704 // This will call ~PluginCommand() and unref the command
705 // onclick callback.
706 delete cmd;
707 }
708 break;
709 }
710 }
711 }
712 m_plugin.items.clear();
713}
714
715void Extension::addScript(const std::string& fn)
716{
717 m_plugin.scripts.push_back(ScriptItem(fn));
718 updateCategory(Category::Scripts);
719}
720
721#endif // ENABLE_SCRIPTING
722
723//////////////////////////////////////////////////////////////////////
724// Extensions
725
726Extensions::Extensions()
727{
728 // Create and get the user extensions directory
729 {
730 ResourceFinder rf2;
731 rf2.includeUserDir("extensions/.");
732 m_userExtensionsPath = rf2.getFirstOrCreateDefault();
733 m_userExtensionsPath = base::normalize_path(m_userExtensionsPath);
734 if (!m_userExtensionsPath.empty() &&
735 m_userExtensionsPath.back() == '.') {
736 m_userExtensionsPath = base::get_file_path(m_userExtensionsPath);
737 }
738 LOG("EXT: User extensions path '%s'\n", m_userExtensionsPath.c_str());
739 }
740
741 ResourceFinder rf;
742 rf.includeUserDir("extensions");
743 rf.includeDataDir("extensions");
744
745 // Load extensions from data/ directory on all possible locations
746 // (installed folder and user folder)
747 while (rf.next()) {
748 auto extensionsDir = rf.filename();
749
750 if (base::is_directory(extensionsDir)) {
751 for (auto fn : base::list_files(extensionsDir)) {
752 const auto dir = base::join_path(extensionsDir, fn);
753 if (!base::is_directory(dir))
754 continue;
755
756 const bool isBuiltinExtension =
757 (m_userExtensionsPath != base::get_file_path(dir));
758
759 auto fullFn = base::join_path(dir, kPackageJson);
760 fullFn = base::normalize_path(fullFn);
761
762 LOG("EXT: Loading extension '%s'...\n", fullFn.c_str());
763 if (!base::is_file(fullFn)) {
764 LOG("EXT: File '%s' not found\n", fullFn.c_str());
765 continue;
766 }
767
768 try {
769 loadExtension(dir, fullFn, isBuiltinExtension);
770 }
771 catch (const std::exception& ex) {
772 LOG("EXT: Error loading JSON file: %s\n",
773 ex.what());
774 }
775 }
776 }
777 }
778}
779
780Extensions::~Extensions()
781{
782 for (auto ext : m_extensions)
783 delete ext;
784}
785
786void Extensions::executeInitActions()
787{
788 for (auto& ext : m_extensions)
789 ext->executeInitActions();
790
791 ScriptsChange(nullptr);
792}
793
794void Extensions::executeExitActions()
795{
796 for (auto& ext : m_extensions)
797 ext->executeExitActions();
798
799 ScriptsChange(nullptr);
800}
801
802std::string Extensions::languagePath(const std::string& langId)
803{
804 for (auto ext : m_extensions) {
805 if (!ext->isEnabled()) // Ignore disabled extensions
806 continue;
807
808 auto it = ext->languages().find(langId);
809 if (it != ext->languages().end())
810 return it->second;
811 }
812 return std::string();
813}
814
815std::string Extensions::themePath(const std::string& themeId)
816{
817 for (auto ext : m_extensions) {
818 if (!ext->isEnabled()) // Ignore disabled extensions
819 continue;
820
821 auto it = ext->themes().find(themeId);
822 if (it != ext->themes().end())
823 return it->second.path;
824 }
825 return std::string();
826}
827
828std::string Extensions::palettePath(const std::string& palId)
829{
830 for (auto ext : m_extensions) {
831 if (!ext->isEnabled()) // Ignore disabled extensions
832 continue;
833
834 auto it = ext->palettes().find(palId);
835 if (it != ext->palettes().end())
836 return it->second;
837 }
838 return std::string();
839}
840
841ExtensionItems Extensions::palettes() const
842{
843 ExtensionItems palettes;
844 for (auto ext : m_extensions) {
845 if (!ext->isEnabled()) // Ignore disabled themes
846 continue;
847
848 for (auto item : ext->palettes())
849 palettes[item.first] = item.second;
850 }
851 return palettes;
852}
853
854const render::DitheringMatrix* Extensions::ditheringMatrix(const std::string& matrixId)
855{
856 for (auto ext : m_extensions) {
857 if (!ext->isEnabled()) // Ignore disabled themes
858 continue;
859
860 auto it = ext->m_ditheringMatrices.find(matrixId);
861 if (it != ext->m_ditheringMatrices.end())
862 return &it->second.matrix();
863 }
864 return nullptr;
865}
866
867std::vector<Extension::DitheringMatrixInfo> Extensions::ditheringMatrices()
868{
869 std::vector<Extension::DitheringMatrixInfo> result;
870 for (auto ext : m_extensions) {
871 if (!ext->isEnabled()) // Ignore disabled themes
872 continue;
873
874 for (auto it : ext->m_ditheringMatrices)
875 result.push_back(it.second);
876 }
877 return result;
878}
879
880void Extensions::enableExtension(Extension* extension, const bool state)
881{
882 extension->enable(state);
883 generateExtensionSignals(extension);
884}
885
886void Extensions::uninstallExtension(Extension* extension,
887 const DeletePluginPref delPref)
888{
889 extension->uninstall(delPref);
890 generateExtensionSignals(extension);
891
892 auto it = std::find(m_extensions.begin(),
893 m_extensions.end(), extension);
894 ASSERT(it != m_extensions.end());
895 if (it != m_extensions.end())
896 m_extensions.erase(it);
897
898 delete extension;
899}
900
901ExtensionInfo Extensions::getCompressedExtensionInfo(const std::string& zipFn)
902{
903 ExtensionInfo info;
904 info.dstPath =
905 base::join_path(m_userExtensionsPath,
906 base::get_file_title(zipFn));
907
908 // First of all we read the package.json file inside the .zip to
909 // know 1) the extension name, 2) that the .json file can be parsed
910 // correctly, 3) the final destination directory.
911 ReadArchive in(zipFn);
912 archive_entry* entry;
913 while ((entry = in.readEntry()) != nullptr) {
914 const char* entryFnPtr = archive_entry_pathname(entry);
915
916 // This can happen, e.g. if the file contains "!" + unicode chars
917 // (maybe a bug in archive library?)
918 // TODO try to find the real cause of this on libarchive
919 if (!entryFnPtr)
920 continue;
921
922 const std::string entryFn = entryFnPtr;
923 if (base::get_file_name(entryFn) != kPackageJson)
924 continue;
925
926 info.commonPath = base::get_file_path(entryFn);
927 if (!info.commonPath.empty() &&
928 entryFn.size() > info.commonPath.size()) {
929 info.commonPath.push_back(entryFn[info.commonPath.size()]);
930 }
931
932 std::stringstream out;
933 in.copyDataTo(out);
934
935 std::string err;
936 auto json = json11::Json::parse(out.str(), err);
937 if (err.empty()) {
938 info.name = json["name"].string_value();
939 info.version = json["version"].string_value();
940 info.dstPath = base::join_path(m_userExtensionsPath, info.name);
941 }
942 break;
943 }
944 return info;
945}
946
947Extension* Extensions::installCompressedExtension(const std::string& zipFn,
948 const ExtensionInfo& info)
949{
950 base::paths installedFiles;
951
952 // Uncompress zipFn in info.dstPath
953 {
954 ReadArchive in(zipFn);
955 WriteArchive out;
956
957 archive_entry* entry;
958 while ((entry = in.readEntry()) != nullptr) {
959 const char* entryFnPtr = archive_entry_pathname(entry);
960 if (!entryFnPtr)
961 continue;
962
963 // Fix the entry filename to write the file in the disk
964 std::string fn = entryFnPtr;
965
966 LOG("EXT: Original filename in zip <%s>...\n", fn.c_str());
967
968 // Do not install __info.json file if it's inside the .zip as
969 // some users are distirbuting extensions with the __info.json
970 // file inside.
971 if (base::string_to_lower(base::get_file_name(fn)) == kInfoJson) {
972 LOG("EXT: Ignoring <%s>...\n", fn.c_str());
973 continue;
974 }
975
976 if (!info.commonPath.empty()) {
977 // Check mismatch with package.json common path
978 if (fn.compare(0, info.commonPath.size(), info.commonPath) != 0)
979 continue;
980
981 fn.erase(0, info.commonPath.size());
982 if (fn.empty())
983 continue;
984 }
985
986 installedFiles.push_back(fn);
987
988 const std::string fullFn = base::join_path(info.dstPath, fn);
989#if _WIN32
990 archive_entry_copy_pathname_w(entry, base::from_utf8(fullFn).c_str());
991#else
992 archive_entry_set_pathname(entry, fullFn.c_str());
993#endif
994
995 LOG("EXT: Uncompressing file <%s> to <%s>\n",
996 fn.c_str(), fullFn.c_str());
997
998 out.writeEntry(in, entry);
999 }
1000 }
1001
1002 // Save the list of installed files in "__info.json" file
1003 {
1004 json11::Json::object obj;
1005 obj["installedFiles"] = json11::Json(installedFiles);
1006 json11::Json json(obj);
1007
1008 const std::string fullFn = base::join_path(info.dstPath, kInfoJson);
1009 LOG("EXT: Saving list of installed files in <%s>\n", fullFn.c_str());
1010 write_json_file(fullFn, json);
1011 }
1012
1013 // Load the extension
1014 Extension* extension = loadExtension(
1015 info.dstPath,
1016 base::join_path(info.dstPath, kPackageJson),
1017 false);
1018 if (!extension)
1019 throw base::Exception("Error adding the new extension");
1020
1021 // Generate signals
1022 NewExtension(extension);
1023 generateExtensionSignals(extension);
1024
1025 return extension;
1026}
1027
1028Extension* Extensions::loadExtension(const std::string& path,
1029 const std::string& fullPackageFilename,
1030 const bool isBuiltinExtension)
1031{
1032 json11::Json json;
1033 read_json_file(fullPackageFilename, json);
1034 auto name = json["name"].string_value();
1035 auto version = json["version"].string_value();
1036 auto displayName = json["displayName"].string_value();
1037
1038 LOG("EXT: Extension '%s' loaded\n", name.c_str());
1039
1040 std::unique_ptr<Extension> extension(
1041 new Extension(path,
1042 name,
1043 version,
1044 displayName,
1045 // Extensions are enabled by default
1046 get_config_bool("extensions", name.c_str(), true),
1047 isBuiltinExtension));
1048
1049 auto contributes = json["contributes"];
1050 if (contributes.is_object()) {
1051 // Keys
1052 auto keys = contributes["keys"];
1053 if (keys.is_array()) {
1054 for (const auto& key : keys.array_items()) {
1055 std::string keyId = key["id"].string_value();
1056 std::string keyPath = key["path"].string_value();
1057
1058 // The path must be always relative to the extension
1059 keyPath = base::join_path(path, keyPath);
1060
1061 LOG("EXT: New keyboard shortcuts '%s' in '%s'\n",
1062 keyId.c_str(),
1063 keyPath.c_str());
1064
1065 extension->addKeys(keyId, keyPath);
1066 }
1067 }
1068
1069 // Languages
1070 auto languages = contributes["languages"];
1071 if (languages.is_array()) {
1072 for (const auto& lang : languages.array_items()) {
1073 std::string langId = lang["id"].string_value();
1074 std::string langPath = lang["path"].string_value();
1075
1076 // The path must be always relative to the extension
1077 langPath = base::join_path(path, langPath);
1078
1079 LOG("EXT: New language id=%s path=%s\n",
1080 langId.c_str(),
1081 langPath.c_str());
1082
1083 extension->addLanguage(langId, langPath);
1084 }
1085 }
1086
1087 // Themes
1088 auto themes = contributes["themes"];
1089 if (themes.is_array()) {
1090 for (const auto& theme : themes.array_items()) {
1091 std::string themeId = theme["id"].string_value();
1092 std::string themePath = theme["path"].string_value();
1093 std::string themeVariant = theme["variant"].string_value();
1094
1095 // The path must be always relative to the extension
1096 themePath = base::join_path(path, themePath);
1097
1098 LOG("EXT: New theme id=%s path=%s variant=%s\n",
1099 themeId.c_str(),
1100 themePath.c_str(),
1101 themeVariant.c_str());
1102
1103 extension->addTheme(themeId, themePath, themeVariant);
1104 }
1105 }
1106
1107 // Palettes
1108 auto palettes = contributes["palettes"];
1109 if (palettes.is_array()) {
1110 for (const auto& palette : palettes.array_items()) {
1111 std::string palId = palette["id"].string_value();
1112 std::string palPath = palette["path"].string_value();
1113
1114 // The path must be always relative to the extension
1115 palPath = base::join_path(path, palPath);
1116
1117 LOG("EXT: New palette id=%s path=%s\n",
1118 palId.c_str(),
1119 palPath.c_str());
1120
1121 extension->addPalette(palId, palPath);
1122 }
1123 }
1124
1125 // Dithering matrices
1126 auto ditheringMatrices = contributes["ditheringMatrices"];
1127 if (ditheringMatrices.is_array()) {
1128 for (const auto& ditheringMatrix : ditheringMatrices.array_items()) {
1129 std::string matId = ditheringMatrix["id"].string_value();
1130 std::string matPath = ditheringMatrix["path"].string_value();
1131 std::string matName = ditheringMatrix["name"].string_value();
1132 if (matName.empty())
1133 matName = matId;
1134
1135 // The path must be always relative to the extension
1136 matPath = base::join_path(path, matPath);
1137
1138 LOG("EXT: New dithering matrix id=%s path=%s\n",
1139 matId.c_str(),
1140 matPath.c_str());
1141
1142 extension->addDitheringMatrix(matId, matPath, matName);
1143 }
1144 }
1145
1146#ifdef ENABLE_SCRIPTING
1147 // Scripts
1148 auto scripts = contributes["scripts"];
1149 if (scripts.is_array()) {
1150 for (const auto& script : scripts.array_items()) {
1151 std::string scriptPath = script["path"].string_value();
1152 if (scriptPath.empty())
1153 continue;
1154
1155 // The path must be always relative to the extension
1156 scriptPath = base::join_path(path, scriptPath);
1157
1158 LOG("EXT: New script path=%s\n", scriptPath.c_str());
1159
1160 extension->addScript(scriptPath);
1161 }
1162 }
1163 // Simple version of packages.json with {... "scripts": "file.lua" ...}
1164 else if (scripts.is_string() &&
1165 !scripts.string_value().empty()) {
1166 std::string scriptPath = scripts.string_value();
1167
1168 // The path must be always relative to the extension
1169 scriptPath = base::join_path(path, scriptPath);
1170
1171 LOG("EXT: New script path=%s\n", scriptPath.c_str());
1172
1173 extension->addScript(scriptPath);
1174 }
1175#endif // ENABLE_SCRIPTING
1176 }
1177
1178 if (extension)
1179 m_extensions.push_back(extension.get());
1180 return extension.release();
1181}
1182
1183void Extensions::generateExtensionSignals(Extension* extension)
1184{
1185 if (extension->hasKeys()) KeysChange(extension);
1186 if (extension->hasLanguages()) LanguagesChange(extension);
1187 if (extension->hasThemes()) ThemesChange(extension);
1188 if (extension->hasPalettes()) PalettesChange(extension);
1189 if (extension->hasDitheringMatrices()) DitheringMatricesChange(extension);
1190#ifdef ENABLE_SCRIPTING
1191 if (extension->hasScripts()) ScriptsChange(extension);
1192#endif
1193}
1194
1195} // namespace app
1196