--- /dev/null
+/***************************************************************************
+* Copyright (C) 2010 by Kita Developers *
+* ikemo@users.sourceforge.jp *
+* *
+* This program is free software; you can redistribute it and/or modify *
+* it under the terms of the GNU General Public License as published by *
+* the Free Software Foundation; either version 2 of the License, or *
+* (at your option) any later version. *
+***************************************************************************/
+#include "boarddatabase.h"
+
+#include <QtCore/QDateTime>
+#include <QtCore/QDir>
+#include <QtCore/QTextCodec>
+#include <QtCore/QTextStream>
+
+#include <kfilterdev.h>
+#include <kio/netaccess.h>
+#include <kio/slaveconfig.h>
+
+#include "cache.h"
+#include "config.h"
+#include "favoriteboards.h"
+#include "favoritethreads.h"
+#include "thread.h"
+#include "threadindex.h"
+#include "threadinfo.h"
+
+using namespace Kita;
+
+BoardDataList BoardDatabase::m_boardDataList;
+QTextCodec* BoardDatabase::m_cp932Codec = 0;
+QTextCodec* BoardDatabase::m_eucJpCodec = 0;
+BoardData* BoardDatabase::m_previousBoardData = 0;
+QString BoardDatabase::m_previousBoardUrl;
+
+BoardDatabase::BoardDatabase(const KUrl& url) : m_url(url)
+{
+}
+
+void BoardDatabase::setUrl(const KUrl& url)
+{
+ m_url = url;
+}
+
+/* (hostname)/(rootPath)/(bbsPath)/ */ /* public */
+QString BoardDatabase::boardUrl()
+{
+ BoardData * bdata = getBoardData();
+ return (bdata == 0) ? QString() : bdata->basePath();
+}
+
+/* public */
+QStringList BoardDatabase::allBoardUrlList()
+{
+ QStringList urlList;
+
+ BoardData *data;
+ foreach (data, m_boardDataList)
+ urlList += data->basePath();
+ return urlList;
+}
+
+/* (hostname)/(rootPath) */ /* public */
+QString BoardDatabase::boardRoot()
+{
+ BoardData * bdata = getBoardData();
+ return (bdata == 0) ? QString() : bdata->hostName() + bdata->rootPath();
+}
+
+/* (bbspath) */ /* public */
+QString BoardDatabase::boardPath()
+{
+ BoardData * bdata = getBoardData();
+ return (bdata == 0) ? QString() : bdata->bbsPath();
+}
+
+/* (ext) */ /* public */
+QString BoardDatabase::ext()
+{
+ BoardData * bdata = getBoardData();
+ return (bdata == 0) ? QString() : bdata->ext();
+}
+
+/* ID of board for writing */ /* public */
+QString BoardDatabase::boardId()
+{
+ BoardData * bdata = getBoardData();
+ return (bdata == 0) ? QString() : bdata->bbsPath().mid(1); /* remove "/" */
+}
+
+/* (hostname)/(rootPath)/(bbsPath)/subject.txt */ /* public */
+QString BoardDatabase::subjectUrl()
+{
+ BoardData * bdata = getBoardData();
+ return (bdata == 0) ? QString() : bdata->basePath() + "subject.txt";
+}
+
+/* public */
+QString BoardDatabase::boardName()
+{
+ BoardData * bdata = getBoardData();
+ return (bdata == 0) ? QString() : bdata->boardName();
+}
+
+/* public */
+int BoardDatabase::type()
+{
+ BoardData * bdata = getBoardData();
+ return (bdata == 0) ? Board_Unknown : bdata->type();
+}
+
+/*---------------------------*/
+/* ThreadList */
+
+
+/* get list of pointers of Thread classes. */
+/*
+ Input:
+
+ oldLogs: If true, search cache and get list of pointer of old threads.
+ online: online or offline mode.
+
+ Output:
+
+ threadList: list of pointers of Thread classes.
+ oldLogList: list of pointers of old threads.
+
+ */ /* public */
+void BoardDatabase::getThreadList(
+
+ /* input */
+ bool oldLogs,
+ bool online,
+
+ /* output */
+ QList<Thread*>& threadList,
+ QList<Thread*>& oldLogList)
+{
+ threadList.clear();
+ oldLogList.clear();
+
+ /* get all obtained threads list from cache */
+ if (m_url.prettyUrl() == "http://virtual/obtained/") {
+
+ QStringList bbslist = allBoardUrlList();
+
+ /* search all cache dirs */
+ QString thread;
+ foreach (thread, bbslist) {
+ getCachedThreadList(thread, threadList);
+ }
+
+ return ;
+ }
+
+ /*-------------------------*/
+
+ BoardData* bdata = getBoardData();
+ if (bdata == 0) return ;
+
+ /* download subject.txt */
+ if (online) {
+
+ /* make directory */
+ Cache cache(m_url);
+ QString cacheDir = cache.getDirPath();
+ if (!QDir::root().mkpath(cacheDir)) return;
+
+ KIO::SlaveConfig::self() ->setConfigData("http",
+ m_url.host() ,
+ "UserAgent",
+ QString("Monazilla/1.00 (Kita/%1)").arg(VERSION));
+ QString subjectPath = cache.getSubjectPath();
+ KIO::NetAccess::download(subjectUrl(), subjectPath, 0);
+ }
+
+ /* open and read subject.txt */
+ readSubjectTxt(bdata, threadList);
+
+ /* get old logs */
+ if (oldLogs) {
+
+ QList<Thread*> tmpList;
+ tmpList.clear();
+ getCachedThreadList(m_url, tmpList);
+
+ for (int i = 0; i < tmpList.count(); i++) {
+ if (threadList.contains(tmpList.at(i)) == 0)
+ oldLogList.append(tmpList.at(i));
+ }
+ }
+}
+
+/* read the cache dir & get the list of all threads. */ /* private */
+void BoardDatabase::getCachedThreadList(const KUrl& url,
+ QList<Thread*>& threadList)
+{
+ Cache cache(url);
+ QString cacheDir = cache.getDirPath();
+ QDir d(cacheDir);
+ if (d.exists()) {
+
+ /* get all file names */
+ QString ext = getBoardData(url)->ext();
+ QString boardUrl = getBoardData(url)->basePath();
+ QStringList filter('*' + ext);
+ QStringList flist = d.entryList(filter);
+ QString file;
+ foreach (file, flist) {
+ if (file.isEmpty()) continue;
+
+ QString datUrl = boardUrl + "dat/" + file;
+
+ /* read idx file */
+ Thread* thread = Thread::getByUrlNew(datUrl);
+ if (thread == 0) {
+
+ thread = Thread::getByUrl(datUrl);
+ if (thread == 0)
+ continue;
+ ThreadIndex threadIndex(datUrl);
+ threadIndex.loadIndex(thread, false);
+ }
+
+ if (thread != 0)
+ threadList.append(thread);
+ }
+ }
+}
+
+/* open subject.txt and get list of Thread classes */ /* private */
+bool BoardDatabase::readSubjectTxt(BoardData* bdata, QList<Thread*>& threadList)
+{
+ /* get all names of cached files to read idx. */
+ QStringList cacheList;
+ if (!bdata->readIdx()) {
+ Cache cache(m_url);
+ QString cacheDir = cache.getDirPath();
+ QDir d(cacheDir);
+ if (d.exists()) {
+ QString ext = getBoardData()->ext();
+ QStringList filter('*' + ext);
+ cacheList = d.entryList(filter);
+ }
+ }
+
+ /* open subject.txt */
+ Cache cache(m_url);
+ QString subjectPath = cache.getSubjectPath();
+ QIODevice * device = KFilterDev::deviceForFile(subjectPath, "application/x-gzip");
+ if (!device->open(QIODevice::ReadOnly))
+ return false;
+
+ QTextStream stream(device);
+
+ if (type() == Board_JBBS) {
+ if (!m_eucJpCodec) m_eucJpCodec = QTextCodec::codecForName("eucJP");
+ stream.setCodec(m_eucJpCodec);
+ } else {
+ if (!m_cp932Codec) m_cp932Codec = QTextCodec::codecForName("Shift-JIS");
+ stream.setCodec(m_cp932Codec);
+ }
+
+ QRegExp regexp;
+ switch (type()) {
+
+ case Board_MachiBBS:
+ case Board_JBBS:
+ regexp.setPattern("(\\d+\\.cgi),(.*)\\((\\d+)\\)");
+ break;
+
+ default:
+ regexp.setPattern("(\\d+\\.dat)<>(.*)\\((\\d+)\\)");
+ break;
+ }
+ QString line;
+
+ while (!(line = stream.readLine()).isEmpty()) {
+ int pos = regexp.indexIn(line);
+ if (pos != -1) {
+ QString fname = regexp.cap(1);
+ QString subject = regexp.cap(2);
+ QString num = regexp.cap(3);
+
+ /* get pointer of Thread class */
+ QString datUrl = boardUrl() + "dat/" + fname;
+ Thread* thread = Thread::getByUrl(datUrl);
+ ThreadIndex threadIndex(datUrl);
+ if (threadList.indexOf(thread) == -1) {
+ threadList.append(thread);
+ }
+
+ /* set thread name */
+ thread->setThreadName(subject);
+
+ /* load index file */
+ if (!bdata->readIdx()) {
+
+ if (cacheList.contains(fname)) {
+ threadIndex.loadIndex(thread, false);
+ }
+ }
+
+ /* update res num */
+ int newNum = num.toInt();
+ if (thread->readNum()) { /* cache exists */
+ int oldNum = thread->resNum();
+
+ if (newNum > oldNum) {
+ threadIndex.setResNum(newNum);
+ }
+ }
+ thread->setResNum(newNum);
+ }
+ }
+
+ device->close();
+ bdata->setReadIdx(true); /* never read idx files again */
+
+ return true;
+}
+
+/*---------------------------*/
+/* BoardData */
+
+/* reset all BoardData */ /* public */
+void BoardDatabase::clearBoardData()
+{
+ BoardData *data;
+ foreach (data, m_boardDataList)
+ delete data;
+
+ m_boardDataList.clear();
+ m_previousBoardData = 0;
+ m_previousBoardUrl.clear();
+}
+
+/**
+ *
+ * @param[in] board
+ * @param[in] boardName
+ * @param[in] type
+ * @param[in] test
+ *
+ * @param[out] oldURL
+ *
+ * @retval Board_enrollEnrolled if board is already enrolled. oldURL is QString().
+ * @retval Board_enrollNew if board is new board. oldURL is QString().
+ * @retval Board_enrollMoved if board is moved. oldURL is old URL.
+ *
+ * @note board is NOT enrolled when board is moved.
+ * To enroll new URL, call BoardDatabase::moveBoard().
+ *
+ * "int type" is type of board. It could be "Board_Unknown". See also parseBoardURL().
+ *
+ * If "bool test" is true, this function just checks if the board is enrolled (never enroll board).
+ *
+ */
+/* public */
+int BoardDatabase::enrollBoard(const QString& boardName, QString& oldUrl, int type, bool test)
+{
+ QString hostname;
+ QString rootPath;
+ QString delimiter;
+ QString bbsPath;
+ QString ext;
+ type = parseBoardUrl(type, hostname, rootPath, delimiter, bbsPath, ext);
+ oldUrl.clear();
+
+ if (type == Board_Unknown) return Board_enrollFailed;
+
+ /* check if the board is enrolled or moved. */
+ BoardData *data;
+ foreach (data, m_boardDataList) {
+
+ if (data->boardName() == boardName
+ && data->type() == type
+ && data->bbsPath() == bbsPath) {
+
+ if (data->hostName() == hostname
+ && data->rootPath() == rootPath) { /* enrolled */
+ return Board_enrollEnrolled;
+ } else { /* moved */
+ oldUrl = data->basePath();
+ return Board_enrollMoved;
+ }
+ }
+ }
+
+ /* test only */
+ if (test)
+ return Board_enrollNew;
+
+ /* enroll new board */
+ BoardData* bdata = new BoardData(boardName, hostname, rootPath, delimiter, bbsPath, ext, type);
+ m_boardDataList.append(bdata);
+
+ return Board_enrollNew;
+}
+
+/* parse board URL */
+/* return board type. */ /* private */
+int BoardDatabase::parseBoardUrl(
+
+ /* input */
+ int type, /* If type = Board_Unknown, type will be decided according to url. */
+
+ /* output */
+ QString& hostname,
+ QString& rootPath,
+ QString& delimiter,
+ QString& bbsPath,
+ QString& ext)
+{
+ hostname = m_url.protocol() + "://" + m_url.host();
+ rootPath.clear();
+ delimiter.clear();
+ bbsPath.clear();
+ ext.clear();
+
+ /* decide type */
+ if (type == Board_Unknown) {
+
+ if (m_url.host().contains("machi.to"))
+ type = Board_MachiBBS;
+ else if (m_url.host().contains("jbbs.livedoor.jp"))
+ type = Board_JBBS;
+ else
+ type = Board_2ch;
+ }
+
+ /* parse */
+ switch (type) {
+
+ case Board_MachiBBS: /* MACHI : http:// *.machi.to/(bbsPath)/ */
+
+ delimiter = "/bbs/read.pl";
+ bbsPath = m_url.fileName();
+ ext = ".cgi";
+ break;
+
+ case Board_JBBS: /* JBBS : http://jbbs.livedoor.jp/(bbsPath)/ */
+
+ delimiter = "/bbs/read.cgi";
+ bbsPath = m_url.prettyUrl().remove(hostname);
+ type = Board_JBBS;
+ ext = ".cgi";
+ break;
+
+ case Board_FlashCGI: /* test for Flash CGI/Mini Thread */
+
+ delimiter = "/test/read.cgi";
+ bbsPath = m_url.fileName();
+ rootPath = m_url.prettyUrl().remove(hostname + '/').remove(bbsPath + '/');
+ if (rootPath.length() == 0)
+ rootPath.clear();
+ ext = ".dat";
+ break;
+
+ default: /* 2ch : http://(hostname)/(rootPath)/(bbsPath)/ */
+
+ delimiter = "/test/read.cgi";
+ bbsPath = m_url.fileName();
+ rootPath = m_url.prettyUrl().remove(hostname + '/').remove(bbsPath + '/');
+ if (rootPath.length() == 0)
+ rootPath.clear();
+ ext = ".dat";
+ type = Board_2ch;
+ break;
+ }
+
+ /* For example, if bbsPath = "linux/", then m_bbsPath = "/linux" */
+ const QRegExp exp("/$");
+ rootPath.remove(exp);
+ bbsPath.remove(exp);
+ if (!rootPath.isEmpty() && rootPath.at(0) != '/')
+ rootPath = '/' + rootPath;
+ if (!bbsPath.isEmpty() && bbsPath.at(0) != '/')
+ bbsPath = '/' + bbsPath;
+
+ return type;
+}
+
+/* public */
+bool BoardDatabase::isEnrolled()
+{
+ return getBoardData() != 0;
+}
+
+/* public */
+BoardData* BoardDatabase::getBoardData(const KUrl& url)
+{
+ if (url.isEmpty())
+ return 0;
+ QString urlstr = url.prettyUrl();
+
+ /* cache */
+ if (m_previousBoardData != 0 && m_previousBoardUrl == urlstr)
+ return m_previousBoardData;
+
+ BoardData *data;
+ foreach (data, m_boardDataList) {
+
+ int count = data->keyBasePathList().count();
+ for (int i = 0; i < count ; ++i) {
+ if (urlstr.contains(data->keyBasePathList()[i])
+ || urlstr.contains(data->keyCgiBasePathList()[i])) {
+
+ /* cache */
+ m_previousBoardData = data;
+ m_previousBoardUrl = urlstr;
+
+ return data;
+ }
+ }
+ }
+ return 0;
+}
+
+/* public */
+BoardData* BoardDatabase::getBoardData()
+{
+ return getBoardData(m_url);
+}
+
+/*--------------------------------*/
+/* BBSHISTORY */
+
+
+/* load the bbs history file (BBSHISTORY), and create keys of Data Base. */
+/* Before calling this, enroll the board by enrollBoard(). */
+/*
+ ex) If the host of board moved like :
+
+ http:://aaa.com -> http://bbb.com -> http://ccc.com -> http://ddd.com
+
+ then, BBSHISTORY is
+
+ http://ccc.com
+ http://bbb.com
+ http://aaa.com
+
+*/ /* public */
+bool BoardDatabase::loadBBSHistory()
+{
+ BoardData * bdata = getBoardData();
+ if (bdata == 0)
+ return false;
+
+ QStringList keyHosts(bdata->hostName());
+
+ Cache cache(m_url);
+ QFile file(cache.getBBSHistoryPath());
+ if (file.open(QIODevice::ReadOnly)) {
+
+ QTextStream ts(&file);
+
+ QString line;
+ while (!ts.atEnd()) {
+
+ line = ts.readLine();
+ keyHosts += line;
+ }
+
+ bdata->createKeys(keyHosts);
+ file.close();
+
+ return true;
+ }
+
+ return false;
+}
+
+/* public */
+bool BoardDatabase::moveBoard(const KUrl& fromUrl, const KUrl& toUrl)
+{
+ QString oldhost = fromUrl.protocol() + "://" + fromUrl.host();
+ QString newhost = toUrl.protocol() + "://" + toUrl.host();
+
+ const QRegExp exp("/$");
+ QString oldUrl = fromUrl.prettyUrl();
+ QString newUrl = toUrl.prettyUrl();
+ oldUrl.remove(exp);
+ newUrl.remove(exp);
+ oldUrl += '/';
+ newUrl += '/';
+
+ if (oldUrl == newUrl) return false;
+
+ /* Is oldURL enrolled? */
+ BoardData* bdata = getBoardData(oldUrl);
+ if (bdata == 0) {
+
+ /* Is newURL enrolled? */
+ bdata = getBoardData(newUrl);
+ if (bdata == 0) return false;
+ }
+
+
+ /*---------------------------*/
+ /* update BoardData */
+
+ /* get the path of old cache */
+ bdata->setHostName(oldhost);
+ QStringList keyHosts = bdata->keyHostList();
+ keyHosts.removeOne(oldhost);
+ keyHosts.prepend(oldhost);
+ bdata->createKeys(keyHosts);
+ Cache cache(bdata->basePath());
+ QString oldCachePath = cache.getDirPath();
+
+ /* update URL */
+ bdata->setHostName(newhost);
+
+ /* update keys */
+ /* The order of keyHosts will be like this:
+
+ newhost
+ oldhost
+ foohost1
+ foohost2
+
+ */
+ keyHosts = bdata->keyHostList();
+ keyHosts.removeOne(oldhost);
+ keyHosts.prepend(oldhost);
+ keyHosts.removeOne(newhost);
+ keyHosts.prepend(newhost);
+ bdata->createKeys(keyHosts);
+
+ /* reset BoardData */
+ bdata->setReadIdx(false);
+ bdata->setSettingLoaded(false);
+
+
+ /*---------------------------*/
+ /* move cache dir */
+
+ QDir qdir;
+ if (! qdir.exists(oldCachePath)) return true;
+
+ /* mkdir new server dir */
+ Cache newCache(bdata->basePath());
+ QString newCachePath = Cache::baseDir() + newCache.serverDir();
+ QDir::root().mkpath(newCachePath);
+
+ /* backup old dir */
+ newCachePath += newCache.boardDir();
+ if (qdir.exists (newCachePath)) {
+ QString bkupPath = newCachePath;
+ bkupPath.truncate(bkupPath.length() - 1); /* remove '/' */
+ bkupPath +=
+ '.' + QString::number(QDateTime::currentDateTime().toTime_t());
+ qdir.rename(newCachePath, bkupPath);
+ }
+
+ /* move cache dir */
+ if (qdir.exists(oldCachePath)) {
+ qdir.rename(oldCachePath, newCachePath);
+ } else
+ QDir::root().mkpath(newCachePath);
+
+ /* make old dir */
+ if (! qdir.exists(oldCachePath)) {
+ QDir::root().mkpath(oldCachePath);
+ /* create BBS_MOVED */
+ QString movedPath = oldCachePath + "/BBS_MOVED";
+ QFile file(movedPath);
+ if (file.open(QIODevice::WriteOnly)) {
+ QTextStream stream(&file);
+ stream << newUrl << endl;
+ }
+ file.close();
+ }
+
+ /*---------------------------*/
+ /* update BBSHISTRY */
+
+ Cache historyCache(bdata->basePath());
+ QFile file(historyCache.getBBSHistoryPath());
+ if (file.open(QIODevice::WriteOnly)) {
+
+ QTextStream ts(&file);
+
+ keyHosts.removeOne(newhost);
+ QString host;
+ foreach (host, keyHosts) {
+ ts << host << endl;
+ }
+
+ file.close();
+ }
+
+
+ /*---------------------------*/
+ /* update other information */
+ FavoriteThreads::replace(oldUrl, newUrl);
+ Thread::replace(oldUrl, newUrl);
+ ThreadInfo::replace(oldUrl, newUrl);
+ FavoriteBoards::replace(oldUrl, newUrl);
+
+ return true;
+}