1 /***************************************************************************
2   qgsversionmigration.cpp - QgsVersionMigration
3 
4  ---------------------
5  begin                : 30.7.2017
6  copyright            : (C) 2017 by Nathan Woodrow
7  email                : woodrow.nathan at gmail dot com
8  ***************************************************************************
9  *                                                                         *
10  *   This program is free software; you can redistribute it and/or modify  *
11  *   it under the terms of the GNU General Public License as published by  *
12  *   the Free Software Foundation; either version 2 of the License, or     *
13  *   (at your option) any later version.                                   *
14  *                                                                         *
15  ***************************************************************************/
16 #include "qgsversionmigration.h"
17 #include "qgssettings.h"
18 #include "qgslogger.h"
19 #include "qsettings.h"
20 #include "qgsmessagelog.h"
21 #include "qgsapplication.h"
22 #include "qgssymbol.h"
23 #include "qgsstyle.h"
24 #include "qgssymbollayerutils.h"
25 #include "qgsreadwritecontext.h"
26 #include "qgsuserprofilemanager.h"
27 
28 #include <QFile>
29 #include <QTextStream>
30 #include <QDir>
31 #include <QSqlDatabase>
32 #include <QSqlQuery>
33 #include <QSqlError>
34 #include <QDomDocument>
35 
canMigrate(int fromVersion,int toVersion)36 std::unique_ptr<QgsVersionMigration> QgsVersionMigration::canMigrate( int fromVersion, int toVersion )
37 {
38   if ( fromVersion == 20000 && toVersion >= 29900 )
39   {
40     return qgis::make_unique< Qgs2To3Migration >();
41   }
42   return nullptr;
43 }
44 
runMigration()45 QgsError Qgs2To3Migration::runMigration()
46 {
47   QgsError errors;
48   QgsError settingsErrors = migrateSettings();
49   if ( !settingsErrors.isEmpty() )
50   {
51     const QList<QgsErrorMessage> errorList( settingsErrors.messageList( ) );
52     for ( const auto &err : errorList )
53     {
54       errors.append( err );
55     }
56   }
57   QgsError stylesErrors = migrateStyles();
58   if ( !stylesErrors.isEmpty() )
59   {
60     const QList<QgsErrorMessage> errorList( stylesErrors.messageList( ) );
61     for ( const auto &err : errorList )
62     {
63       errors.append( err );
64     }
65   }
66   QgsError authDbErrors = migrateAuthDb();
67   if ( !authDbErrors.isEmpty() )
68   {
69     const QList<QgsErrorMessage> errorList( authDbErrors.messageList( ) );
70     for ( const auto &err : errorList )
71     {
72       errors.append( err );
73     }
74   }
75   return errors;
76 }
77 
requiresMigration()78 bool Qgs2To3Migration::requiresMigration()
79 {
80   QgsSettings settings;
81   bool alreadyMigrated = settings.value( QStringLiteral( "migration/settings" ), false ).toBool();
82   int  settingsMigrationVersion = settings.value( QStringLiteral( "migration/fileVersion" ), 0 ).toInt();
83   QFile migrationFile( migrationFilePath() );
84   if ( migrationFile.open( QIODevice::ReadOnly | QIODevice::Text ) )
85   {
86     QTextStream in( &migrationFile );
87     QString line = in.readLine();
88     if ( line.startsWith( "#" ) && line.contains( QStringLiteral( "version=" ) ) )
89     {
90       QStringList parts = line.split( '=' );
91       mMigrationFileVersion = parts.at( 1 ).toInt();
92       QgsDebugMsgLevel( QStringLiteral( "File version is=%1" ).arg( mMigrationFileVersion ), 2 );
93     }
94     migrationFile.close();
95   }
96   else
97   {
98     QgsDebugMsg( QStringLiteral( "Can not open %1" ).arg( migrationFile.fileName() ) );
99     mMigrationFileVersion = settingsMigrationVersion;
100   }
101 
102   return ( !alreadyMigrated || settingsMigrationVersion != mMigrationFileVersion );
103 }
104 
migrateStyles()105 QgsError Qgs2To3Migration::migrateStyles()
106 {
107   QgsError error;
108   QString oldHome = QStringLiteral( "%1/.qgis2" ).arg( QDir::homePath() );
109   QString oldStyleFile = QStringLiteral( "%1/symbology-ng-style.db" ).arg( oldHome );
110   QgsDebugMsgLevel( QStringLiteral( "OLD STYLE FILE %1" ).arg( oldStyleFile ), 2 );
111   QSqlDatabase db = QSqlDatabase::addDatabase( "QSQLITE", "migration" );
112   db.setDatabaseName( oldStyleFile );
113   if ( !db.open() )
114   {
115     error.append( db.lastError().text() );
116     QgsDebugMsg( db.lastError().text() );
117     return error;
118   }
119 
120   QSqlQuery query( db );
121   QSqlQuery tagQuery( "SELECT name FROM tag"
122                       "JOIN tagmap ON tagmap.tag_id = tag.id"
123                       "WHERE tagmap.symbol_id = :symbol_id", db );
124 
125   QgsStyle *style = QgsStyle::defaultStyle();
126   if ( query.exec( "SELECT id, name, xml FROM symbol" ) )
127   {
128     while ( query.next() )
129     {
130       QString symbol_id = query.value( 0 ).toString();
131       QString name = query.value( 1 ).toString();
132       QString xml = query.value( 2 ).toString();
133       QDomDocument doc;
134       if ( !doc.setContent( xml ) )
135       {
136         QgsDebugMsg( "Cannot open symbol " + name );
137         continue;
138       }
139 
140       tagQuery.bindValue( ":symbol_id", symbol_id );
141 
142       QStringList tags;
143       if ( tagQuery.exec() )
144       {
145         while ( query.next() )
146         {
147           QString tagname = query.value( 0 ).toString();
148           tags << tagname;
149         }
150       }
151 
152       QDomElement symElement = doc.documentElement();
153       QgsDebugMsgLevel( QStringLiteral( "MIGRATION: Importing %1" ).arg( name ), 2 );
154       QgsSymbol *symbol = QgsSymbolLayerUtils::loadSymbol( symElement, QgsReadWriteContext() );
155       tags << "QGIS 2";
156       if ( style->symbolId( name ) == 0 )
157       {
158         style->saveSymbol( name, symbol, false, tags );
159       }
160     }
161   }
162 
163   QgsDebugMsgLevel( oldStyleFile, 2 );
164   return error;
165 }
166 
migrateSettings()167 QgsError Qgs2To3Migration::migrateSettings()
168 {
169   QgsError error;
170 
171   QgsSettings newSettings;
172 
173   // The platform default location for the settings from 2.x
174   mOldSettings = new QSettings( "QGIS", "QGIS2" );
175 
176   QFile inputFile( migrationFilePath() );
177   std::map<QString, QgsSettings::Section> sections;
178   sections["none"] = QgsSettings::NoSection;
179   sections["core"] = QgsSettings::Core;
180   sections["gui"] = QgsSettings::Gui;
181   sections["server"] = QgsSettings::Server;
182   sections["plugins"] = QgsSettings::Plugins;
183   sections["auth"] = QgsSettings::Auth;
184   sections["app"] = QgsSettings::App;
185   sections["providers"] = QgsSettings::Providers;
186   sections["misc"] = QgsSettings::Misc;
187 
188   QList<QPair<QString, QString>> keys;
189 
190   if ( inputFile.open( QIODevice::ReadOnly | QIODevice::Text ) )
191   {
192     QTextStream in( &inputFile );
193     while ( !in.atEnd() )
194     {
195       QString line = in.readLine();
196 
197       if ( line.startsWith( "#" ) )
198         continue;
199 
200       if ( line.isEmpty() )
201         continue;
202 
203       const QStringList parts = line.split( ';' );
204 
205       Q_ASSERT_X( parts.count() == 2, "QgsVersionMigration::migrateSettings()", "Can't split line in 2 parts." );
206 
207       QString oldKey = parts.at( 0 );
208       QString newKey = parts.at( 1 );
209 
210       if ( oldKey.endsWith( "/*" ) )
211       {
212         oldKey = oldKey.replace( "/*", "" );
213         QList<QPair<QString, QString>> keyList = walk( oldKey, newKey );
214         keys.append( keyList );
215       }
216       else
217       {
218         QPair<QString, QString> key = transformKey( oldKey, newKey );
219         keys.append( key );
220       }
221 
222     }
223     inputFile.close();
224     newSettings.setValue( QStringLiteral( "migration/settings" ), true );
225     // Set the dev gen so we can force a migration.
226     newSettings.setValue( QStringLiteral( "migration/fileVersion" ), mMigrationFileVersion );
227   }
228   else
229   {
230     QString msg = QString( "Can not open %1" ).arg( inputFile.fileName() );
231     QgsDebugMsg( msg );
232     error.append( msg );
233   }
234 
235   if ( keys.count() > 0 )
236   {
237     QgsDebugMsgLevel( QStringLiteral( "MIGRATION: Translating settings keys" ), 2 );
238     QList<QPair<QString, QString>>::iterator i;
239     for ( i = keys.begin(); i != keys.end(); ++i )
240     {
241       QPair<QString, QString> pair = *i;
242 
243       QString oldKey = pair.first;
244       QString newKey = pair.second;
245 
246       if ( oldKey.contains( oldKey ) )
247       {
248         QgsDebugMsgLevel( QStringLiteral( " -> %1 -> %2" ).arg( oldKey, newKey ), 2 );
249         newSettings.setValue( newKey, mOldSettings->value( oldKey ) );
250       }
251     }
252   }
253   return error;
254 }
255 
migrateAuthDb()256 QgsError Qgs2To3Migration::migrateAuthDb()
257 {
258   QgsError error;
259   QString oldHome = QStringLiteral( "%1/.qgis2" ).arg( QDir::homePath() );
260   QString oldAuthDbFilePath = QStringLiteral( "%1/qgis-auth.db" ).arg( oldHome );
261   // Try to retrieve the current profile folder (I didn't find an QgsApplication API for it)
262   QDir settingsDir = QFileInfo( QgsSettings().fileName() ).absoluteDir();
263   settingsDir.cdUp();
264   QString newAuthDbFilePath = QStringLiteral( "%1/qgis-auth.db" ).arg( settingsDir.absolutePath() );
265   // Do not overwrite!
266   if ( QFile( newAuthDbFilePath ).exists( ) )
267   {
268     QString msg = QStringLiteral( "Could not copy old auth DB to %1: file already exists!" ).arg( newAuthDbFilePath );
269     QgsDebugMsg( msg );
270     error.append( msg );
271   }
272   else
273   {
274     QFile oldDbFile( oldAuthDbFilePath );
275     if ( oldDbFile.exists( ) )
276     {
277       if ( oldDbFile.copy( newAuthDbFilePath ) )
278       {
279         QgsDebugMsgLevel( QStringLiteral( "Old auth DB successfully copied to %1" ).arg( newAuthDbFilePath ), 2 );
280       }
281       else
282       {
283         QString msg = QStringLiteral( "Could not copy auth DB %1 to %2" ).arg( oldAuthDbFilePath, newAuthDbFilePath );
284         QgsDebugMsg( msg );
285         error.append( msg );
286       }
287     }
288     else
289     {
290       QString msg = QStringLiteral( "Could not copy auth DB %1 to %2: old DB does not exists!" ).arg( oldAuthDbFilePath, newAuthDbFilePath );
291       QgsDebugMsg( msg );
292       error.append( msg );
293     }
294   }
295   return error;
296 }
297 
walk(QString group,QString newkey)298 QList<QPair<QString, QString> > Qgs2To3Migration::walk( QString group, QString newkey )
299 {
300   mOldSettings->beginGroup( group );
301   QList<QPair<QString, QString> > foundKeys;
302   const auto constChildGroups = mOldSettings->childGroups();
303   for ( const QString &group : constChildGroups )
304   {
305     QList<QPair<QString, QString> > data = walk( group, newkey );
306     foundKeys.append( data );
307   }
308 
309   const auto constChildKeys = mOldSettings->childKeys();
310   for ( const QString &key : constChildKeys )
311   {
312     QString fullKey = mOldSettings->group() + "/" + key;
313     foundKeys.append( transformKey( fullKey, newkey ) );
314   }
315   mOldSettings->endGroup();
316   return foundKeys;
317 }
318 
transformKey(QString fullOldKey,QString newKeyPart)319 QPair<QString, QString> Qgs2To3Migration::transformKey( QString fullOldKey, QString newKeyPart )
320 {
321   QString newKey = newKeyPart;
322   QString oldKey = fullOldKey;
323 
324   if ( newKeyPart == QLatin1String( "*" ) )
325   {
326     newKey = fullOldKey;
327   }
328 
329   if ( newKeyPart.endsWith( "/*" ) )
330   {
331     QStringList newKeyparts = newKeyPart.split( '/' );
332     // Throw away the *
333     newKeyparts.removeLast();
334     QStringList oldKeyParts = fullOldKey.split( '/' );
335     for ( int i = 0; i < newKeyparts.count(); ++i )
336     {
337       oldKeyParts.replace( i, newKeyparts.at( i ) );
338     }
339     newKey = oldKeyParts.join( "/" );
340   }
341 
342   return qMakePair( oldKey, newKey );
343 }
344 
migrationFilePath()345 QString Qgs2To3Migration::migrationFilePath()
346 {
347   return QgsApplication::resolvePkgPath() +  "/resources/2to3migration.txt";
348 }
349