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