/****************************************************************************
** Dooble - The Secure Internet Web Browser
**
** Copyright (c) 2008, 2009, 2010, 2011, 2012 Alexis Megas,
** Gunther van Dooble, and the Dooble Team.
** All rights reserved.
**
** License: GPL2 only:
** 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; version 2 of the License only.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
** or see here: http://www.gnu.org/licenses/gpl.html
**
** For the WebKit library, please see: http://webkit.org.
**
** THE CODE IS PROVIDED BY THE AUTHORS ''AS IS'' AND ANY
** EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
** PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
** ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
** GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
** INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
** IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
** ARISING IN ANY WAY OUT OF THE USE OF THIS APPLICATION, EVEN IF ADVISED
** OF THE POSSIBILITY OF SUCH DAMAGE.
**
** Please report all praise, requests, bugs, and problems to the project
** team and administrators: http://sf.net/projects/dooble.
**
** You can find us listed at our project page. New team members are welcome.
** The name of the authors should not be used to endorse or promote products
** derived from Dooble without specific prior written permission.
** If you use this code for other projects, please let us know.
**
** Web sites:
**   http://sf.net/projects/dooble
**   http://dooble.sf.net
****************************************************************************/

#include <QDir>
#include <QtCore>
#include <QSqlQuery>
#include <QProgressBar>
#include <QSqlDatabase>

#include "dmisc.h"
#include "dooble.h"
#include "dcookies.h"

dcookies::dcookies(void):QNetworkCookieJar()
{
  /*
  ** settingsWindow/cookiesShouldBe
  ** 0 - deleted upon exit
  ** 1 - kept forever (that's a long time)
  ** 2 - kept until they expire
  */

  m_writeTimer = 0;
  m_stopWriteThread = false;
  m_timer = new QTimer(this);
  connect(m_timer,
	  SIGNAL(timeout(void)),
	  this,
	  SLOT(slotRemoveDomains(void)));
  connect(this,
	  SIGNAL(cookieReceived(const QString &,
				const QUrl &,
				const QDateTime &)),
	  dooble::s_cookiesBlockWindow,
	  SLOT(slotAdd(const QString &,
		       const QUrl &,
		       const QDateTime &)));

  int interval = computedTimerInterval();

  if(interval > 0)
    m_timer->start(interval);

  createCookieDatabase();
}

dcookies::~dcookies()
{
  m_future.waitForFinished();
}

QList<QNetworkCookie> dcookies::allCookies(void) const
{
  /*
  ** If cookies are disabled, should this method return an empty
  ** list or the current list? For now, it will return the
  ** current list of cookies minus the expired cookies.
  */

  QList<QNetworkCookie> list(QNetworkCookieJar::allCookies());

  if(dooble::s_settings.value("settingsWindow/cookiesShouldBe", 0).
     toInt() == 2)
    {
      QDateTime now(QDateTime::currentDateTime());

      /*
      ** Remove expired cookies.
      */

      for(int i = list.size() - 1; i >= 0; i--)
	if(list.at(i).isSessionCookie())
	  continue;
	else if(list.at(i).expirationDate().toLocalTime() <= now)
	  list.removeAt(i);
    }

  return list;
}

QList<QNetworkCookie> dcookies::cookiesForUrl(const QUrl &url) const
{
  /*
  ** If cookies are disabled, should this method return an empty
  ** list or the current list? For now, it will return the
  ** current list of cookies minus the expired cookies.
  */

  QList<QNetworkCookie> list(QNetworkCookieJar::cookiesForUrl(url));

  if(dooble::s_settings.value("settingsWindow/cookiesShouldBe", 0).
     toInt() == 2)
    {
      QDateTime now(QDateTime::currentDateTime());

      /*
      ** Remove expired cookies.
      */

      for(int i = list.size() - 1; i >= 0; i--)
	if(list.at(i).isSessionCookie())
	  continue;
	else if(list.at(i).expirationDate().toLocalTime() <= now)
	  list.removeAt(i);
    }

  return list;
}

bool dcookies::setCookiesFromUrl(const QList<QNetworkCookie> &cookieList,
				 const QUrl &url)
{
  if(!dooble::s_settings.value("settingsWindow/cookiesEnabled", true).toBool())
    return false;
  else if(cookieList.isEmpty())
    return false;

  /*
  ** Other exceptions models are only populated if some
  ** action has been denied. With cookies, knowledge of the
  ** cookie may be beneficial.
  */

  QDateTime now(QDateTime::currentDateTime());

  emit cookieReceived(url.host(), url, now);

  if(dooble::s_cookiesBlockWindow->allowed(url.host()))
    {
      emit exceptionRaised(dooble::s_cookiesBlockWindow, url);
      return false;
    }

  /*
  ** Now proceed with adding new cookies to the jar.
  */

  bool added = false;
  bool acceptThirdParty = dooble::s_settings.value
    ("settingsWindow/acceptThirdPartyCookies", true).toBool();

  for(int i = 0; i < cookieList.size(); i++)
    {
      if(!acceptThirdParty)
	if(url.host() != cookieList.at(i).domain())
	  if(cookieList.at(i).domain() != "" &&
	     cookieList.at(i).domain() != ".")
	    /*
	    ** Sorry, but third-party cookies are not
	    ** allowed. Goodbye!
	    */

	    continue;

      QList<QNetworkCookie> cookie;

      cookie.append(cookieList.at(i));

      if(!QNetworkCookieJar::setCookiesFromUrl(cookie, url))
	{
	  QList<QNetworkCookie> all(QNetworkCookieJar::allCookies());

	  for(int j = all.size() - 1; j >= 0; j--)
	    if(cookie.at(0).domain() == all.at(j).domain() &&
	       cookie.at(0).name() == all.at(j).name() &&
	       cookie.at(0).path() == all.at(j).path())
	      {
		/*
		** cookie.at(0) already exists. Let's remove it before
		** adding it.
		*/

		all.removeAt(j);
		break;
	      }

	  all.append(cookie.at(0));
	  setAllCookies(all);
	  added = true;
	}
      else
	added = true;
    }

  if(added)
    emit changed();

  if(dmisc::passphraseWasAuthenticated())
    if(added)
      makeTimeToWrite();

  return added;
}

void dcookies::removeDomains(const QStringList &domains)
{
  bool removed = false;
  QList<QNetworkCookie> all(QNetworkCookieJar::allCookies());

  for(int i = all.size() - 1; i >= 0; i--)
    if(domains.contains(all.at(i).domain()))
      {
	removed = true;
	all.removeAt(i);
      }

  setAllCookies(all);

  if(dmisc::passphraseWasAuthenticated())
    if(removed)
      deleteAndSaveCookies();

  removed = false;

  for(int i = 0; i < domains.size(); i++)
    if(m_favorites.keys().contains(domains.at(i)))
      {
	removed = true;
	m_favorites.remove(domains.at(i));
      }

  if(removed)
    deleteAndSaveFavorites();
}

void dcookies::removeCookie(const QNetworkCookie &cookie)
{
  bool removed = false;
  QList<QNetworkCookie> all(QNetworkCookieJar::allCookies());

  for(int i = all.size() - 1; i >= 0; i--)
    if(all.at(i).domain() == cookie.domain() &&
       all.at(i).name() == cookie.name() &&
       all.at(i).path() == cookie.path())
      {
	/*
	** Remove exactly one cookie.
	*/

	all.removeAt(i);
	removed = true;
	break;
      }

  setAllCookies(all);

  if(dmisc::passphraseWasAuthenticated())
    if(removed)
      deleteAndSaveCookies();
}

void dcookies::createCookieDatabase(void)
{
  if(!dooble::s_settings.value("settingsWindow/cookiesEnabled",
			       true).toBool())
    return;

  {
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "cookies");

    db.setDatabaseName(dooble::s_homePath + QDir::separator() + "cookies.db");

    if(db.open())
      {
	QSqlQuery query(db);

	query.exec("CREATE TABLE IF NOT EXISTS cookies ("
		   "raw_form BLOB NOT NULL)");
	query.exec("CREATE TABLE IF NOT EXISTS favorites ("
		   "domain TEXT PRIMARY KEY NOT NULL)");
      }

    db.close();
  }

  QSqlDatabase::removeDatabase("cookies");
}

void dcookies::allowDomain(const QString &domain, const bool allowed)
{
  if(allowed)
    m_favorites[domain] = true;
  else
    m_favorites.remove(domain);

  deleteAndSaveFavorites();
}

void dcookies::populate(void)
{
  m_favorites.clear();
  setAllCookies(QList<QNetworkCookie> ());

  if(!dmisc::passphraseWasAuthenticated())
    return;

  if(!m_writeTimer)
    {
      m_writeTimer = new QTimer(this);
      m_writeTimer->setInterval(1500);
      m_writeTimer->setSingleShot(true);
      connect(m_writeTimer,
	      SIGNAL(timeout(void)),
	      this,
	      SLOT(slotWriteTimeout(void)));
    }

  {
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "cookies");

    db.setDatabaseName(dooble::s_homePath +
		       QDir::separator() + "cookies.db");

    if(db.open())
      {
	QDateTime now(QDateTime::currentDateTime());
	QSqlQuery query(db);
	QList<QNetworkCookie> all;

	if(query.exec("SELECT raw_form FROM cookies"))
	  while(query.next())
	    {
	      QByteArray bytes
		(dmisc::decodedString
		 (query.value(0).toByteArray()));

	      /*
	      ** Strip padding introduced by encryption.
	      */

	      for(int i = bytes.size() - 1; i >= 0; i--)
		if(bytes.at(i) != 0)
		  {
		    bytes = bytes.mid(0, i + 1);
		    break;
		  }

	      QList<QNetworkCookie> parsed
		(QNetworkCookie::parseCookies(bytes));

	      if(parsed.isEmpty())
		{
		  QSqlQuery deleteQuery(db);

		  deleteQuery.exec("PRAGMA synchronous = OFF");
		  deleteQuery.prepare("DELETE FROM cookies WHERE "
				      "raw_form = ?");
		  deleteQuery.bindValue(0, query.value(0));
		  deleteQuery.exec();
		  continue;
		}
	      else  if(dooble::s_settings.value("settingsWindow/"
						"cookiesShouldBe", 0).
		       toInt() == 2)
		{
		  /*
		  ** The parsed container should contain at most one
		  ** cookie. Why? Well, we write the raw form of a cookie
		  ** to the cookies.db file.
		  */

		  for(int i = parsed.size() - 1; i >= 0; i--)
		    if(parsed.at(i).expirationDate().toLocalTime() <= now)
		      {
			parsed.removeAt(i);

			QSqlQuery deleteQuery(db);

			deleteQuery.exec("PRAGMA synchronous = OFF");
			deleteQuery.prepare("DELETE FROM cookies WHERE "
					    "raw_form = ?");
			deleteQuery.bindValue(0, query.value(0));
			deleteQuery.exec();
		      }
		}

	      if(!parsed.isEmpty())
		all.append(parsed);
	    }

	if(!all.isEmpty())
	  setAllCookies(all);

	if(query.exec("SELECT domain FROM favorites"))
	  while(query.next())
	    {
	      QNetworkCookie cookie;

	      cookie.setDomain
		(QString::fromUtf8
		 (dmisc::decodedString
		  (QByteArray::fromBase64
		   (query.value(0).toByteArray()))));

	      if(!QUrl::fromEncoded(cookie.domain().toUtf8(),
				    QUrl::StrictMode).isValid())
		{
		  QSqlQuery deleteQuery(db);

		  deleteQuery.exec("PRAGMA synchronous = OFF");
		  deleteQuery.prepare("DELETE FROM favorites WHERE "
				      "domain = ?");
		  deleteQuery.bindValue(0, query.value(0));
		  deleteQuery.exec();
		  continue;
		}

	      m_favorites[cookie.domain()] = true;
	    }
      }

    db.close();
  }

  QSqlDatabase::removeDatabase("cookies");
}

int dcookies::computedTimerInterval(void) const
{
  int interval = 0;

  if(dooble::s_settings.value("settingsWindow/cookiesEnabled",
			      true).toBool() &&
     dooble::s_settings.value("settingsWindow/cookieTimerEnabled",
			      false).toBool())
    {
      int unit = dooble::s_settings.value
	("settingsWindow/cookieTimerUnit", 0).toInt();

      if(!(unit == 0 || unit == 1))
	unit = 0;

      interval = dooble::s_settings.value
	("settingsWindow/cookieTimerInterval", 1).toInt();

      if(unit == 0)
	{
	  if(interval < 1 || interval > 60)
	    interval = 1;

	  interval = 3600000 * interval;
	}
      else
	{
	  if(interval < 1 || interval > 3600)
	    interval = 1;

	  interval = 60000 * interval;
	}
    }

  return interval;
}

void dcookies::slotCookieTimerChanged(void)
{
  int interval = computedTimerInterval();

  if(interval > 0)
    {
      if(interval != m_timer->interval())
	m_timer->start(interval);
    }
  else
    m_timer->stop();
}

bool dcookies::isFavorite(const QString &domain) const
{
  if(m_favorites.contains(domain))
    return m_favorites.value(domain);
  else
    return false;
}

void dcookies::slotRemoveDomains(void)
{
  QStringList domains;
  QList<QNetworkCookie> all(QNetworkCookieJar::allCookies());

  /*
  ** Locate the domains that are not in the
  ** m_favorites container.
  */

  for(int i = 0; i < all.size(); i++)
    if(m_favorites.contains(all.at(i).domain()))
      {
	if(!m_favorites.value(all.at(i).domain()))
	  if(!domains.contains(all.at(i).domain()))
	    domains.append(all.at(i).domain());
      }
    else if(!domains.contains(all.at(i).domain()))
      domains.append(all.at(i).domain());

  removeDomains(domains);
  emit domainsRemoved(domains);
}

void dcookies::reencode(QProgressBar *progress)
{
  if(!dmisc::passphraseWasAuthenticated())
    return;

  if(progress)
    {
      progress->setMaximum(-1);
      progress->setVisible(true);
    }

  createCookieDatabase();
  deleteAndSaveCookies();
  deleteAndSaveFavorites();

  if(progress)
    progress->setVisible(false);
}

QList<QNetworkCookie> dcookies::allCookiesAndFavorites(void) const
{
  QList<QNetworkCookie> favorites;

  for(int i = 0; i < m_favorites.keys().size(); i++)
    {
      QNetworkCookie cookie;

      cookie.setDomain(m_favorites.keys().at(i));
      favorites.append(cookie);
    }

  return QNetworkCookieJar::allCookies() + favorites;
}

void dcookies::deleteAndSaveCookies(void)
{
  QMutexLocker locker(&m_writeMutex);

  {
    QSqlDatabase db = QSqlDatabase::addDatabase
      ("QSQLITE", "cookies_write_thread");

    db.setDatabaseName(dooble::s_homePath + QDir::separator() + "cookies.db");

    if(db.open())
      {
	QSqlQuery query(db);
	QList<QNetworkCookie> all(allCookies());

	query.exec("CREATE TABLE IF NOT EXISTS cookies ("
		   "raw_form BLOB NOT NULL)");
	query.exec("PRAGMA synchronous = OFF");
	query.exec("DELETE FROM cookies");
	query.exec("VACUUM");
	query.prepare("INSERT OR REPLACE INTO cookies (raw_form) VALUES (?)");
	m_stopMutex.lock();
	m_stopWriteThread = false;
	m_stopMutex.unlock();

	while(!all.isEmpty())
	  {
	    m_stopMutex.lock();

	    bool stop = m_stopWriteThread;

	    m_stopMutex.unlock();

	    if(stop)
	      break;

	    QNetworkCookie cookie(all.takeFirst());

	    if(cookie.isSessionCookie())
	      continue;

	    query.bindValue
	      (0,
	       dmisc::encodedString(cookie.toRawForm(), true));
	    query.exec();
	  }
      }

    db.close();
  }

  QSqlDatabase::removeDatabase("cookies_write_thread");
}

void dcookies::deleteAndSaveFavorites(void)
{
  if(!dmisc::passphraseWasAuthenticated())
    return;

  createCookieDatabase();

  {
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "cookies");

    db.setDatabaseName(dooble::s_homePath + QDir::separator() + "cookies.db");

    if(db.open())
      {
	QSqlQuery query(db);

	query.exec("PRAGMA synchronous = OFF");
	query.exec("DELETE FROM favorites");
	query.prepare("INSERT OR REPLACE INTO favorites (domain) VALUES (?)");

	for(int i = 0; i < m_favorites.keys().size(); i++)
	  {
	    query.bindValue(0, dmisc::encodedString(m_favorites.keys().at(i).
						    toUtf8(),
						    true).toBase64());
	    query.exec();
	  }
      }

    db.close();
  }

  QSqlDatabase::removeDatabase("cookies");
}

void dcookies::slotWriteTimeout(void)
{
  m_stopMutex.lock();
  m_stopWriteThread = true;
  m_stopMutex.unlock();
  m_future.waitForFinished();
  m_future = QtConcurrent::run(this, &dcookies::deleteAndSaveCookies);
}

void dcookies::makeTimeToWrite(void)
{
  if(m_writeTimer)
    m_writeTimer->start();
}

void dcookies::clear(void)
{
  bool removed = false;
  QList<QNetworkCookie> all(QNetworkCookieJar::allCookies());

  for(int i = all.size() - 1; i >= 0; i--)
    if(!m_favorites.contains(all.at(i).domain()))
      {
	removed = true;
	all.removeAt(i);
      }

  if(removed)
    {
      setAllCookies(all);
      emit changed();

      if(dmisc::passphraseWasAuthenticated())
	slotWriteTimeout();
    }
}
