1 /*
2  * This program source code file is part of KiCad, a free EDA CAD application.
3  *
4  * Copyright (C) 2020 Jon Evans <jon@craftyjon.com>
5  * Copyright (C) 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 #include "settings/json_settings.h"
22 #include <regex>
23 #include <wx/debug.h>
24 #include <wx/dir.h>
25 #include <wx/filename.h>
26 #include <wx/snglinst.h>
27 #include <wx/stdpaths.h>
28 #include <wx/utils.h>
29 
30 #include <build_version.h>
31 #include <confirm.h>
32 #include <dialogs/dialog_migrate_settings.h>
33 #include <gestfich.h>
34 #include <kiplatform/environment.h>
35 #include <kiway.h>
36 #include <lockfile.h>
37 #include <macros.h>
38 #include <pgm_base.h>
39 #include <paths.h>
40 #include <project.h>
41 #include <project/project_archiver.h>
42 #include <project/project_file.h>
43 #include <project/project_local_settings.h>
44 #include <settings/color_settings.h>
45 #include <settings/common_settings.h>
46 #include <settings/json_settings_internals.h>
47 #include <settings/settings_manager.h>
48 #include <wildcards_and_files_ext.h>
49 
50 
SETTINGS_MANAGER(bool aHeadless)51 SETTINGS_MANAGER::SETTINGS_MANAGER( bool aHeadless ) :
52         m_headless( aHeadless ),
53         m_kiway( nullptr ),
54         m_common_settings( nullptr ),
55         m_migration_source(),
56         m_migrateLibraryTables( true )
57 {
58     PATHS::EnsureUserPathsExist();
59 
60     // Check if the settings directory already exists, and if not, perform a migration if possible
61     if( !MigrateIfNeeded() )
62     {
63         m_ok = false;
64         return;
65     }
66 
67     m_ok = true;
68 
69     // create the common settings shared by all applications.  Not loaded immediately
70     m_common_settings = RegisterSettings( new COMMON_SETTINGS, false );
71 }
72 
~SETTINGS_MANAGER()73 SETTINGS_MANAGER::~SETTINGS_MANAGER()
74 {
75     m_settings.clear();
76     m_color_settings.clear();
77     m_projects.clear();
78 }
79 
80 
registerSettings(JSON_SETTINGS * aSettings,bool aLoadNow)81 JSON_SETTINGS* SETTINGS_MANAGER::registerSettings( JSON_SETTINGS* aSettings, bool aLoadNow )
82 {
83     std::unique_ptr<JSON_SETTINGS> ptr( aSettings );
84 
85     ptr->SetManager( this );
86 
87     wxLogTrace( traceSettings, "Registered new settings object <%s>", ptr->GetFullFilename() );
88 
89     if( aLoadNow )
90         ptr->LoadFromFile( GetPathForSettingsFile( ptr.get() ) );
91 
92     m_settings.push_back( std::move( ptr ) );
93     return m_settings.back().get();
94 }
95 
96 
Load()97 void SETTINGS_MANAGER::Load()
98 {
99     // TODO(JE) We should check for dirty settings here and write them if so, because
100     // Load() could be called late in the application lifecycle
101 
102     for( auto&& settings : m_settings )
103         settings->LoadFromFile( GetPathForSettingsFile( settings.get() ) );
104 }
105 
106 
Load(JSON_SETTINGS * aSettings)107 void SETTINGS_MANAGER::Load( JSON_SETTINGS* aSettings )
108 {
109     auto it = std::find_if( m_settings.begin(), m_settings.end(),
110                             [&aSettings]( const std::unique_ptr<JSON_SETTINGS>& aPtr )
111                             {
112                                 return aPtr.get() == aSettings;
113                             } );
114 
115     if( it != m_settings.end() )
116         ( *it )->LoadFromFile( GetPathForSettingsFile( it->get() ) );
117 }
118 
119 
Save()120 void SETTINGS_MANAGER::Save()
121 {
122     for( auto&& settings : m_settings )
123     {
124         // Never automatically save color settings, caller should use SaveColorSettings
125         if( dynamic_cast<COLOR_SETTINGS*>( settings.get() ) )
126             continue;
127 
128         settings->SaveToFile( GetPathForSettingsFile( settings.get() ) );
129     }
130 }
131 
132 
Save(JSON_SETTINGS * aSettings)133 void SETTINGS_MANAGER::Save( JSON_SETTINGS* aSettings )
134 {
135     auto it = std::find_if( m_settings.begin(), m_settings.end(),
136                             [&aSettings]( const std::unique_ptr<JSON_SETTINGS>& aPtr )
137                             {
138                                 return aPtr.get() == aSettings;
139                             } );
140 
141     if( it != m_settings.end() )
142     {
143         wxLogTrace( traceSettings, "Saving %s", ( *it )->GetFullFilename() );
144         ( *it )->SaveToFile( GetPathForSettingsFile( it->get() ) );
145     }
146 }
147 
148 
FlushAndRelease(JSON_SETTINGS * aSettings,bool aSave)149 void SETTINGS_MANAGER::FlushAndRelease( JSON_SETTINGS* aSettings, bool aSave )
150 {
151     auto it = std::find_if( m_settings.begin(), m_settings.end(),
152                             [&aSettings]( const std::unique_ptr<JSON_SETTINGS>& aPtr )
153                             {
154                                 return aPtr.get() == aSettings;
155                             } );
156 
157     if( it != m_settings.end() )
158     {
159         wxLogTrace( traceSettings, "Flush and release %s", ( *it )->GetFullFilename() );
160 
161         if( aSave )
162             ( *it )->SaveToFile( GetPathForSettingsFile( it->get() ) );
163 
164         size_t typeHash = typeid( *it->get() ).hash_code();
165 
166         if( m_app_settings_cache.count( typeHash ) )
167             m_app_settings_cache.erase( typeHash );
168 
169         m_settings.erase( it );
170     }
171 }
172 
173 
GetColorSettings(const wxString & aName)174 COLOR_SETTINGS* SETTINGS_MANAGER::GetColorSettings( const wxString& aName )
175 {
176     if( m_color_settings.count( aName ) )
177         return m_color_settings.at( aName );
178 
179     if( !aName.empty() )
180     {
181         COLOR_SETTINGS* ret = loadColorSettingsByName( aName );
182 
183         if( !ret )
184         {
185             ret = registerColorSettings( aName );
186             *ret = *m_color_settings.at( "_builtin_default" );
187             ret->SetFilename( wxT( "user" ) );
188             ret->SetReadOnly( false );
189         }
190 
191         return ret;
192     }
193 
194     // This had better work
195     return m_color_settings.at( "_builtin_default" );
196 }
197 
198 
loadColorSettingsByName(const wxString & aName)199 COLOR_SETTINGS* SETTINGS_MANAGER::loadColorSettingsByName( const wxString& aName )
200 {
201     wxLogTrace( traceSettings, "Attempting to load color theme %s", aName );
202 
203     wxFileName fn( GetColorSettingsPath(), aName, "json" );
204 
205     if( !fn.IsOk() || !fn.Exists() )
206     {
207         wxLogTrace( traceSettings, "Theme file %s.json not found, falling back to user", aName );
208         return nullptr;
209     }
210 
211     COLOR_SETTINGS* settings = RegisterSettings( new COLOR_SETTINGS( aName ) );
212 
213     if( settings->GetFilename() != aName.ToStdString() )
214     {
215         wxLogTrace( traceSettings, "Warning: stored filename is actually %s, ",
216                     settings->GetFilename() );
217     }
218 
219     m_color_settings[aName] = settings;
220 
221     return settings;
222 }
223 
224 
225 class JSON_DIR_TRAVERSER : public wxDirTraverser
226 {
227 private:
228     std::function<void( const wxFileName& )> m_action;
229 
230 public:
JSON_DIR_TRAVERSER(std::function<void (const wxFileName &)> aAction)231     explicit JSON_DIR_TRAVERSER( std::function<void( const wxFileName& )> aAction )
232             : m_action( std::move( aAction ) )
233     {
234     }
235 
OnFile(const wxString & aFilePath)236     wxDirTraverseResult OnFile( const wxString& aFilePath ) override
237     {
238         wxFileName file( aFilePath );
239 
240         if( file.GetExt() == "json" )
241             m_action( file );
242 
243         return wxDIR_CONTINUE;
244     }
245 
OnDir(const wxString & dirPath)246     wxDirTraverseResult OnDir( const wxString& dirPath ) override
247     {
248         return wxDIR_CONTINUE;
249     }
250 };
251 
252 
registerColorSettings(const wxString & aName,bool aAbsolutePath)253 COLOR_SETTINGS* SETTINGS_MANAGER::registerColorSettings( const wxString& aName, bool aAbsolutePath )
254 {
255     if( !m_color_settings.count( aName ) )
256     {
257         COLOR_SETTINGS* colorSettings = RegisterSettings( new COLOR_SETTINGS( aName,
258                                                                               aAbsolutePath ) );
259         m_color_settings[aName] = colorSettings;
260     }
261 
262     return m_color_settings.at( aName );
263 }
264 
265 
AddNewColorSettings(const wxString & aName)266 COLOR_SETTINGS* SETTINGS_MANAGER::AddNewColorSettings( const wxString& aName )
267 {
268     if( aName.EndsWith( wxT( ".json" ) ) )
269         return registerColorSettings( aName.BeforeLast( '.' ) );
270     else
271         return registerColorSettings( aName );
272 }
273 
274 
GetMigratedColorSettings()275 COLOR_SETTINGS* SETTINGS_MANAGER::GetMigratedColorSettings()
276 {
277     if( !m_color_settings.count( "user" ) )
278     {
279         COLOR_SETTINGS* settings = registerColorSettings( wxT( "user" ) );
280         settings->SetName( wxT( "User" ) );
281         Save( settings );
282     }
283 
284     return m_color_settings.at( "user" );
285 }
286 
287 
loadAllColorSettings()288 void SETTINGS_MANAGER::loadAllColorSettings()
289 {
290     // Create the built-in color settings
291     for( COLOR_SETTINGS* settings : COLOR_SETTINGS::CreateBuiltinColorSettings() )
292         m_color_settings[settings->GetFilename()] = RegisterSettings( settings, false );
293 
294     wxFileName third_party_path;
295     const ENV_VAR_MAP& env = Pgm().GetLocalEnvVariables();
296     auto               it = env.find( "KICAD6_3RD_PARTY" );
297 
298     if( it != env.end() && !it->second.GetValue().IsEmpty() )
299         third_party_path.SetPath( it->second.GetValue() );
300     else
301         third_party_path.SetPath( PATHS::GetDefault3rdPartyPath() );
302 
303     third_party_path.AppendDir( "colors" );
304 
305     wxDir third_party_colors_dir( third_party_path.GetFullPath() );
306     wxString color_settings_path = GetColorSettingsPath();
307 
308     // Search for and load any other settings
309     JSON_DIR_TRAVERSER loader( [&]( const wxFileName& aFilename )
310                                {
311                                    registerColorSettings( aFilename.GetName() );
312                                } );
313 
314     JSON_DIR_TRAVERSER thirdPartyLoader(
315             [&]( const wxFileName& aFilename )
316             {
317                 COLOR_SETTINGS* settings = registerColorSettings( aFilename.GetFullPath(), true );
318                 settings->SetReadOnly( true );
319             } );
320 
321     wxDir colors_dir( color_settings_path );
322 
323     if( colors_dir.IsOpened() )
324     {
325         if( third_party_colors_dir.IsOpened() )
326            third_party_colors_dir.Traverse( thirdPartyLoader );
327 
328         colors_dir.Traverse( loader );
329     }
330 }
331 
332 
ReloadColorSettings()333 void SETTINGS_MANAGER::ReloadColorSettings()
334 {
335     m_color_settings.clear();
336     loadAllColorSettings();
337 }
338 
339 
SaveColorSettings(COLOR_SETTINGS * aSettings,const std::string & aNamespace)340 void SETTINGS_MANAGER::SaveColorSettings( COLOR_SETTINGS* aSettings, const std::string& aNamespace )
341 {
342     // The passed settings should already be managed
343     wxASSERT( std::find_if( m_color_settings.begin(), m_color_settings.end(),
344                             [aSettings] ( const std::pair<wxString, COLOR_SETTINGS*>& el )
345                             {
346                                 return el.second->GetFilename() == aSettings->GetFilename();
347                             }
348                             ) != m_color_settings.end() );
349 
350     if( aSettings->IsReadOnly() )
351         return;
352 
353     if( !aSettings->Store() )
354     {
355         wxLogTrace( traceSettings, "Color scheme %s not modified; skipping save",
356                     aNamespace );
357         return;
358     }
359 
360     wxASSERT( aSettings->Contains( aNamespace ) );
361 
362     wxLogTrace( traceSettings, "Saving color scheme %s, preserving %s",
363                 aSettings->GetFilename(),
364                 aNamespace );
365 
366     OPT<nlohmann::json> backup = aSettings->GetJson( aNamespace );
367     wxString path = GetColorSettingsPath();
368 
369     aSettings->LoadFromFile( path );
370 
371     if( backup )
372         ( *aSettings->Internals() )[aNamespace].update( *backup );
373 
374     aSettings->Load();
375 
376     aSettings->SaveToFile( path, true );
377 }
378 
379 
GetPathForSettingsFile(JSON_SETTINGS * aSettings)380 wxString SETTINGS_MANAGER::GetPathForSettingsFile( JSON_SETTINGS* aSettings )
381 {
382     wxASSERT( aSettings );
383 
384     switch( aSettings->GetLocation() )
385     {
386     case SETTINGS_LOC::USER:
387         return GetUserSettingsPath();
388 
389     case SETTINGS_LOC::PROJECT:
390         return Prj().GetProjectPath();
391 
392     case SETTINGS_LOC::COLORS:
393         return GetColorSettingsPath();
394 
395     case SETTINGS_LOC::NONE:
396         return "";
397 
398     default:
399         wxASSERT_MSG( false, "Unknown settings location!" );
400     }
401 
402     return "";
403 }
404 
405 
406 class MIGRATION_TRAVERSER : public wxDirTraverser
407 {
408 private:
409     wxString m_src;
410     wxString m_dest;
411     wxString m_errors;
412     bool     m_migrateTables;
413 
414 public:
MIGRATION_TRAVERSER(const wxString & aSrcDir,const wxString & aDestDir,bool aMigrateTables)415     MIGRATION_TRAVERSER( const wxString& aSrcDir, const wxString& aDestDir, bool aMigrateTables ) :
416             m_src( aSrcDir ),
417             m_dest( aDestDir ),
418             m_migrateTables( aMigrateTables )
419     {
420     }
421 
GetErrors()422     wxString GetErrors() { return m_errors; }
423 
OnFile(const wxString & aSrcFilePath)424     wxDirTraverseResult OnFile( const wxString& aSrcFilePath ) override
425     {
426         wxFileName file( aSrcFilePath );
427 
428         if( !m_migrateTables && ( file.GetName() == wxT( "sym-lib-table" ) ||
429                                   file.GetName() == wxT( "fp-lib-table" ) ) )
430         {
431             return wxDIR_CONTINUE;
432         }
433 
434         // Skip migrating PCM installed packages as packages themselves are not moved
435         if( file.GetFullName() == wxT( "installed_packages.json" ) )
436             return wxDIR_CONTINUE;
437 
438         // Don't migrate hotkeys config files; we don't have a reasonable migration handler for them
439         // and so there is no way to resolve conflicts at the moment
440         if( file.GetExt() == wxT( "hotkeys" ) )
441             return wxDIR_CONTINUE;
442 
443         wxString path = file.GetPath();
444 
445         path.Replace( m_src, m_dest, false );
446         file.SetPath( path );
447 
448         wxLogTrace( traceSettings, "Copying %s to %s", aSrcFilePath, file.GetFullPath() );
449 
450         // For now, just copy everything
451         KiCopyFile( aSrcFilePath, file.GetFullPath(), m_errors );
452 
453         return wxDIR_CONTINUE;
454     }
455 
OnDir(const wxString & dirPath)456     wxDirTraverseResult OnDir( const wxString& dirPath ) override
457     {
458         wxFileName dir( dirPath );
459 
460         // Whitelist of directories to migrate
461         if( dir.GetName() == "colors" ||
462             dir.GetName() == "3d" )
463         {
464 
465             wxString path = dir.GetPath();
466 
467             path.Replace( m_src, m_dest, false );
468             dir.SetPath( path );
469 
470             wxMkdir( dir.GetFullPath() );
471 
472             return wxDIR_CONTINUE;
473         }
474         else
475         {
476             return wxDIR_IGNORE;
477         }
478     }
479 };
480 
481 
MigrateIfNeeded()482 bool SETTINGS_MANAGER::MigrateIfNeeded()
483 {
484     if( m_headless )
485     {
486         wxLogTrace( traceSettings, "Settings migration not checked; running headless" );
487         return false;
488     }
489 
490     wxFileName path( GetUserSettingsPath(), "" );
491     wxLogTrace( traceSettings, "Using settings path %s", path.GetFullPath() );
492 
493     if( path.DirExists() )
494     {
495         wxFileName common = path;
496         common.SetName( "kicad_common" );
497         common.SetExt( "json" );
498 
499         if( common.Exists() )
500         {
501             wxLogTrace( traceSettings, "Path exists and has a kicad_common, continuing!" );
502             return true;
503         }
504     }
505 
506     // Now we have an empty path, let's figure out what to put in it
507     DIALOG_MIGRATE_SETTINGS dlg( this );
508 
509     if( dlg.ShowModal() != wxID_OK )
510     {
511         wxLogTrace( traceSettings, "Migration dialog canceled; exiting" );
512         return false;
513     }
514 
515     if( !path.DirExists() )
516     {
517         wxLogTrace( traceSettings, "Path didn't exist; creating it" );
518         path.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
519     }
520 
521     if( m_migration_source.IsEmpty() )
522     {
523         wxLogTrace( traceSettings, "No migration source given; starting with defaults" );
524         return true;
525     }
526 
527     wxLogTrace( traceSettings, "Migrating from path %s", m_migration_source );
528 
529     MIGRATION_TRAVERSER traverser( m_migration_source, path.GetFullPath(), m_migrateLibraryTables );
530     wxDir source_dir( m_migration_source );
531 
532     source_dir.Traverse( traverser );
533 
534     if( !traverser.GetErrors().empty() )
535         DisplayErrorMessage( nullptr, traverser.GetErrors() );
536 
537     // Remove any library configuration if we didn't choose to import
538     if( !m_migrateLibraryTables )
539     {
540         COMMON_SETTINGS common;
541         wxString        commonPath = GetPathForSettingsFile( &common );
542         common.LoadFromFile( commonPath );
543 
544         const std::vector<wxString> libKeys = {
545             wxT( "KICAD6_SYMBOL_DIR" ),
546             wxT( "KICAD6_3DMODEL_DIR" ),
547             wxT( "KICAD6_FOOTPRINT_DIR" ),
548             wxT( "KICAD6_TEMPLATE_DIR" ), // Stores the default library table to be copied
549 
550             // Deprecated keys
551             wxT( "KICAD_PTEMPLATES" ),
552             wxT( "KISYS3DMOD" ),
553             wxT( "KISYSMOD" ),
554             wxT( "KICAD_SYMBOL_DIR" ),
555         };
556 
557         for( const wxString& key : libKeys )
558             common.m_Env.vars.erase( key );
559 
560         common.SaveToFile( commonPath  );
561     }
562 
563     return true;
564 }
565 
566 
GetPreviousVersionPaths(std::vector<wxString> * aPaths)567 bool SETTINGS_MANAGER::GetPreviousVersionPaths( std::vector<wxString>* aPaths )
568 {
569     wxASSERT( aPaths );
570 
571     aPaths->clear();
572 
573     wxDir dir;
574     std::vector<wxFileName> base_paths;
575 
576     base_paths.emplace_back( wxFileName( calculateUserSettingsPath( false ), "" ) );
577 
578     // If the env override is set, also check the default paths
579     if( wxGetEnv( wxT( "KICAD_CONFIG_HOME" ), nullptr ) )
580         base_paths.emplace_back( wxFileName( calculateUserSettingsPath( false, false ), "" ) );
581 
582 #ifdef __WXGTK__
583     // When running inside FlatPak, KIPLATFORM::ENV::GetUserConfigPath() will return a sandboxed
584     // path.  In case the user wants to move from non-FlatPak KiCad to FlatPak KiCad, let's add our
585     // best guess as to the non-FlatPak config path.  Unfortunately FlatPak also hides the host
586     // XDG_CONFIG_HOME, so if the user customizes their config path, they will have to browse
587     // for it.
588     {
589         wxFileName wxGtkPath;
590         wxGtkPath.AssignDir( "~/.config/kicad" );
591         wxGtkPath.MakeAbsolute();
592         base_paths.emplace_back( wxGtkPath.GetPath() );
593 
594         // We also want to pick up regular flatpak if we are nightly
595         wxGtkPath.AssignDir( "~/.var/app/org.kicad.KiCad/config/kicad" );
596         wxGtkPath.MakeAbsolute();
597         base_paths.emplace_back( wxGtkPath.GetPath() );
598     }
599 #endif
600 
601     wxString subdir;
602     std::string mine = GetSettingsVersion();
603 
604     auto check_dir = [&] ( const wxString& aSubDir )
605     {
606         // Only older versions are valid for migration
607         if( compareVersions( aSubDir.ToStdString(), mine ) <= 0 )
608         {
609             wxString sub_path = dir.GetNameWithSep() + aSubDir;
610 
611             if( IsSettingsPathValid( sub_path ) )
612             {
613                 aPaths->push_back( sub_path );
614                 wxLogTrace( traceSettings, "GetPreviousVersionName: %s is valid", sub_path );
615             }
616         }
617     };
618 
619     std::set<wxString> checkedPaths;
620 
621     for( auto base_path : base_paths )
622     {
623         if( checkedPaths.count( base_path.GetFullPath() ) )
624             continue;
625 
626         checkedPaths.insert( base_path.GetFullPath() );
627 
628         if( !dir.Open( base_path.GetFullPath() ) )
629         {
630             wxLogTrace( traceSettings, "GetPreviousVersionName: could not open base path %s",
631                         base_path.GetFullPath() );
632             continue;
633         }
634 
635         wxLogTrace( traceSettings, "GetPreviousVersionName: checking base path %s",
636                     base_path.GetFullPath() );
637 
638         if( dir.GetFirst( &subdir, wxEmptyString, wxDIR_DIRS ) )
639         {
640             if( subdir != mine )
641                 check_dir( subdir );
642 
643             while( dir.GetNext( &subdir ) )
644             {
645                 if( subdir != mine )
646                     check_dir( subdir );
647             }
648         }
649 
650         // If we didn't find one yet, check for legacy settings without a version directory
651         if( IsSettingsPathValid( dir.GetNameWithSep() ) )
652         {
653             wxLogTrace( traceSettings,
654                         "GetPreviousVersionName: root path %s is valid", dir.GetName() );
655             aPaths->push_back( dir.GetName() );
656         }
657     }
658 
659     return aPaths->size() > 0;
660 }
661 
662 
IsSettingsPathValid(const wxString & aPath)663 bool SETTINGS_MANAGER::IsSettingsPathValid( const wxString& aPath )
664 {
665     wxFileName test( aPath, "kicad_common" );
666 
667     if( test.Exists() )
668         return true;
669 
670     test.SetExt( "json" );
671 
672     return test.Exists();
673 }
674 
675 
GetColorSettingsPath()676 wxString SETTINGS_MANAGER::GetColorSettingsPath()
677 {
678     wxFileName path;
679 
680     path.AssignDir( GetUserSettingsPath() );
681     path.AppendDir( "colors" );
682 
683     if( !path.DirExists() )
684     {
685         if( !wxMkdir( path.GetPath() ) )
686         {
687             wxLogTrace( traceSettings,
688                         "GetColorSettingsPath(): Path %s missing and could not be created!",
689                         path.GetPath() );
690         }
691     }
692 
693     return path.GetPath();
694 }
695 
696 
GetUserSettingsPath()697 wxString SETTINGS_MANAGER::GetUserSettingsPath()
698 {
699     static wxString user_settings_path;
700 
701     if( user_settings_path.empty() )
702         user_settings_path = calculateUserSettingsPath();
703 
704     return user_settings_path;
705 }
706 
707 
calculateUserSettingsPath(bool aIncludeVer,bool aUseEnv)708 wxString SETTINGS_MANAGER::calculateUserSettingsPath( bool aIncludeVer, bool aUseEnv )
709 {
710     wxFileName cfgpath;
711 
712     // http://docs.wxwidgets.org/3.0/classwx_standard_paths.html#a7c7cf595d94d29147360d031647476b0
713 
714     wxString envstr;
715     if( aUseEnv && wxGetEnv( wxT( "KICAD_CONFIG_HOME" ), &envstr ) && !envstr.IsEmpty() )
716     {
717         // Override the assignment above with KICAD_CONFIG_HOME
718         cfgpath.AssignDir( envstr );
719     }
720     else
721     {
722         cfgpath.AssignDir( KIPLATFORM::ENV::GetUserConfigPath() );
723 
724         cfgpath.AppendDir( TO_STR( KICAD_CONFIG_DIR ) );
725     }
726 
727     if( aIncludeVer )
728         cfgpath.AppendDir( GetSettingsVersion() );
729 
730     return cfgpath.GetPath();
731 }
732 
733 
GetSettingsVersion()734 std::string SETTINGS_MANAGER::GetSettingsVersion()
735 {
736     // CMake computes the major.minor string for us.
737     return GetMajorMinorVersion().ToStdString();
738 }
739 
740 
compareVersions(const std::string & aFirst,const std::string & aSecond)741 int SETTINGS_MANAGER::compareVersions( const std::string& aFirst, const std::string& aSecond )
742 {
743     int a_maj = 0;
744     int a_min = 0;
745     int b_maj = 0;
746     int b_min = 0;
747 
748     if( !extractVersion( aFirst, &a_maj, &a_min ) || !extractVersion( aSecond, &b_maj, &b_min ) )
749     {
750         wxLogTrace( traceSettings, "compareSettingsVersions: bad input (%s, %s)", aFirst, aSecond );
751         return -1;
752     }
753 
754     if( a_maj < b_maj )
755     {
756         return -1;
757     }
758     else if( a_maj > b_maj )
759     {
760         return 1;
761     }
762     else
763     {
764         if( a_min < b_min )
765         {
766             return -1;
767         }
768         else if( a_min > b_min )
769         {
770             return 1;
771         }
772         else
773         {
774             return 0;
775         }
776     }
777 }
778 
779 
extractVersion(const std::string & aVersionString,int * aMajor,int * aMinor)780 bool SETTINGS_MANAGER::extractVersion( const std::string& aVersionString, int* aMajor, int* aMinor )
781 {
782     std::regex  re_version( "(\\d+)\\.(\\d+)" );
783     std::smatch match;
784 
785     if( std::regex_match( aVersionString, match, re_version ) )
786     {
787         try
788         {
789             *aMajor = std::stoi( match[1].str() );
790             *aMinor = std::stoi( match[2].str() );
791         }
792         catch( ... )
793         {
794             return false;
795         }
796 
797         return true;
798     }
799 
800     return false;
801 }
802 
803 
LoadProject(const wxString & aFullPath,bool aSetActive)804 bool SETTINGS_MANAGER::LoadProject( const wxString& aFullPath, bool aSetActive )
805 {
806     // Normalize path to new format even if migrating from a legacy file
807     wxFileName path( aFullPath );
808 
809     if( path.GetExt() == LegacyProjectFileExtension )
810         path.SetExt( ProjectFileExtension );
811 
812     wxString fullPath = path.GetFullPath();
813 
814     // If already loaded, we are all set.  This might be called more than once over a project's
815     // lifetime in case the project is first loaded by the KiCad manager and then eeschema or
816     // pcbnew try to load it again when they are launched.
817     if( m_projects.count( fullPath ) )
818         return true;
819 
820     bool readOnly = false;
821     std::unique_ptr<wxSingleInstanceChecker> lockFile = ::LockFile( fullPath );
822 
823     if( !lockFile )
824     {
825         wxLogTrace( traceSettings, "Project %s is locked; opening read-only", fullPath );
826         readOnly = true;
827     }
828 
829     // No MDI yet
830     if( aSetActive && !m_projects.empty() )
831     {
832         PROJECT* oldProject = m_projects.begin()->second;
833         unloadProjectFile( oldProject, false );
834         m_projects.erase( m_projects.begin() );
835 
836         auto it = std::find_if( m_projects_list.begin(), m_projects_list.end(),
837                                 [&]( const std::unique_ptr<PROJECT>& ptr )
838                                 {
839                                     return ptr.get() == oldProject;
840                                 } );
841 
842         wxASSERT( it != m_projects_list.end() );
843         m_projects_list.erase( it );
844     }
845 
846     wxLogTrace( traceSettings, "Load project %s", fullPath );
847 
848     std::unique_ptr<PROJECT> project = std::make_unique<PROJECT>();
849     project->setProjectFullName( fullPath );
850 
851     bool success = loadProjectFile( *project );
852 
853     if( success )
854     {
855         project->SetReadOnly( readOnly || project->GetProjectFile().IsReadOnly() );
856 
857         if( lockFile )
858             m_project_lock.reset( lockFile.release() );
859     }
860 
861     m_projects_list.push_back( std::move( project ) );
862     m_projects[fullPath] = m_projects_list.back().get();
863 
864     wxString fn( path.GetName() );
865 
866     PROJECT_LOCAL_SETTINGS* settings = new PROJECT_LOCAL_SETTINGS( m_projects[fullPath], fn );
867 
868     if( aSetActive )
869         settings = RegisterSettings( settings );
870 
871     m_projects[fullPath]->setLocalSettings( settings );
872 
873     if( aSetActive && m_kiway )
874         m_kiway->ProjectChanged();
875 
876     return success;
877 }
878 
879 
UnloadProject(PROJECT * aProject,bool aSave)880 bool SETTINGS_MANAGER::UnloadProject( PROJECT* aProject, bool aSave )
881 {
882     if( !aProject || !m_projects.count( aProject->GetProjectFullName() ) )
883         return false;
884 
885     if( !unloadProjectFile( aProject, aSave ) )
886         return false;
887 
888     wxString projectPath = aProject->GetProjectFullName();
889     wxLogTrace( traceSettings, "Unload project %s", projectPath );
890 
891     PROJECT* toRemove = m_projects.at( projectPath );
892     auto it = std::find_if( m_projects_list.begin(), m_projects_list.end(),
893                             [&]( const std::unique_ptr<PROJECT>& ptr )
894                             {
895                                 return ptr.get() == toRemove;
896                             } );
897 
898     wxASSERT( it != m_projects_list.end() );
899     m_projects_list.erase( it );
900 
901     m_projects.erase( projectPath );
902 
903     // Immediately reload a null project; this is required until the rest of the application
904     // is refactored to not assume that Prj() always works
905     if( m_projects.empty() )
906         LoadProject( "" );
907 
908     // Remove the reference in the environment to the previous project
909     wxSetEnv( PROJECT_VAR_NAME, "" );
910 
911     // Release lock on the file, in case we had one
912     m_project_lock = nullptr;
913 
914     if( m_kiway )
915         m_kiway->ProjectChanged();
916 
917     return true;
918 }
919 
920 
Prj() const921 PROJECT& SETTINGS_MANAGER::Prj() const
922 {
923     // No MDI yet:  First project in the list is the active project
924     wxASSERT_MSG( m_projects_list.size(), "no project in list" );
925     return *m_projects_list.begin()->get();
926 }
927 
928 
IsProjectOpen() const929 bool SETTINGS_MANAGER::IsProjectOpen() const
930 {
931     return !m_projects.empty();
932 }
933 
934 
GetProject(const wxString & aFullPath) const935 PROJECT* SETTINGS_MANAGER::GetProject( const wxString& aFullPath ) const
936 {
937     if( m_projects.count( aFullPath ) )
938         return m_projects.at( aFullPath );
939 
940     return nullptr;
941 }
942 
943 
GetOpenProjects() const944 std::vector<wxString> SETTINGS_MANAGER::GetOpenProjects() const
945 {
946     std::vector<wxString> ret;
947 
948     for( const std::pair<const wxString, PROJECT*>& pair : m_projects )
949         ret.emplace_back( pair.first );
950 
951     return ret;
952 }
953 
954 
SaveProject(const wxString & aFullPath)955 bool SETTINGS_MANAGER::SaveProject( const wxString& aFullPath )
956 {
957     wxString path = aFullPath;
958 
959     if( path.empty() )
960         path = Prj().GetProjectFullName();
961 
962     // TODO: refactor for MDI
963     if( Prj().IsReadOnly() )
964         return false;
965 
966     if( !m_project_files.count( path ) )
967         return false;
968 
969     PROJECT_FILE* project     = m_project_files.at( path );
970     wxString      projectPath = GetPathForSettingsFile( project );
971 
972     project->SaveToFile( projectPath );
973     Prj().GetLocalSettings().SaveToFile( projectPath );
974 
975     return true;
976 }
977 
978 
SaveProjectAs(const wxString & aFullPath)979 void SETTINGS_MANAGER::SaveProjectAs( const wxString& aFullPath )
980 {
981     wxString oldName = Prj().GetProjectFullName();
982 
983     if( aFullPath.IsSameAs( oldName ) )
984     {
985         SaveProject( aFullPath );
986         return;
987     }
988 
989     // Changing this will cause UnloadProject to not save over the "old" project when loading below
990     Prj().setProjectFullName( aFullPath );
991 
992     wxFileName fn( aFullPath );
993 
994     PROJECT_FILE* project = m_project_files.at( oldName );
995 
996     // Ensure read-only flags are copied; this allows doing a "Save As" on a standalong board/sch
997     // without creating project files if the checkbox is turned off
998     project->SetReadOnly( Prj().IsReadOnly() );
999     Prj().GetLocalSettings().SetReadOnly( Prj().IsReadOnly() );
1000 
1001     project->SetFilename( fn.GetName() );
1002     project->SaveToFile( fn.GetPath() );
1003 
1004     Prj().GetLocalSettings().SetFilename( fn.GetName() );
1005     Prj().GetLocalSettings().SaveToFile( fn.GetPath() );
1006 
1007     m_project_files[fn.GetFullPath()] = project;
1008     m_project_files.erase( oldName );
1009 
1010     m_projects[fn.GetFullPath()] = m_projects[oldName];
1011     m_projects.erase( oldName );
1012 }
1013 
1014 
SaveProjectCopy(const wxString & aFullPath)1015 void SETTINGS_MANAGER::SaveProjectCopy( const wxString& aFullPath )
1016 {
1017     PROJECT_FILE* project = m_project_files.at( Prj().GetProjectFullName() );
1018     wxString      oldName = project->GetFilename();
1019     wxFileName    fn( aFullPath );
1020 
1021     bool readOnly = project->IsReadOnly();
1022     project->SetReadOnly( false );
1023 
1024     project->SetFilename( fn.GetName() );
1025     project->SaveToFile( fn.GetPath() );
1026     project->SetFilename( oldName );
1027 
1028     Prj().GetLocalSettings().SetFilename( fn.GetName() );
1029     Prj().GetLocalSettings().SaveToFile( fn.GetPath() );
1030     Prj().GetLocalSettings().SetFilename( oldName );
1031 
1032     project->SetReadOnly( readOnly );
1033 }
1034 
1035 
loadProjectFile(PROJECT & aProject)1036 bool SETTINGS_MANAGER::loadProjectFile( PROJECT& aProject )
1037 {
1038     wxFileName fullFn( aProject.GetProjectFullName() );
1039     wxString fn( fullFn.GetName() );
1040 
1041     PROJECT_FILE* file = RegisterSettings( new PROJECT_FILE( fn ), false );
1042 
1043     m_project_files[aProject.GetProjectFullName()] = file;
1044 
1045     aProject.setProjectFile( file );
1046     file->SetProject( &aProject );
1047 
1048     wxString path( fullFn.GetPath() );
1049 
1050     return file->LoadFromFile( path );
1051 }
1052 
1053 
unloadProjectFile(PROJECT * aProject,bool aSave)1054 bool SETTINGS_MANAGER::unloadProjectFile( PROJECT* aProject, bool aSave )
1055 {
1056     if( !aProject )
1057         return false;
1058 
1059     wxString name = aProject->GetProjectFullName();
1060 
1061     if( !m_project_files.count( name ) )
1062         return false;
1063 
1064     PROJECT_FILE* file = m_project_files[name];
1065 
1066     auto it = std::find_if( m_settings.begin(), m_settings.end(),
1067                             [&file]( const std::unique_ptr<JSON_SETTINGS>& aPtr )
1068                             {
1069                               return aPtr.get() == file;
1070                             } );
1071 
1072     if( it != m_settings.end() )
1073     {
1074         wxString projectPath = GetPathForSettingsFile( it->get() );
1075 
1076         FlushAndRelease( &aProject->GetLocalSettings(), aSave );
1077 
1078         if( aSave )
1079             ( *it )->SaveToFile( projectPath );
1080 
1081         m_settings.erase( it );
1082     }
1083 
1084     m_project_files.erase( name );
1085 
1086     return true;
1087 }
1088 
1089 
GetProjectBackupsPath() const1090 wxString SETTINGS_MANAGER::GetProjectBackupsPath() const
1091 {
1092     return Prj().GetProjectPath() + Prj().GetProjectName() + PROJECT_BACKUPS_DIR_SUFFIX;
1093 }
1094 
1095 
1096 wxString SETTINGS_MANAGER::backupDateTimeFormat = wxT( "%Y-%m-%d_%H%M%S" );
1097 
1098 
BackupProject(REPORTER & aReporter) const1099 bool SETTINGS_MANAGER::BackupProject( REPORTER& aReporter ) const
1100 {
1101     wxDateTime timestamp = wxDateTime::Now();
1102 
1103     wxString fileName = wxString::Format( wxT( "%s-%s" ), Prj().GetProjectName(),
1104                                           timestamp.Format( backupDateTimeFormat ) );
1105 
1106     wxFileName target;
1107     target.SetPath( GetProjectBackupsPath() );
1108     target.SetName( fileName );
1109     target.SetExt( ArchiveFileExtension );
1110 
1111     wxDir dir( target.GetPath() );
1112 
1113     if( !target.DirExists() && !wxMkdir( target.GetPath() ) )
1114     {
1115         wxLogTrace( traceSettings, "Could not create project backup path %s", target.GetPath() );
1116         return false;
1117     }
1118 
1119     if( !target.IsDirWritable() )
1120     {
1121         wxLogTrace( traceSettings, "Backup directory %s is not writable", target.GetPath() );
1122         return false;
1123     }
1124 
1125     wxLogTrace( traceSettings, "Backing up project to %s", target.GetPath() );
1126 
1127     PROJECT_ARCHIVER archiver;
1128 
1129     return archiver.Archive( Prj().GetProjectPath(), target.GetFullPath(), aReporter );
1130 }
1131 
1132 
1133 class VECTOR_INSERT_TRAVERSER : public wxDirTraverser
1134 {
1135 public:
VECTOR_INSERT_TRAVERSER(std::vector<wxString> & aVec,std::function<bool (const wxString &)> aCond)1136     VECTOR_INSERT_TRAVERSER( std::vector<wxString>& aVec,
1137                              std::function<bool( const wxString& )> aCond ) :
1138             m_files( aVec ),
1139             m_condition( aCond )
1140     {
1141     }
1142 
OnFile(const wxString & aFile)1143     wxDirTraverseResult OnFile( const wxString& aFile ) override
1144     {
1145         if( m_condition( aFile ) )
1146             m_files.emplace_back( aFile );
1147 
1148         return wxDIR_CONTINUE;
1149     }
1150 
OnDir(const wxString & aDirName)1151     wxDirTraverseResult OnDir( const wxString& aDirName ) override
1152     {
1153         return wxDIR_CONTINUE;
1154     }
1155 
1156 private:
1157     std::vector<wxString>& m_files;
1158 
1159     std::function<bool( const wxString& )> m_condition;
1160 };
1161 
1162 
TriggerBackupIfNeeded(REPORTER & aReporter) const1163 bool SETTINGS_MANAGER::TriggerBackupIfNeeded( REPORTER& aReporter ) const
1164 {
1165     COMMON_SETTINGS::AUTO_BACKUP settings = GetCommonSettings()->m_Backup;
1166 
1167     if( !settings.enabled )
1168         return true;
1169 
1170     wxString prefix = Prj().GetProjectName() + '-';
1171 
1172     auto modTime =
1173             [&prefix]( const wxString& aFile )
1174             {
1175                 wxDateTime dt;
1176                 wxString fn( wxFileName( aFile ).GetName() );
1177                 fn.Replace( prefix, "" );
1178                 dt.ParseFormat( fn, backupDateTimeFormat );
1179                 return dt;
1180             };
1181 
1182     wxFileName projectPath( Prj().GetProjectPath() );
1183 
1184     // Skip backup if project path isn't valid or writable
1185     if( !projectPath.IsOk() || !projectPath.Exists() || !projectPath.IsDirWritable() )
1186         return true;
1187 
1188     wxString backupPath = GetProjectBackupsPath();
1189 
1190     if( !wxDirExists( backupPath ) )
1191     {
1192         wxLogTrace( traceSettings, "Backup path %s doesn't exist, creating it", backupPath );
1193 
1194         if( !wxMkdir( backupPath ) )
1195         {
1196             wxLogTrace( traceSettings, "Could not create backups path!  Skipping backup" );
1197             return false;
1198         }
1199     }
1200 
1201     wxDir dir( backupPath );
1202 
1203     if( !dir.IsOpened() )
1204     {
1205         wxLogTrace( traceSettings, "Could not open project backups path %s", dir.GetName() );
1206         return false;
1207     }
1208 
1209     std::vector<wxString> files;
1210 
1211     VECTOR_INSERT_TRAVERSER traverser( files,
1212             [&modTime]( const wxString& aFile )
1213             {
1214                 return modTime( aFile ).IsValid();
1215             } );
1216 
1217     dir.Traverse( traverser, wxT( "*.zip" ) );
1218 
1219     // Sort newest-first
1220     std::sort( files.begin(), files.end(),
1221                [&]( const wxString& aFirst, const wxString& aSecond ) -> bool
1222                {
1223                    wxDateTime first  = modTime( aFirst );
1224                    wxDateTime second = modTime( aSecond );
1225 
1226                    return first.GetTicks() > second.GetTicks();
1227                } );
1228 
1229     // Do we even need to back up?
1230     if( !files.empty() )
1231     {
1232         wxDateTime lastTime = modTime( files[0] );
1233 
1234         if( lastTime.IsValid() )
1235         {
1236             wxTimeSpan delta = wxDateTime::Now() - modTime( files[0] );
1237 
1238             if( delta.IsShorterThan( wxTimeSpan::Seconds( settings.min_interval ) ) )
1239                 return true;
1240         }
1241     }
1242 
1243     // Now that we know a backup is needed, apply the retention policy
1244 
1245     // Step 1: if we're over the total file limit, remove the oldest
1246     if( !files.empty() && settings.limit_total_files > 0 )
1247     {
1248         while( files.size() > static_cast<size_t>( settings.limit_total_files ) )
1249         {
1250             wxRemoveFile( files.back() );
1251             files.pop_back();
1252         }
1253     }
1254 
1255     // Step 2: Stay under the total size limit
1256     if( settings.limit_total_size > 0 )
1257     {
1258         wxULongLong totalSize = 0;
1259 
1260         for( const wxString& file : files )
1261             totalSize += wxFileName::GetSize( file );
1262 
1263         while( !files.empty() && totalSize > static_cast<wxULongLong>( settings.limit_total_size ) )
1264         {
1265             totalSize -= wxFileName::GetSize( files.back() );
1266             wxRemoveFile( files.back() );
1267             files.pop_back();
1268         }
1269     }
1270 
1271     // Step 3: Stay under the daily limit
1272     if( settings.limit_daily_files > 0 && files.size() > 1 )
1273     {
1274         wxDateTime day = modTime( files[0] );
1275         int        num = 1;
1276 
1277         wxASSERT( day.IsValid() );
1278 
1279         std::vector<wxString> filesToDelete;
1280 
1281         for( size_t i = 1; i < files.size(); i++ )
1282         {
1283             wxDateTime dt = modTime( files[i] );
1284 
1285             if( dt.IsSameDate( day ) )
1286             {
1287                 num++;
1288 
1289                 if( num > settings.limit_daily_files )
1290                     filesToDelete.emplace_back( files[i] );
1291             }
1292             else
1293             {
1294                 day = dt;
1295                 num = 1;
1296             }
1297         }
1298 
1299         for( const wxString& file : filesToDelete )
1300             wxRemoveFile( file );
1301     }
1302 
1303     return BackupProject( aReporter );
1304 }
1305