1 | /* |
2 | This file is part of the KDE libraries |
3 | Copyright (c) 2005-2010 David Jarvie <djarvie@kde.org> |
4 | Copyright (c) 2005 S.R.Haque <srhaque@iee.org>. |
5 | |
6 | This library is free software; you can redistribute it and/or |
7 | modify it under the terms of the GNU Library General Public |
8 | License as published by the Free Software Foundation; either |
9 | version 2 of the License, or (at your option) any later version. |
10 | |
11 | This library is distributed in the hope that it will be useful, |
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
14 | Library General Public License for more details. |
15 | |
16 | You should have received a copy of the GNU Library General Public License |
17 | along with this library; see the file COPYING.LIB. If not, write to |
18 | the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
19 | Boston, MA 02110-1301, USA. |
20 | */ |
21 | |
22 | #include "ktimezoned.moc" |
23 | #include "ktimezonedbase.moc" |
24 | |
25 | #include <climits> |
26 | #include <cstdlib> |
27 | |
28 | #include <QFile> |
29 | #include <QFileInfo> |
30 | #include <QDir> |
31 | #include <QRegExp> |
32 | #include <QStringList> |
33 | #include <QTextStream> |
34 | #include <QtDBus/QtDBus> |
35 | |
36 | #include <kglobal.h> |
37 | #include <klocale.h> |
38 | #include <kcodecs.h> |
39 | #include <kstandarddirs.h> |
40 | #include <kstringhandler.h> |
41 | #include <ktemporaryfile.h> |
42 | #include <kdebug.h> |
43 | #include <kconfiggroup.h> |
44 | |
45 | #include <kpluginfactory.h> |
46 | #include <kpluginloader.h> |
47 | |
48 | K_PLUGIN_FACTORY(KTimeZonedFactory, |
49 | registerPlugin<KTimeZoned>(); |
50 | ) |
51 | K_EXPORT_PLUGIN(KTimeZonedFactory("ktimezoned" )) |
52 | |
53 | // The maximum allowed length for reading a zone.tab line. This is set to |
54 | // provide plenty of leeway, given that the maximum length of lines in a valid |
55 | // zone.tab will be around 100 - 120 characters. |
56 | const int MAX_ZONE_TAB_LINE_LENGTH = 2000; |
57 | |
58 | // Config file entry names |
59 | const char ZONEINFO_DIR[] = "ZoneinfoDir" ; // path to zoneinfo/ directory |
60 | const char ZONE_TAB[] = "Zonetab" ; // path & name of zone.tab |
61 | const char ZONE_TAB_CACHE[] = "ZonetabCache" ; // type of cached simulated zone.tab |
62 | const char LOCAL_ZONE[] = "LocalZone" ; // name of local time zone |
63 | |
64 | |
65 | KTimeZoned::KTimeZoned(QObject* parent, const QList<QVariant>& l) |
66 | : KTimeZonedBase(parent, l), |
67 | mSource(0), |
68 | mZonetabWatch(0), |
69 | mDirWatch(0) |
70 | { |
71 | init(false); |
72 | } |
73 | |
74 | KTimeZoned::~KTimeZoned() |
75 | { |
76 | delete mSource; |
77 | mSource = 0; |
78 | delete mZonetabWatch; |
79 | mZonetabWatch = 0; |
80 | delete mDirWatch; |
81 | mDirWatch = 0; |
82 | } |
83 | |
84 | void KTimeZoned::init(bool restart) |
85 | { |
86 | if (restart) |
87 | { |
88 | kDebug(1221) << "KTimeZoned::init(restart)" ; |
89 | delete mSource; |
90 | mSource = 0; |
91 | delete mZonetabWatch; |
92 | mZonetabWatch = 0; |
93 | delete mDirWatch; |
94 | mDirWatch = 0; |
95 | } |
96 | |
97 | KConfig config(QLatin1String("ktimezonedrc" )); |
98 | if (restart) |
99 | config.reparseConfiguration(); |
100 | KConfigGroup group(&config, "TimeZones" ); |
101 | mZoneinfoDir = group.readEntry(ZONEINFO_DIR); |
102 | mZoneTab = group.readEntry(ZONE_TAB); |
103 | mConfigLocalZone = group.readEntry(LOCAL_ZONE); |
104 | QString ztc = group.readEntry(ZONE_TAB_CACHE, QString()); |
105 | mZoneTabCache = (ztc == "Solaris" ) ? Solaris : NoCache; |
106 | if (mZoneinfoDir.length() > 1 && mZoneinfoDir.endsWith('/')) |
107 | mZoneinfoDir.truncate(mZoneinfoDir.length() - 1); // strip trailing '/' |
108 | |
109 | // For Unix, read zone.tab. |
110 | |
111 | QString oldZoneinfoDir = mZoneinfoDir; |
112 | QString oldZoneTab = mZoneTab; |
113 | CacheType oldCacheType = mZoneTabCache; |
114 | |
115 | // Open zone.tab if we already know where it is |
116 | QFile f; |
117 | if (!mZoneTab.isEmpty() && !mZoneinfoDir.isEmpty()) |
118 | { |
119 | f.setFileName(mZoneTab); |
120 | if (!f.open(QIODevice::ReadOnly)) |
121 | mZoneTab.clear(); |
122 | else if (mZoneTabCache != NoCache) |
123 | { |
124 | // Check whether the cached zone.tab is still up to date |
125 | #ifdef __GNUC__ |
126 | #warning Implement checking whether Solaris cached zone.tab is up to date |
127 | #endif |
128 | } |
129 | } |
130 | |
131 | if (mZoneTab.isEmpty() || mZoneinfoDir.isEmpty()) |
132 | { |
133 | // Search for zone.tab |
134 | if (!findZoneTab(f)) |
135 | return; |
136 | mZoneTab = f.fileName(); |
137 | |
138 | if (mZoneinfoDir != oldZoneinfoDir |
139 | || mZoneTab != oldZoneTab |
140 | || mZoneTabCache != oldCacheType) |
141 | { |
142 | // Update config file and notify interested applications |
143 | group.writeEntry(ZONEINFO_DIR, mZoneinfoDir); |
144 | group.writeEntry(ZONE_TAB, mZoneTab); |
145 | QString ztc; |
146 | switch (mZoneTabCache) |
147 | { |
148 | case Solaris: ztc = "Solaris" ; break; |
149 | default: break; |
150 | } |
151 | group.writeEntry(ZONE_TAB_CACHE, ztc); |
152 | group.sync(); |
153 | QDBusMessage message = QDBusMessage::createSignal("/Daemon" , "org.kde.KTimeZoned" , "configChanged" ); |
154 | QDBusConnection::sessionBus().send(message); |
155 | } |
156 | } |
157 | |
158 | // Read zone.tab and create a collection of KTimeZone instances |
159 | readZoneTab(f); |
160 | |
161 | mZonetabWatch = new KDirWatch(this); |
162 | mZonetabWatch->addFile(mZoneTab); |
163 | connect(mZonetabWatch, SIGNAL(dirty(const QString&)), SLOT(zonetab_Changed(const QString&))); |
164 | |
165 | // Find the local system time zone and set up file monitors to detect changes |
166 | findLocalZone(); |
167 | } |
168 | |
169 | // Check if the local zone has been updated, and if so, write the new |
170 | // zone to the config file and notify interested parties. |
171 | void KTimeZoned::updateLocalZone() |
172 | { |
173 | if (mConfigLocalZone != mLocalZone) |
174 | { |
175 | KConfig config(QLatin1String("ktimezonedrc" )); |
176 | KConfigGroup group(&config, "TimeZones" ); |
177 | mConfigLocalZone = mLocalZone; |
178 | group.writeEntry(LOCAL_ZONE, mConfigLocalZone); |
179 | group.sync(); |
180 | |
181 | QDBusMessage message = QDBusMessage::createSignal("/Daemon" , "org.kde.KTimeZoned" , "configChanged" ); |
182 | QDBusConnection::sessionBus().send(message); |
183 | } |
184 | } |
185 | |
186 | /* |
187 | * Find the location of the zoneinfo files and store in mZoneinfoDir. |
188 | * Open or if necessary create zone.tab. |
189 | */ |
190 | bool KTimeZoned::findZoneTab(QFile& f) |
191 | { |
192 | #if defined(SOLARIS) || defined(USE_SOLARIS) |
193 | const QString ZONE_TAB_FILE = QLatin1String("/tab/zone_sun.tab" ); |
194 | const QString ZONE_INFO_DIR = QLatin1String("/usr/share/lib/zoneinfo" ); |
195 | #else |
196 | const QString ZONE_TAB_FILE = QLatin1String("/zone.tab" ); |
197 | const QString ZONE_INFO_DIR = QLatin1String("/usr/share/zoneinfo" ); |
198 | #endif |
199 | |
200 | mZoneTabCache = NoCache; |
201 | |
202 | // Find and open zone.tab - it's all easy except knowing where to look. |
203 | // Try the LSB location first. |
204 | QDir dir; |
205 | QString zoneinfoDir = ZONE_INFO_DIR; |
206 | // make a note if the dir exists; whether it contains zone.tab or not |
207 | if (dir.exists(zoneinfoDir)) |
208 | { |
209 | mZoneinfoDir = zoneinfoDir; |
210 | f.setFileName(zoneinfoDir + ZONE_TAB_FILE); |
211 | if (f.open(QIODevice::ReadOnly)) |
212 | return true; |
213 | kDebug(1221) << "Can't open " << f.fileName(); |
214 | } |
215 | |
216 | zoneinfoDir = QLatin1String("/usr/lib/zoneinfo" ); |
217 | if (dir.exists(zoneinfoDir)) |
218 | { |
219 | mZoneinfoDir = zoneinfoDir; |
220 | f.setFileName(zoneinfoDir + ZONE_TAB_FILE); |
221 | if (f.open(QIODevice::ReadOnly)) |
222 | return true; |
223 | kDebug(1221) << "Can't open " << f.fileName(); |
224 | } |
225 | |
226 | zoneinfoDir = ::getenv("TZDIR" ); |
227 | if (!zoneinfoDir.isEmpty() && dir.exists(zoneinfoDir)) |
228 | { |
229 | mZoneinfoDir = zoneinfoDir; |
230 | f.setFileName(zoneinfoDir + ZONE_TAB_FILE); |
231 | if (f.open(QIODevice::ReadOnly)) |
232 | return true; |
233 | kDebug(1221) << "Can't open " << f.fileName(); |
234 | } |
235 | |
236 | zoneinfoDir = QLatin1String("/usr/share/lib/zoneinfo" ); |
237 | if (dir.exists(zoneinfoDir + QLatin1String("/src" ))) |
238 | { |
239 | mZoneinfoDir = zoneinfoDir; |
240 | // Solaris support. Synthesise something that looks like a zone.tab, |
241 | // and cache it between sessions. |
242 | // |
243 | // grep -h ^Zone /usr/share/lib/zoneinfo/src/* | awk '{print "??\t+9999+99999\t" $2}' |
244 | // |
245 | // where the country code is set to "??" and the latitude/longitude |
246 | // values are dummies. |
247 | // |
248 | QDir d(mZoneinfoDir + QLatin1String("/src" )); |
249 | d.setFilter(QDir::Files | QDir::Hidden | QDir::NoSymLinks); |
250 | QStringList fileList = d.entryList(); |
251 | |
252 | mZoneTab = KStandardDirs::locateLocal("cache" , QLatin1String("zone.tab" )); |
253 | f.setFileName(mZoneTab); |
254 | if (!f.open(QIODevice::WriteOnly)) |
255 | { |
256 | kError(1221) << "Could not create zone.tab cache" << endl; |
257 | return false; |
258 | } |
259 | |
260 | QFile zoneFile; |
261 | QList<QByteArray> tokens; |
262 | QByteArray line; |
263 | line.reserve(1024); |
264 | QTextStream tmpStream(&f); |
265 | qint64 r; |
266 | for (int i = 0, end = fileList.count(); i < end; ++i) |
267 | { |
268 | zoneFile.setFileName(d.filePath(fileList[i].toLatin1())); |
269 | if (!zoneFile.open(QIODevice::ReadOnly)) |
270 | { |
271 | kDebug(1221) << "Could not open file '" << zoneFile.fileName().toLatin1() \ |
272 | << "' for reading." << endl; |
273 | continue; |
274 | } |
275 | while (!zoneFile.atEnd()) |
276 | { |
277 | if ((r = zoneFile.readLine(line.data(), 1023)) > 0 |
278 | && line.startsWith("Zone" )) |
279 | { |
280 | line.replace('\t', ' '); // change tabs to spaces |
281 | tokens = line.split(' '); |
282 | for (int j = 0, jend = tokens.count(); j < jend; ++j) |
283 | if (tokens[j].endsWith(' ')) |
284 | tokens[j].chop(1); |
285 | tmpStream << "??\t+9999+99999\t" << tokens[1] << "\n" ; |
286 | } |
287 | } |
288 | zoneFile.close(); |
289 | } |
290 | f.close(); |
291 | if (!f.open(QIODevice::ReadOnly)) |
292 | { |
293 | kError(1221) << "Could not reopen zone.tab cache file for reading." << endl; |
294 | return false; |
295 | } |
296 | mZoneTabCache = Solaris; |
297 | return true; |
298 | } |
299 | return false; |
300 | } |
301 | |
302 | // Parse zone.tab and for each time zone, create a KSystemTimeZone instance. |
303 | // Note that only data needed by this module is specified to KSystemTimeZone. |
304 | void KTimeZoned::readZoneTab(QFile &f) |
305 | { |
306 | // Parse the already open real or fake zone.tab. |
307 | QRegExp lineSeparator("[ \t]" ); |
308 | if (!mSource) |
309 | mSource = new KSystemTimeZoneSource; |
310 | mZones.clear(); |
311 | QTextStream str(&f); |
312 | while (!str.atEnd()) |
313 | { |
314 | // Read the next line, but limit its length to guard against crashing |
315 | // due to a corrupt very large zone.tab (see KDE bug 224868). |
316 | QString line = str.readLine(MAX_ZONE_TAB_LINE_LENGTH); |
317 | if (line.isEmpty() || line[0] == '#') |
318 | continue; |
319 | QStringList tokens = KStringHandler::perlSplit(lineSeparator, line, 4); |
320 | int n = tokens.count(); |
321 | if (n < 3) |
322 | { |
323 | kError(1221) << "readZoneTab(): invalid record: " << line << endl; |
324 | continue; |
325 | } |
326 | |
327 | // Add entry to list. |
328 | if (tokens[0] == "??" ) |
329 | tokens[0] = "" ; |
330 | else if (!tokens[0].isEmpty()) |
331 | mHaveCountryCodes = true; |
332 | mZones.add(KSystemTimeZone(mSource, tokens[2], tokens[0])); |
333 | } |
334 | f.close(); |
335 | } |
336 | |
337 | // Find the local time zone, starting from scratch. |
338 | void KTimeZoned::findLocalZone() |
339 | { |
340 | delete mDirWatch; |
341 | mDirWatch = 0; |
342 | mLocalZone.clear(); |
343 | mLocalIdFile.clear(); |
344 | mLocalIdFile2.clear(); |
345 | mLocalZoneDataFile.clear(); |
346 | |
347 | // SOLUTION 1: DEFINITIVE. |
348 | // First try the simplest solution of checking for well-formed TZ setting. |
349 | const char *envtz = ::getenv("TZ" ); |
350 | if (checkTZ(envtz)) |
351 | { |
352 | mSavedTZ = envtz; |
353 | if (!mLocalZone.isEmpty()) kDebug(1221)<<"TZ: " <<mLocalZone; |
354 | } |
355 | |
356 | if (mLocalZone.isEmpty()) |
357 | { |
358 | // SOLUTION 2: DEFINITIVE. |
359 | // BSD & Linux support: local time zone id in /etc/timezone. |
360 | checkTimezone(); |
361 | } |
362 | if (mLocalZone.isEmpty() && !mZoneinfoDir.isEmpty()) |
363 | { |
364 | // SOLUTION 3: DEFINITIVE. |
365 | // Try to follow any /etc/localtime symlink to a zoneinfo file. |
366 | // SOLUTION 4: DEFINITIVE. |
367 | // Try to match /etc/localtime against the list of zoneinfo files. |
368 | matchZoneFile(QLatin1String("/etc/localtime" )); |
369 | } |
370 | if (mLocalZone.isEmpty()) |
371 | { |
372 | // SOLUTION 5: DEFINITIVE. |
373 | // Look for setting in /etc/rc.conf or /etc/rc.local. |
374 | checkRcFile(); |
375 | } |
376 | if (mLocalZone.isEmpty()) |
377 | { |
378 | // SOLUTION 6: DEFINITIVE. |
379 | // Solaris support using /etc/default/init. |
380 | checkDefaultInit(); |
381 | } |
382 | |
383 | if (mLocalZone.isEmpty()) |
384 | { |
385 | // The local time zone is not defined by a file. |
386 | // Watch for creation of /etc/localtime in case it gets created later. |
387 | // TODO: If under BSD it is possible for /etc/timezone to be missing but |
388 | // created later, we should also watch for its creation. |
389 | mLocalIdFile = QLatin1String("/etc/localtime" ); |
390 | } |
391 | // Watch for changes in the file defining the local time zone so as to be |
392 | // notified of any change in it. |
393 | mDirWatch = new KDirWatch(this); |
394 | mDirWatch->addFile(mLocalIdFile); |
395 | if (!mLocalIdFile2.isEmpty()) |
396 | mDirWatch->addFile(mLocalIdFile2); |
397 | if (!mLocalZoneDataFile.isEmpty()) |
398 | mDirWatch->addFile(mLocalZoneDataFile); |
399 | connect(mDirWatch, SIGNAL(dirty(const QString&)), SLOT(localChanged(const QString&))); |
400 | connect(mDirWatch, SIGNAL(deleted(const QString&)), SLOT(localChanged(const QString&))); |
401 | connect(mDirWatch, SIGNAL(created(const QString&)), SLOT(localChanged(const QString&))); |
402 | |
403 | if (mLocalZone.isEmpty() && !mZoneinfoDir.isEmpty()) |
404 | { |
405 | // SOLUTION 7: HEURISTIC. |
406 | // None of the deterministic stuff above has worked: try a heuristic. We |
407 | // try to find a pair of matching time zone abbreviations...that way, we'll |
408 | // likely return a value in the user's own country. |
409 | tzset(); |
410 | QByteArray tzname0(tzname[0]); // store copies, because zone.parse() will change them |
411 | QByteArray tzname1(tzname[1]); |
412 | int bestOffset = INT_MAX; |
413 | KSystemTimeZoneSource::startParseBlock(); |
414 | const KTimeZones::ZoneMap zmap = mZones.zones(); |
415 | for (KTimeZones::ZoneMap::ConstIterator it = zmap.constBegin(), end = zmap.constEnd(); it != end; ++it) |
416 | { |
417 | KTimeZone zone = it.value(); |
418 | int candidateOffset = qAbs(zone.currentOffset(Qt::LocalTime)); |
419 | if (candidateOffset < bestOffset |
420 | && zone.parse()) |
421 | { |
422 | QList<QByteArray> abbrs = zone.abbreviations(); |
423 | if (abbrs.contains(tzname0) && abbrs.contains(tzname1)) |
424 | { |
425 | // kDebug(1221) << "local=" << zone.name(); |
426 | mLocalZone = zone.name(); |
427 | bestOffset = candidateOffset; |
428 | if (!bestOffset) |
429 | break; |
430 | } |
431 | } |
432 | } |
433 | KSystemTimeZoneSource::endParseBlock(); |
434 | if (!mLocalZone.isEmpty()) |
435 | { |
436 | mLocalMethod = TzName; |
437 | kDebug(1221)<<"tzname: " <<mLocalZone; |
438 | } |
439 | } |
440 | if (mLocalZone.isEmpty()) |
441 | { |
442 | // SOLUTION 8: FAILSAFE. |
443 | mLocalZone = KTimeZone::utc().name(); |
444 | mLocalMethod = Utc; |
445 | if (!mLocalZone.isEmpty()) kDebug(1221)<<"Failsafe: " <<mLocalZone; |
446 | } |
447 | |
448 | // Finally, if the local zone identity has changed, store |
449 | // the new one in the config file. |
450 | updateLocalZone(); |
451 | } |
452 | |
453 | // Called when KDirWatch detects a change in zone.tab |
454 | void KTimeZoned::zonetab_Changed(const QString& path) |
455 | { |
456 | kDebug(1221) << "zone.tab changed" ; |
457 | if (path != mZoneTab) |
458 | { |
459 | kError(1221) << "Wrong path (" << path << ") for zone.tab" ; |
460 | return; |
461 | } |
462 | QDBusMessage message = QDBusMessage::createSignal("/Daemon" , "org.kde.KTimeZoned" , "zonetabChanged" ); |
463 | QList<QVariant> args; |
464 | args += mZoneTab; |
465 | message.setArguments(args); |
466 | QDBusConnection::sessionBus().send(message); |
467 | |
468 | // Reread zone.tab and recreate the collection of KTimeZone instances, |
469 | // in case any zones have been created or deleted and one of them |
470 | // subsequently becomes the local zone. |
471 | QFile f; |
472 | f.setFileName(mZoneTab); |
473 | if (!f.open(QIODevice::ReadOnly)) |
474 | kError(1221) << "Could not open zone.tab (" << mZoneTab << ") to reread" ; |
475 | else |
476 | readZoneTab(f); |
477 | } |
478 | |
479 | // Called when KDirWatch detects a change |
480 | void KTimeZoned::localChanged(const QString& path) |
481 | { |
482 | if (path == mLocalZoneDataFile) |
483 | { |
484 | // Only need to update the definition of the local zone, |
485 | // not its identity. |
486 | QDBusMessage message = QDBusMessage::createSignal("/Daemon" , "org.kde.KTimeZoned" , "zoneDefinitionChanged" ); |
487 | QList<QVariant> args; |
488 | args += mLocalZone; |
489 | message.setArguments(args); |
490 | QDBusConnection::sessionBus().send(message); |
491 | return; |
492 | } |
493 | QString oldDataFile = mLocalZoneDataFile; |
494 | switch (mLocalMethod) |
495 | { |
496 | case EnvTzLink: |
497 | case EnvTzFile: |
498 | { |
499 | const char *envtz = ::getenv("TZ" ); |
500 | if (mSavedTZ != envtz) |
501 | { |
502 | // TZ has changed - start from scratch again |
503 | findLocalZone(); |
504 | return; |
505 | } |
506 | // The contents of the file pointed to by TZ has changed. |
507 | } |
508 | // Fall through to LocaltimeLink |
509 | case LocaltimeLink: |
510 | case LocaltimeCopy: |
511 | // The fallback methods below also set a watch for /etc/localtime in |
512 | // case it gets created. |
513 | case TzName: |
514 | case Utc: |
515 | matchZoneFile(mLocalIdFile); |
516 | break; |
517 | case Timezone: |
518 | checkTimezone(); |
519 | break; |
520 | case RcFile: |
521 | checkRcFile(); |
522 | break; |
523 | case DefaultInit: |
524 | checkDefaultInit(); |
525 | break; |
526 | default: |
527 | return; |
528 | } |
529 | if (oldDataFile != mLocalZoneDataFile) |
530 | { |
531 | if (!oldDataFile.isEmpty()) |
532 | mDirWatch->removeFile(oldDataFile); |
533 | if (!mLocalZoneDataFile.isEmpty()) |
534 | mDirWatch->addFile(mLocalZoneDataFile); |
535 | } |
536 | updateLocalZone(); |
537 | } |
538 | |
539 | bool KTimeZoned::checkTZ(const char *envZone) |
540 | { |
541 | // SOLUTION 1: DEFINITIVE. |
542 | // First try the simplest solution of checking for well-formed TZ setting. |
543 | if (envZone) |
544 | { |
545 | if (envZone[0] == '\0') |
546 | { |
547 | mLocalMethod = EnvTz; |
548 | mLocalZone = KTimeZone::utc().name(); |
549 | mLocalIdFile.clear(); |
550 | mLocalZoneDataFile.clear(); |
551 | return true; |
552 | } |
553 | if (envZone[0] == ':') |
554 | { |
555 | // TZ specifies a file name, either relative to zoneinfo/ or absolute. |
556 | QString TZfile = QFile::decodeName(envZone + 1); |
557 | if (TZfile.startsWith(mZoneinfoDir)) |
558 | { |
559 | // It's an absolute file name in the zoneinfo directory. |
560 | // Convert it to a file name relative to zoneinfo/. |
561 | TZfile = TZfile.mid(mZoneinfoDir.length()); |
562 | } |
563 | if (TZfile.startsWith(QLatin1Char('/'))) |
564 | { |
565 | // It's an absolute file name. |
566 | QString symlink; |
567 | if (matchZoneFile(TZfile)) |
568 | { |
569 | mLocalMethod = static_cast<LocalMethod>(EnvTz | (mLocalMethod & TypeMask)); |
570 | return true; |
571 | } |
572 | } |
573 | else if (!TZfile.isEmpty()) |
574 | { |
575 | // It's a file name relative to zoneinfo/ |
576 | mLocalZone = TZfile; |
577 | if (!mLocalZone.isEmpty()) |
578 | { |
579 | mLocalMethod = EnvTz; |
580 | mLocalZoneDataFile = mZoneinfoDir + '/' + TZfile; |
581 | mLocalIdFile.clear(); |
582 | return true; |
583 | } |
584 | } |
585 | } |
586 | } |
587 | return false; |
588 | } |
589 | |
590 | bool KTimeZoned::checkTimezone() |
591 | { |
592 | // SOLUTION 2: DEFINITIVE. |
593 | // BSD support. |
594 | QFile f; |
595 | f.setFileName(QLatin1String("/etc/timezone" )); |
596 | if (!f.open(QIODevice::ReadOnly)) |
597 | return false; |
598 | // Read the first line of the file. |
599 | QTextStream ts(&f); |
600 | ts.setCodec("ISO-8859-1" ); |
601 | QString zoneName; |
602 | if (!ts.atEnd()) |
603 | zoneName = ts.readLine(); |
604 | f.close(); |
605 | if (zoneName.isEmpty()) |
606 | return false; |
607 | if (!setLocalZone(zoneName)) |
608 | return false; |
609 | mLocalMethod = Timezone; |
610 | mLocalIdFile = f.fileName(); |
611 | kDebug(1221)<<"/etc/timezone: " <<mLocalZone; |
612 | return true; |
613 | } |
614 | |
615 | bool KTimeZoned::matchZoneFile(const QString &path) |
616 | { |
617 | // SOLUTION 3: DEFINITIVE. |
618 | // Try to follow any symlink to a zoneinfo file. |
619 | // Get the path of the file which the symlink points to. |
620 | QFile f; |
621 | f.setFileName(path); |
622 | QFileInfo fi(f); |
623 | if (fi.isSymLink()) |
624 | { |
625 | // The file is a symlink. |
626 | QString zoneInfoFileName = fi.canonicalFilePath(); |
627 | QFileInfo fiz(zoneInfoFileName); |
628 | if (fiz.exists() && fiz.isReadable()) |
629 | { |
630 | if (zoneInfoFileName.startsWith(mZoneinfoDir)) |
631 | { |
632 | // We've got the zoneinfo file path. |
633 | // The time zone name is the part of the path after the zoneinfo directory. |
634 | // Note that some systems (e.g. Gentoo) have zones under zoneinfo which |
635 | // are not in zone.tab, so don't validate against mZones. |
636 | mLocalZone = zoneInfoFileName.mid(mZoneinfoDir.length() + 1); |
637 | // kDebug(1221) << "local=" << mLocalZone; |
638 | } |
639 | else |
640 | { |
641 | // It isn't a zoneinfo file or a copy thereof. |
642 | // Use the absolute path as the time zone name. |
643 | mLocalZone = f.fileName(); |
644 | } |
645 | mLocalMethod = LocaltimeLink; |
646 | mLocalIdFile = f.fileName(); |
647 | mLocalZoneDataFile = zoneInfoFileName; |
648 | kDebug(1221)<<mLocalIdFile<<": " <<mLocalZone; |
649 | return true; |
650 | } |
651 | } |
652 | else if (f.open(QIODevice::ReadOnly)) |
653 | { |
654 | // SOLUTION 4: DEFINITIVE. |
655 | // Try to match the file against the list of zoneinfo files. |
656 | |
657 | // Compute the file's MD5 sum. |
658 | KMD5 context("" ); |
659 | context.reset(); |
660 | context.update(f); |
661 | qlonglong referenceSize = f.size(); |
662 | QString referenceMd5Sum = context.hexDigest(); |
663 | MD5Map::ConstIterator it5, end5; |
664 | KTimeZone local; |
665 | QString zoneName; |
666 | |
667 | if (!mConfigLocalZone.isEmpty()) |
668 | { |
669 | // We know the local zone from last time. |
670 | // Check whether the file still matches it. |
671 | KTimeZone tzone = mZones.zone(mConfigLocalZone); |
672 | if (tzone.isValid()) |
673 | { |
674 | local = compareChecksum(tzone, referenceMd5Sum, referenceSize); |
675 | if (local.isValid()) |
676 | zoneName = local.name(); |
677 | } |
678 | } |
679 | |
680 | if (!local.isValid() && mHaveCountryCodes) |
681 | { |
682 | /* Look for time zones with the user's country code. |
683 | * This has two advantages: 1) it shortens the search; |
684 | * 2) it increases the chance of the correctly titled time zone |
685 | * being found, since multiple time zones can have identical |
686 | * definitions. For example, Europe/Guernsey is identical to |
687 | * Europe/London, but the latter is more likely to be the right |
688 | * zone name for a user with 'gb' country code. |
689 | */ |
690 | QString country = KGlobal::locale()->country().toUpper(); |
691 | const KTimeZones::ZoneMap zmap = mZones.zones(); |
692 | for (KTimeZones::ZoneMap::ConstIterator zit = zmap.constBegin(), zend = zmap.constEnd(); zit != zend; ++zit) |
693 | { |
694 | KTimeZone tzone = zit.value(); |
695 | if (tzone.countryCode() == country) |
696 | { |
697 | local = compareChecksum(tzone, referenceMd5Sum, referenceSize); |
698 | if (local.isValid()) |
699 | { |
700 | zoneName = local.name(); |
701 | break; |
702 | } |
703 | } |
704 | } |
705 | } |
706 | |
707 | if (!local.isValid()) |
708 | { |
709 | // Look for a checksum match with the cached checksum values |
710 | MD5Map oldChecksums = mMd5Sums; // save a copy of the existing checksums |
711 | for (it5 = mMd5Sums.constBegin(), end5 = mMd5Sums.constEnd(); it5 != end5; ++it5) |
712 | { |
713 | if (it5.value() == referenceMd5Sum) |
714 | { |
715 | // The cached checksum matches. Ensure that the file hasn't changed. |
716 | if (compareChecksum(it5, referenceMd5Sum, referenceSize)) |
717 | { |
718 | zoneName = it5.key(); |
719 | local = mZones.zone(zoneName); |
720 | if (local.isValid()) |
721 | break; |
722 | } |
723 | oldChecksums.clear(); // the cache has been cleared |
724 | break; |
725 | } |
726 | } |
727 | |
728 | if (!local.isValid()) |
729 | { |
730 | // The checksum didn't match any in the cache. |
731 | // Continue building missing entries in the cache on the assumption that |
732 | // we haven't previously looked at the zoneinfo file which matches. |
733 | const KTimeZones::ZoneMap zmap = mZones.zones(); |
734 | for (KTimeZones::ZoneMap::ConstIterator zit = zmap.constBegin(), zend = zmap.constEnd(); zit != zend; ++zit) |
735 | { |
736 | KTimeZone zone = zit.value(); |
737 | zoneName = zone.name(); |
738 | if (!mMd5Sums.contains(zoneName)) |
739 | { |
740 | QString candidateMd5Sum = calcChecksum(zoneName, referenceSize); |
741 | if (candidateMd5Sum == referenceMd5Sum) |
742 | { |
743 | // kDebug(1221) << "local=" << zone.name(); |
744 | local = zone; |
745 | break; |
746 | } |
747 | } |
748 | } |
749 | } |
750 | |
751 | if (!local.isValid()) |
752 | { |
753 | // Didn't find the file, so presumably a previously cached checksum must |
754 | // have changed. Delete all the old checksums. |
755 | MD5Map::ConstIterator mit; |
756 | MD5Map::ConstIterator mend = oldChecksums.constEnd(); |
757 | for (mit = oldChecksums.constBegin(); mit != mend; ++mit) |
758 | mMd5Sums.remove(mit.key()); |
759 | |
760 | // And recalculate the old checksums |
761 | for (mit = oldChecksums.constBegin(); mit != mend; ++mit) |
762 | { |
763 | zoneName = mit.key(); |
764 | QString candidateMd5Sum = calcChecksum(zoneName, referenceSize); |
765 | if (candidateMd5Sum == referenceMd5Sum) |
766 | { |
767 | // kDebug(1221) << "local=" << zoneName; |
768 | local = mZones.zone(zoneName); |
769 | break; |
770 | } |
771 | } |
772 | } |
773 | } |
774 | bool success = false; |
775 | if (local.isValid()) |
776 | { |
777 | // The file matches a zoneinfo file |
778 | mLocalZone = zoneName; |
779 | mLocalZoneDataFile = mZoneinfoDir + '/' + zoneName; |
780 | success = true; |
781 | } |
782 | else |
783 | { |
784 | // The file doesn't match a zoneinfo file. If it's a TZfile, use it directly. |
785 | // Read the file type identifier. |
786 | char buff[4]; |
787 | f.reset(); |
788 | QDataStream str(&f); |
789 | if (str.readRawData(buff, 4) == 4 |
790 | && buff[0] == 'T' && buff[1] == 'Z' && buff[2] == 'i' && buff[3] == 'f') |
791 | { |
792 | // Use its absolute path as the zone name. |
793 | mLocalZone = f.fileName(); |
794 | mLocalZoneDataFile.clear(); |
795 | success = true; |
796 | } |
797 | } |
798 | f.close(); |
799 | if (success) |
800 | { |
801 | mLocalMethod = LocaltimeCopy; |
802 | mLocalIdFile = f.fileName(); |
803 | kDebug(1221)<<mLocalIdFile<<": " <<mLocalZone; |
804 | return true; |
805 | } |
806 | } |
807 | return false; |
808 | } |
809 | |
810 | bool KTimeZoned::checkRcFile() |
811 | { |
812 | // SOLUTION 5: DEFINITIVE. |
813 | // Look for setting in /etc/rc.conf or /etc/rc.local, |
814 | // with priority to /etc/rc.local. |
815 | if (findKey(QLatin1String("/etc/rc.local" ), "TIMEZONE" )) |
816 | { |
817 | mLocalIdFile2.clear(); |
818 | kDebug(1221)<<"/etc/rc.local: " <<mLocalZone; |
819 | } |
820 | else |
821 | { |
822 | if (!findKey(QLatin1String("/etc/rc.conf" ), "TIMEZONE" )) |
823 | return false; |
824 | mLocalIdFile2 = mLocalIdFile; |
825 | mLocalIdFile = QLatin1String("/etc/rc.local" ); |
826 | kDebug(1221)<<"/etc/rc.conf: " <<mLocalZone; |
827 | } |
828 | mLocalMethod = RcFile; |
829 | return true; |
830 | } |
831 | |
832 | bool KTimeZoned::checkDefaultInit() |
833 | { |
834 | // SOLUTION 6: DEFINITIVE. |
835 | // Solaris support using /etc/default/init. |
836 | if (!findKey(QLatin1String("/etc/default/init" ), "TZ" )) |
837 | return false; |
838 | mLocalMethod = DefaultInit; |
839 | kDebug(1221)<<"/etc/default/init: " <<mLocalZone; |
840 | return true; |
841 | } |
842 | |
843 | bool KTimeZoned::findKey(const QString &path, const QString &key) |
844 | { |
845 | QFile f; |
846 | f.setFileName(path); |
847 | if (!f.open(QIODevice::ReadOnly)) |
848 | return false; |
849 | QString line; |
850 | QString zoneName; |
851 | QRegExp keyexp('^' + key + "\\s*=\\s*" ); |
852 | QTextStream ts(&f); |
853 | ts.setCodec("ISO-8859-1" ); |
854 | while (!ts.atEnd()) |
855 | { |
856 | line = ts.readLine(); |
857 | if (keyexp.indexIn(line) == 0) |
858 | { |
859 | zoneName = line.mid(keyexp.matchedLength()); |
860 | break; |
861 | } |
862 | } |
863 | f.close(); |
864 | if (zoneName.isEmpty()) |
865 | return false; |
866 | if (!setLocalZone(zoneName)) |
867 | return false; |
868 | kDebug(1221) << "Key:" << key << "->" << zoneName; |
869 | mLocalIdFile = f.fileName(); |
870 | return true; |
871 | } |
872 | |
873 | // Check whether the zone name is valid, either as a zone in zone.tab or |
874 | // as another file in the zoneinfo directory. |
875 | // If valid, set the local zone information. |
876 | bool KTimeZoned::setLocalZone(const QString &zoneName) |
877 | { |
878 | KTimeZone local = mZones.zone(zoneName); |
879 | if (!local.isValid()) |
880 | { |
881 | // It isn't a recognised zone in zone.tab. |
882 | // Note that some systems (e.g. Gentoo) have zones under zoneinfo which |
883 | // are not in zone.tab, so check if it points to another zone file. |
884 | if (mZoneinfoDir.isEmpty()) |
885 | return false; |
886 | QString path = mZoneinfoDir + '/' + zoneName; |
887 | QFile qf; |
888 | qf.setFileName(path); |
889 | QFileInfo fi(qf); |
890 | if (fi.isSymLink()) |
891 | fi.setFile(fi.canonicalFilePath()); |
892 | if (!fi.exists() || !fi.isReadable()) |
893 | return false; |
894 | } |
895 | mLocalZone = zoneName; |
896 | mLocalZoneDataFile = mZoneinfoDir.isEmpty() ? QString() : mZoneinfoDir + '/' + zoneName; |
897 | return true; |
898 | } |
899 | |
900 | // Check whether the checksum for a time zone matches a given saved checksum. |
901 | KTimeZone KTimeZoned::compareChecksum(const KTimeZone &zone, const QString &referenceMd5Sum, qlonglong size) |
902 | { |
903 | MD5Map::ConstIterator it5 = mMd5Sums.constFind(zone.name()); |
904 | if (it5 == mMd5Sums.constEnd()) |
905 | { |
906 | // No checksum has been computed yet for this zone file. |
907 | // Compute it now. |
908 | QString candidateMd5Sum = calcChecksum(zone.name(), size); |
909 | if (candidateMd5Sum == referenceMd5Sum) |
910 | { |
911 | // kDebug(1221) << "local=" << zone.name(); |
912 | return zone; |
913 | } |
914 | return KTimeZone(); |
915 | } |
916 | if (it5.value() == referenceMd5Sum) |
917 | { |
918 | // The cached checksum matches. Ensure that the file hasn't changed. |
919 | if (compareChecksum(it5, referenceMd5Sum, size)) |
920 | return mZones.zone(it5.key()); |
921 | } |
922 | return KTimeZone(); |
923 | } |
924 | |
925 | // Check whether a checksum matches a given saved checksum. |
926 | // Returns false if the file no longer matches and cache was cleared. |
927 | bool KTimeZoned::compareChecksum(MD5Map::ConstIterator it5, const QString &referenceMd5Sum, qlonglong size) |
928 | { |
929 | // The cached checksum matches. Ensure that the file hasn't changed. |
930 | QString zoneName = it5.key(); |
931 | QString candidateMd5Sum = calcChecksum(zoneName, size); |
932 | if (candidateMd5Sum.isNull()) |
933 | mMd5Sums.remove(zoneName); // no match - wrong file size |
934 | else if (candidateMd5Sum == referenceMd5Sum) |
935 | return true; |
936 | |
937 | // File(s) have changed, so clear the cache |
938 | mMd5Sums.clear(); |
939 | mMd5Sums[zoneName] = candidateMd5Sum; // reinsert the newly calculated checksum |
940 | return false; |
941 | } |
942 | |
943 | // Calculate the MD5 checksum for the given zone file, provided that its size matches. |
944 | // The calculated checksum is cached. |
945 | QString KTimeZoned::calcChecksum(const QString &zoneName, qlonglong size) |
946 | { |
947 | QString path = mZoneinfoDir + '/' + zoneName; |
948 | QFileInfo fi(path); |
949 | if (static_cast<qlonglong>(fi.size()) == size) |
950 | { |
951 | // Only do the heavy lifting for file sizes which match. |
952 | QFile f; |
953 | f.setFileName(path); |
954 | if (f.open(QIODevice::ReadOnly)) |
955 | { |
956 | KMD5 context("" ); |
957 | context.reset(); |
958 | context.update(f); |
959 | QString candidateMd5Sum = context.hexDigest(); |
960 | f.close(); |
961 | mMd5Sums[zoneName] = candidateMd5Sum; // cache the new checksum |
962 | return candidateMd5Sum; |
963 | } |
964 | } |
965 | return QString(); |
966 | } |
967 | |