1 /*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2021 Andrew Lutsenko, anlutsenko at gmail dot com
5 * Copyright (C) 1992-2021 KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation, either version 3 of the License, or (at your
10 * option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 // kicad_curl.h *must be* included before any wxWidgets header to avoid conflicts
22 // at least on Windows/msys2
23 #include "kicad_curl/kicad_curl_easy.h"
24 #include <kicad_curl/kicad_curl.h>
25
26 #include "core/wx_stl_compat.h"
27 #include "kicad_build_version.h"
28 #include "paths.h"
29 #include "pcm.h"
30 #include "pgm_base.h"
31 #include "picosha2.h"
32 #include "settings/settings_manager.h"
33
34 #include <fstream>
35 #include <iomanip>
36 #include <memory>
37 #include <wx/dir.h>
38 #include <wx/filefn.h>
39 #include <wx/fs_zip.h>
40 #include <wx/image.h>
41 #include <wx/mstream.h>
42 #include <wx/sstream.h>
43 #include <wx/tokenzr.h>
44 #include <wx/wfstream.h>
45 #include <wx/zipstrm.h>
46
47
48 const std::tuple<int, int> PLUGIN_CONTENT_MANAGER::m_kicad_version =
49 KICAD_MAJOR_MINOR_VERSION_TUPLE;
50
51
52 class THROWING_ERROR_HANDLER : public nlohmann::json_schema::error_handler
53 {
error(const json::json_pointer & ptr,const json & instance,const std::string & message)54 void error( const json::json_pointer& ptr, const json& instance,
55 const std::string& message ) override
56 {
57 throw std::invalid_argument( std::string( "At " ) + ptr.to_string() + ", value:\n"
58 + instance.dump() + "\n" + message + "\n" );
59 }
60 };
61
62
PLUGIN_CONTENT_MANAGER(wxWindow * aParent)63 PLUGIN_CONTENT_MANAGER::PLUGIN_CONTENT_MANAGER( wxWindow* aParent ) : m_dialog( aParent )
64 {
65 // Get 3rd party path
66 const ENV_VAR_MAP& env = Pgm().GetLocalEnvVariables();
67 auto it = env.find( "KICAD6_3RD_PARTY" );
68
69 if( it != env.end() && !it->second.GetValue().IsEmpty() )
70 m_3rdparty_path = it->second.GetValue();
71 else
72 m_3rdparty_path = PATHS::GetDefault3rdPartyPath();
73
74 // Read and store pcm schema
75 wxFileName schema_file( PATHS::GetStockDataPath( true ), "pcm.v1.schema.json" );
76 schema_file.Normalize();
77 schema_file.AppendDir( "schemas" );
78
79 std::ifstream schema_stream( schema_file.GetFullPath().ToUTF8() );
80 nlohmann::json schema;
81
82 try
83 {
84 schema_stream >> schema;
85 m_schema_validator.set_root_schema( schema );
86 }
87 catch( std::exception& e )
88 {
89 if( !schema_file.FileExists() )
90 wxLogError( wxString::Format( _( "schema file '%s' not found" ),
91 schema_file.GetFullPath() ) );
92 else
93 wxLogError( wxString::Format( _( "Error loading schema: %s" ), e.what() ) );
94 }
95
96 // Load currently installed packages
97 wxFileName f( SETTINGS_MANAGER::GetUserSettingsPath(), "installed_packages.json" );
98
99 if( f.FileExists() )
100 {
101 std::ifstream installed_stream( f.GetFullPath().ToUTF8() );
102 nlohmann::json installed;
103
104 try
105 {
106 installed_stream >> installed;
107
108 if( installed.contains( "packages" ) && installed["packages"].is_array() )
109 {
110 for( const auto& js_entry : installed["packages"] )
111 {
112 PCM_INSTALLATION_ENTRY entry = js_entry.get<PCM_INSTALLATION_ENTRY>();
113 m_installed.emplace( entry.package.identifier, entry );
114 }
115 }
116 }
117 catch( std::exception& e )
118 {
119 wxLogError( wxString::Format( _( "Error loading installed packages list: %s" ),
120 e.what() ) );
121 }
122 }
123
124 // As a fall back populate installed from names of directories
125
126 for( const wxString& dir : PCM_PACKAGE_DIRECTORIES )
127 {
128 wxFileName d( m_3rdparty_path, "" );
129 d.AppendDir( dir );
130
131 if( d.DirExists() )
132 {
133 wxDir package_dir( d.GetPath() );
134
135 if( !package_dir.IsOpened() )
136 continue;
137
138 wxString subdir;
139 bool more = package_dir.GetFirst( &subdir, "", wxDIR_DIRS | wxDIR_HIDDEN );
140
141 while( more )
142 {
143 wxString actual_package_id = subdir;
144 actual_package_id.Replace( '_', '.' );
145
146 if( m_installed.find( actual_package_id ) == m_installed.end() )
147 {
148 PCM_INSTALLATION_ENTRY entry;
149 wxFileName subdir_file( d.GetPath(), subdir );
150
151 // wxFileModificationTime bugs out on windows for directories
152 wxStructStat stat;
153 int stat_code = wxStat( subdir_file.GetFullPath(), &stat );
154
155 entry.package.name = subdir;
156 entry.package.identifier = actual_package_id;
157 entry.current_version = "0.0";
158 entry.repository_name = wxT( "<unknown>" );
159
160 if( stat_code == 0 )
161 entry.install_timestamp = stat.st_mtime;
162
163 PACKAGE_VERSION version;
164 version.version = "0.0";
165 version.status = PVS_STABLE;
166 version.kicad_version = KICAD_MAJOR_MINOR_VERSION;
167
168 entry.package.versions.emplace_back( version );
169
170 m_installed.emplace( actual_package_id, entry );
171 }
172
173 more = package_dir.GetNext( &subdir );
174 }
175 }
176 }
177
178 // Calculate package compatibility
179 std::for_each( m_installed.begin(), m_installed.end(),
180 [&]( auto& entry )
181 {
182 preparePackage( entry.second.package );
183 } );
184 }
185
186
DownloadToStream(const wxString & aUrl,std::ostream * aOutput,WX_PROGRESS_REPORTER * aReporter,const size_t aSizeLimit)187 bool PLUGIN_CONTENT_MANAGER::DownloadToStream( const wxString& aUrl, std::ostream* aOutput,
188 WX_PROGRESS_REPORTER* aReporter,
189 const size_t aSizeLimit )
190 {
191 bool size_exceeded = false;
192
193 TRANSFER_CALLBACK callback = [&]( size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow )
194 {
195 if( aSizeLimit > 0 && ( dltotal > aSizeLimit || dlnow > aSizeLimit ) )
196 {
197 size_exceeded = true;
198
199 // Non zero return means abort.
200 return true;
201 }
202
203 if( dltotal > 1024 )
204 {
205 aReporter->SetCurrentProgress( dlnow / (double) dltotal );
206 aReporter->Report( wxString::Format( _( "Downloading %lld/%lld Kb" ), dlnow / 1024,
207 dltotal / 1024 ) );
208 }
209 else
210 {
211 aReporter->SetCurrentProgress( 0.0 );
212 }
213
214 return !aReporter->KeepRefreshing();
215 };
216
217 KICAD_CURL_EASY curl;
218 curl.SetOutputStream( aOutput );
219 curl.SetURL( aUrl.ToUTF8().data() );
220 curl.SetFollowRedirects( true );
221 curl.SetTransferCallback( callback, 250000L );
222
223 int code = curl.Perform();
224
225 if( !aReporter->IsCancelled() )
226 aReporter->SetCurrentProgress( 1.0 );
227
228 if( code != CURLE_OK )
229 {
230 if( code == CURLE_ABORTED_BY_CALLBACK && size_exceeded )
231 wxMessageBox( _( "Download is too large." ) );
232 else if( code != CURLE_ABORTED_BY_CALLBACK )
233 wxLogError( wxString( curl.GetErrorText( code ) ) );
234
235 return false;
236 }
237
238 return true;
239 }
240
241
FetchRepository(const wxString & aUrl,PCM_REPOSITORY & aRepository,WX_PROGRESS_REPORTER * aReporter)242 bool PLUGIN_CONTENT_MANAGER::FetchRepository( const wxString& aUrl, PCM_REPOSITORY& aRepository,
243 WX_PROGRESS_REPORTER* aReporter )
244 {
245 std::stringstream repository_stream;
246
247 aReporter->SetTitle( _( "Fetching repository" ) );
248
249 if( !DownloadToStream( aUrl, &repository_stream, aReporter, 20480 ) )
250 {
251 wxLogError( _( "Unable to load repository url" ) );
252 return false;
253 }
254
255 nlohmann::json repository_json;
256
257 try
258 {
259 repository_stream >> repository_json;
260
261 ValidateJson( repository_json, nlohmann::json_uri( "#/definitions/Repository" ) );
262
263 aRepository = repository_json.get<PCM_REPOSITORY>();
264 }
265 catch( const std::exception& e )
266 {
267 wxLogError( wxString::Format( _( "Unable to parse repository:\n\n%s" ), e.what() ) );
268 return false;
269 }
270
271 return true;
272 }
273
274
ValidateJson(const nlohmann::json & aJson,const nlohmann::json_uri & aUri) const275 void PLUGIN_CONTENT_MANAGER::ValidateJson( const nlohmann::json& aJson,
276 const nlohmann::json_uri& aUri ) const
277 {
278 THROWING_ERROR_HANDLER error_handler;
279 m_schema_validator.validate( aJson, error_handler, aUri );
280 }
281
282
fetchPackages(const wxString & aUrl,const boost::optional<wxString> & aHash,std::vector<PCM_PACKAGE> & aPackages,WX_PROGRESS_REPORTER * aReporter)283 bool PLUGIN_CONTENT_MANAGER::fetchPackages( const wxString& aUrl,
284 const boost::optional<wxString>& aHash,
285 std::vector<PCM_PACKAGE>& aPackages,
286 WX_PROGRESS_REPORTER* aReporter )
287 {
288 std::stringstream packages_stream;
289
290 aReporter->SetTitle( _( "Fetching repository packages" ) );
291
292 if( !DownloadToStream( aUrl, &packages_stream, aReporter ) )
293 {
294 wxLogError( _( "Unable to load repository packages url." ) );
295 return false;
296 }
297
298 std::istringstream isstream( packages_stream.str() );
299
300 if( aHash && !VerifyHash( isstream, aHash.get() ) )
301 {
302 wxLogError( _( "Packages hash doesn't match. Repository may be corrupted." ) );
303 return false;
304 }
305
306 try
307 {
308 nlohmann::json packages_json = nlohmann::json::parse( packages_stream.str() );
309 ValidateJson( packages_json, nlohmann::json_uri( "#/definitions/PackageArray" ) );
310
311 aPackages = packages_json["packages"].get<std::vector<PCM_PACKAGE>>();
312 }
313 catch( std::exception& e )
314 {
315 wxLogError( wxString::Format( _( "Unable to parse packages metadata:\n\n%s" ), e.what() ) );
316 return false;
317 }
318
319 return true;
320 }
321
322
VerifyHash(std::istream & aStream,const wxString & aHash) const323 bool PLUGIN_CONTENT_MANAGER::VerifyHash( std::istream& aStream, const wxString& aHash ) const
324 {
325 std::vector<unsigned char> bytes( picosha2::k_digest_size );
326
327 picosha2::hash256( std::istreambuf_iterator<char>( aStream ), std::istreambuf_iterator<char>(),
328 bytes.begin(), bytes.end() );
329 std::string hex_str = picosha2::bytes_to_hex_string( bytes.begin(), bytes.end() );
330
331 return aHash.compare( hex_str ) == 0;
332 }
333
334
335 const PCM_REPOSITORY&
getCachedRepository(const wxString & aRepositoryId) const336 PLUGIN_CONTENT_MANAGER::getCachedRepository( const wxString& aRepositoryId ) const
337 {
338 wxASSERT_MSG( m_repository_cache.find( aRepositoryId ) != m_repository_cache.end(),
339 "Repository is not cached." );
340
341 return m_repository_cache.at( aRepositoryId );
342 }
343
344
CacheRepository(const wxString & aRepositoryId)345 const bool PLUGIN_CONTENT_MANAGER::CacheRepository( const wxString& aRepositoryId )
346 {
347 if( m_repository_cache.find( aRepositoryId ) != m_repository_cache.end() )
348 return true;
349
350 const auto repository_tuple =
351 std::find_if( m_repository_list.begin(), m_repository_list.end(),
352 [&aRepositoryId]( const std::tuple<wxString, wxString, wxString>& t )
353 {
354 return std::get<0>( t ) == aRepositoryId;
355 } );
356
357 if( repository_tuple == m_repository_list.end() )
358 return false;
359
360 wxString url = std::get<2>( *repository_tuple );
361
362 nlohmann::json js;
363 PCM_REPOSITORY current_repo;
364
365 std::unique_ptr<WX_PROGRESS_REPORTER> reporter(
366 new WX_PROGRESS_REPORTER( m_dialog, wxT( "" ), 1 ) );
367
368 if( !FetchRepository( url, current_repo, reporter.get() ) )
369 return false;
370
371 bool packages_cache_exists = false;
372
373 // First load repository data from local filesystem if available.
374 wxFileName repo_cache = wxFileName( m_3rdparty_path, "repository.json" );
375 repo_cache.AppendDir( "cache" );
376 repo_cache.AppendDir( aRepositoryId );
377 wxFileName packages_cache( repo_cache.GetPath(), "packages.json" );
378
379 if( repo_cache.FileExists() && packages_cache.FileExists() )
380 {
381 std::ifstream repo_stream( repo_cache.GetFullPath().ToUTF8() );
382 repo_stream >> js;
383 PCM_REPOSITORY saved_repo = js.get<PCM_REPOSITORY>();
384
385 if( saved_repo.packages.update_timestamp == current_repo.packages.update_timestamp )
386 {
387 // Cached repo is up to date, use data on disk
388 js.clear();
389 std::ifstream packages_cache_stream( packages_cache.GetFullPath().ToUTF8() );
390
391 try
392 {
393 packages_cache_stream >> js;
394 saved_repo.package_list = js["packages"].get<std::vector<PCM_PACKAGE>>();
395
396 std::for_each( saved_repo.package_list.begin(), saved_repo.package_list.end(),
397 &preparePackage );
398
399 m_repository_cache[aRepositoryId] = std::move( saved_repo );
400
401 packages_cache_exists = true;
402 }
403 catch( ... )
404 {
405 wxLogError( _( "Packages cache for current repository is "
406 "corrupted, it will be redownloaded." ) );
407 }
408 }
409 }
410
411 if( !packages_cache_exists )
412 {
413 // Cache doesn't exist or is out of date
414 if( !fetchPackages( current_repo.packages.url, current_repo.packages.sha256,
415 current_repo.package_list, reporter.get() ) )
416 {
417 return false;
418 }
419
420 std::for_each( current_repo.package_list.begin(), current_repo.package_list.end(),
421 &preparePackage );
422
423 repo_cache.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
424
425 std::ofstream repo_cache_stream( repo_cache.GetFullPath().ToUTF8() );
426 repo_cache_stream << std::setw( 4 ) << nlohmann::json( current_repo ) << std::endl;
427
428 std::ofstream packages_cache_stream( packages_cache.GetFullPath().ToUTF8() );
429 js.clear();
430 js["packages"] = nlohmann::json( current_repo.package_list );
431 packages_cache_stream << std::setw( 4 ) << js << std::endl;
432
433 m_repository_cache[aRepositoryId] = std::move( current_repo );
434 }
435
436 if( current_repo.resources )
437 {
438 // Check resources file date, redownload if needed
439 PCM_RESOURCE_REFERENCE& resources = current_repo.resources.get();
440
441 wxFileName resource_file( repo_cache.GetPath(), "resources.zip" );
442
443 time_t mtime = 0;
444
445 if( resource_file.FileExists() )
446 mtime = wxFileModificationTime( resource_file.GetFullPath() );
447
448 if( mtime + 600 < getCurrentTimestamp() && mtime < (time_t) resources.update_timestamp )
449 {
450 std::ofstream resources_stream( resource_file.GetFullPath().ToUTF8(),
451 std::ios_base::binary );
452
453 reporter->SetTitle( _( "Downloading resources" ) );
454
455 // 100 Mb resource file limit
456 bool success = DownloadToStream( resources.url, &resources_stream, reporter.get(),
457 100 * 1024 * 1024 );
458
459 resources_stream.close();
460
461 if( success )
462 {
463 std::ifstream read_stream( resource_file.GetFullPath().ToUTF8(),
464 std::ios_base::binary );
465
466
467 if( resources.sha256 && !VerifyHash( read_stream, resources.sha256.get() ) )
468 {
469 read_stream.close();
470 wxLogError(
471 _( "Resources file hash doesn't match and will not be used. Repository "
472 "may be corrupted." ) );
473 wxRemoveFile( resource_file.GetFullPath() );
474 }
475 }
476 else
477 {
478 // Not critical, just clean up the file
479 wxRemoveFile( resource_file.GetFullPath() );
480 }
481 }
482 }
483
484 return true;
485 }
486
487
preparePackage(PCM_PACKAGE & aPackage)488 void PLUGIN_CONTENT_MANAGER::preparePackage( PCM_PACKAGE& aPackage )
489 {
490 // Parse package version strings
491 for( PACKAGE_VERSION& ver : aPackage.versions )
492 {
493 int epoch = 0, major = 0, minor = 0, patch = 0;
494
495 if( ver.version_epoch )
496 epoch = ver.version_epoch.get();
497
498 wxStringTokenizer version_tokenizer( ver.version, "." );
499
500 major = wxAtoi( version_tokenizer.GetNextToken() );
501
502 if( version_tokenizer.HasMoreTokens() )
503 minor = wxAtoi( version_tokenizer.GetNextToken() );
504
505 if( version_tokenizer.HasMoreTokens() )
506 patch = wxAtoi( version_tokenizer.GetNextToken() );
507
508 ver.parsed_version = std::make_tuple( epoch, major, minor, patch );
509
510 // Determine compatibility
511 ver.compatible = true;
512
513 auto parse_major_minor = []( const wxString& version )
514 {
515 wxStringTokenizer tokenizer( version, "." );
516 int ver_major = wxAtoi( tokenizer.GetNextToken() );
517 int ver_minor = wxAtoi( tokenizer.GetNextToken() );
518 return std::tuple<int, int>( ver_major, ver_minor );
519 };
520
521 if( parse_major_minor( ver.kicad_version ) > m_kicad_version )
522 ver.compatible = false;
523
524 if( ver.kicad_version_max
525 && parse_major_minor( ver.kicad_version_max.get() ) < m_kicad_version )
526 ver.compatible = false;
527
528 #ifdef __WXMSW__
529 wxString platform = wxT( "windows" );
530 #endif
531 #ifdef __WXOSX__
532 wxString platform = wxT( "macos" );
533 #endif
534 #ifdef __WXGTK__
535 wxString platform = wxT( "linux" );
536 #endif
537
538 if( ver.platforms.size() > 0
539 && std::find( ver.platforms.begin(), ver.platforms.end(), platform )
540 == ver.platforms.end() )
541 {
542 ver.compatible = false;
543 }
544 }
545
546 // Sort by descending version
547 std::sort( aPackage.versions.begin(), aPackage.versions.end(),
548 []( const PACKAGE_VERSION& a, const PACKAGE_VERSION& b )
549 {
550 return a.parsed_version > b.parsed_version;
551 } );
552 }
553
554
555 const std::vector<PCM_PACKAGE>&
GetRepositoryPackages(const wxString & aRepositoryId) const556 PLUGIN_CONTENT_MANAGER::GetRepositoryPackages( const wxString& aRepositoryId ) const
557 {
558 return getCachedRepository( aRepositoryId ).package_list;
559 }
560
561
SetRepositoryList(const STRING_PAIR_LIST & aRepositories)562 void PLUGIN_CONTENT_MANAGER::SetRepositoryList( const STRING_PAIR_LIST& aRepositories )
563 {
564 // Clean up cache folder if repository is not in new list
565 for( const auto& entry : m_repository_list )
566 {
567 auto it = std::find_if( aRepositories.begin(), aRepositories.end(),
568 [&]( const auto& new_entry )
569 {
570 return new_entry.first == std::get<1>( entry );
571 } );
572
573 if( it == aRepositories.end() )
574 {
575 DiscardRepositoryCache( std::get<0>( entry ) );
576 }
577 }
578
579 m_repository_list.clear();
580 m_repository_cache.clear();
581
582 for( const auto& repo : aRepositories )
583 {
584 std::string url_sha = picosha2::hash256_hex_string( repo.second );
585 m_repository_list.push_back(
586 std::make_tuple( url_sha.substr( 0, 16 ), repo.first, repo.second ) );
587 }
588 }
589
590
DiscardRepositoryCache(const wxString & aRepositoryId)591 void PLUGIN_CONTENT_MANAGER::DiscardRepositoryCache( const wxString& aRepositoryId )
592 {
593 if( m_repository_cache.count( aRepositoryId ) > 0 )
594 m_repository_cache.erase( aRepositoryId );
595
596 wxFileName repo_cache( m_3rdparty_path, "" );
597 repo_cache.AppendDir( "cache" );
598 repo_cache.AppendDir( aRepositoryId );
599
600 if( repo_cache.DirExists() )
601 repo_cache.Rmdir( wxPATH_RMDIR_RECURSIVE );
602 }
603
604
MarkInstalled(const PCM_PACKAGE & aPackage,const wxString & aVersion,const wxString & aRepositoryId)605 void PLUGIN_CONTENT_MANAGER::MarkInstalled( const PCM_PACKAGE& aPackage, const wxString& aVersion,
606 const wxString& aRepositoryId )
607 {
608 PCM_INSTALLATION_ENTRY entry;
609 entry.package = aPackage;
610 entry.current_version = aVersion;
611 entry.repository_id = aRepositoryId;
612
613 if( !aRepositoryId.IsEmpty() )
614 entry.repository_name = getCachedRepository( aRepositoryId ).name;
615 else
616 entry.repository_name = _( "Local file" );
617
618 entry.install_timestamp = getCurrentTimestamp();
619
620 m_installed.emplace( aPackage.identifier, entry );
621 }
622
623
MarkUninstalled(const PCM_PACKAGE & aPackage)624 void PLUGIN_CONTENT_MANAGER::MarkUninstalled( const PCM_PACKAGE& aPackage )
625 {
626 m_installed.erase( aPackage.identifier );
627 }
628
629
GetPackageState(const wxString & aRepositoryId,const wxString & aPackageId)630 PCM_PACKAGE_STATE PLUGIN_CONTENT_MANAGER::GetPackageState( const wxString& aRepositoryId,
631 const wxString& aPackageId )
632 {
633 if( m_installed.find( aPackageId ) != m_installed.end() )
634 return PPS_INSTALLED;
635
636 if( aRepositoryId.IsEmpty() || !CacheRepository( aRepositoryId ) )
637 return PPS_UNAVAILABLE;
638
639 const PCM_REPOSITORY& repo = getCachedRepository( aRepositoryId );
640
641 auto pkg_it = std::find_if( repo.package_list.begin(), repo.package_list.end(),
642 [&aPackageId]( const PCM_PACKAGE& pkg )
643 {
644 return pkg.identifier == aPackageId;
645 } );
646
647 if( pkg_it == repo.package_list.end() )
648 return PPS_UNAVAILABLE;
649
650 const PCM_PACKAGE& pkg = *pkg_it;
651
652 auto ver_it = std::find_if( pkg.versions.begin(), pkg.versions.end(),
653 []( const PACKAGE_VERSION& ver )
654 {
655 return ver.compatible;
656 } );
657
658 if( ver_it == pkg.versions.end() )
659 return PPS_UNAVAILABLE;
660 else
661 return PPS_AVAILABLE;
662 }
663
664
getCurrentTimestamp() const665 time_t PLUGIN_CONTENT_MANAGER::getCurrentTimestamp() const
666 {
667 return std::chrono::duration_cast<std::chrono::seconds>(
668 std::chrono::system_clock::now().time_since_epoch() )
669 .count();
670 }
671
672
~PLUGIN_CONTENT_MANAGER()673 PLUGIN_CONTENT_MANAGER::~PLUGIN_CONTENT_MANAGER()
674 {
675 // Save current installed packages list.
676
677 try
678 {
679 nlohmann::json js;
680 js["packages"] = nlohmann::json::array();
681
682 for( const auto& entry : m_installed )
683 {
684 js["packages"].emplace_back( entry.second );
685 }
686
687 wxFileName f( SETTINGS_MANAGER::GetUserSettingsPath(), "installed_packages.json" );
688 std::ofstream stream( f.GetFullPath().ToUTF8() );
689
690 stream << std::setw( 4 ) << js << std::endl;
691 }
692 catch( nlohmann::detail::exception& )
693 {
694 // Ignore
695 }
696 }
697
698
GetInstalledPackages() const699 const std::vector<PCM_INSTALLATION_ENTRY> PLUGIN_CONTENT_MANAGER::GetInstalledPackages() const
700 {
701 std::vector<PCM_INSTALLATION_ENTRY> v;
702
703 std::for_each( m_installed.begin(), m_installed.end(),
704 [&v]( const auto& entry )
705 {
706 v.push_back( entry.second );
707 } );
708
709 return v;
710 }
711
712
713 const wxString&
GetInstalledPackageVersion(const wxString & aPackageId) const714 PLUGIN_CONTENT_MANAGER::GetInstalledPackageVersion( const wxString& aPackageId ) const
715 {
716 wxASSERT_MSG( m_installed.find( aPackageId ) != m_installed.end(),
717 "Installed package not found." );
718
719 return m_installed.at( aPackageId ).current_version;
720 }
721
722
GetPackageSearchRank(const PCM_PACKAGE & aPackage,const wxString & aSearchTerm)723 int PLUGIN_CONTENT_MANAGER::GetPackageSearchRank( const PCM_PACKAGE& aPackage,
724 const wxString& aSearchTerm )
725 {
726 wxArrayString terms = wxStringTokenize( aSearchTerm.Lower(), " ", wxTOKEN_STRTOK );
727 int rank = 0;
728
729 const auto find_term_matches = [&]( const wxString& str )
730 {
731 int result = 0;
732 wxString lower = str.Lower();
733
734 for( const wxString& term : terms )
735 if( lower.Find( term ) != wxNOT_FOUND )
736 result += 1;
737
738 return result;
739 };
740
741 // Match on package id
742 if( terms.size() == 1 && terms[0] == aPackage.identifier )
743 rank += 10000;
744
745 if( terms.size() == 1 && find_term_matches( aPackage.identifier ) )
746 rank += 1000;
747
748 // Match on package name
749 rank += 500 * find_term_matches( aPackage.name );
750
751 // Match on tags
752 for( const std::string& tag : aPackage.tags )
753 rank += 100 * find_term_matches( wxString( tag ) );
754
755 // Match on package description
756 rank += 10 * find_term_matches( aPackage.description );
757 rank += 10 * find_term_matches( aPackage.description_full );
758
759 // Match on author/maintainer
760 rank += find_term_matches( aPackage.author.name );
761
762 if( aPackage.maintainer )
763 rank += 3 * find_term_matches( aPackage.maintainer.get().name );
764
765 // Match on resources
766 for( const auto& entry : aPackage.resources )
767 {
768 rank += find_term_matches( entry.first );
769 rank += find_term_matches( entry.second );
770 }
771
772 // Match on license
773 if( terms.size() == 1 && terms[0] == aPackage.license )
774 rank += 1;
775
776 return rank;
777 }
778
779
780 std::unordered_map<wxString, wxBitmap>
GetRepositoryPackageBitmaps(const wxString & aRepositoryId)781 PLUGIN_CONTENT_MANAGER::GetRepositoryPackageBitmaps( const wxString& aRepositoryId )
782 {
783 std::unordered_map<wxString, wxBitmap> bitmaps;
784
785 wxFileName resources_file = wxFileName( m_3rdparty_path, "resources.zip" );
786 resources_file.AppendDir( "cache" );
787 resources_file.AppendDir( aRepositoryId );
788
789 if( !resources_file.FileExists() )
790 return bitmaps;
791
792 wxFFileInputStream stream( resources_file.GetFullPath() );
793 wxZipInputStream zip( stream );
794
795 if( !zip.IsOk() || zip.GetTotalEntries() == 0 )
796 return bitmaps;
797
798 for( wxArchiveEntry* entry = zip.GetNextEntry(); entry; entry = zip.GetNextEntry() )
799 {
800 wxArrayString path_parts =
801 wxSplit( entry->GetName(), wxFileName::GetPathSeparator(), (wxChar) 0 );
802
803 if( path_parts.size() != 2 || path_parts[1] != "icon.png" )
804 continue;
805
806 try
807 {
808 wxMemoryInputStream image_stream( zip, entry->GetSize() );
809 wxImage image( image_stream, wxBITMAP_TYPE_PNG );
810 bitmaps.emplace( path_parts[0], wxBitmap( image ) );
811 }
812 catch( ... )
813 {
814 // Log and ignore
815 wxLogTrace( "Error loading png bitmap for entry %s from %s", entry->GetName(),
816 resources_file.GetFullPath() );
817 }
818 }
819
820 return bitmaps;
821 }
822
823
GetInstalledPackageBitmaps()824 std::unordered_map<wxString, wxBitmap> PLUGIN_CONTENT_MANAGER::GetInstalledPackageBitmaps()
825 {
826 std::unordered_map<wxString, wxBitmap> bitmaps;
827
828 wxFileName resources_dir_fn( m_3rdparty_path, "" );
829 resources_dir_fn.AppendDir( "resources" );
830 wxDir resources_dir( resources_dir_fn.GetPath() );
831
832 if( !resources_dir.IsOpened() )
833 return bitmaps;
834
835 wxString subdir;
836 bool more = resources_dir.GetFirst( &subdir, "", wxDIR_DIRS | wxDIR_HIDDEN );
837
838 while( more )
839 {
840 wxFileName icon( resources_dir_fn.GetPath(), "icon.png" );
841 icon.AppendDir( subdir );
842
843 if( icon.FileExists() )
844 {
845 wxString actual_package_id = subdir;
846 actual_package_id.Replace( '_', '.' );
847
848 try
849 {
850 wxBitmap bitmap( icon.GetFullPath(), wxBITMAP_TYPE_PNG );
851 bitmaps.emplace( actual_package_id, bitmap );
852 }
853 catch( ... )
854 {
855 // Log and ignore
856 wxLogTrace( "Error loading png bitmap from %s", icon.GetFullPath() );
857 }
858 }
859
860 more = resources_dir.GetNext( &subdir );
861 }
862
863 return bitmaps;
864 }
865