OpenCPN Partial API docs
Loading...
Searching...
No Matches
download_mgr.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
25#include "config.h"
26
27#include <fstream>
28#include <set>
29#include <sstream>
30
31#include <wx/bitmap.h>
32#include <wx/button.h>
33#include <wx/debug.h>
34#include <wx/file.h>
35#include <wx/image.h>
36#include <wx/log.h>
37#include <wx/panel.h>
38#include <wx/progdlg.h>
39#include <wx/sizer.h>
40#include <wx/statline.h>
41#include <wx/uri.h>
42
43#include "catalog_mgr.h"
44#include "download_mgr.h"
45#include "downloader.h"
46#include "OCPNPlatform.h"
47#include "picosha2.h"
48#include "plugin_handler.h"
49#include "plugin_cache.h"
50#include "pluginmanager.h"
51#include "semantic_vers.h"
52#include "styles.h"
53#include "svg_utils.h"
54
55extern PlugInManager* g_pi_manager;
56extern ocpnStyle::StyleManager* g_StyleManager;
57extern OCPNPlatform* g_Platform;
58
59#undef major // walk around gnu's major() and minor() macros.
60#undef minor
61
62// Main window reload event
63wxDEFINE_EVENT(EVT_PLUGINS_RELOAD, wxCommandEvent);
64
65namespace download_mgr {
66
70static bool checksum_ok(const std::string& path,
71 const PluginMetadata& metadata) {
72 wxLogDebug("Checksum test on %s", metadata.name.c_str());
73 if (metadata.checksum == "") {
74 wxLogDebug("No metadata checksum, aborting check,");
75 return true;
76 }
77 const size_t pos = metadata.checksum.find(':');
78 std::string checksum(metadata.checksum);
79 if (pos == std::string::npos) {
80 checksum = std::string("sha256:") + checksum;
81 }
82 std::ifstream f(path, std::ios::binary);
83 picosha2::hash256_one_by_one hasher;
84 while (!f.eof()) {
85 char buff[2048];
86 f.read(buff, sizeof(buff));
87 const std::string block(buff, f.gcount());
88 hasher.process(block.begin(), block.end());
89 }
90 hasher.finish();
91 std::string tarball_hash =
92 std::string("sha256:") + picosha2::get_hash_hex_string(hasher);
93
94 if (tarball_hash == checksum) {
95 wxLogDebug("Checksum ok: %s", tarball_hash.c_str());
96 return true;
97 }
98 wxLogMessage("Checksum fail on %s, tarball: %s, metadata: %s",
99 metadata.name.c_str(), tarball_hash.c_str(), checksum.c_str());
100 return false;
101}
102
107static ssize_t PlugInIxByName(const std::string name, ArrayOfPlugIns* plugins) {
108 for (unsigned i = 0; i < plugins->GetCount(); i += 1) {
109 if (name == plugins->Item(i)->m_common_name.Lower().ToStdString()) {
110 return i;
111 }
112 }
113 return -1;
114}
115
117static PlugInContainer* PlugInByName(const std::string name,
118 ArrayOfPlugIns* plugins) {
119 auto ix = PlugInIxByName(name, plugins);
120 return ix == -1 ? 0 : plugins->Item(ix);
121}
122
124static void LoadPNGIcon(const char* path, int size, wxBitmap& bitmap) {
125 wxPNGHandler handler;
126 if (!wxImage::FindHandler(handler.GetName())) {
127 wxImage::AddHandler(new wxPNGHandler());
128 }
129 auto img = new wxImage();
130 bool ok = img->LoadFile(path, wxBITMAP_TYPE_PNG);
131 if (!ok) {
132 bitmap = wxBitmap();
133 return;
134 }
135 img->Rescale(size, size);
136 bitmap = wxBitmap(*img);
137}
138
145class PluginIconPanel : public wxPanel {
146public:
147 PluginIconPanel(wxWindow* parent, std::string plugin_name)
148 : wxPanel(parent), m_plugin_name(plugin_name) {
149 auto size = GetClientSize();
150 auto minsize = GetTextExtent("OpenCPN");
151 SetMinClientSize(wxSize(minsize.GetWidth(), size.GetHeight()));
152 Layout();
153 Bind(wxEVT_PAINT, &PluginIconPanel::OnPaint, this);
154 }
155
156 void OnPaint(wxPaintEvent& event) {
157 auto size = GetClientSize();
158 int minsize = wxMin(size.GetHeight(), size.GetWidth());
159 auto offset = minsize / 10;
160
161 LoadIcon(m_plugin_name.c_str(), m_bitmap, 2 * minsize / 3);
162 wxPaintDC dc(this);
163 if (!m_bitmap.IsOk()) {
164 wxLogMessage("AddPluginPanel: bitmap is not OK!");
165 return;
166 }
167 dc.DrawBitmap(m_bitmap, offset, offset, true);
168 }
169
170protected:
171 wxBitmap m_bitmap;
172 const std::string m_plugin_name;
173
174 void LoadIcon(const char* plugin_name, wxBitmap& bitmap, int size = 32) {
175 wxFileName path(g_Platform->GetSharedDataDir(), plugin_name);
176 path.AppendDir("uidata");
177 path.AppendDir("plugins");
178 path.SetExt("svg");
179 bool ok = false;
180 if (path.IsFileReadable()) {
181 bitmap = LoadSVG(path.GetFullPath(), size, size);
182 ok = bitmap.IsOk();
183 }
184 if (!ok) {
185 path.SetExt("png");
186 if (path.IsFileReadable()) {
187 LoadPNGIcon(path.GetFullPath(), size, bitmap);
188 ok = bitmap.IsOk();
189 }
190 }
191 if (!ok) {
192 auto style = g_StyleManager->GetCurrentStyle();
193 bitmap = wxBitmap(style->GetIcon(_T("default_pi")));
194 }
195 }
196};
197
199class InstallButton : public wxPanel {
200public:
201 InstallButton(wxWindow* parent, PluginMetadata metadata)
202 : wxPanel(parent), m_metadata(metadata), m_remove(false) {
203 PlugInContainer* found =
204 PlugInByName(metadata.name,
205 PluginLoader::getInstance()->GetPlugInArray());
206 std::string label(_("Install"));
207 if (found) {
208 label = getUpdateLabel(found, metadata);
209 m_remove = true;
210 }
211 auto button = new wxButton(this, wxID_ANY, label);
212 auto pluginHandler = PluginHandler::getInstance();
213 button->Enable(pluginHandler->isPluginWritable(metadata.name));
214 auto box = new wxBoxSizer(wxHORIZONTAL);
215 box->Add(button);
216 SetSizer(box);
217 Bind(wxEVT_COMMAND_BUTTON_CLICKED, &InstallButton::OnClick, this);
218 }
219
220 void OnClick(wxCommandEvent& event) {
221 auto path = ocpn::lookup_tarball(m_metadata.tarball_url.c_str());
222 if (m_remove && path != "") {
223 wxLogMessage("Uninstalling %s", m_metadata.name.c_str());
224 PluginHandler::getInstance()->uninstall(m_metadata.name);
225 }
226 wxLogMessage("Installing %s", m_metadata.name.c_str());
227
228 auto pluginHandler = PluginHandler::getInstance();
229 bool cacheResult = pluginHandler->installPluginFromCache(m_metadata);
230
231 if (!cacheResult) {
232 auto downloader = new GuiDownloader(this, m_metadata);
233 downloader->run(this, m_remove);
234 auto loader = PluginLoader::getInstance();
235 auto pic = PlugInByName(m_metadata.name, loader->GetPlugInArray());
236 if (!pic) {
237 wxLogMessage("Installation of %s failed", m_metadata.name.c_str());
238 return;
239 }
240 auto upwards = GetParent()->GetParent()->GetParent();
241 auto main_window = dynamic_cast<PluginDownloadDialog*>(upwards);
242 wxASSERT(main_window != 0);
243 auto listPanels = dynamic_cast<PluginListPanel*>(
244 main_window->GetRealParent()->GetPrevSibling());
245 wxASSERT(listPanels != 0);
246 listPanels->ReloadPluginPanels();
247 auto window = GetSizer()->GetItem((size_t)0)->GetWindow();
248 auto btn = dynamic_cast<wxButton*>(window);
249 wxASSERT(btn != 0);
250 btn->SetLabel(_("Reinstall"));
251 }
252 }
253
254private:
255 PluginMetadata m_metadata;
256 bool m_remove;
257
258 const char* getUpdateLabel(PlugInContainer* pic, PluginMetadata metadata) {
259 SemanticVersion currentVersion(pic->m_version_major, pic->m_version_minor);
260 if (pic->m_version_str != "") {
261 currentVersion = SemanticVersion::parse(pic->m_version_str.ToStdString());
262 }
263 auto newVersion = SemanticVersion::parse(metadata.version);
264 if (newVersion > currentVersion) {
265 return _("Update");
266 } else if (newVersion == currentVersion) {
267 return _("Reinstall");
268 } else {
269 return _("Downgrade");
270 }
271 }
272};
273
275class CandidateButtonsPanel : public wxPanel {
276public:
277 CandidateButtonsPanel(wxWindow* parent, const PluginMetadata* plugin)
278 : wxPanel(parent) {
279 auto flags = wxSizerFlags().Border();
280
281 auto vbox = new wxBoxSizer(wxVERTICAL);
282 vbox->Add(new InstallButton(this, *plugin),
283 flags.DoubleBorder().Top().Right());
284 vbox->Add(1, 1, 1, wxEXPAND); // Expanding, stretchable spacer
285 m_info_btn = new WebsiteButton(this, plugin->info_url.c_str());
286 m_info_btn->Hide();
287 vbox->Add(m_info_btn, flags.DoubleBorder().Bottom().Right());
288 SetSizer(vbox);
289 Fit();
290 }
291
292 void HideDetails(bool hide) {
293 m_info_btn->Show(!hide);
294 GetParent()->Layout();
295 }
296
297private:
298 WebsiteButton* m_info_btn;
299};
300
302class PluginTextPanel : public wxPanel {
303public:
304 PluginTextPanel(wxWindow* parent, const PluginMetadata* plugin,
305 CandidateButtonsPanel* buttons, bool bshowTuple = false)
306 : wxPanel(parent), m_descr(0), m_buttons(buttons) {
307 auto flags = wxSizerFlags().Border();
308
309 MORE = "<span foreground=\'blue\'>";
310 MORE += _("More");
311 MORE += "...</span>";
312 LESS = "<span foreground=\'blue\'>";
313 LESS += _("Less");
314 LESS += "...</span>";
315
316 auto sum_hbox = new wxBoxSizer(wxHORIZONTAL);
317 m_summary = staticText(plugin->summary);
318 sum_hbox->Add(m_summary);
319 sum_hbox->AddSpacer(10);
320 m_more = staticText("");
321 m_more->SetLabelMarkup(MORE);
322 sum_hbox->Add(m_more, wxSizerFlags());
323
324 auto vbox = new wxBoxSizer(wxVERTICAL);
325 wxString nameText(plugin->name + " " + plugin->version);
326 if (bshowTuple) nameText += " " + plugin->target;
327 auto name = staticText(nameText);
328 m_descr = staticText(plugin->description);
329 m_descr->Hide();
330 vbox->Add(name, flags);
331 vbox->Add(sum_hbox, flags);
332 vbox->Add(m_descr, flags.Expand());
333 SetSizer(vbox);
334
335 m_more->Bind(wxEVT_LEFT_DOWN, &PluginTextPanel::OnClick, this);
336 m_descr->Bind(wxEVT_LEFT_DOWN, &PluginTextPanel::OnClick, this);
337 }
338
339 void OnClick(wxMouseEvent& event) {
340 m_descr->Show(!m_descr->IsShown());
341 m_more->SetLabelMarkup(m_descr->IsShown() ? LESS : MORE);
342 m_buttons->HideDetails(!m_descr->IsShown());
343 GetParent()->SendSizeEvent();
344 GetParent()->GetParent()->GetParent()->Layout();
345 GetParent()->GetParent()->GetParent()->Refresh(true);
346 GetParent()->GetParent()->GetParent()->Update();
347 }
348
349protected:
350 wxString MORE, LESS;
351
352 wxStaticText* staticText(const wxString& text) {
353 return new wxStaticText(this, wxID_ANY, text, wxDefaultPosition,
354 wxDefaultSize, wxALIGN_LEFT);
355 }
356
357 wxStaticText* m_descr;
358 wxStaticText* m_more;
359 wxStaticText* m_summary;
360 CandidateButtonsPanel* m_buttons;
361};
362
366class MainButtonsPanel : public wxPanel {
367public:
368 MainButtonsPanel(wxWindow* parent, wxWindow* victim)
369 : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(200, 32)) {
370 auto sizer = new wxBoxSizer(wxHORIZONTAL);
371 auto spacing = GetTextExtent("m").GetWidth();
372 sizer->Add(1, 1, 1, wxEXPAND); // Expanding, stretchable space
373 sizer->Add(new UpdateCatalogDialogBtn(this), wxSizerFlags());
374 sizer->Add(spacing, 0);
375 sizer->Add(new UpdateCatalogNowBtn(this), wxSizerFlags());
376 sizer->Add(spacing, 0);
377 sizer->Add(new wxButton(this, wxID_OK, _("Done")), wxSizerFlags());
378 SetSizer(sizer);
379 }
380
381protected:
383 class UpdateCatalogNowBtn : public wxButton {
384 public:
385 UpdateCatalogNowBtn(wxWindow* parent)
386 : wxButton(parent, wxID_ANY, _("Update plugin catalog")) {
387 Bind(wxEVT_COMMAND_BUTTON_CLICKED, [=](wxCommandEvent&) {
388 new SimpleCatalogDialog(this);
389 wxCommandEvent evt(EVT_PLUGINS_RELOAD);
390 wxPostEvent(GetParent(), evt);
391 });
392 }
393 };
394
396 class UpdateCatalogDialogBtn : public wxButton {
397 public:
398 UpdateCatalogDialogBtn(wxWindow* parent)
399 : wxButton(parent, wxID_ANY, _("Advanced catalog update...")) {
400 Bind(wxEVT_COMMAND_BUTTON_CLICKED,
401 [=](wxCommandEvent&) { new AdvancedCatalogDialog(this); });
402 }
403 };
404};
405
410class OcpnScrolledWindow : public wxScrolledWindow {
411public:
412 OcpnScrolledWindow(wxWindow* parent)
413 : wxScrolledWindow(parent), m_grid(new wxFlexGridSizer(3, 0, 0)) {
414 auto box = new wxBoxSizer(wxVERTICAL);
415 populateGrid(m_grid);
416 box->Add(m_grid, wxSizerFlags().Proportion(1).Expand());
417 auto button_panel = new MainButtonsPanel(this, parent);
418 box->Add(button_panel, wxSizerFlags().Right().Border().Expand());
419 Bind(EVT_PLUGINS_RELOAD, [&](wxCommandEvent& ev) { Reload(); });
420
421 SetSizer(box);
422 FitInside();
423 // TODO: Compute size using wxWindow::GetEffectiveMinSize()
424 SetScrollRate(1, 1);
425 };
426
427 void populateGrid(wxFlexGridSizer* grid) {
429 struct metadata_compare {
430 bool operator()(const PluginMetadata& lhs,
431 const PluginMetadata& rhs) const {
432 return lhs.key() < rhs.key();
433 }
434 };
435
436 auto flags = wxSizerFlags();
437 grid->SetCols(3);
438 grid->AddGrowableCol(2);
439 auto available = PluginHandler::getInstance()->getAvailable();
440 std::set<PluginMetadata, metadata_compare> unique_plugins;
441 for (auto plugin : PluginHandler::getInstance()->getAvailable()) {
442 unique_plugins.insert(plugin);
443 }
444 for (auto plugin : unique_plugins) {
445 if (!PluginHandler::isCompatible(plugin)) {
446 continue;
447 }
448 grid->Add(new PluginIconPanel(this, plugin.name), flags.Expand());
449 auto buttons = new CandidateButtonsPanel(this, &plugin);
450 grid->Add(new PluginTextPanel(this, &plugin, buttons,
451 unique_plugins.size() > 1),
452 flags.Proportion(1).Right());
453 grid->Add(buttons, flags.DoubleBorder());
454 grid->Add(new wxStaticLine(this), wxSizerFlags(0).Expand());
455 grid->Add(new wxStaticLine(this), wxSizerFlags(0).Expand());
456 grid->Add(new wxStaticLine(this), wxSizerFlags(0).Expand());
457 }
458 }
459
460 void Reload() {
461 Hide();
462 m_grid->Clear();
463 populateGrid(m_grid);
464 Layout();
465 Show();
466 FitInside();
467 Refresh(true);
468 }
469
470private:
471 wxFlexGridSizer* m_grid;
472};
473
474} // namespace download_mgr
475
478 : wxDialog(parent, wxID_ANY, _("Plugin Manager"), wxDefaultPosition,
479 wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) {
480 auto vbox = new wxBoxSizer(wxVERTICAL);
481 auto scrwin = new download_mgr::OcpnScrolledWindow(this);
482 vbox->Add(scrwin, wxSizerFlags(1).Expand());
483
484 // The list has no natural height. Allocate 20 lines of text so some
485 // items are displayed initially in Layout()
486 int min_height = GetTextExtent("abcdefghijklmnopqrst").GetHeight() * 20;
487
488 // There seem to be no way have dynamic, wrapping text:
489 // https://forums.wxwidgets.org/viewtopic.php?f=1&t=46662
490 int width = GetParent()->GetClientSize().GetWidth();
491 SetMinClientSize(wxSize(width, min_height));
492
493 SetSizer(vbox);
494 Fit();
495 Layout();
496}
497
500 : Downloader(plugin.tarball_url),
501 m_downloaded(0),
502 m_dialog(0),
503 m_plugin(plugin),
504 m_parent(parent) {}
505
506std::string GuiDownloader::run(wxWindow* parent, bool remove_current) {
507 bool ok;
508 bool downloaded = false;
509 std::string path = ocpn::lookup_tarball(m_plugin.tarball_url.c_str());
510 if (!path.size()) {
511 long size = get_filesize();
512 std::string label(_("Downloading "));
513 label += url;
514 m_dialog =
515 new wxProgressDialog(_("Downloading"), label.c_str(), size, parent,
516 wxPD_AUTO_HIDE | wxPD_APP_MODAL | wxPD_CAN_ABORT);
517#ifdef __OCPN__ANDROID__
518 m_dialog->SetBackgroundColour(wxColour(0x7c, 0xb0, 0xe9)); // light blue
519#endif
520
521 ok = download(path);
522 g_Platform->HideBusySpinner();
523
524 if (!ok) {
525 delete m_dialog;
526 showErrorDialog("Download error");
527 return "";
528 }
529
530 // Download aborted?
531 if (m_dialog == 0) {
532 showErrorDialog("Download aborted");
533 return "";
534 } else {
535 delete m_dialog;
536 }
537
538 if (!download_mgr::checksum_ok(path, m_plugin)) {
539 showErrorDialog("Checksum error");
540 return "";
541 }
542
543 m_dialog = 0; // make sure that on_chunk() doesn't misbehave.
544 downloaded = true;
545 }
546
547 auto pluginHandler = PluginHandler::getInstance();
548 if (remove_current) {
549 wxLogMessage("Uninstalling %s", m_plugin.name.c_str());
550 pluginHandler->uninstall(m_plugin.name);
551 }
552 ok = pluginHandler->installPlugin(m_plugin, path);
553 if (!ok) {
554 showErrorDialog("Installation error");
555 return "";
556 }
557
558 if (downloaded) {
559 // Cache the tarball from the tmp location to the plugin cache.
560 wxURI uri(wxString(m_plugin.tarball_url.c_str()));
561 wxFileName fn(uri.GetPath());
562 auto basename = fn.GetFullName().ToStdString();
563 if (ocpn::store_tarball(path.c_str(), basename.c_str())) {
564 wxLogMessage("Copied %s to local cache at %s", path.c_str(),
565 basename.c_str());
566 }
567 }
568
569 wxMessageDialog* dlg = new wxMessageDialog(
570 m_parent,
571 m_plugin.name + " " + m_plugin.version + _(" successfully installed"),
572 _("Installation complete"), wxOK | wxCENTRE | wxICON_INFORMATION);
573 dlg->ShowModal();
574 return path;
575}
576
577void GuiDownloader::on_chunk(const char* buff, unsigned bytes) {
578 Downloader::on_chunk(buff, bytes);
579 m_downloaded += bytes;
580 if (m_dialog && !m_dialog->Update(m_downloaded)) {
581 // User pushed Cancel button
582 delete m_dialog;
583 m_dialog = 0;
584 }
585}
586
587void GuiDownloader::showErrorDialog(const char* msg) {
588 auto dlg = new wxMessageDialog(m_parent, "", _("Installation error"),
589 wxOK | wxICON_ERROR);
590 auto last_error_msg = last_error();
591 std::string text = msg;
592 if (last_error_msg != "") {
593 text = text + ": " + error_msg;
594 }
595 text = text + "\nPlease check system log for more info.";
596 dlg->SetMessage(text);
597 dlg->ShowModal();
598 dlg->Destroy();
599}
Catalog handler GUI.
Definition: catalog_mgr.h:33
Handle downloading of files from remote urls.
Definition: downloader.h:34
bool download(std::ostream *stream)
Download url into stream, return false on errors.
Definition: downloader.cpp:55
long get_filesize()
Try to get remote filesize, return 0 on failure.
Definition: downloader.cpp:99
std::string last_error()
Last Curl error message.
Definition: downloader.cpp:49
virtual void on_chunk(const char *buff, unsigned bytes)
Called when given bytes has been transferred from remote.
Definition: downloader.cpp:51
Add progress and final message dialogs to the basic Downloader.
Definition: download_mgr.h:47
void on_chunk(const char *buff, unsigned bytes) override
Called when given bytes has been transferred from remote.
GuiDownloader(wxWindow *parent, PluginMetadata plugin)
Add progress and final message dialogs to the basic Downloader.
PluginDownloadDialog(wxWindow *parent)
Top-level install plugins dialog.
bool uninstall(const std::string plugin)
Uninstall an installed plugin.
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.
Invokes client browser on plugin info_url when clicked.
The two buttons 'install' and 'website', the latter optionally hidden.
Download and install a PluginMetadata item when clicked.
Button invoking the advanced catalog dialog.
Invokes the simple update catalog procedure.
Three buttons bottom-right for plugin catalog maintenance.
The list of download candidates in a scrolled window + OK and Settings button.
void populateGrid(wxFlexGridSizer *grid)
A plugin icon, scaled to about 2/3 of available space.
Plugin name, version, summary + an optionally shown description.
std::string lookup_tarball(const char *uri)
Get path to tarball in cache for given filename.
bool store_tarball(const char *path, const char *basename)
Store a tarball in tarball cache, return success/fail.
Plugin metadata, reflects the xml format directly.
Versions uses a modified semantic versioning scheme: major.minor.revision.post-tag+build.
Definition: semantic_vers.h:51
static SemanticVersion parse(std::string s)
Parse a version string, sets major == -1 on errors.
Runtime representation of a plugin block.