OpenCPN Partial API docs
Loading...
Searching...
No Matches
plugin_handler.cpp
1/******************************************************************************
2 *
3 * Project: OpenCPN
4 *
5 ***************************************************************************
6 * Copyright (C) 2019 Alec Leamas *
7 * *
8 * This program is free software; you can redistribute it and/or modify *
9 * it under the terms of the GNU General Public License as published by *
10 * the Free Software Foundation; either version 2 of the License, or *
11 * (at your option) any later version. *
12 * *
13 * This program is distributed in the hope that it will be useful, *
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
16 * GNU General Public License for more details. *
17 * *
18 * You should have received a copy of the GNU General Public License *
19 * along with this program; if not, write to the *
20 * Free Software Foundation, Inc., *
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
22 ***************************************************************************
23 */
24#include <algorithm>
25#include <cstdio>
26#include <fstream>
27#include <iomanip>
28#include <memory>
29#include <ostream>
30#include <regex>
31#include <set>
32#include <sstream>
33#include <stdexcept>
34#include <streambuf>
35#include <unordered_map>
36
37#include "wx/wxprec.h"
38
39#ifndef WX_PRECOMP
40#include "wx/wx.h"
41#endif
42
43#include <wx/dir.h>
44#include <wx/file.h>
45#include <wx/string.h>
46#include <wx/window.h>
47#include <wx/uri.h>
48
49#include <archive.h>
50#include <archive_entry.h>
51typedef __LA_INT64_T la_int64_t; // "older" libarchive versions support
52
53#if defined(__MINGW32__) && defined(Yield)
54#undef Yield // from win.h, conflicts with mingw headers
55#endif
56
57#include "base_platform.h"
58#include "catalog_handler.h"
59#include "catalog_parser.h"
60#include "config.h"
61#include "downloader.h"
62#include "gui_lib.h"
63#include "logger.h"
64#include "navutil.h"
65#include "ocpn_utils.h"
66#include "plugin_cache.h"
67#include "plugin_handler.h"
68#include "plugin_loader.h"
69#include "plugin_paths.h"
70
71#ifdef _WIN32
72static std::string SEP("\\");
73#else
74static std::string SEP("/");
75#endif
76
77#ifndef F_OK // windows: missing unistd.h.
78#define F_OK 0
79#endif
80
81extern BasePlatform* g_BasePlatform;
82extern wxString g_winPluginDir;
83extern MyConfig* pConfig;
84extern bool g_bportable;
85
86extern wxString g_compatOS;
87extern wxString g_compatOsVersion;
88
90static std::vector<std::string> split(const std::string& s,
91 const std::string& delim) {
92 std::vector<std::string> result;
93 size_t pos = s.find(delim);
94 if (pos == std::string::npos) {
95 result.push_back(s);
96 return result;
97 }
98 result.push_back(s.substr(0, pos));
99 result.push_back(s.substr(pos + delim.length()));
100 return result;
101}
102
103inline std::string basename(const std::string path) {
104 wxFileName wxFile(path);
105 return wxFile.GetFullName().ToStdString();
106}
107
108bool isRegularFile(const char* path) {
109 wxFileName wxFile(path);
110 return wxFile.FileExists() && !wxFile.IsDir();
111}
112
113static void mkdir(const std::string path) {
114#if defined(_WIN32) && !defined(__MINGW32__)
115 _mkdir(path.c_str());
116#elif defined(__MINGW32__)
117 mkdir(path.c_str());
118#else
119 mkdir(path.c_str(), 0755);
120#endif
121}
122
127static ssize_t PlugInIxByName(const std::string name, ArrayOfPlugIns* plugins) {
128 for (unsigned i = 0; i < plugins->GetCount(); i += 1) {
129 if (name == plugins->Item(i)->m_common_name.ToStdString()) {
130 return i;
131 }
132 }
133 return -1;
134}
135
136static std::string pluginsConfigDir() {
137 std::string pluginDataDir = g_BasePlatform->GetPrivateDataDir().ToStdString();
138 pluginDataDir += SEP + "plugins";
139 if (!ocpn::exists(pluginDataDir)) {
140 mkdir(pluginDataDir);
141 }
142 pluginDataDir += SEP + "install_data";
143 if (!ocpn::exists(pluginDataDir)) {
144 mkdir(pluginDataDir);
145 }
146 return pluginDataDir;
147}
148
149static std::string dirListPath(std::string name) {
150 std::transform(name.begin(), name.end(), name.begin(), ::tolower);
151 return pluginsConfigDir() + SEP + name + ".dirs";
152}
153
155class Plugin {
156public:
157 Plugin(const PluginMetadata& metadata) {
158 m_abi = metadata.target;
159 m_abi_version = metadata.target_version;
160 m_major_version = ocpn::split(m_abi_version.c_str(), ".")[0];
161 m_name = metadata.name;
162 wxLogDebug("Plugin: setting up, name: %s", m_name);
163 wxLogDebug("Plugin: init: abi: %s, abi_version: %s, major ver: %s", m_abi,
164 m_abi_version, m_major_version);
165 }
166 const std::string& abi() const { return m_abi; }
167 const std::string& abi_version() const { return m_abi_version; }
168 const std::string& major_version() const { return m_major_version; }
169 const std::string& name() const { return m_name; }
170
171private:
172 std::string m_abi;
173 std::string m_abi_version;
174 std::string m_major_version;
175 std::string m_name;
176};
177
179class Host {
180public:
181 Host(CompatOs* compatOs) {
182 m_abi = compatOs->name();
183 m_abi_version = compatOs->version();
184 m_major_version = ocpn::split(m_abi_version.c_str(), ".")[0];
185 wxLogDebug("Host: init: abi: %s, abi_version: %s, major ver: %s", m_abi,
186 m_abi_version, m_major_version);
187 }
188
189 bool is_version_compatible(const Plugin& plugin) const {
190 if (ocpn::startswith(plugin.abi(), "ubuntu")) {
191 return plugin.abi_version() == m_abi_version;
192 }
193 return plugin.major_version() == m_major_version;
194 }
195
196 // Test if plugin abi is a Debian version compatible with host's Ubuntu
197 // abi version on a x86_64 platform.
198 bool is_debian_plugin_compatible(const Plugin& plugin) const {
199 if (!ocpn::startswith(m_abi, "ubuntu")) return false;
200 static const std::vector<std::string> compat_versions = {
201 // clang-format: off
202 "debian-x86_64;11;ubuntu-gtk3-x86_64;20.04",
203 "debian-wx32-x86_64;11;ubuntu-wx32-x86_64;22.04",
204 "debian-x86_64;12;ubuntu-x86_64;23.04",
205 "debian-x86_64;12;ubuntu-x86_64;23.10",
206 "debian-x86_64;12;ubuntu-x86_64;24.04",
207 "debian-x86_64;sid;ubuntu-x86_64;24.04"
208 }; // clang-format: on
209 if (ocpn::startswith(plugin.abi(), "debian")) {
210 wxLogDebug("Checking for debian plugin on a ubuntu-x86_64 host");
211 const std::string compat_version =
212 plugin.abi() + ";" + plugin.major_version() + ";" + m_abi + ";" + m_abi_version;
213 for (auto& cv : compat_versions) {
214 if (compat_version == cv) {
215 return true;
216 }
217 }
218 }
219 return false;
220 }
221
222 const std::string& abi() const { return m_abi; }
223
224 const std::string& abi_version() const { return m_abi_version; }
225
226 const std::string& major_version() const { return m_major_version; }
227
228private:
229 std::string m_abi;
230 std::string m_abi_version;
231 std::string m_major_version;
232};
233
234CompatOs* CompatOs::getInstance() {
235 static std::string last_global_os("");
236 static CompatOs* instance = 0;
237
238 if (!instance || last_global_os != g_compatOS) {
239 instance = new (CompatOs);
240 last_global_os = g_compatOS;
241 }
242 return instance;
243};
244
245CompatOs::CompatOs() : _name(PKG_TARGET), _version(PKG_TARGET_VERSION) {
246 // Get the specified system definition,
247 // from the environment override,
248 // or the config file override
249 // or the baked in (build system) values.
250
251 std::string compatOS(_name);
252 std::string compatOsVersion(_version);
253
254 if (getenv("OPENCPN_COMPAT_TARGET") != 0) {
255 _name = getenv("OPENCPN_COMPAT_TARGET");
256 if (_name.find(':') != std::string::npos) {
257 auto tokens = ocpn::split(_name.c_str(), ":");
258 _name = tokens[0];
259 _version = tokens[1];
260 }
261 } else if (g_compatOS != "") {
262 // CompatOS and CompatOsVersion in opencpn.conf/.ini file.
263 _name = g_compatOS;
264 if (g_compatOsVersion != "") {
265 _version = g_compatOsVersion;
266 }
267 } else if (ocpn::startswith(_name, "ubuntu") && (_version == "22.04")) {
268 int wxv = wxMAJOR_VERSION * 10 + wxMINOR_VERSION;
269 if(wxv >= 32){
270 auto tokens = ocpn::split(_name.c_str(), "-");
271 _name = std::string(tokens[0]) + std::string("-wx32-") + tokens[1];
272 }
273 }
274
275 _name = ocpn::tolower(_name);
276 _version = ocpn::tolower(_version);
277}
278
279std::string PluginHandler::fileListPath(std::string name) {
280 std::transform(name.begin(), name.end(), name.begin(), ::tolower);
281 return pluginsConfigDir() + SEP + name + ".files";
282}
283
284PluginHandler::PluginHandler() {}
285
286bool PluginHandler::isCompatible(const PluginMetadata& metadata, const char* os,
287 const char* os_version) {
288
289 static const std::vector<std::string> simple_abis = {"msvc", "msvc-wx32",
290 "darwin", "darwin-wx32", "android-armhf", "android-arm64"};
291
292 auto compatOS = CompatOs::getInstance();
293 Host host(compatOS);
294 Plugin plugin(metadata);
295
296 auto found = std::find(simple_abis.begin(), simple_abis.end(), plugin.abi());
297 if (found != simple_abis.end()) {
298 bool ok = plugin.abi() == host.abi();
299 wxLogDebug("Returning %s for %s", (ok ? "ok" : "fail"), host.abi());
300 wxLogDebug(" ");
301 return ok;
302 }
303 bool rv = false;
304 if (host.abi() == plugin.abi() && host.is_version_compatible(plugin)) {
305 rv = true;
306 wxLogDebug("Found matching abi version %s", plugin.abi_version());
307 } else if (host.is_debian_plugin_compatible(plugin)) {
308 rv = true;
309 wxLogDebug("Found Debian version matching Ubuntu host");
310 }
311 DEBUG_LOG << "Plugin compatibility check Final: "
312 << (rv ? "ACCEPTED: " : "REJECTED: ") << metadata.name;
313 return rv;
314}
315
316std::string PluginHandler::versionPath(std::string name) {
317 std::transform(name.begin(), name.end(), name.begin(), ::tolower);
318 return pluginsConfigDir() + SEP + name + ".version";
319}
320
321typedef std::unordered_map<std::string, std::string> pathmap_t;
322
327static pathmap_t getInstallPaths() {
328 using namespace std;
329
330 pathmap_t pathmap;
332 pathmap["bin"] = paths->UserBindir();
333 pathmap["lib"] = paths->UserLibdir();
334 pathmap["lib64"] = paths->UserLibdir();
335 pathmap["share"] = paths->UserDatadir();
336 return pathmap;
337}
338
339static void saveFilelist(std::string filelist, std::string name) {
340 using namespace std;
341 string listpath = PluginHandler::fileListPath(name);
342 ofstream diskfiles(listpath);
343 if (!diskfiles.is_open()) {
344 wxLogWarning("Cannot create installed files list.");
345 return;
346 }
347 diskfiles << filelist;
348}
349
350static void saveDirlist(std::string name) {
351 using namespace std;
352 string path = dirListPath(name);
353 ofstream dirs(path);
354 if (!dirs.is_open()) {
355 wxLogWarning("Cannot create installed files list.");
356 return;
357 }
358 pathmap_t pathmap = getInstallPaths();
359 unordered_map<string, string>::iterator it;
360 for (it = pathmap.begin(); it != pathmap.end(); it++) {
361 dirs << it->first << ": " << it->second << endl;
362 }
363}
364
365static void saveVersion(const std::string& name, const std::string& version) {
366 using namespace std;
367 string path = PluginHandler::versionPath(name);
368 ofstream stream(path);
369 if (!stream.is_open()) {
370 wxLogWarning("Cannot create version file.");
371 return;
372 }
373 stream << version << endl;
374}
375
376static int copy_data(struct archive* ar, struct archive* aw) {
377 int r;
378 const void* buff;
379 size_t size;
380 la_int64_t offset;
381
382 while (true) {
383 r = archive_read_data_block(ar, &buff, &size, &offset);
384 if (r == ARCHIVE_EOF) return (ARCHIVE_OK);
385 if (r < ARCHIVE_OK) {
386 std::string s(archive_error_string(ar));
387 return (r);
388 }
389 r = archive_write_data_block(aw, buff, size, offset);
390 if (r < ARCHIVE_OK) {
391 std::string s(archive_error_string(aw));
392 wxLogWarning("Error copying install data: %s", archive_error_string(aw));
393 return (r);
394 }
395 }
396}
397
398static bool win_entry_set_install_path(struct archive_entry* entry,
399 pathmap_t installPaths) {
400 using namespace std;
401
402 string path = archive_entry_pathname(entry);
403
404 // Check # components, drop the single top-level path
405 int slashes = count(path.begin(), path.end(), '/');
406 if (slashes < 1) {
407 archive_entry_set_pathname(entry, "");
408 return true;
409 }
410 if (ocpn::startswith(path, "./")) {
411 path = path.substr(1);
412 }
413
414 // Remove top-level directory part
415 int slashpos = path.find_first_of('/', 1);
416 if (slashpos < 0) {
417 archive_entry_set_pathname(entry, "");
418 return true;
419 }
420
421 string prefix = path.substr(0, slashpos);
422 path = path.substr(prefix.size() + 1);
423
424 // Map remaining path to installation directory
425 if (ocpn::endswith(path, ".dll") || ocpn::endswith(path, ".exe")) {
426 slashpos = path.find_first_of('/');
427 path = path.substr(slashpos + 1);
428 path = installPaths["bin"] + "\\" + path;
429 } else if (ocpn::startswith(path, "share")) {
430 // The "share" directory should be a direct sibling of "plugins" directory
431 wxFileName fn(installPaths["share"].c_str(),
432 ""); // should point to .../opencpn/plugins
433 fn.RemoveLastDir(); // should point to ".../opencpn
434 path = fn.GetFullPath().ToStdString() + path;
435 } else if (ocpn::startswith(path, "plugins")) {
436 slashpos = path.find_first_of('/');
437 // share path already ends in plugins/, drop prefix from archive entry.
438 path = path.substr(slashpos + 1);
439 path = installPaths["share"] + "\\" + path;
440
441 } else if (archive_entry_filetype(entry) == AE_IFREG) {
442 wxString msg(_T("PluginHandler::Invalid install path on file: "));
443 msg += wxString(path.c_str());
444 wxLogDebug(msg);
445 return false;
446 }
447 wxString s(path);
448 s.Replace("/", "\\"); // std::regex_replace FTBS on gcc 4.8.4
449 s.Replace("\\\\", "\\");
450 archive_entry_set_pathname(entry, s.c_str());
451 return true;
452}
453
454static bool flatpak_entry_set_install_path(struct archive_entry* entry,
455 pathmap_t installPaths) {
456 using namespace std;
457
458 string path = archive_entry_pathname(entry);
459 int slashes = count(path.begin(), path.end(), '/');
460 if (slashes < 2) {
461 archive_entry_set_pathname(entry, "");
462 return true;
463 }
464 if (ocpn::startswith(path, "./")) {
465 path = path.substr(2);
466 }
467 int slashpos = path.find_first_of('/', 1);
468 string prefix = path.substr(0, slashpos);
469 path = path.substr(prefix.size() + 1);
470 slashpos = path.find_first_of('/');
471 string location = path.substr(0, slashpos);
472 string suffix = path.substr(slashpos + 1);
473 if (installPaths.find(location) == installPaths.end() &&
474 archive_entry_filetype(entry) == AE_IFREG) {
475 wxString msg(_T("PluginHandler::Invalid install path on file: "));
476 msg += wxString(path.c_str());
477 wxLogDebug(msg);
478 return false;
479 }
480 string dest = installPaths[location] + "/" + suffix;
481 archive_entry_set_pathname(entry, dest.c_str());
482
483 return true;
484}
485
486static bool linux_entry_set_install_path(struct archive_entry* entry,
487 pathmap_t installPaths) {
488 using namespace std;
489
490 string path = archive_entry_pathname(entry);
491 int slashes = count(path.begin(), path.end(), '/');
492 if (slashes < 2) {
493 archive_entry_set_pathname(entry, "");
494 return true;
495 }
496
497 int slashpos = path.find_first_of('/', 1);
498 if (ocpn::startswith(path, "./"))
499 slashpos = path.find_first_of('/', 2); // skip the './'
500
501 string prefix = path.substr(0, slashpos);
502 path = path.substr(prefix.size() + 1);
503 if (ocpn::startswith(path, "usr/")) {
504 path = path.substr(strlen("usr/"));
505 }
506 if (ocpn::startswith(path, "local/")) {
507 path = path.substr(strlen("local/"));
508 }
509 slashpos = path.find_first_of('/');
510 string location = path.substr(0, slashpos);
511 string suffix = path.substr(slashpos + 1);
512 if (installPaths.find(location) == installPaths.end() &&
513 archive_entry_filetype(entry) == AE_IFREG) {
514 wxString msg(_T("PluginHandler::Invalid install path on file: "));
515 msg += wxString(path.c_str());
516 wxLogDebug(msg);
517 return false;
518 }
519
520 string dest = installPaths[location] + "/" + suffix;
521
522 if (g_bportable) {
523 // A data dir?
524 if (ocpn::startswith(location, "share") &&
525 ocpn::startswith(suffix, "opencpn/plugins/")) {
526 slashpos = suffix.find_first_of("opencpn/plugins/");
527 suffix = suffix.substr(16);
528
529 dest =
530 g_BasePlatform->GetPrivateDataDir().ToStdString() + "/plugins/" + suffix;
531 }
532 if (ocpn::startswith(location, "lib") &&
533 ocpn::startswith(suffix, "opencpn/")) {
534 suffix = suffix.substr(8);
535
536 dest = g_BasePlatform->GetPrivateDataDir().ToStdString() + "/plugins/lib/" +
537 suffix;
538 }
539 }
540
541 archive_entry_set_pathname(entry, dest.c_str());
542 return true;
543}
544
545static bool apple_entry_set_install_path(struct archive_entry* entry,
546 pathmap_t installPaths) {
547 using namespace std;
548
549 const string base = PluginPaths::getInstance()->Homedir() +
550 "/Library/Application Support/OpenCPN";
551
552 string path = archive_entry_pathname(entry);
553 if (ocpn::startswith(path, "./")) path = path.substr(2);
554
555 string dest("");
556 size_t slashes = count(path.begin(), path.end(), '/');
557 if (slashes < 3) {
558 archive_entry_set_pathname(entry, "");
559 return true;
560 }
561 auto parts = split(path, "Contents/Resources");
562 if (parts.size() >= 2) {
563 dest = base + "/Contents/Resources" + parts[1];
564 }
565 if (dest == "") {
566 parts = split(path, "Contents/SharedSupport");
567 if (parts.size() >= 2) {
568 dest = base + "/Contents/SharedSupport" + parts[1];
569 }
570 }
571 if (dest == "") {
572 parts = split(path, "Contents/PlugIns");
573 if (parts.size() >= 2) {
574 dest = base + "/Contents/PlugIns" + parts[1];
575 }
576 }
577 if (dest == "" && archive_entry_filetype(entry) == AE_IFREG) {
578 wxString msg(_T("PluginHandler::Invalid install path on file: "));
579 msg += wxString(path.c_str());
580 wxLogDebug(msg);
581 return false;
582 }
583 archive_entry_set_pathname(entry, dest.c_str());
584 return true;
585}
586
587static bool android_entry_set_install_path(struct archive_entry* entry,
588 pathmap_t installPaths) {
589 using namespace std;
590
591 string path = archive_entry_pathname(entry);
592 int slashes = count(path.begin(), path.end(), '/');
593 if (slashes < 2) {
594 archive_entry_set_pathname(entry, "");
595 return true;
596 ;
597 }
598
599 int slashpos = path.find_first_of('/', 1);
600 if (ocpn::startswith(path, "./"))
601 slashpos = path.find_first_of('/', 2); // skip the './'
602
603 string prefix = path.substr(0, slashpos);
604 path = path.substr(prefix.size() + 1);
605 if (ocpn::startswith(path, "usr/")) {
606 path = path.substr(strlen("usr/"));
607 }
608 if (ocpn::startswith(path, "local/")) {
609 path = path.substr(strlen("local/"));
610 }
611 slashpos = path.find_first_of('/');
612 string location = path.substr(0, slashpos);
613 string suffix = path.substr(slashpos + 1);
614 if (installPaths.find(location) == installPaths.end() &&
615 archive_entry_filetype(entry) == AE_IFREG) {
616 wxString msg(_T("PluginHandler::Invalid install path on file: "));
617 msg += wxString(path.c_str());
618 wxLogDebug(msg);
619 return false;
620 }
621
622 if ((location == "lib") && ocpn::startswith(suffix, "opencpn")) {
623 auto parts = split(suffix, "/");
624 if (parts.size() == 2) suffix = parts[1];
625 }
626
627 if ((location == "share") && ocpn::startswith(suffix, "opencpn")) {
628 auto parts = split(suffix, "opencpn/");
629 if (parts.size() == 2) suffix = parts[1];
630 }
631
633 string dest = installPaths[location] + "/" + suffix;
634
635 archive_entry_set_pathname(entry, dest.c_str());
636 return true;
637}
638
639static bool entry_set_install_path(struct archive_entry* entry,
640 pathmap_t installPaths) {
641 const std::string src = archive_entry_pathname(entry);
642 bool rv;
643#ifdef __OCPN__ANDROID__
644 rv = android_entry_set_install_path(entry, installPaths);
645#else
646 const auto osSystemId = wxPlatformInfo::Get().GetOperatingSystemId();
647 if (g_BasePlatform->isFlatpacked()) {
648 rv = flatpak_entry_set_install_path(entry, installPaths);
649 } else if (osSystemId & wxOS_UNIX_LINUX) {
650 rv = linux_entry_set_install_path(entry, installPaths);
651 } else if (osSystemId & wxOS_WINDOWS) {
652 rv = win_entry_set_install_path(entry, installPaths);
653 } else if (osSystemId & wxOS_MAC) {
654 rv = apple_entry_set_install_path(entry, installPaths);
655 } else {
656 wxLogMessage("set_install_path() invoked, unsupported platform %s",
657 wxPlatformInfo::Get().GetOperatingSystemDescription());
658 rv = false;
659 }
660#endif
661 const std::string dest = archive_entry_pathname(entry);
662 if (rv) {
663 if (dest.size()) {
664 MESSAGE_LOG << "Installing " << src << " into " << dest << std::endl;
665 }
666 }
667 return rv;
668}
669
670bool PluginHandler::archive_check(int r, const char* msg, struct archive* a) {
671 if (r < ARCHIVE_OK) {
672 std::string s(msg);
673
674 if (archive_error_string(a)) s = s + ": " + archive_error_string(a);
675 wxLogMessage(s.c_str());
676 last_error_msg = s;
677 }
678 return r >= ARCHIVE_WARN;
679}
680
681bool PluginHandler::explodeTarball(struct archive* src, struct archive* dest,
682 std::string& filelist,
683 const std::string& metadata_path) {
684 struct archive_entry* entry = 0;
685 pathmap_t pathmap = getInstallPaths();
686 while (true) {
687 int r = archive_read_next_header(src, &entry);
688 if (r == ARCHIVE_EOF) {
689 return true;
690 }
691 if (!archive_check(r, "archive read header error", src)) {
692 return false;
693 }
694 std::string path = archive_entry_pathname(entry);
695 bool is_metadata = std::string::npos != path.find("metadata.xml");
696 if (is_metadata) {
697 if (metadata_path == "") continue;
698 archive_entry_set_pathname(entry, metadata_path.c_str());
699 }
700 else if (!entry_set_install_path(entry, pathmap)) continue;
701 if (strlen(archive_entry_pathname(entry)) == 0) {
702 continue;
703 }
704 if (!is_metadata) {
705 filelist.append(std::string(archive_entry_pathname(entry)) + "\n");
706 }
707 r = archive_write_header(dest, entry);
708 archive_check(r, "archive write install header error", dest);
709 if (r >= ARCHIVE_OK && archive_entry_size(entry) > 0) {
710 r = copy_data(src, dest);
711 if (!archive_check(r, "archive copy data error", dest)) {
712 return false;
713 }
714 }
715 r = archive_write_finish_entry(dest);
716 if (!archive_check(r, "archive finish write error", dest)) {
717 return false;
718 }
719 }
720 return false; // notreached
721}
722
723/*
724 * Extract tarball into platform-specific user directories.
725 *
726 * The installed tarball has paths like topdir/dest/suffix_path... e. g.
727 * oesenc_pi_ubuntu_10_64/usr/local/share/opencpn/plugins/oesenc_pi/README.
728 * In this path, the topdir part must exist but is discarded. Next parts
729 * being being standard prefixes like /usr/local or /usr are also
730 * discarded. The remaining path (here share) is mapped to a user
731 * directory. On linux, it ends up in ~/.local/share. The suffix
732 * part is then installed as-is into this directory.
733 *
734 * Windows tarballs has dll and binary files in the top directory. They
735 * go to winInstallDir/Program Files. Message catalogs exists under a
736 * share/ toplevel directory, they go in winInstallDir/share. The
737 * plugin data is installed under winInstallDir/plugins/<plugin name>,
738 * and must be looked up by the plugins using GetPluginDataDir(plugin);
739 * Windows requires that PATH is set to include the binary dir and tha
740 * a bindtextdomain call is invoked to define the message catalog paths.
741 *
742 * For linux, the expected destinations are bin, lib and share.
743 *
744 * Parameters:
745 * - path: path to tarball
746 * - filelist: On return contains a list of files installed.
747 * - last_error_msg: Updated when returning false.
748 *
749 */
750bool PluginHandler::extractTarball(const std::string path,
751 std::string& filelist,
752 const std::string metadata_path) {
753 struct archive* src = archive_read_new();
754 archive_read_support_filter_gzip(src);
755 archive_read_support_format_tar(src);
756 int r = archive_read_open_filename(src, path.c_str(), 10240);
757 if (r != ARCHIVE_OK) {
758 std::ostringstream os;
759 os << "Cannot read installation tarball: " << path;
760 wxLogWarning(os.str().c_str());
761 last_error_msg = os.str();
762 return false;
763 }
764 struct archive* dest = archive_write_disk_new();
765 archive_write_disk_set_options(dest, ARCHIVE_EXTRACT_TIME);
766 bool ok = explodeTarball(src, dest, filelist, metadata_path);
767 archive_read_free(src);
768 archive_write_free(dest);
769 return ok;
770}
771
772PluginHandler* PluginHandler::getInstance() {
773 static PluginHandler* instance = 0;
774 if (!instance) {
775 instance = new (PluginHandler);
776 }
777 return instance;
778}
779
780bool PluginHandler::isPluginWritable(std::string name) {
781 if (isRegularFile(PluginHandler::fileListPath(name).c_str())) {
782 return true;
783 }
784 auto loader = PluginLoader::getInstance();
785 return PlugInIxByName(name, loader->GetPlugInArray()) == -1;
786}
787
788static std::string computeMetadataPath(void) {
789 std::string path = g_BasePlatform->GetPrivateDataDir().ToStdString();
790 path += SEP;
791 path += "ocpn-plugins.xml";
792 if (ocpn::exists(path)) {
793 return path;
794 }
795
796 // If default location for composit plugin metadata is not found,
797 // we look in the plugin cache directory, which will normally contain
798 // he last "master" catalog downloaded
799 path = ocpn::lookup_metadata();
800 if (path != "") {
801 return path;
802 }
803
804 // And if that does not work, use the empty metadata file found in the
805 // distribution "data" directory
806 path = g_BasePlatform->GetSharedDataDir();
807 path += SEP;
808 path += "ocpn-plugins.xml";
809 if (!ocpn::exists(path)) {
810 wxLogWarning("Non-existing plugins file: %s", path);
811 }
812 return path;
813}
814
816 if (metadataPath.size() > 0) {
817 return metadataPath;
818 }
819 metadataPath = computeMetadataPath();
820 wxLogDebug("Using metadata path: %s", metadataPath.c_str());
821 return metadataPath;
822}
823
824static void parseMetadata(const std::string path, CatalogCtx& ctx) {
825 using namespace std;
826
827 wxLogMessage("PluginHandler: using metadata path: %s", path);
828 ctx.depth = 0;
829 if (!ocpn::exists(path)) {
830 wxLogWarning("Non-existing plugins metadata file: %s", path.c_str());
831 return;
832 }
833 ifstream ifpath(path);
834 std::string xml((istreambuf_iterator<char>(ifpath)),
835 istreambuf_iterator<char>());
836 ParseCatalog(xml, &ctx);
837}
838
839const std::map<std::string, int> PluginHandler::getCountByTarget() {
840 auto plugins = getInstalled();
841 auto a = getAvailable();
842 plugins.insert(plugins.end(), a.begin(), a.end());
843 std::map<std::string, int> count_by_target;
844 for (const auto& p : plugins) {
845 if (p.target == "") {
846 continue; // Built-in plugins like dashboard et. al.
847 }
848 auto key = p.target + ":" + p.target_version;
849 if (count_by_target.find(key) == count_by_target.end()) {
850 count_by_target[key] = 1;
851 } else {
852 count_by_target[key] += 1;
853 }
854 }
855 return count_by_target;
856}
857
858void PluginHandler::cleanupFiles(const std::string& manifestFile,
859 const std::string& plugname) {
860 std::ifstream diskfiles(manifestFile);
861 if (diskfiles.is_open()) {
862 std::stringstream buffer;
863 buffer << diskfiles.rdbuf();
864 PluginHandler::cleanup(buffer.str(), plugname);
865 }
866}
867
868void PluginHandler::cleanup(const std::string& filelist,
869 const std::string& plugname) {
870 wxLogMessage("Cleaning up failed install of %s", plugname.c_str());
871
872 std::istringstream files(filelist);
873 while (!files.eof()) {
874 char line[256];
875 files.getline(line, sizeof(line));
876 if (isRegularFile(line)) {
877 int r = remove(line);
878 if (r != 0) {
879 wxLogWarning("Cannot remove file %s: %s", line, strerror(r));
880 }
881 }
882 }
883
884 // Make another limited recursive pass, and remove any empty directories
885 bool done = false;
886 int iloop = 0;
887 while (!done && (iloop < 6)) {
888 done = true;
889 std::istringstream dirs(filelist);
890 while (!dirs.eof()) {
891 char line[256];
892 dirs.getline(line, sizeof(line));
893
894 wxFileName wxFile(line);
895 if (wxFile.IsDir() && wxFile.DirExists()) {
896 wxDir dir(wxFile.GetFullPath());
897 if (dir.IsOpened()) {
898 if (!dir.HasFiles() && !dir.HasSubDirs()) {
899 wxFile.Rmdir(wxPATH_RMDIR_RECURSIVE);
900 done = false;
901 }
902 }
903 }
904 }
905 iloop++;
906 }
907
908 std::string path = PluginHandler::fileListPath(plugname);
909 if (ocpn::exists(path)) {
910 remove(path.c_str());
911 }
912 remove(dirListPath(plugname).c_str()); // Best effort try, failures
913 remove(PluginHandler::versionPath(plugname).c_str()); // are non-critical.
914}
915
916const std::vector<PluginMetadata> PluginHandler::getAvailable() {
917 using namespace std;
918 CatalogCtx ctx;
919
920 auto catalogHandler = CatalogHandler::getInstance();
921
922 // std::string path = g_BasePlatform->GetPrivateDataDir().ToStdString();
923 // path += SEP;
924 // path += "ocpn-plugins.xml";
925 std::string path = getMetadataPath();
926 if (!ocpn::exists(path)) {
927 return ctx.plugins;
928 }
929 std::ifstream file;
930 file.open(path, std::ios::in);
931 if (file.is_open()) {
932 std::string xml((std::istreambuf_iterator<char>(file)),
933 std::istreambuf_iterator<char>());
934 file.close();
935 auto status = catalogHandler->DoParseCatalog(xml, &ctx);
936 if (status == CatalogHandler::ServerStatus::OK) {
937 catalogData.undef = false;
938 catalogData.version = ctx.version;
939 catalogData.date = ctx.date;
940 }
941 }
942
943 return ctx.plugins;
944}
945
946const std::vector<PluginMetadata> PluginHandler::getInstalled() {
947 using namespace std;
948 vector<PluginMetadata> plugins;
949
950 auto loader = PluginLoader::getInstance();
951 ArrayOfPlugIns* mgr_plugins = loader->GetPlugInArray();
952 for (unsigned int i = 0; i < mgr_plugins->GetCount(); i += 1) {
953 PlugInContainer* p = mgr_plugins->Item(i);
954 PluginMetadata plugin;
955 auto name = string(p->m_common_name);
956 // std::transform(name.begin(), name.end(), name.begin(), ::tolower);
957 plugin.name = name;
958 std::stringstream ss;
959 ss << p->m_version_major << "." << p->m_version_minor;
960 plugin.version = ss.str();
961 plugin.readonly = !isPluginWritable(plugin.name);
962 string path = PluginHandler::versionPath(plugin.name);
963 if (path != "" && wxFileName::IsFileReadable(path)) {
964 std::ifstream stream;
965 stream.open(path, ifstream::in);
966 stream >> plugin.version;
967 }
968 plugins.push_back(plugin);
969 }
970 return plugins;
971}
972
973bool PluginHandler::installPlugin(PluginMetadata plugin, std::string path) {
974 std::string filelist;
975 if (!extractTarball(path, filelist)) {
976 std::ostringstream os;
977 os << "Cannot unpack plugin: " << plugin.name << " at " << path;
978 last_error_msg = os.str();
979 PluginHandler::cleanup(filelist, plugin.name);
980 return false;
981 }
982 // remove(path.c_str());
983 saveFilelist(filelist, plugin.name);
984 saveDirlist(plugin.name);
985 saveVersion(plugin.name, plugin.version);
986
987 return true;
988}
989
991 std::string path;
992 char fname[4096];
993
994 if (tmpnam(fname) == NULL) {
995 wxLogWarning("Cannot create temporary file");
996 path = "";
997 return false;
998 }
999 path = std::string(fname);
1000 std::ofstream stream;
1001 stream.open(path.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
1002 DEBUG_LOG << "Downloading: " << plugin.name << std::endl;
1003 auto downloader = Downloader(plugin.tarball_url);
1004 downloader.download(&stream);
1005
1006 return installPlugin(plugin, path);
1007}
1008
1009bool PluginHandler::installPlugin(std::string path) {
1010 std::string filelist;
1011 std::string temp_path(tmpnam(0));
1012 if (!extractTarball(path, filelist, temp_path)) {
1013 std::ostringstream os;
1014 os << "Cannot unpack plugin tarball at : " << path;
1015 if (filelist != "") cleanup(filelist, "unknown_name");
1016 last_error_msg = os.str();
1017 return false;
1018 }
1019 struct CatalogCtx ctx;
1020 std::ifstream istream(temp_path);
1021 std::stringstream buff;
1022 buff << istream.rdbuf();
1023 remove(temp_path.c_str());
1024
1025 auto xml = std::string("<plugins>") + buff.str() + "</plugins>";
1026
1027 ParseCatalog(xml, &ctx);
1028 auto name = ctx.plugins[0].name;
1029 auto version = ctx.plugins[0].version;
1030
1031 saveFilelist(filelist, name);
1032 saveDirlist(name);
1033 saveVersion(name, version);
1034
1035 return true;
1036}
1037
1038bool PluginHandler::uninstall(const std::string plugin_name) {
1039 using namespace std;
1040
1041 auto loader = PluginLoader::getInstance();
1042 auto ix = PlugInIxByName(plugin_name, loader->GetPlugInArray());
1043 auto pic = loader->GetPlugInArray()->Item(ix);
1044 // g_pi_manager->ClosePlugInPanel(pic, wxID_OK);
1045 loader->UnLoadPlugIn(ix);
1046 string path = PluginHandler::fileListPath(plugin_name);
1047 if (!ocpn::exists(path)) {
1048 wxLogWarning("Cannot find installation data for %s (%s)",
1049 plugin_name.c_str(), path);
1050 return false;
1051 }
1052 ifstream files(path);
1053 while (!files.eof()) {
1054 char line[256];
1055 files.getline(line, sizeof(line));
1056 if (isRegularFile(line)) {
1057 int r = remove(line);
1058 if (r != 0) {
1059 wxLogWarning("Cannot remove file %s: %s", line, strerror(r));
1060 }
1061 }
1062 }
1063 files.close();
1064
1065 // Make another limited recursive pass, and remove any empty directories
1066 bool done = false;
1067 int iloop = 0;
1068 while (!done && (iloop < 6)) {
1069 done = true;
1070 ifstream dirs(path);
1071 while (!dirs.eof()) {
1072 char line[256];
1073 dirs.getline(line, sizeof(line));
1074 string dirc(line);
1075
1076 wxFileName wxFile(line);
1077 if (wxFile.IsDir() && wxFile.DirExists()) {
1078 wxDir dir(wxFile.GetFullPath());
1079 if (dir.IsOpened()) {
1080 if (!dir.HasFiles() && !dir.HasSubDirs()) {
1081 wxFile.Rmdir(wxPATH_RMDIR_RECURSIVE);
1082 done = false;
1083 }
1084 }
1085 }
1086 }
1087 dirs.close();
1088
1089 iloop++;
1090 }
1091
1092 int r = remove(path.c_str());
1093 if (r != 0) {
1094 wxLogWarning("Cannot remove file %s: %s", path.c_str(), strerror(r));
1095 }
1096 remove(dirListPath(plugin_name).c_str()); // Best effort try, failures
1097 remove(PluginHandler::versionPath(plugin_name).c_str()); // are OK.
1098
1099 return true;
1100}
1101
1103 // Look for the desired file
1104 wxURI uri(wxString(plugin.tarball_url.c_str()));
1105 wxFileName fn(uri.GetPath());
1106 wxString tarballFile = fn.GetFullName();
1107 std::string cacheFile = ocpn::lookup_tarball(tarballFile);
1108
1109#ifdef __WXOSX__
1110 // Depending on the browser settings, MacOS will sometimes automatically
1111 // de-compress the tar.gz file, leaving a simple ".tar" file in its expected
1112 // place. Check for this case, and "do the right thing"
1113 if (cacheFile == "") {
1114 fn.ClearExt();
1115 wxFileName fn1(fn.GetFullName());
1116 if (fn1.GetExt().IsSameAs("tar")) {
1117 tarballFile = fn.GetFullName();
1118 cacheFile = ocpn::lookup_tarball(tarballFile);
1119 }
1120 }
1121#endif
1122
1123 if (cacheFile != "") {
1124 wxLogMessage("Installing %s from local cache", tarballFile.c_str());
1125 bool bOK = installPlugin(plugin, cacheFile);
1126 if (!bOK) {
1127 wxLogWarning("Cannot install tarball file %s", cacheFile.c_str());
1128 evt_download_failed.Notify(cacheFile);
1129 return false;
1130 }
1131 evt_download_ok.Notify(plugin.name + " " + plugin.version);
1132 return true;
1133 }
1134 return false;
1135}
Handle downloading of files from remote urls.
Definition: downloader.h:34
const void Notify()
Notify all listeners, no data supplied.
Host ABI encapsulation and plugin compatibility checks.
const std::vector< PluginMetadata > getInstalled()
Return list of all installed plugins.
static void cleanup(const std::string &filelist, const std::string &plugname)
Cleanup failed installation attempt using filelist for plugin.
bool installPluginFromCache(PluginMetadata plugin)
Install plugin tarball from local cache.
const std::map< std::string, int > getCountByTarget()
Map of available plugin targets -> number of occurences.
bool isPluginWritable(std::string name)
Check if given plugin can be installed/updated.
static std::string fileListPath(std::string name)
Return path to installation manifest for given plugin.
static std::string versionPath(std::string name)
Return path to file containing version for given plugin.
bool uninstall(const std::string plugin)
Uninstall an installed plugin.
bool installPlugin(PluginMetadata plugin)
Download and install a new, not installed plugin.
std::string getMetadataPath()
Return path to metadata XML file.
const std::vector< PluginMetadata > getAvailable()
Return list of available, not installed plugins.
static bool isCompatible(const PluginMetadata &metadata, const char *os=PKG_TARGET, const char *os_version=PKG_TARGET_VERSION)
Return true if given plugin is loadable on given os/version.
std::string UserLibdir()
The single, user-writable directory for installing .dll files.
Definition: plugin_paths.h:14
std::string Homedir() const
home directory, convenience stuff.
Definition: plugin_paths.h:38
std::string UserDatadir()
The single, user-writable common parent for plugin data directories, typically ending in 'plugins'.
Definition: plugin_paths.h:23
std::string UserBindir()
The single, user-writable directory for installing helper binaries.
Definition: plugin_paths.h:17
static PluginPaths * getInstance()
Return the singleton instance.
Plugin ABI encapsulation.
std::string lookup_tarball(const char *uri)
Get path to tarball in cache for given filename.
std::string lookup_metadata(const char *name)
Get metadata path for a given name defaulting to ocpn-plugins.xml)
The result from parsing the xml catalog i.
Plugin metadata, reflects the xml format directly.