FlightGear next
Addon.cxx
Go to the documentation of this file.
1// -*- coding: utf-8 -*-
2//
3// Addon.cxx --- FlightGear class holding add-on metadata
4// Copyright (C) 2017, 2018 Florent Rougon
5//
6// This program is free software; you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation; either version 2 of the License, or
9// (at your option) any later version.
10//
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15//
16// You should have received a copy of the GNU General Public License along
17// with this program; if not, write to the Free Software Foundation, Inc.,
18// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20#include <map>
21#include <ostream>
22#include <sstream>
23#include <string>
24#include <utility>
25#include <vector>
26
27#include <simgear/misc/sg_dir.hxx>
28#include <simgear/misc/sg_path.hxx>
29#include <simgear/misc/strutils.hxx>
30#include <simgear/nasal/cppbind/Ghost.hxx>
31#include <simgear/nasal/cppbind/NasalHash.hxx>
32#include <simgear/nasal/naref.h>
33#include <simgear/props/props.hxx>
34#include <simgear/props/props_io.hxx>
35
36#include <Main/fg_props.hxx>
37#include <Main/globals.hxx>
39
40#include "addon_fwd.hxx"
41#include "Addon.hxx"
43#include "AddonVersion.hxx"
44#include "exceptions.hxx"
45#include "pointer_traits.hxx"
46
47namespace strutils = simgear::strutils;
48
49using std::string;
50using std::vector;
51
52namespace flightgear
53{
54
55namespace addons
56{
57
58// ***************************************************************************
59// * QualifiedUrl *
60// ***************************************************************************
61
62QualifiedUrl::QualifiedUrl(UrlType type, string url, string detail)
63 : _type(type),
64 _url(std::move(url)),
65 _detail(std::move(detail))
66{ }
67
69{ return _type; }
70
72{ _type = type; }
73
74std::string QualifiedUrl::getUrl() const
75{ return _url; }
76
77void QualifiedUrl::setUrl(const std::string& url)
78{ _url = url; }
79
80std::string QualifiedUrl::getDetail() const
81{ return _detail; }
82
83void QualifiedUrl::setDetail(const std::string& detail)
84{ _detail = detail; }
85
86// ***************************************************************************
87// * Addon *
88// ***************************************************************************
89
90Addon::Addon(std::string id, AddonVersion version, SGPath basePath,
91 std::string minFGVersionRequired, std::string maxFGVersionRequired,
92 SGPropertyNode* addonNode)
93 : _id(std::move(id)),
94 _version(
95 shared_ptr_traits<AddonVersionRef>::makeStrongRef(std::move(version))),
96 _basePath(std::move(basePath)),
97 _storagePath(globals->get_fg_home() / ("Export/Addons/" + _id)),
98 _minFGVersionRequired(std::move(minFGVersionRequired)),
99 _maxFGVersionRequired(std::move(maxFGVersionRequired)),
100 _addonNode(addonNode)
101{
102 if (_minFGVersionRequired.empty()) {
103 // This add-on metadata class appeared in FlightGear 2017.4.0
104 _minFGVersionRequired = "2017.4.0";
105 }
106
107 if (_maxFGVersionRequired.empty()) {
108 _maxFGVersionRequired = "none"; // special value
109 }
110}
111
112std::string Addon::getId() const
113{ return _id; }
114
115std::string Addon::getName() const
116{ return _name; }
117
118void Addon::setName(const std::string& addonName)
119{ _name = addonName; }
120
122{ return _version; }
123
124void Addon::setVersion(const AddonVersion& addonVersion)
125{
126 using ptr_traits = shared_ptr_traits<AddonVersionRef>;
127 _version.reset(ptr_traits::makeStrongRef(addonVersion));
128}
129
130std::vector<AuthorRef> Addon::getAuthors() const
131{ return _authors; }
132
133void Addon::setAuthors(const std::vector<AuthorRef>& addonAuthors)
134{ _authors = addonAuthors; }
135
136std::vector<MaintainerRef> Addon::getMaintainers() const
137{ return _maintainers; }
138
139void Addon::setMaintainers(const std::vector<MaintainerRef>& addonMaintainers)
140{ _maintainers = addonMaintainers; }
141
142std::string Addon::getShortDescription() const
143{ return _shortDescription; }
144
145void Addon::setShortDescription(const std::string& addonShortDescription)
146{ _shortDescription = addonShortDescription; }
147
148std::string Addon::getLongDescription() const
149{ return _longDescription; }
150
151void Addon::setLongDescription(const std::string& addonLongDescription)
152{ _longDescription = addonLongDescription; }
153
155{ return _licenseDesignation; }
156
157void Addon::setLicenseDesignation(const std::string& addonLicenseDesignation)
158{ _licenseDesignation = addonLicenseDesignation; }
159
161{ return _licenseFile; }
162
163void Addon::setLicenseFile(const SGPath& addonLicenseFile)
164{ _licenseFile = addonLicenseFile; }
165
166std::string Addon::getLicenseUrl() const
167{ return _licenseUrl; }
168
169void Addon::setLicenseUrl(const std::string& addonLicenseUrl)
170{ _licenseUrl = addonLicenseUrl; }
171
172std::vector<std::string> Addon::getTags() const
173{ return _tags; }
174
175void Addon::setTags(const std::vector<std::string>& addonTags)
176{ _tags = addonTags; }
177
178SGPath Addon::getBasePath() const
179{ return _basePath; }
180
181void Addon::setBasePath(const SGPath& addonBasePath)
182{ _basePath = addonBasePath; }
183
185{ return _storagePath; }
186
188{
189 if (_storagePath.exists()) {
190 if (!_storagePath.isDir()) {
191 string msg =
192 "Unable to create add-on storage directory because the entry already "
193 "exists, but is not a directory: '" + _storagePath.utf8Str() + "'";
194 // Log + throw, because if called from Nasal, only throwing would cause
195 // the exception message to 1) be truncated and 2) only appear in the
196 // log, not stopping the sim. Then users would have to figure out why
197 // their add-on doesn't work...
198 SG_LOG(SG_GENERAL, SG_POPUP, msg);
200 }
201 } else {
202 const SGPath authorizedPath = SGPath(_storagePath).validate(/* write */
203 true);
204 if (authorizedPath.isNull()) {
205 string msg =
206 "Unable to create add-on storage directory because of the FlightGear "
207 "security policy (refused by SGPath::validate()): '" +
208 _storagePath.utf8Str() + "'";
209 SG_LOG(SG_GENERAL, SG_POPUP, msg);
211 } else {
212 simgear::Dir(authorizedPath).create(0777);
213 }
214 }
215
216 // The sensitive operation (creating the directory) is behind us; return
217 // _storagePath instead of authorizedPath for consistency with the
218 // getStoragePath() method (_storagePath and authorizedPath could be
219 // different in case the former contains symlink components). Further
220 // sensitive operations beneath _storagePath must use SGPath::validate()
221 // again every time, of course (otherwise, attackers could use symlinks in
222 // _storagePath to bypass the security policy).
223 return _storagePath;
224}
225
226std::string Addon::resourcePath(const std::string& relativePath) const
227{
228 if (strutils::starts_with(relativePath, "/")) {
230 "addon-specific resource path '" + relativePath + "' shouldn't start "
231 "with a '/'");
232 }
233
234 return "[addon=" + getId() + "]" + relativePath;
235}
236
238{ return _minFGVersionRequired; }
239
240void Addon::setMinFGVersionRequired(const string& minFGVersionRequired)
241{ _minFGVersionRequired = minFGVersionRequired; }
242
244{ return _maxFGVersionRequired; }
245
246void Addon::setMaxFGVersionRequired(const string& maxFGVersionRequired)
247{
248 if (maxFGVersionRequired.empty()) {
249 _maxFGVersionRequired = "none"; // special value
250 } else {
251 _maxFGVersionRequired = maxFGVersionRequired;
252 }
253}
254
255std::string Addon::getHomePage() const
256{ return _homePage; }
257
258void Addon::setHomePage(const std::string& addonHomePage)
259{ _homePage = addonHomePage; }
260
261std::string Addon::getDownloadUrl() const
262{ return _downloadUrl; }
263
264void Addon::setDownloadUrl(const std::string& addonDownloadUrl)
265{ _downloadUrl = addonDownloadUrl; }
266
267std::string Addon::getSupportUrl() const
268{ return _supportUrl; }
269
270void Addon::setSupportUrl(const std::string& addonSupportUrl)
271{ _supportUrl = addonSupportUrl; }
272
274{ return _codeRepositoryUrl; }
275
276void Addon::setCodeRepositoryUrl(const std::string& addonCodeRepositoryUrl)
277{ _codeRepositoryUrl = addonCodeRepositoryUrl; }
278
279std::string Addon::getTriggerProperty() const
280{ return _triggerProperty; }
281
282void Addon::setTriggerProperty(const std::string& addonTriggerProperty)
283{ _triggerProperty = addonTriggerProperty; }
284
285SGPropertyNode_ptr Addon::getAddonNode() const
286{ return _addonNode; }
287
288void Addon::setAddonNode(SGPropertyNode* addonNode)
289{ _addonNode = SGPropertyNode_ptr(addonNode); }
290
292{
293 auto nas = globals->get_subsystem<FGNasalSys>();
294 return nas->wrappedPropsNode(_addonNode.get());
295}
296
297SGPropertyNode_ptr Addon::getLoadedFlagNode() const
298{
299 return { _addonNode->getChild("loaded", 0, 1) };
300}
301
303{ return _loadSequenceNumber; }
304
306{ _loadSequenceNumber = num; }
307
308std::multimap<UrlType, QualifiedUrl> Addon::getUrls() const
309{
310 std::multimap<UrlType, QualifiedUrl> res;
311
312 auto appendIfNonEmpty = [&res](UrlType type, string url, string detail = "") {
313 if (!url.empty()) {
314 res.emplace(type, QualifiedUrl(type, std::move(url), std::move(detail)));
315 }
316 };
317
318 for (const auto& author: _authors) {
319 appendIfNonEmpty(UrlType::author, author->getUrl(), author->getName());
320 }
321
322 for (const auto& maint: _maintainers) {
323 appendIfNonEmpty(UrlType::maintainer, maint->getUrl(), maint->getName());
324 }
325
326 appendIfNonEmpty(UrlType::homePage, getHomePage());
327 appendIfNonEmpty(UrlType::download, getDownloadUrl());
328 appendIfNonEmpty(UrlType::support, getSupportUrl());
330 appendIfNonEmpty(UrlType::license, getLicenseUrl());
331
332 return res;
333}
334
335std::vector<SGPropertyNode_ptr> Addon::getMenubarNodes() const
336{ return _menubarNodes; }
337
338void Addon::setMenubarNodes(const std::vector<SGPropertyNode_ptr>& menubarNodes)
339{ _menubarNodes = menubarNodes; }
340
342{
343 SGPropertyNode* menuRootNode = fgGetNode("/sim/menubar/default", true);
344
345 for (const auto& node: getMenubarNodes()) {
346 SGPropertyNode* childNode = menuRootNode->addChild("menu");
347 ::copyProperties(node.ptr(), childNode);
348 }
349}
350
351std::string Addon::str() const
352{
353 std::ostringstream oss;
354 oss << "addon '" << _id << "' (version = " << *_version
355 << ", base path = '" << _basePath.utf8Str()
356 << "', minFGVersionRequired = '" << _minFGVersionRequired
357 << "', maxFGVersionRequired = '" << _maxFGVersionRequired << "')";
358
359 return oss.str();
360}
361
362// Static method
363SGPath Addon::getMetadataFile(const SGPath& addonPath)
364{
365 return MetadataParser::getMetadataFile(addonPath);
366}
367
368SGPath Addon::getMetadataFile() const
369{
370 return getMetadataFile(getBasePath());
371}
372
373// Static method
374Addon Addon::fromAddonDir(const SGPath& addonPath)
375{
377
378 // Object holding all the add-on metadata
379 Addon addon{std::move(metadata.id), std::move(metadata.version), addonPath,
380 std::move(metadata.minFGVersionRequired),
381 std::move(metadata.maxFGVersionRequired)};
382 addon.setName(std::move(metadata.name));
383 addon.setAuthors(std::move(metadata.authors));
384 addon.setMaintainers(std::move(metadata.maintainers));
385 addon.setShortDescription(std::move(metadata.shortDescription));
386 addon.setLongDescription(std::move(metadata.longDescription));
387 addon.setLicenseDesignation(std::move(metadata.licenseDesignation));
388 addon.setLicenseFile(std::move(metadata.licenseFile));
389 addon.setLicenseUrl(std::move(metadata.licenseUrl));
390 addon.setTags(std::move(metadata.tags));
391 addon.setHomePage(std::move(metadata.homePage));
392 addon.setDownloadUrl(std::move(metadata.downloadUrl));
393 addon.setSupportUrl(std::move(metadata.supportUrl));
394 addon.setCodeRepositoryUrl(std::move(metadata.codeRepositoryUrl));
395
396 SGPath menuFile = addonPath / "addon-menubar-items.xml";
397
398 if (menuFile.exists()) {
399 addon.setMenubarNodes(readMenubarItems(menuFile));
400 }
401
402 return addon;
403}
404
405// Static method
406std::vector<SGPropertyNode_ptr>
407Addon::readMenubarItems(const SGPath& menuFile)
408{
409 SGPropertyNode rootNode;
410
411 try {
412 readProperties(menuFile, &rootNode);
413 } catch (const sg_exception &e) {
414 throw errors::error_loading_menubar_items_file(
415 "unable to load add-on menu bar items from file '" +
416 menuFile.utf8Str() + "': " + e.getFormattedMessage());
417 }
418
419 // Check the 'meta' section
420 SGPropertyNode *metaNode = rootNode.getChild("meta");
421 if (metaNode == nullptr) {
422 throw errors::error_loading_menubar_items_file(
423 "no /meta node found in add-on menu bar items file '" +
424 menuFile.utf8Str() + "'");
425 }
426
427 // Check the file type
428 SGPropertyNode *fileTypeNode = metaNode->getChild("file-type");
429 if (fileTypeNode == nullptr) {
430 throw errors::error_loading_menubar_items_file(
431 "no /meta/file-type node found in add-on menu bar items file '" +
432 menuFile.utf8Str() + "'");
433 }
434
435 string fileType = fileTypeNode->getStringValue();
436 if (fileType != "FlightGear add-on menu bar items") {
437 throw errors::error_loading_menubar_items_file(
438 "Invalid /meta/file-type value for add-on menu bar items file '" +
439 menuFile.utf8Str() + "': '" + fileType + "' "
440 "(expected 'FlightGear add-on menu bar items')");
441 }
442
443 // Check the format version
444 SGPropertyNode *fmtVersionNode = metaNode->getChild("format-version");
445 if (fmtVersionNode == nullptr) {
446 throw errors::error_loading_menubar_items_file(
447 "no /meta/format-version node found in add-on menu bar items file '" +
448 menuFile.utf8Str() + "'");
449 }
450
451 int formatVersion = fmtVersionNode->getIntValue();
452 if (formatVersion != 1) {
453 throw errors::error_loading_menubar_items_file(
454 "unknown format version in add-on menu bar items file '" +
455 menuFile.utf8Str() + "': " + std::to_string(formatVersion));
456 }
457
458 SG_LOG(SG_GENERAL, SG_DEBUG,
459 "Loaded add-on menu bar items from '" << menuFile.utf8Str() + "'");
460
461 SGPropertyNode *menubarItemsNode = rootNode.getChild("menubar-items");
462 std::vector<SGPropertyNode_ptr> res;
463
464 if (menubarItemsNode != nullptr) {
465 res = menubarItemsNode->getChildren("menu");
466 }
467
468 return res;
469}
470
472{
474 setName(std::move(metadata.name));
475 setShortDescription(std::move(metadata.shortDescription));
476 setLongDescription(std::move(metadata.longDescription));
477}
478
479// Static method
480void Addon::setupGhost(nasal::Hash& addonsModule)
481{
482 nasal::Ghost<AddonRef>::init("addons.Addon")
483 .member("id", &Addon::getId)
484 .member("name", &Addon::getName)
485 .member("version", &Addon::getVersion)
486 .member("authors", &Addon::getAuthors)
487 .member("maintainers", &Addon::getMaintainers)
488 .member("shortDescription", &Addon::getShortDescription)
489 .member("longDescription", &Addon::getLongDescription)
490 .member("licenseDesignation", &Addon::getLicenseDesignation)
491 .member("licenseFile", &Addon::getLicenseFile)
492 .member("licenseUrl", &Addon::getLicenseUrl)
493 .member("tags", &Addon::getTags)
494 .member("basePath", &Addon::getBasePath)
495 .member("storagePath", &Addon::getStoragePath)
496 .method("createStorageDir", &Addon::createStorageDir)
497 .method("resourcePath", &Addon::resourcePath)
498 .member("minFGVersionRequired", &Addon::getMinFGVersionRequired)
499 .member("maxFGVersionRequired", &Addon::getMaxFGVersionRequired)
500 .member("homePage", &Addon::getHomePage)
501 .member("downloadUrl", &Addon::getDownloadUrl)
502 .member("supportUrl", &Addon::getSupportUrl)
503 .member("codeRepositoryUrl", &Addon::getCodeRepositoryUrl)
504 .member("triggerProperty", &Addon::getTriggerProperty)
505 .member("node", &Addon::getAddonPropsNode)
506 .member("loadSequenceNumber", &Addon::getLoadSequenceNumber);
507}
508
509std::ostream& operator<<(std::ostream& os, const Addon& addon)
510{
511 return os << addon.str();
512}
513
514} // of namespace addons
515
516} // of namespace flightgear
naRef wrappedPropsNode(SGPropertyNode *aProps)
create Nasal props.Node for an SGPropertyNode* This is the actual ghost, wrapped in a Nasal sugar cla...
static SGPath getMetadataFile(const SGPath &addonPath)
static Addon::Metadata parseMetadataFile(const SGPath &addonPath)
std::vector< MaintainerRef > maintainers
void retranslate()
update string values (description, etc) based on the active locale
Definition Addon.cxx:471
SGPropertyNode_ptr getLoadedFlagNode() const
Definition Addon.cxx:297
void setLicenseFile(const SGPath &addonLicenseFile)
Definition Addon.cxx:163
naRef getAddonPropsNode() const
Definition Addon.cxx:291
void setLongDescription(const std::string &addonLongDescription)
Definition Addon.cxx:151
void setSupportUrl(const std::string &addonSupportUrl)
Definition Addon.cxx:270
void setCodeRepositoryUrl(const std::string &addonCodeRepositoryUrl)
Definition Addon.cxx:276
void setBasePath(const SGPath &addonBasePath)
Definition Addon.cxx:181
void setLicenseUrl(const std::string &addonLicenseUrl)
Definition Addon.cxx:169
Addon(std::string id, AddonVersion version=AddonVersion(), SGPath basePath=SGPath(), std::string minFGVersionRequired="", std::string maxFGVersionRequired="", SGPropertyNode *addonNode=nullptr)
Definition Addon.cxx:90
std::string str() const
Definition Addon.cxx:351
std::string getMaxFGVersionRequired() const
Definition Addon.cxx:243
void setLoadSequenceNumber(int num)
Definition Addon.cxx:305
void setMaintainers(const std::vector< MaintainerRef > &addonMaintainers)
Definition Addon.cxx:139
std::string getSupportUrl() const
Definition Addon.cxx:267
std::string getShortDescription() const
Definition Addon.cxx:142
std::string getMinFGVersionRequired() const
Definition Addon.cxx:237
void setMinFGVersionRequired(const std::string &minFGVersionRequired)
Definition Addon.cxx:240
void setAddonNode(SGPropertyNode *addonNode)
Definition Addon.cxx:288
void setTriggerProperty(const std::string &addonTriggerProperty)
Definition Addon.cxx:282
int getLoadSequenceNumber() const
Definition Addon.cxx:302
void setName(const std::string &addonName)
Definition Addon.cxx:118
SGPath getStoragePath() const
Definition Addon.cxx:184
void setMaxFGVersionRequired(const std::string &maxFGVersionRequired)
Definition Addon.cxx:246
SGPath getBasePath() const
Definition Addon.cxx:178
std::multimap< UrlType, QualifiedUrl > getUrls() const
Definition Addon.cxx:308
void setDownloadUrl(const std::string &addonDownloadUrl)
Definition Addon.cxx:264
static Addon fromAddonDir(const SGPath &addonPath)
Definition Addon.cxx:374
std::string getTriggerProperty() const
Definition Addon.cxx:279
std::string getHomePage() const
Definition Addon.cxx:255
SGPath getLicenseFile() const
Definition Addon.cxx:160
void setVersion(const AddonVersion &addonVersion)
Definition Addon.cxx:124
void addToFGMenubar() const
Definition Addon.cxx:341
std::string getDownloadUrl() const
Definition Addon.cxx:261
AddonVersionRef getVersion() const
Definition Addon.cxx:121
std::vector< AuthorRef > getAuthors() const
Definition Addon.cxx:130
std::vector< MaintainerRef > getMaintainers() const
Definition Addon.cxx:136
std::vector< SGPropertyNode_ptr > getMenubarNodes() const
Definition Addon.cxx:335
std::string getName() const
Definition Addon.cxx:115
std::string getLicenseUrl() const
Definition Addon.cxx:166
std::string getId() const
Definition Addon.cxx:112
std::string getLongDescription() const
Definition Addon.cxx:148
void setAuthors(const std::vector< AuthorRef > &addonAuthors)
Definition Addon.cxx:133
SGPropertyNode_ptr getAddonNode() const
Definition Addon.cxx:285
static void setupGhost(nasal::Hash &addonsModule)
Definition Addon.cxx:480
std::string resourcePath(const std::string &relativePath) const
Definition Addon.cxx:226
void setHomePage(const std::string &addonHomePage)
Definition Addon.cxx:258
void setTags(const std::vector< std::string > &addonTags)
Definition Addon.cxx:175
void setLicenseDesignation(const std::string &addonLicenseDesignation)
Definition Addon.cxx:157
SGPath createStorageDir() const
Definition Addon.cxx:187
std::string getLicenseDesignation() const
Definition Addon.cxx:154
std::string getCodeRepositoryUrl() const
Definition Addon.cxx:273
std::vector< std::string > getTags() const
Definition Addon.cxx:172
void setShortDescription(const std::string &addonShortDescription)
Definition Addon.cxx:145
void setMenubarNodes(const std::vector< SGPropertyNode_ptr > &menubarNodes)
Definition Addon.cxx:338
std::string getUrl() const
Definition Addon.cxx:74
QualifiedUrl(UrlType type, std::string url, std::string detail="")
Definition Addon.cxx:62
void setUrl(const std::string &url)
Definition Addon.cxx:77
void setDetail(const std::string &detail)
Definition Addon.cxx:83
void setType(UrlType type)
Definition Addon.cxx:71
std::string getDetail() const
Definition Addon.cxx:80
FGGlobals * globals
Definition globals.cxx:142
std::ostream & operator<<(std::ostream &os, const Addon &addon)
Definition Addon.cxx:509
SGSharedPtr< AddonVersion > AddonVersionRef
Definition addon_fwd.hxx:47
FlightPlan.hxx - defines a full flight-plan object, including departure, cruise, arrival information ...
Definition Addon.cxx:53
SGPropertyNode * fgGetNode(const char *path, bool create)
Get a property node.
Definition proptest.cpp:27