OSDN Git Service

Use `record' instead of `enroll'
[kita/kita.git] / src / libkita / boarddatabase.cpp
1 /***************************************************************************
2 *   Copyright (C) 2010 by Kita Developers                                 *
3 *   ikemo@users.sourceforge.jp                                            *
4 *                                                                         *
5 *   This program is free software; you can redistribute it and/or modify  *
6 *   it under the terms of the GNU General Public License as published by  *
7 *   the Free Software Foundation; either version 2 of the License, or     *
8 *   (at your option) any later version.                                   *
9 ***************************************************************************/
10 #include "boarddatabase.h"
11
12 #include <QtCore/QDateTime>
13 #include <QtCore/QDir>
14 #include <QtCore/QTextCodec>
15 #include <QtCore/QTextStream>
16
17 #include <kfilterdev.h>
18 #include <kio/netaccess.h>
19 #include <kio/slaveconfig.h>
20
21 #include "cache.h"
22 #include "config.h"
23 #include "favoriteboards.h"
24 #include "favoritethreads.h"
25 #include "thread.h"
26 #include "threadindex.h"
27 #include "threadinfo.h"
28
29 using namespace Kita;
30
31 BoardDataList BoardDatabase::m_boardDataList;
32 QTextCodec* BoardDatabase::m_cp932Codec = 0;
33 QTextCodec* BoardDatabase::m_eucJpCodec = 0;
34 BoardData* BoardDatabase::m_previousBoardData = 0;
35 QString BoardDatabase::m_previousBoardUrl;
36
37 BoardDatabase::BoardDatabase(const KUrl& url) : m_url(url)
38 {
39 }
40
41 void BoardDatabase::setUrl(const KUrl& url)
42 {
43     m_url = url;
44 }
45
46 /* (hostname)/(rootPath)/(bbsPath)/ */ /* public */
47 QString BoardDatabase::boardUrl()
48 {
49     BoardData * bdata = getBoardData();
50     return (bdata == 0) ? QString() : bdata->basePath();
51 }
52
53 /* public */
54 QStringList BoardDatabase::allBoardUrlList()
55 {
56     QStringList urlList;
57
58     BoardData *data;
59     foreach (data, m_boardDataList)
60         urlList += data->basePath();
61     return urlList;
62 }
63
64 /* (hostname)/(rootPath) */ /* public */
65 QString BoardDatabase::boardRoot()
66 {
67     BoardData * bdata = getBoardData();
68     return (bdata == 0) ? QString() : bdata->hostName() + bdata->rootPath();
69 }
70
71 /* (bbspath) */ /* public */
72 QString BoardDatabase::boardPath()
73 {
74     BoardData * bdata = getBoardData();
75     return (bdata == 0) ? QString() : bdata->bbsPath();
76 }
77
78 /* (ext) */ /* public */
79 QString BoardDatabase::ext()
80 {
81     BoardData * bdata = getBoardData();
82     return (bdata == 0) ? QString() : bdata->ext();
83 }
84
85 /* ID of board for writing */ /* public */
86 QString BoardDatabase::boardId()
87 {
88     BoardData * bdata = getBoardData();
89     return (bdata == 0) ? QString() : bdata->bbsPath().mid(1); /* remove "/" */
90 }
91
92 /* (hostname)/(rootPath)/(bbsPath)/subject.txt */ /* public */
93 QString BoardDatabase::subjectUrl()
94 {
95     BoardData * bdata = getBoardData();
96     return (bdata == 0) ? QString() : bdata->basePath() + "subject.txt";
97 }
98
99 /* public */
100 QString BoardDatabase::boardName()
101 {
102     BoardData * bdata = getBoardData();
103     return (bdata == 0) ? QString() : bdata->boardName();
104 }
105
106 /* public */
107 int BoardDatabase::type()
108 {
109     BoardData * bdata = getBoardData();
110     return (bdata == 0) ? Board_Unknown : bdata->type();
111 }
112
113 /*---------------------------*/
114 /* ThreadList */
115
116
117 /*  get list of pointers of Thread classes.     */
118 /*
119   Input:
120  
121   oldLogs: If true, search cache and get list of pointer of old threads.
122   online: online or offline mode.
123  
124   Output:
125  
126   threadList: list of pointers of Thread classes.
127   oldLogList: list of pointers of old threads.
128  
129                                                  */ /* public */
130 void BoardDatabase::getThreadList(
131
132     /* input */
133     bool oldLogs,
134     bool online,
135
136     /* output */
137     QList<Thread*>& threadList,
138     QList<Thread*>& oldLogList)
139 {
140     threadList.clear();
141     oldLogList.clear();
142
143     /* get all obtained threads list from cache */
144     if (m_url.prettyUrl() == "http://virtual/obtained/") {
145
146         QStringList bbslist = allBoardUrlList();
147
148         /* search all cache dirs */
149         QString thread;
150         foreach (thread, bbslist) {
151             getCachedThreadList(thread, threadList);
152         }
153
154         return ;
155     }
156
157     /*-------------------------*/
158
159     BoardData* bdata = getBoardData();
160     if (bdata == 0) return ;
161
162     /* download subject.txt */
163     if (online) {
164
165         /* make directory */
166         Cache cache(m_url);
167         QString cacheDir = cache.getDirPath();
168         if (!QDir::root().mkpath(cacheDir)) return;
169
170         KIO::SlaveConfig::self() ->setConfigData("http",
171                 m_url.host() ,
172                 "UserAgent",
173                 QString("Monazilla/1.00 (Kita/%1)").arg(VERSION));
174         QString subjectPath = cache.getSubjectPath();
175         KIO::NetAccess::download(subjectUrl(), subjectPath, 0);
176     }
177
178     /* open and read subject.txt */
179     readSubjectTxt(bdata, threadList);
180
181     /* get old logs */
182     if (oldLogs) {
183
184         QList<Thread*> tmpList;
185         tmpList.clear();
186         getCachedThreadList(m_url, tmpList);
187
188         for (int i = 0; i < tmpList.count(); i++) {
189             if (threadList.contains(tmpList.at(i)) == 0)
190                 oldLogList.append(tmpList.at(i));
191         }
192     }
193 }
194
195 /* read the cache dir & get the list of all threads. */ /* private */
196 void BoardDatabase::getCachedThreadList(const KUrl& url,
197     QList<Thread*>& threadList)
198 {
199     Cache cache(url);
200     QString cacheDir = cache.getDirPath();
201     QDir d(cacheDir);
202     if (d.exists()) {
203
204         /* get all file names */
205         QString ext = getBoardData(url)->ext();
206         QString boardUrl = getBoardData(url)->basePath();
207         QStringList filter('*' + ext);
208         QStringList flist = d.entryList(filter);
209         QString file;
210         foreach (file, flist) {
211             if (file.isEmpty()) continue;
212
213             QString datUrl = boardUrl + "dat/" + file;
214
215             /* read idx file */
216             Thread* thread = Thread::getByUrlNew(datUrl);
217             if (thread == 0) {
218
219                 thread = Thread::getByUrl(datUrl);
220                 if (thread == 0)
221                     continue;
222                 ThreadIndex threadIndex(datUrl);
223                 threadIndex.loadIndex(thread, false);
224             }
225
226             if (thread != 0)
227                 threadList.append(thread);
228         }
229     }
230 }
231
232 /* open subject.txt and get list of Thread classes */ /* private */
233 bool BoardDatabase::readSubjectTxt(BoardData* bdata, QList<Thread*>& threadList)
234 {
235     /* get all names of cached files to read idx.  */
236     QStringList cacheList;
237     if (!bdata->readIdx()) {
238         Cache cache(m_url);
239         QString cacheDir = cache.getDirPath();
240         QDir d(cacheDir);
241         if (d.exists()) {
242             QString ext = getBoardData()->ext();
243             QStringList filter('*' + ext);
244             cacheList = d.entryList(filter);
245         }
246     }
247
248     /* open subject.txt */
249     Cache cache(m_url);
250     QString subjectPath = cache.getSubjectPath();
251     QIODevice * device = KFilterDev::deviceForFile(subjectPath, "application/x-gzip");
252     if (!device->open(QIODevice::ReadOnly))
253         return false;
254
255     QTextStream stream(device);
256
257     if (type() == Board_JBBS) {
258         if (!m_eucJpCodec) m_eucJpCodec = QTextCodec::codecForName("eucJP");
259         stream.setCodec(m_eucJpCodec);
260     } else {
261         if (!m_cp932Codec) m_cp932Codec = QTextCodec::codecForName("Shift-JIS");
262         stream.setCodec(m_cp932Codec);
263     }
264
265     QRegExp regexp;
266     switch (type()) {
267
268     case Board_MachiBBS:
269     case Board_JBBS:
270         regexp.setPattern("(\\d+\\.cgi),(.*)\\((\\d+)\\)");
271         break;
272
273     default:
274         regexp.setPattern("(\\d+\\.dat)<>(.*)\\((\\d+)\\)");
275         break;
276     }
277     QString line;
278
279     while (!(line = stream.readLine()).isEmpty()) {
280         int pos = regexp.indexIn(line);
281         if (pos != -1) {
282             QString fname = regexp.cap(1);
283             QString subject = regexp.cap(2);
284             QString num = regexp.cap(3);
285
286             /* get pointer of Thread class */
287             QString datUrl = boardUrl() + "dat/" + fname;
288             Thread* thread = Thread::getByUrl(datUrl);
289             ThreadIndex threadIndex(datUrl);
290             if (threadList.indexOf(thread) == -1) {
291                 threadList.append(thread);
292             }
293
294             /* set thread name */
295             thread->setThreadName(subject);
296
297             /* load index file */
298             if (!bdata->readIdx()) {
299
300                 if (cacheList.contains(fname)) {
301                     threadIndex.loadIndex(thread, false);
302                 }
303             }
304
305             /* update res num */
306             int newNum = num.toInt();
307             if (thread->readNum()) { /* cache exists */
308                 int oldNum = thread->resNum();
309
310                 if (newNum > oldNum) {
311                     threadIndex.setResNum(newNum);
312                 }
313             }
314             thread->setResNum(newNum);
315         }
316     }
317
318     device->close();
319     bdata->setReadIdx(true); /* never read idx files again */
320
321     return true;
322 }
323
324 /*---------------------------*/
325 /* BoardData */
326
327 /* reset all BoardData */ /* public */
328 void BoardDatabase::clearBoardData()
329 {
330     BoardData *data;
331     foreach (data, m_boardDataList)
332         delete data;
333
334     m_boardDataList.clear();
335     m_previousBoardData = 0;
336     m_previousBoardUrl.clear();
337 }
338
339 /**
340  *
341  * @param[in] board
342  * @param[in] boardName
343  * @param[in] type
344  * @param[in] test
345  *
346  * @param[out] oldURL
347  *
348  * @retval Board_recordRecorded if board is already recorded. oldURL is QString().
349  * @retval Board_recordNew if board is new board. oldURL is QString().
350  * @retval Board_recordMoved if board is moved. oldURL is old URL.
351  *
352  * @note board is NOT recorded when board is moved.
353  * To record new URL, call BoardDatabase::moveBoard(). 
354  *
355  * "int type" is type of board. It could be "Board_Unknown". See also parseBoardURL().
356  * 
357  * If "bool test" is true, this function just checks if the board is recorded (never record board).
358  *
359  */ 
360 /* public */
361 int BoardDatabase::recordBoard(const QString& boardName, QString& oldUrl, int type, bool test)
362 {
363     QString hostname;
364     QString rootPath;
365     QString delimiter;
366     QString bbsPath;
367     QString ext;
368     type = parseBoardUrl(type, hostname, rootPath, delimiter, bbsPath, ext);
369     oldUrl.clear();
370
371     if (type == Board_Unknown) return Board_recordFailed;
372
373     /* check if the board is recorded or moved. */
374     BoardData *data;
375     foreach (data, m_boardDataList) {
376
377         if (data->boardName() == boardName
378                 && data->type() == type
379                 && data->bbsPath() == bbsPath) {
380
381             if (data->hostName() == hostname
382                     && data->rootPath() == rootPath) { /* recorded */
383                 return Board_recordRecorded;
384             } else { /* moved */
385                 oldUrl = data->basePath();
386                 return Board_recordMoved;
387             }
388         }
389     }
390
391     /* test only */
392     if (test)
393         return Board_recordNew;
394
395     /* record new board */
396     BoardData* bdata = new BoardData(boardName, hostname, rootPath, delimiter, bbsPath, ext, type);
397     m_boardDataList.append(bdata);
398
399     return Board_recordNew;
400 }
401
402 /* parse board URL      */
403 /* return board type.   */ /* private */
404 int BoardDatabase::parseBoardUrl(
405
406     /* input */
407     int type,   /* If type = Board_Unknown, type will be decided according to url. */
408
409     /* output */
410     QString& hostname,
411     QString& rootPath,
412     QString& delimiter,
413     QString& bbsPath,
414     QString& ext)
415 {
416     hostname = m_url.protocol() + "://" + m_url.host();
417     rootPath.clear();
418     delimiter.clear();
419     bbsPath.clear();
420     ext.clear();
421
422     /* decide type */
423     if (type == Board_Unknown) {
424
425         if (m_url.host().contains("machi.to"))
426             type = Board_MachiBBS;
427         else if (m_url.host().contains("jbbs.livedoor.jp"))
428             type = Board_JBBS;
429         else
430             type = Board_2ch;
431     }
432
433     /* parse */
434     switch (type) {
435
436     case Board_MachiBBS:     /* MACHI : http:// *.machi.to/(bbsPath)/ */
437
438         delimiter = "/bbs/read.pl";
439         bbsPath = m_url.fileName();
440         ext = ".cgi";
441         break;
442
443     case Board_JBBS:   /* JBBS : http://jbbs.livedoor.jp/(bbsPath)/ */
444
445         delimiter = "/bbs/read.cgi";
446         bbsPath = m_url.prettyUrl().remove(hostname);
447         type = Board_JBBS;
448         ext = ".cgi";
449         break;
450
451     case Board_FlashCGI:  /* test for Flash CGI/Mini Thread  */
452
453         delimiter = "/test/read.cgi";
454         bbsPath = m_url.fileName();
455         rootPath = m_url.prettyUrl().remove(hostname + '/').remove(bbsPath + '/');
456         if (rootPath.length() == 0)
457             rootPath.clear();
458         ext = ".dat";
459         break;
460
461     default:   /* 2ch : http://(hostname)/(rootPath)/(bbsPath)/ */
462
463         delimiter = "/test/read.cgi";
464         bbsPath = m_url.fileName();
465         rootPath = m_url.prettyUrl().remove(hostname + '/').remove(bbsPath + '/');
466         if (rootPath.length() == 0)
467             rootPath.clear();
468         ext = ".dat";
469         type = Board_2ch;
470         break;
471     }
472
473     /* For example, if bbsPath = "linux/", then m_bbsPath = "/linux" */
474     const QRegExp exp("/$");
475     rootPath.remove(exp);
476     bbsPath.remove(exp);
477     if (!rootPath.isEmpty() && rootPath.at(0) != '/')
478         rootPath = '/' + rootPath;
479     if (!bbsPath.isEmpty() && bbsPath.at(0) != '/')
480         bbsPath = '/' + bbsPath;
481
482     return type;
483 }
484
485 /* public */
486 bool BoardDatabase::isRecorded()
487 {
488     return getBoardData() != 0;
489 }
490
491 /* private */
492 BoardData* BoardDatabase::getBoardData(const KUrl& url)
493 {
494     if (url.isEmpty())
495         return 0;
496     QString urlstr = url.prettyUrl();
497
498     /* cache */
499     if (m_previousBoardData != 0 && m_previousBoardUrl == urlstr)
500         return m_previousBoardData;
501
502     BoardData *data;
503     foreach (data, m_boardDataList) {
504
505         int count = data->keyBasePathList().count();
506         for (int i = 0; i < count ; ++i) {
507             if (urlstr.contains(data->keyBasePathList()[i])
508                     || urlstr.contains(data->keyCgiBasePathList()[i])) {
509
510                 /* cache */
511                 m_previousBoardData = data;
512                 m_previousBoardUrl = urlstr;
513
514                 return data;
515             }
516         }
517     }
518     return 0;
519 }
520
521 /* public */
522 BoardData* BoardDatabase::getBoardData()
523 {
524     return getBoardData(m_url);
525 }
526
527 /*--------------------------------*/
528 /* BBSHISTORY */
529
530
531 /* load the bbs history file (BBSHISTORY), and create keys of Data Base.  */
532 /* Before calling this, record the board by recordBoard().                  */
533 /*
534     ex) If the host of board moved like :
535  
536     http:://aaa.com -> http://bbb.com -> http://ccc.com -> http://ddd.com
537
538     then, BBSHISTORY is
539  
540     http://ccc.com
541     http://bbb.com
542     http://aaa.com
543  
544 */ /* public */
545 bool BoardDatabase::loadBBSHistory()
546 {
547     BoardData * bdata = getBoardData();
548     if (bdata == 0)
549         return false;
550
551     QStringList keyHosts(bdata->hostName());
552
553     Cache cache(m_url);
554     QFile file(cache.getBBSHistoryPath());
555     if (file.open(QIODevice::ReadOnly)) {
556
557         QTextStream ts(&file);
558
559         QString line;
560         while (!ts.atEnd()) {
561
562             line = ts.readLine();
563             keyHosts += line;
564         }
565
566         bdata->createKeys(keyHosts);
567         file.close();
568
569         return true;
570     }
571
572     return false;
573 }
574
575 /* public */
576 bool BoardDatabase::moveBoard(const KUrl& fromUrl, const KUrl& toUrl)
577 {
578     QString oldhost = fromUrl.protocol() + "://" + fromUrl.host();
579     QString newhost = toUrl.protocol() + "://" + toUrl.host();
580
581     const QRegExp exp("/$");
582     QString oldUrl = fromUrl.prettyUrl();
583     QString newUrl = toUrl.prettyUrl();
584     oldUrl.remove(exp);
585     newUrl.remove(exp);
586     oldUrl += '/';
587     newUrl += '/';
588
589     if (oldUrl == newUrl) return false;
590
591     /* Is oldURL recorded? */
592     BoardData* bdata = getBoardData(oldUrl);
593     if (bdata == 0) {
594
595         /* Is newURL recorded? */
596         bdata = getBoardData(newUrl);
597         if (bdata == 0) return false;
598     }
599
600
601     /*---------------------------*/
602     /* update BoardData */
603
604     /* get the path of old cache */
605     bdata->setHostName(oldhost);
606     QStringList keyHosts = bdata->keyHostList();
607     keyHosts.removeOne(oldhost);
608     keyHosts.prepend(oldhost);
609     bdata->createKeys(keyHosts);
610     Cache cache(bdata->basePath());
611     QString oldCachePath = cache.getDirPath();
612
613     /* update URL */
614     bdata->setHostName(newhost);
615
616     /* update keys */
617     /* The order of keyHosts will be like this:
618        
619       newhost      
620       oldhost      
621       foohost1
622       foohost2
623       
624     */
625     keyHosts = bdata->keyHostList();
626     keyHosts.removeOne(oldhost);
627     keyHosts.prepend(oldhost);
628     keyHosts.removeOne(newhost);
629     keyHosts.prepend(newhost);
630     bdata->createKeys(keyHosts);
631
632     /* reset BoardData */
633     bdata->setReadIdx(false);
634     bdata->setSettingLoaded(false);
635
636
637     /*---------------------------*/
638     /* move cache dir */
639
640     QDir qdir;
641     if (! qdir.exists(oldCachePath)) return true;
642
643     /* mkdir new server dir */
644     Cache newCache(bdata->basePath());
645     QString newCachePath = Cache::baseDir() + newCache.serverDir();
646     QDir::root().mkpath(newCachePath);
647
648     /* backup old dir */
649     newCachePath += newCache.boardDir();
650     if (qdir.exists (newCachePath)) {
651         QString bkupPath = newCachePath;
652         bkupPath.truncate(bkupPath.length() - 1); /* remove '/' */
653         bkupPath +=
654             '.' + QString::number(QDateTime::currentDateTime().toTime_t());
655         qdir.rename(newCachePath, bkupPath);
656     }
657
658     /* move cache dir */
659     if (qdir.exists(oldCachePath)) {
660         qdir.rename(oldCachePath, newCachePath);
661     } else
662         QDir::root().mkpath(newCachePath);
663
664     /* make old dir */
665     if (! qdir.exists(oldCachePath)) {
666         QDir::root().mkpath(oldCachePath);
667         /* create BBS_MOVED */
668         QString movedPath = oldCachePath + "/BBS_MOVED";
669         QFile file(movedPath);
670         if (file.open(QIODevice::WriteOnly)) {
671             QTextStream stream(&file);
672             stream << newUrl << endl;
673         }
674         file.close();
675     }
676
677     /*---------------------------*/
678     /* update BBSHISTRY */
679
680     Cache historyCache(bdata->basePath());
681     QFile file(historyCache.getBBSHistoryPath());
682     if (file.open(QIODevice::WriteOnly)) {
683
684         QTextStream ts(&file);
685
686         keyHosts.removeOne(newhost);
687         QString host;
688         foreach (host, keyHosts) {
689             ts << host << endl;
690         }
691
692         file.close();
693     }
694
695
696     /*---------------------------*/
697     /* update other information */
698     FavoriteThreads::replace(oldUrl, newUrl);
699     Thread::replace(oldUrl, newUrl);
700     ThreadInfo::replace(oldUrl, newUrl);
701     FavoriteBoards::replace(oldUrl, newUrl);
702
703     return true;
704 }