1 /* === This file is part of Calamares - <https://calamares.io> ===
2  *
3  *   SPDX-FileCopyrightText: 2011 Lennart Poettering
4  *   SPDX-FileCopyrightText: Kay Sievers
5  *   SPDX-FileCopyrightText: 2014-2016 Teo Mrnjavac <teo@kde.org>
6  *   SPDX-FileCopyrightText: 2014 Kevin Kofler <kevin.kofler@chello.at>
7  *   SPDX-License-Identifier: GPL-3.0-or-later
8  *
9  *   Portions from systemd (localed.c):
10  *   Copyright 2011 Lennart Poettering
11  *   Copyright 2013 Kay Sievers
12  *   (originally under LGPLv2.1+, used under the LGPL to GPL conversion clause)
13  *
14  *   Calamares is Free Software: see the License-Identifier above.
15  *
16  */
17 
18 #include "SetKeyboardLayoutJob.h"
19 
20 #include "GlobalStorage.h"
21 #include "JobQueue.h"
22 #include "utils/CalamaresUtilsSystem.h"
23 #include "utils/Logger.h"
24 #include "utils/String.h"
25 
26 #include <QDir>
27 #include <QFile>
28 #include <QFileInfo>
29 #include <QSettings>
30 #include <QTextStream>
31 
32 
SetKeyboardLayoutJob(const QString & model,const QString & layout,const QString & variant,const AdditionalLayoutInfo & additionalLayoutInfo,const QString & xOrgConfFileName,const QString & convertedKeymapPath,bool writeEtcDefaultKeyboard)33 SetKeyboardLayoutJob::SetKeyboardLayoutJob( const QString& model,
34                                             const QString& layout,
35                                             const QString& variant,
36                                             const AdditionalLayoutInfo& additionalLayoutInfo,
37                                             const QString& xOrgConfFileName,
38                                             const QString& convertedKeymapPath,
39                                             bool writeEtcDefaultKeyboard )
40     : Calamares::Job()
41     , m_model( model )
42     , m_layout( layout )
43     , m_variant( variant )
44     , m_additionalLayoutInfo( additionalLayoutInfo )
45     , m_xOrgConfFileName( xOrgConfFileName )
46     , m_convertedKeymapPath( convertedKeymapPath )
47     , m_writeEtcDefaultKeyboard( writeEtcDefaultKeyboard )
48 {
49 }
50 
51 
52 QString
prettyName() const53 SetKeyboardLayoutJob::prettyName() const
54 {
55     return tr( "Set keyboard model to %1, layout to %2-%3" ).arg( m_model ).arg( m_layout ).arg( m_variant );
56 }
57 
58 
59 QString
findConvertedKeymap(const QString & convertedKeymapPath) const60 SetKeyboardLayoutJob::findConvertedKeymap( const QString& convertedKeymapPath ) const
61 {
62     cDebug() << "Looking for converted keymap in" << convertedKeymapPath;
63 
64     // No search path supplied, assume the distribution does not provide
65     // converted keymaps
66     if ( convertedKeymapPath.isEmpty() )
67     {
68         return QString();
69     }
70 
71     QDir convertedKeymapDir( convertedKeymapPath );
72     QString name = m_variant.isEmpty() ? m_layout : ( m_layout + '-' + m_variant );
73 
74     if ( convertedKeymapDir.exists( name + ".map" ) || convertedKeymapDir.exists( name + ".map.gz" ) )
75     {
76         cDebug() << Logger::SubEntry << "Found converted keymap" << name;
77         return name;
78     }
79 
80     return QString();
81 }
82 
83 
84 STATICTEST QString
findLegacyKeymap(const QString & layout,const QString & model,const QString & variant)85 findLegacyKeymap( const QString& layout, const QString& model, const QString& variant )
86 {
87     cDebug() << "Looking for legacy keymap" << layout << model << variant << "in QRC";
88 
89     int bestMatching = 0;
90     QString name;
91 
92     QFile file( ":/kbd-model-map" );
93     if ( !file.open( QIODevice::ReadOnly | QIODevice::Text ) )
94     {
95         cDebug() << Logger::SubEntry << "Could not read QRC";
96         return QString();
97     }
98 
99     QTextStream stream( &file );
100     while ( !stream.atEnd() )
101     {
102         QString line = stream.readLine().trimmed();
103         if ( line.isEmpty() || line.startsWith( '#' ) )
104         {
105             continue;
106         }
107 
108         QStringList mapping = line.split( '\t', SplitSkipEmptyParts );
109         if ( mapping.size() < 5 )
110         {
111             continue;
112         }
113 
114         int matching = 0;
115 
116         // Determine how well matching this entry is
117         // We assume here that we have one X11 layout. If the UI changes to
118         // allow more than one layout, this should change too.
119         if ( layout == mapping[ 1 ] )
120         // If we got an exact match, this is best
121         {
122             matching = 10;
123         }
124         // Look for an entry whose first layout matches ours
125         else if ( mapping[ 1 ].startsWith( layout + ',' ) )
126         {
127             matching = 5;
128         }
129 
130         if ( matching > 0 )
131         {
132             if ( model.isEmpty() || model == mapping[ 2 ] )
133             {
134                 matching++;
135             }
136 
137             QString mappingVariant = mapping[ 3 ];
138             if ( mappingVariant == "-" )
139             {
140                 mappingVariant = QString();
141             }
142             else if ( mappingVariant.startsWith( ',' ) )
143             {
144                 mappingVariant.remove( 1, 0 );
145             }
146 
147             if ( variant == mappingVariant )
148             {
149                 matching++;
150             }
151 
152             // We ignore mapping[4], the xkb options, for now. If we ever
153             // allow setting options in the UI, we should match them here.
154         }
155 
156         // The best matching entry so far, then let's save that
157         if ( matching >= qMax( bestMatching, 1 ) )
158         {
159             cDebug() << Logger::SubEntry << "Found legacy keymap" << mapping[ 0 ] << "with score" << matching;
160 
161             if ( matching > bestMatching )
162             {
163                 bestMatching = matching;
164                 name = mapping[ 0 ];
165             }
166         }
167     }
168 
169     return name;
170 }
171 
172 QString
findLegacyKeymap() const173 SetKeyboardLayoutJob::findLegacyKeymap() const
174 {
175     return ::findLegacyKeymap( m_layout, m_model, m_variant );
176 }
177 
178 
179 bool
writeVConsoleData(const QString & vconsoleConfPath,const QString & convertedKeymapPath) const180 SetKeyboardLayoutJob::writeVConsoleData( const QString& vconsoleConfPath, const QString& convertedKeymapPath ) const
181 {
182     cDebug() << "Writing vconsole data to" << vconsoleConfPath;
183 
184     QString keymap = findConvertedKeymap( convertedKeymapPath );
185     if ( keymap.isEmpty() )
186     {
187         keymap = findLegacyKeymap();
188     }
189     if ( keymap.isEmpty() )
190     {
191         cDebug() << "Trying to use X11 layout" << m_layout << "as the virtual console layout";
192         keymap = m_layout;
193     }
194 
195     QStringList existingLines;
196 
197     // Read in the existing vconsole.conf, if it exists
198     QFile file( vconsoleConfPath );
199     if ( file.exists() )
200     {
201         file.open( QIODevice::ReadOnly | QIODevice::Text );
202         QTextStream stream( &file );
203         while ( !stream.atEnd() )
204         {
205             existingLines << stream.readLine();
206         }
207         file.close();
208         if ( stream.status() != QTextStream::Ok )
209         {
210             cError() << "Could not read lines from" << file.fileName();
211             return false;
212         }
213     }
214 
215     // Write out the existing lines and replace the KEYMAP= line
216     if ( !file.open( QIODevice::WriteOnly | QIODevice::Text ) )
217     {
218         cError() << "Could not open" << file.fileName() << "for writing.";
219         return false;
220     }
221     QTextStream stream( &file );
222     bool found = false;
223     for ( const QString& existingLine : qAsConst( existingLines ) )
224     {
225         if ( existingLine.trimmed().startsWith( "KEYMAP=" ) )
226         {
227             stream << "KEYMAP=" << keymap << '\n';
228             found = true;
229         }
230         else
231         {
232             stream << existingLine << '\n';
233         }
234     }
235     // Add a KEYMAP= line if there wasn't any
236     if ( !found )
237     {
238         stream << "KEYMAP=" << keymap << '\n';
239     }
240     stream.flush();
241     file.close();
242 
243     cDebug() << Logger::SubEntry << "Written KEYMAP=" << keymap << "to vconsole.conf" << stream.status();
244 
245     return ( stream.status() == QTextStream::Ok );
246 }
247 
248 
249 bool
writeX11Data(const QString & keyboardConfPath) const250 SetKeyboardLayoutJob::writeX11Data( const QString& keyboardConfPath ) const
251 {
252     cDebug() << "Writing X11 configuration to" << keyboardConfPath;
253 
254     QFile file( keyboardConfPath );
255     if ( !file.open( QIODevice::WriteOnly | QIODevice::Text ) )
256     {
257         cError() << "Could not open" << file.fileName() << "for writing.";
258         return false;
259     }
260     QTextStream stream( &file );
261 
262     stream << "# Read and parsed by systemd-localed. It's probably wise not to edit this file\n"
263               "# manually too freely.\n"
264               "Section \"InputClass\"\n"
265               "        Identifier \"system-keyboard\"\n"
266               "        MatchIsKeyboard \"on\"\n";
267 
268 
269     if ( m_additionalLayoutInfo.additionalLayout.isEmpty() )
270     {
271         if ( !m_layout.isEmpty() )
272         {
273             stream << "        Option \"XkbLayout\" \"" << m_layout << "\"\n";
274         }
275 
276         if ( !m_variant.isEmpty() )
277         {
278             stream << "        Option \"XkbVariant\" \"" << m_variant << "\"\n";
279         }
280     }
281     else
282     {
283         if ( !m_layout.isEmpty() )
284         {
285             stream << "        Option \"XkbLayout\" \"" << m_additionalLayoutInfo.additionalLayout << "," << m_layout
286                    << "\"\n";
287         }
288 
289         if ( !m_variant.isEmpty() )
290         {
291             stream << "        Option \"XkbVariant\" \"" << m_additionalLayoutInfo.additionalVariant << "," << m_variant
292                    << "\"\n";
293         }
294 
295         stream << "        Option \"XkbOptions\" \"" << m_additionalLayoutInfo.groupSwitcher << "\"\n";
296     }
297 
298     stream << "EndSection\n";
299     stream.flush();
300 
301     file.close();
302 
303     cDebug() << Logger::SubEntry << "Written XkbLayout" << m_layout << "; XkbModel" << m_model << "; XkbVariant"
304              << m_variant << "to X.org file" << keyboardConfPath << stream.status();
305 
306     return ( stream.status() == QTextStream::Ok );
307 }
308 
309 
310 bool
writeDefaultKeyboardData(const QString & defaultKeyboardPath) const311 SetKeyboardLayoutJob::writeDefaultKeyboardData( const QString& defaultKeyboardPath ) const
312 {
313     cDebug() << "Writing default keyboard data to" << defaultKeyboardPath;
314 
315     QFile file( defaultKeyboardPath );
316     if ( !file.open( QIODevice::WriteOnly | QIODevice::Text ) )
317     {
318         cError() << "Could not open" << defaultKeyboardPath << "for writing";
319         return false;
320     }
321     QTextStream stream( &file );
322 
323     stream << "# KEYBOARD CONFIGURATION FILE\n\n"
324               "# Consult the keyboard(5) manual page.\n\n";
325 
326     stream << "XKBMODEL=\"" << m_model << "\"\n";
327     stream << "XKBLAYOUT=\"" << m_layout << "\"\n";
328     stream << "XKBVARIANT=\"" << m_variant << "\"\n";
329     stream << "XKBOPTIONS=\"\"\n\n";
330     stream << "BACKSPACE=\"guess\"\n";
331     stream.flush();
332 
333     file.close();
334 
335     cDebug() << Logger::SubEntry << "Written XKBMODEL" << m_model << "; XKBLAYOUT" << m_layout << "; XKBVARIANT"
336              << m_variant << "to /etc/default/keyboard file" << defaultKeyboardPath << stream.status();
337 
338     return ( stream.status() == QTextStream::Ok );
339 }
340 
341 
342 Calamares::JobResult
exec()343 SetKeyboardLayoutJob::exec()
344 {
345     cDebug() << "Executing SetKeyboardLayoutJob";
346     // Read the location of the destination's / in the host file system from
347     // the global settings
348     Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
349     QDir destDir( gs->value( "rootMountPoint" ).toString() );
350 
351     {
352         // Get the path to the destination's /etc/vconsole.conf
353         QString vconsoleConfPath = destDir.absoluteFilePath( "etc/vconsole.conf" );
354 
355         // Get the path to the destination's path to the converted key mappings
356         QString convertedKeymapPath = m_convertedKeymapPath;
357         if ( !convertedKeymapPath.isEmpty() )
358         {
359             while ( convertedKeymapPath.startsWith( '/' ) )
360             {
361                 convertedKeymapPath.remove( 0, 1 );
362             }
363             convertedKeymapPath = destDir.absoluteFilePath( convertedKeymapPath );
364         }
365 
366         if ( !writeVConsoleData( vconsoleConfPath, convertedKeymapPath ) )
367         {
368             return Calamares::JobResult::error( tr( "Failed to write keyboard configuration for the virtual console." ),
369                                                 tr( "Failed to write to %1" ).arg( vconsoleConfPath ) );
370         }
371     }
372 
373     {
374         // Get the path to the destination's /etc/X11/xorg.conf.d/00-keyboard.conf
375         QString xorgConfDPath;
376         QString keyboardConfPath;
377         if ( QDir::isAbsolutePath( m_xOrgConfFileName ) )
378         {
379             keyboardConfPath = m_xOrgConfFileName;
380             while ( keyboardConfPath.startsWith( '/' ) )
381             {
382                 keyboardConfPath.remove( 0, 1 );
383             }
384             keyboardConfPath = destDir.absoluteFilePath( keyboardConfPath );
385             xorgConfDPath = QFileInfo( keyboardConfPath ).path();
386         }
387         else
388         {
389             xorgConfDPath = destDir.absoluteFilePath( "etc/X11/xorg.conf.d" );
390             keyboardConfPath = QDir( xorgConfDPath ).absoluteFilePath( m_xOrgConfFileName );
391         }
392         destDir.mkpath( xorgConfDPath );
393 
394         if ( !writeX11Data( keyboardConfPath ) )
395         {
396             return Calamares::JobResult::error( tr( "Failed to write keyboard configuration for X11." ),
397                                                 tr( "Failed to write to %1" ).arg( keyboardConfPath ) );
398         }
399     }
400 
401     {
402         QString defaultKeyboardPath;
403         if ( QDir( destDir.absoluteFilePath( "etc/default" ) ).exists() )
404         {
405             defaultKeyboardPath = destDir.absoluteFilePath( "etc/default/keyboard" );
406         }
407 
408         if ( !defaultKeyboardPath.isEmpty() && m_writeEtcDefaultKeyboard )
409         {
410             if ( !writeDefaultKeyboardData( defaultKeyboardPath ) )
411             {
412                 return Calamares::JobResult::error(
413                     tr( "Failed to write keyboard configuration to existing /etc/default directory." ),
414                     tr( "Failed to write to %1" ).arg( defaultKeyboardPath ) );
415             }
416         }
417     }
418 
419     return Calamares::JobResult::ok();
420 }
421