<commit_before>/*   -*-C++-*- -*-coding: utf-8-unix;-*-
  Classified Ads is Copyright (c) <NAME> 2013-2022.

  This file is part of Classified Ads.

  Classified Ads is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  Classified Ads 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
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with Classified Ads; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include "model.h"

#include <assert.h>
#include <stdio.h>

#include <QDir>
#include <QFile>
#include <QObject>
#include <QSqlError>
#include <QSqlQuery>
#include <QSslCertificate>
#include <QSslKey>
#include <QThread>
#include <QUuid>

#include "../log.h"
#include "../util/hash.h"
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
#include <QRandomGenerator>
#endif
#include <openssl/evp.h>   // OpenSSL_add_all_algorithms
#include <openssl/rand.h>  // random seed things
#include <openssl/ssl.h>   // library init

#include <QMutex>
#include <QTimerEvent>
#include <QWaitCondition>

#include "../net/connection.h"
#include "../net/networklistener.h"
#include "../net/node.h"
#include "../net/protocol_message_formatter.h"
#include "binaryfilemodel.h"
#include "cadbrecordmodel.h"
#include "camodel.h"
#include "contentencryptionmodel.h"
#include "netrequestexecutor.h"
#include "privmsgmodel.h"
#include "profilecommentmodel.h"
#include "profilemodel.h"
#include "searchmodel.h"
#include "trusttreemodel.h"
#ifdef WIN32
#include <Wincrypt.h>
#include <windows.h>
#endif
#include "tclmodel.h"

Model::Model(MController* aController)
    : iController(aController),
      iMutex(NULL),
      iConnections(NULL),
      iNodeModel(NULL),
      iNetReqExecutor(NULL),
      iNetReqQueue(NULL),
      iContentEncryptionModel(NULL),
      iProfileModel(NULL),
      iBinaryFileModel(NULL),
      iCaModel(NULL),
      iTimerId(-1),
      iPrivMsgModel(NULL),
      iProfileCommentModel(NULL),
      iSearchModel(NULL),
      iCaDbRecordModel(NULL),
      iTrustTreeModel(NULL),
      iTimeOfLastNetworkAddrCheck(QDateTime::currentDateTimeUtc().toTime_t()),
      iTclModel(NULL)
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
      ,
      iLegacyCryptoProvider(NULL),
      iDefaultCryptoProvider(NULL)
#endif
{
  LOG_STR("Model::Model()");
  connect(this, SIGNAL(error(MController::CAErrorSituation, const QString&)),
          iController,
          SLOT(handleError(MController::CAErrorSituation, const QString&)),
          Qt::QueuedConnection);
  iMutex = new QMutex(QMutex::Recursive);  // same thread can lock recursive
                                           // mutex multiple times
  initPseudoRandom();
  if (!openDB()) {
    QString errorText(lastError().text());
    emit(error(MController::DataBaseNotMountable, errorText));
  }
  iNodeModel = new NodeModel(aController, *this);
  iConnections = new QList<Connection*>();
  iNetReqQueue = new QList<NetworkRequestExecutor::NetworkRequestQueueItem>();
  iNetReqExecutor = new NetworkRequestExecutor(aController, *this);
  iContentEncryptionModel = new ContentEncryptionModel(aController, *this);
  iProfileModel = new ProfileModel(aController, *this);
  iBinaryFileModel = new BinaryFileModel(aController, *this);
  iCaModel = new ClassifiedAdsModel(aController, *this);
  iPrivMsgModel = new PrivMessageModel(aController, *this);
  iSearchModel = new SearchModel(*this, *iController);
  iSearchModel->setObjectName("CA SearchModel");
  iCaDbRecordModel = new CaDbRecordModel(aController, *this);
  iTrustTreeModel = new TrustTreeModel(aController, *this);
  iProfileCommentModel = new ProfileCommentModel(aController, *this);
  iNetReqExecutor->setInterval(1000);  // start is called by controller
  iTclModel = new TclModel(aController, *this);

  connect(iBinaryFileModel,
          SIGNAL(contentReceived(const Hash&, const ProtocolItemType)), this,
          SLOT(notifyOfContentReceived(const Hash&, const ProtocolItemType)),
          Qt::QueuedConnection);
  connect(
      iPrivMsgModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      this,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);

  connect(
      iProfileCommentModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      this,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);

  connect(iProfileModel,
          SIGNAL(contentReceived(const Hash&, const ProtocolItemType)), this,
          SLOT(notifyOfContentReceived(const Hash&, const ProtocolItemType)),
          Qt::QueuedConnection);
  connect(
      iCaModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      this,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);
  connect(
      iPrivMsgModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      this,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);
  // connect same to controller too:
  connect(iBinaryFileModel,
          SIGNAL(contentReceived(const Hash&, const ProtocolItemType)),
          iController,
          SLOT(notifyOfContentReceived(const Hash&, const ProtocolItemType)),
          Qt::QueuedConnection);
  connect(
      iPrivMsgModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      iController,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);
  connect(iProfileModel,
          SIGNAL(contentReceived(const Hash&, const ProtocolItemType)),
          iController,
          SLOT(notifyOfContentReceived(const Hash&, const ProtocolItemType)),
          Qt::QueuedConnection);
  connect(
      iCaModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      iController,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);
  connect(
      iPrivMsgModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      iController,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);

  connect(
      iProfileCommentModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      iController,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);

  connect(
      iCaDbRecordModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      iController,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);

  iTimerId = startTimer(60000 * 10);  // 10-minute timer
}

Model::~Model() {
  LOG_STR("Model::~Model()");
  if (iTimerId != -1) {
    killTimer(iTimerId);
  }
  if (iNetReqExecutor) {
    iNetReqExecutor->stop();
    delete iNetReqExecutor;
  }

  closeAllConnections(true);
  delete iConnections;
  delete iNodeModel;
  delete iProfileCommentModel;
  delete iPrivMsgModel;
  delete iProfileModel;
  delete iNetReqQueue;
  delete iBinaryFileModel;
  delete iCaDbRecordModel;
  delete iSearchModel;
  delete iTrustTreeModel;
  delete iTclModel;
  delete iCaModel;
  delete iContentEncryptionModel;
  while (iDbPool.count()) {
    const QThread* firstKey(iDbPool.firstKey());
    QSqlDatabase dbToClose(iDbPool.take(firstKey));
    if (dbToClose.isOpen()) {
      dbToClose.close();
    }
    QLOG_STR("Removed db at model destructor");
  }
  iController = NULL;  // not owned, just set null
  delete iMutex;
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
  if (iLegacyCryptoProvider) {
    OSSL_PROVIDER_unload(iLegacyCryptoProvider);
  }
  if (iDefaultCryptoProvider) {
    OSSL_PROVIDER_unload(iDefaultCryptoProvider);
  }
#endif
}

void Model::closeAllConnections(bool aDeleteAlso) {
  if (iConnections) {
    LOG_STR2("Model::closeAllConnections %d", iConnections->count());
    // normally networklistener has been deleted before
    // model and networklistener is parent of connections,
    // that now have been deleted too due to parent->child
    // relationship -> usually the loop below is looped 0 times.
    for (int i = iConnections->count() - 1; i >= 0; i--) {
      Connection* c = iConnections->at(i);
      if (c) {
        c->iNeedsToRun = false;
      }
      if (aDeleteAlso) {
        delete c;
      }
    }
  }
}

bool Model::lock() {
#ifdef __DEBUG__
  assert(iMutex->tryLock(1000 * 15) == true)
#else
  iMutex->lock();
#endif
      return true;
}

void Model::unlock() { iMutex->unlock(); }

bool Model::openDB() {
  bool tables_needed = false;
  QSqlDatabase db(dataBaseConnection(&tables_needed));
  if (db.isOpen() && tables_needed) {
    if (createTables(db) == false) {
      return false;
    }
  }
  if (db.isOpen()) {
    // do cleanup to rectify previous bugs:
    QSqlQuery query(db);
    // removes every comment that does not begin with { as all json
    // is supposed but it still is not marked as private (==encrypted)
    bool ret = query.exec(
        "delete from profile where profiledata not like x'7b25' and is_private "
        "= 'false' and hash1 not in ( select hash1 from privatekeys )");
    if (ret == false) {
      QLOG_STR("Profile cleanup: " + query.lastError().text());
    } else {
      QLOG_STR("Cleaned away faulty profiles " +
               QString::number(query.numRowsAffected()));
    }
    ret = query.exec(
        "delete from profilecomment where commentdata not like x'7b25' and "
        "flags&1 = 0");
    if (ret == false) {
      QLOG_STR("Profile comment cleanup: " + query.lastError().text());
    } else {
      QLOG_STR("Cleaned away faulty comments " +
               QString::number(query.numRowsAffected()));
    }
  }

  if (db.isOpen()) {
    createTablesV2(db);
    createTablesV3(db);
    createTablesV4(db);
    QSqlQuery versionQuery("select sqlite_version()", db);
    if (versionQuery.exec() && versionQuery.next()) {
      QLOG_STR("Sqlite version " + versionQuery.value(0).toString());
    }
  }
  return db.isOpen();
}

QSqlError Model::lastError() {
  const QSqlDatabase db(dataBaseConnection());
  QSqlError e = db.lastError();
  QLOG_STR(e.text());
  return e;
}

bool Model::createTables(QSqlDatabase aDb) {
  bool ret = false;

  QSqlQuery query(aDb);
  ret = query.exec(
      "create table settings "
      "(datastore_version unsigned int primary key,"
      "node_listenport unsigned int,"
      "profile_maxrows unsigned int,"
      "private_message_maxrows unsigned int,"
      "binaryfile_maxrows unsigned int,"
      "classified_ad_maxrows unsigned int,"
      "profilecomment_maxrows unsigned int,"
      "node_maxrows unsigned int,"
      "node_key varchar(5000),"
      "node_cert varchar(5000),"
      "dns_name varchar(256))");
  if (ret) {
    QSqlQuery query(aDb);
    ret = query.exec("insert into settings (datastore_version) values (1)");
  }
  LOG_STR2("Table settings created 1 is ok, =  %d", ret);
  if (ret) {
    QSqlQuery q2(aDb);
    ret = q2.exec(
        "create table node "
        "(hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "listenport unsigned int,"
        "last_conn_time unsigned int,"
        "does_listen unsigned int,"
        "ipv4addr unsigned int,"
        "ipv6addr1 unsigned int,"
        "ipv6addr2 unsigned int,"
        "ipv6addr3 unsigned int,"
        "ipv6addr4 unsigned int,"
        "time_last_reference unsigned int,"
        "last_nodelist_time unsigned int,"
        "dns_name varchar(256),"
        "tor_name varchar(256),"
        "last_mutual_conn_time unsigned int)");
    LOG_STR2("Table node created, retval =  %d", ret);
  }
  if (ret) {
    QSqlQuery q3(aDb);
    // note how index is not unique. set of hash1+2+3+4+5 is
    // (fairly) unique but setting hash1 to be unique would
    // prevent storage of documents(nodes) where hash collides in
    // first 32 bits - that's fairly bigger chance compared
    // to hash collision in "full" 160 bits.
    ret = q3.exec("create index node_hash1_ind on node(hash1)");
    LOG_STR2("Index node(hash1) created, retval =  %d", ret);
  }
  if (ret) {
    QSqlQuery q4(aDb);
    ret = q4.exec(
        "create table privatekeys(prikey varchar(5000) not null,"
        "hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "last_msgpoll_time unsigned int,"
        "private_data blob)");
    LOG_STR2("Table for private keys created %d", ret);
  }
  if (ret) {
    QSqlQuery q5(aDb);
    ret = q5.exec(
        "create table profile(pubkey varchar(5000) not null,"
        "hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "profiledata blob,"
        "is_private int,"
        "time_last_reference unsigned int,"
        "signature varchar(1024),"
        "display_name varchar(160),"
        "time_of_publish unsigned int,"
        "recvd_from unsigned int,"
        "time_of_update_poll unsigned int)");
    LOG_STR2("Table for profile data created %d", ret);
  }
  if (ret) {
    QSqlQuery q6(aDb);
    ret = q6.exec("create index profile_hash1_ind on profile(hash1)");
    LOG_STR2("Index for profile data table created %d", ret);
  }
  if (ret) {
    QSqlQuery q7(aDb);
    // usage of the following table is this:
    // when new item (profile,ca,privmsg,binary blob) is published
    // it is inserted into this table.
    // networking module will take items (identified by hash1-5)
    // from this table one-by-one, connect to 5 nodes one-by-one
    // (marked by bangpath1-5 where each bangpath contains the
    // low-order bits of the noderef of node where item
    // has already been sent)
    // when 5th node has been filled, row is removed from table
    // and stuff is considered published
    // itemtype is one of enum ProtocolItemType from protocol.h
    //
    // some object types have actually 2 addresses and are
    // published twice: classified ads to content address and
    // the group controller address ; profile comments to
    // content address and profile address.
    ret = q7.exec(
        "create table publish(itemtype int not null,"
        "hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "secondary_addr_hash1 unsigned int ,"
        "secondary_addr_hash2 unsigned int ,"
        "secondary_addr_hash3 unsigned int ,"
        "secondary_addr_hash4 unsigned int ,"
        "secondary_addr_hash5 unsigned int ,"
        "bangpath1 unsigned int,"
        "bangpath2 unsigned int,"
        "bangpath3 unsigned int,"
        "bangpath4 unsigned int,"
        "bangpath5 unsigned int,"
        "hostcount int not null,"
        "last_time_tried unsigned int)");
    LOG_STR2("Table for publish data created %d", ret);
  }

  if (ret) {
    QSqlQuery q8(aDb);
    ret = q8.exec(
        "create table binaryfile("
        "hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "publisher_hash1 unsigned int not null,"
        "publisher_hash2 unsigned int not null,"
        "publisher_hash3 unsigned int not null,"
        "publisher_hash4 unsigned int not null,"
        "publisher_hash5 unsigned int not null,"
        "contentdata blob,"
        "is_private int,"
        "is_compressed int,"
        "time_last_reference unsigned int,"
        "signature varchar(1024),"
        "display_name varchar(160),"
        "time_of_publish unsigned int,"
        "metadata blob ,"
        "recvd_from unsigned int  )");

    LOG_STR2("Table for binary files created %d", ret);
  }
  if (ret) {
    QSqlQuery q9(aDb);
    ret = q9.exec("create index binaryfile_hash1_ind on binaryfile(hash1)");
    LOG_STR2("Index for binary blob data table created %d", ret);
  }
  if (ret) {
    QSqlQuery q10(aDb);
    ret = q10.exec(
        "create table classified_ad("
        "hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "group_hash1 unsigned int not null,"
        "group_hash2 unsigned int not null,"
        "group_hash3 unsigned int not null,"
        "group_hash4 unsigned int not null,"
        "group_hash5 unsigned int not null,"
        "reply_to unsigned int,"
        "time_last_reference unsigned int,"
        "time_added unsigned int not null,"
        "signature varchar(1024),"
        "display_name varchar(160),"
        "time_of_publish unsigned int,"
        "jsondata blob ,"
        "recvd_from unsigned int  )");

    LOG_STR2("Table for classified_ads created %d", ret);
  }
  if (ret) {
    QSqlQuery q11(aDb);
    ret = q11.exec("create index ca_hash1_ind on classified_ad(hash1)");
    LOG_STR2("Index for classified data table created %d", ret);
  }
  if (ret) {
    QSqlQuery q12(aDb);
    ret = q12.exec("create index ca_ghash1_ind on classified_ad(group_hash1)");
    LOG_STR2("Index for classified data table group hash created %d", ret);
  }

  if (ret) {
    QSqlQuery q13(aDb);
    ret = q13.exec(
        "create table private_message("
        "hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "dnode_hash1 unsigned int,"
        "dnode_hash2 unsigned int,"
        "dnode_hash3 unsigned int,"
        "dnode_hash4 unsigned int,"
        "dnode_hash5 unsigned int,"
        "recipient_hash1 unsigned int not null,"
        "recipient_hash2 unsigned int not null,"
        "recipient_hash3 unsigned int not null,"
        "recipient_hash4 unsigned int not null,"
        "recipient_hash5 unsigned int not null,"
        "sender_hash1 unsigned int,"
        "is_read int,"
        "time_last_reference unsigned int,"
        "signature varchar(1024),"
        "time_of_publish unsigned int,"
        "messagedata blob ,"
        "recvd_from unsigned int  )");

    LOG_STR2("Table for private messages created %d", ret);
  }
  if (ret) {
    QSqlQuery q14(aDb);
    ret = q14.exec("create index privmsg_hash1_ind on private_message(hash1)");
    QSqlQuery q15(aDb);
    ret = q15.exec(
        "create index privmsg_dhash1_ind on private_message(dnode_hash1)");
    QSqlQuery q16(aDb);
    ret = q16.exec(
        "create index privmsg_rhash1_ind on private_message(recipient_hash1)");
    LOG_STR2("Index for private message data table created %d", ret);
  }
  if (ret) {
    QSqlQuery q17(aDb);
    ret = q17.exec(
        "create table profilecomment("
        "hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "profile_hash1 unsigned int,"
        "profile_hash2 unsigned int,"
        "profile_hash3 unsigned int,"
        "profile_hash4 unsigned int,"
        "profile_hash5 unsigned int,"
        "sender_hash1 unsigned int,"
        "time_last_reference unsigned int,"
        "signature varchar(1024),"
        "time_of_publish unsigned int,"
        "commentdata blob ,"
        "recvd_from unsigned int,"
        "flags unsigned int,"
        "display_name varchar(160) )");  // display name might be message
                                         // subject

    LOG_STR2("Table for profile comments created %d", ret);
  }
  if (ret) {
    QSqlQuery q18(aDb);
    ret =
        q18.exec("create index profcomment_hash1_ind on profilecomment(hash1)");
    QSqlQuery q19(aDb);
    ret = q19.exec(
        "create index profcomment_phash1_ind on profilecomment(profile_hash1)");
  }
  // in sqlite there is special table type for full text search.
  if (SearchModel::queryIfFTSSupported(aDb)) {
    SearchModel::createFTSTables(aDb);
  }
  return ret;
}

// method check datastore version. if is 1, upgrades to version 2.
bool Model::createTablesV2(QSqlDatabase aDb) {
  QSqlQuery query(aDb);
  bool ret;
  ret = query.prepare("select datastore_version from settings");
  ret = query.exec();

  if (!ret) {
    QLOG_STR("select datastore_version from settings: " +
             query.lastError().text());
    emit error(MController::DbTransactionError, query.lastError().text());
  } else {
    if (query.next() && !query.isNull(0)) {
      int datastoreVersion(query.value(0).toInt());
      if (datastoreVersion == 1) {
        // upgrade datastore to version 2
        QSqlQuery q(aDb);
        ret = q.exec(
            "alter table settings add column ringtone int not null default 0");
        if (ret) {
          QSqlQuery q3(aDb);
          ret = q3.exec(
              "alter table settings add column call_acceptance int not null "
              "default 0");
          if (!ret) {
            QLOG_STR("alter table settings: " + q3.lastError().text());
            emit error(MController::DbTransactionError, q3.lastError().text());
          }
        }
        if (ret) {
          QSqlQuery q2(aDb);
          q2.exec("update settings set datastore_version = 2");
        }
      }
    }
  }
  return ret;
}

// method check datastore version. if is 2, upgrades to version 3.
bool Model::createTablesV3(QSqlDatabase aDb) {
  QSqlQuery query(aDb);
  bool ret;
  ret = query.prepare("select datastore_version from settings");
  ret = query.exec();

  if (!ret) {
    QLOG_STR("select datastore_version from settings: " +
             query.lastError().text());
    emit error(MController::DbTransactionError, query.lastError().text());
  } else {
    if (query.next() && !query.isNull(0)) {
      int datastoreVersion(query.value(0).toInt());
      if (datastoreVersion == 2) {
        // upgrade datastore to version 3
        QSqlQuery q(aDb);
        ret = q.exec(
            "create table tclprogs "
            "(hash1 unsigned int not null,"
            "hash2 unsigned int not null,"
            "hash3 unsigned int not null,"
            "hash4 unsigned int not null,"
            "hash5 unsigned int not null,"
            "prog_name varchar(256) not null,"
            "prog_text blob not null,"
            "prog_settings blob,"
            "time_modified unsigned int not null)");
        if (ret) {
          QSqlQuery q2(aDb);
          q2.exec("update settings set datastore_version = 3");
        } else {
          QLOG_STR("alter table settings: " + q.lastError().text());
          emit error(MController::DbTransactionError, q.lastError().text());
        }
      }
    }
  }
  return ret;
}

bool Model::createTablesV4(QSqlDatabase aDb) {
  QLOG_STR("createTablesV4 in");
  QSqlQuery query(aDb);
  bool ret;
  ret = query.prepare("select datastore_version from settings");
  ret = query.exec();

  if (!ret) {
    QLOG_STR("select datastore_version from settings: " +
             query.lastError().text());
    emit error(MController::DbTransactionError, query.lastError().text());
  } else {
    if (query.next() && !query.isNull(0)) {
      int datastoreVersion(query.value(0).toInt());
      if (datastoreVersion == 3) {
        // upgrade datastore to version 4
        QSqlQuery q(aDb);
        ret = q.exec(
            "create table dbrecord ( hash1 unsigned int not null,"
            "hash2 unsigned int not null,"
            "hash3 unsigned int not null,"
            "hash4 unsigned int not null,"
            "hash5 unsigned int not null,"
            "collection_hash1 unsigned int not null,"
            "collection_hash2 unsigned int not null,"
            "collection_hash3 unsigned int not null,"
            "collection_hash4 unsigned int not null,"
            "collection_hash5 unsigned int not null,"
            "sender_hash1 unsigned int not null,"
            "sender_hash2 unsigned int not null,"
            "sender_hash3 unsigned int not null,"
            "sender_hash4 unsigned int not null,"
            "sender_hash5 unsigned int not null,"
            "time_last_reference unsigned int not null,"
            "signature varchar(1024) not null,"
            "time_of_publish unsigned int not null,"
            "data blob,"
            "recvd_from  unsigned int not null,"
            "searchstring varchar(4096),"
            "searchnumber bigint,"
            "isencrypted tinyint not null,"
            "issignatureverified tinyint not null);");  // will store boolean
                                                        // value
        if (!ret) {
          QLOG_STR("create table dbrecord: " + q.lastError().text());
          emit error(MController::DbTransactionError, q.lastError().text());
        }
        if (ret && SearchModel::queryIfFTSSupported(aDb)) {
          QSqlQuery ftsQ(aDb);
          ret = ftsQ.exec(
              "create virtual table dbrecord_search using fts3(searchstring)");
          if (!ret) {
            QLOG_STR("FTS3 table dbrecord_search creation: " +
                     ftsQ.lastError().text());
            return false;
          } else {
            QSqlQuery ftsQ2(aDb);
            ret = ftsQ2.exec(
                "CREATE TRIGGER dbrecord_search_del BEFORE DELETE ON dbrecord "
                "BEGIN"
                "  DELETE FROM dbrecord_search WHERE docid=old.hash1;"
                "END;");
            if (!ret) {
              QLOG_STR("CREATE TRIGGER dbrecord_search_del: " +
                       ftsQ2.lastError().text());
              emit error(MController::DbTransactionError,
                         ftsQ2.lastError().text());
            }
          }
        } else {
          QLOG_STR("FTS was not supported");
        }
        if (ret) {
          QSqlQuery q3(aDb);
          ret = q3.exec(
              "alter table settings add column dbrecord_maxrows int not null "
              "default 1000");
          if (!ret) {
            QLOG_STR("add dbrecord_maxrows: " + q3.lastError().text());
            emit error(MController::DbTransactionError, q3.lastError().text());
            return false;
          }
          QSqlQuery q4(aDb);
          ret = q4.exec("update settings set dbrecord_maxrows = 100000");
          if (!ret) {
            QLOG_STR("update dbrecord_maxrows: " + q4.lastError().text());
            emit error(MController::DbTransactionError, q4.lastError().text());
            return false;
          }
          QSqlQuery q5(aDb);
          ret = q5.exec("alter table tclprogs add column prog_data blob");
          if (!ret) {
            QLOG_STR("alter table tclprogs: " + q5.lastError().text());
            emit error(MController::DbTransactionError, q5.lastError().text());
            return false;
          }
        }
        if (ret) {
          QSqlQuery q2(aDb);
          q2.exec("update settings set datastore_version = 4");
        }
      }
    }
  }
  return ret;
}

void Model::initPseudoRandom() {
  SSL_load_error_strings();
  SSL_library_init();
  LOG_STR2("Openssl %s\n", SSLeay_version(SSLEAY_VERSION));
#ifdef WIN32
  OpenSSL_add_all_algorithms();
#endif
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
  LOG_STR("Loading OpenSSL legacy provider for blowfish supoprt");
  /* Load Multiple providers into the default (NULL) library context */
  iLegacyCryptoProvider = OSSL_PROVIDER_load(NULL, "legacy");
  if (iLegacyCryptoProvider == NULL) {
    printf("Failed to load Legacy provider\n");
    exit(EXIT_FAILURE);
  }
  iDefaultCryptoProvider = OSSL_PROVIDER_load(NULL, "default");
  if (iDefaultCryptoProvider == NULL) {
    printf("Failed to load Default provider\n");
    OSSL_PROVIDER_unload(iLegacyCryptoProvider);
    iLegacyCryptoProvider = NULL;
    exit(EXIT_FAILURE);
  }
#endif
  // first initialize random numbah generator
  QString randomFileName("");
  if (QFile("/dev/urandom").exists()) {  // fast
    randomFileName = "/dev/urandom";
  } else if (QFile("/dev/arandom").exists()) {  // bsd?
    randomFileName = "/dev/arandom";
  } else if (QFile("/dev/random").exists()) {  // slow but good
    randomFileName = "/dev/random";
  } else {
#ifdef WIN32
#define RANDOM_LEN (15 * sizeof(quint32))
    // this code is from MSDN examples
    HCRYPTPROV hCryptProv = 0;
    BYTE pbData[RANDOM_LEN] = {0};

    LPCWSTR UserName = L"<NAME>";     // name of the key container
    if (CryptAcquireContext(&hCryptProv,    // handle to the CSP
                            UserName,       // container name
                            NULL,           // use the default provider
                            PROV_RSA_FULL,  // provider type
                            0)) {           // flag values
      QLOG_STR("Win32 seed: A cryptographic context has been acquired.");
    } else {
      if (CryptAcquireContext(&hCryptProv, UserName, NULL, PROV_RSA_FULL,
                              CRYPT_NEWKEYSET)) {
        QLOG_STR("Win32 seed: A new key container has been created.\n");<commit_msg>If OpenSSL initialization fails, provide user with meaningful error message.<commit_after>/*   -*-C++-*- -*-coding: utf-8-unix;-*-
  Classified Ads is Copyright (c) <NAME> 2013-2022.

  This file is part of Classified Ads.

  Classified Ads is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  Classified Ads 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
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with Classified Ads; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include "model.h"

#include <assert.h>
#include <stdio.h>

#include <QDir>
#include <QFile>
#include <QObject>
#include <QSqlError>
#include <QSqlQuery>
#include <QSslCertificate>
#include <QSslKey>
#include <QThread>
#include <QUuid>

#include "../log.h"
#include "../util/hash.h"
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
#include <QRandomGenerator>
#endif
#include <openssl/evp.h>   // OpenSSL_add_all_algorithms
#include <openssl/rand.h>  // random seed things
#include <openssl/ssl.h>   // library init

#include <QMutex>
#include <QTimerEvent>
#include <QWaitCondition>

#include "../net/connection.h"
#include "../net/networklistener.h"
#include "../net/node.h"
#include "../net/protocol_message_formatter.h"
#include "binaryfilemodel.h"
#include "cadbrecordmodel.h"
#include "camodel.h"
#include "contentencryptionmodel.h"
#include "netrequestexecutor.h"
#include "privmsgmodel.h"
#include "profilecommentmodel.h"
#include "profilemodel.h"
#include "searchmodel.h"
#include "trusttreemodel.h"
#ifdef WIN32
#include <Wincrypt.h>
#include <windows.h>
#endif
#include "tclmodel.h"

Model::Model(MController* aController)
    : iController(aController),
      iMutex(NULL),
      iConnections(NULL),
      iNodeModel(NULL),
      iNetReqExecutor(NULL),
      iNetReqQueue(NULL),
      iContentEncryptionModel(NULL),
      iProfileModel(NULL),
      iBinaryFileModel(NULL),
      iCaModel(NULL),
      iTimerId(-1),
      iPrivMsgModel(NULL),
      iProfileCommentModel(NULL),
      iSearchModel(NULL),
      iCaDbRecordModel(NULL),
      iTrustTreeModel(NULL),
      iTimeOfLastNetworkAddrCheck(QDateTime::currentDateTimeUtc().toTime_t()),
      iTclModel(NULL)
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
      ,
      iLegacyCryptoProvider(NULL),
      iDefaultCryptoProvider(NULL)
#endif
{
  LOG_STR("Model::Model()");
  connect(this, SIGNAL(error(MController::CAErrorSituation, const QString&)),
          iController,
          SLOT(handleError(MController::CAErrorSituation, const QString&)),
          Qt::QueuedConnection);
  iMutex = new QMutex(QMutex::Recursive);  // same thread can lock recursive
                                           // mutex multiple times
  initPseudoRandom();
  if (!openDB()) {
    QString errorText(lastError().text());
    emit(error(MController::DataBaseNotMountable, errorText));
  }
  iNodeModel = new NodeModel(aController, *this);
  iConnections = new QList<Connection*>();
  iNetReqQueue = new QList<NetworkRequestExecutor::NetworkRequestQueueItem>();
  iNetReqExecutor = new NetworkRequestExecutor(aController, *this);
  iContentEncryptionModel = new ContentEncryptionModel(aController, *this);
  iProfileModel = new ProfileModel(aController, *this);
  iBinaryFileModel = new BinaryFileModel(aController, *this);
  iCaModel = new ClassifiedAdsModel(aController, *this);
  iPrivMsgModel = new PrivMessageModel(aController, *this);
  iSearchModel = new SearchModel(*this, *iController);
  iSearchModel->setObjectName("CA SearchModel");
  iCaDbRecordModel = new CaDbRecordModel(aController, *this);
  iTrustTreeModel = new TrustTreeModel(aController, *this);
  iProfileCommentModel = new ProfileCommentModel(aController, *this);
  iNetReqExecutor->setInterval(1000);  // start is called by controller
  iTclModel = new TclModel(aController, *this);

  connect(iBinaryFileModel,
          SIGNAL(contentReceived(const Hash&, const ProtocolItemType)), this,
          SLOT(notifyOfContentReceived(const Hash&, const ProtocolItemType)),
          Qt::QueuedConnection);
  connect(
      iPrivMsgModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      this,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);

  connect(
      iProfileCommentModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      this,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);

  connect(iProfileModel,
          SIGNAL(contentReceived(const Hash&, const ProtocolItemType)), this,
          SLOT(notifyOfContentReceived(const Hash&, const ProtocolItemType)),
          Qt::QueuedConnection);
  connect(
      iCaModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      this,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);
  connect(
      iPrivMsgModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      this,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);
  // connect same to controller too:
  connect(iBinaryFileModel,
          SIGNAL(contentReceived(const Hash&, const ProtocolItemType)),
          iController,
          SLOT(notifyOfContentReceived(const Hash&, const ProtocolItemType)),
          Qt::QueuedConnection);
  connect(
      iPrivMsgModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      iController,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);
  connect(iProfileModel,
          SIGNAL(contentReceived(const Hash&, const ProtocolItemType)),
          iController,
          SLOT(notifyOfContentReceived(const Hash&, const ProtocolItemType)),
          Qt::QueuedConnection);
  connect(
      iCaModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      iController,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);
  connect(
      iPrivMsgModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      iController,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);

  connect(
      iProfileCommentModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      iController,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);

  connect(
      iCaDbRecordModel,
      SIGNAL(contentReceived(const Hash&, const Hash&, const ProtocolItemType)),
      iController,
      SLOT(notifyOfContentReceived(const Hash&, const Hash&,
                                   const ProtocolItemType)),
      Qt::QueuedConnection);

  iTimerId = startTimer(60000 * 10);  // 10-minute timer
}

Model::~Model() {
  LOG_STR("Model::~Model()");
  if (iTimerId != -1) {
    killTimer(iTimerId);
  }
  if (iNetReqExecutor) {
    iNetReqExecutor->stop();
    delete iNetReqExecutor;
  }

  closeAllConnections(true);
  delete iConnections;
  delete iNodeModel;
  delete iProfileCommentModel;
  delete iPrivMsgModel;
  delete iProfileModel;
  delete iNetReqQueue;
  delete iBinaryFileModel;
  delete iCaDbRecordModel;
  delete iSearchModel;
  delete iTrustTreeModel;
  delete iTclModel;
  delete iCaModel;
  delete iContentEncryptionModel;
  while (iDbPool.count()) {
    const QThread* firstKey(iDbPool.firstKey());
    QSqlDatabase dbToClose(iDbPool.take(firstKey));
    if (dbToClose.isOpen()) {
      dbToClose.close();
    }
    QLOG_STR("Removed db at model destructor");
  }
  iController = NULL;  // not owned, just set null
  delete iMutex;
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
  if (iLegacyCryptoProvider) {
    OSSL_PROVIDER_unload(iLegacyCryptoProvider);
  }
  if (iDefaultCryptoProvider) {
    OSSL_PROVIDER_unload(iDefaultCryptoProvider);
  }
#endif
}

void Model::closeAllConnections(bool aDeleteAlso) {
  if (iConnections) {
    LOG_STR2("Model::closeAllConnections %d", iConnections->count());
    // normally networklistener has been deleted before
    // model and networklistener is parent of connections,
    // that now have been deleted too due to parent->child
    // relationship -> usually the loop below is looped 0 times.
    for (int i = iConnections->count() - 1; i >= 0; i--) {
      Connection* c = iConnections->at(i);
      if (c) {
        c->iNeedsToRun = false;
      }
      if (aDeleteAlso) {
        delete c;
      }
    }
  }
}

bool Model::lock() {
#ifdef __DEBUG__
  assert(iMutex->tryLock(1000 * 15) == true)
#else
  iMutex->lock();
#endif
      return true;
}

void Model::unlock() { iMutex->unlock(); }

bool Model::openDB() {
  bool tables_needed = false;
  QSqlDatabase db(dataBaseConnection(&tables_needed));
  if (db.isOpen() && tables_needed) {
    if (createTables(db) == false) {
      return false;
    }
  }
  if (db.isOpen()) {
    // do cleanup to rectify previous bugs:
    QSqlQuery query(db);
    // removes every comment that does not begin with { as all json
    // is supposed but it still is not marked as private (==encrypted)
    bool ret = query.exec(
        "delete from profile where profiledata not like x'7b25' and is_private "
        "= 'false' and hash1 not in ( select hash1 from privatekeys )");
    if (ret == false) {
      QLOG_STR("Profile cleanup: " + query.lastError().text());
    } else {
      QLOG_STR("Cleaned away faulty profiles " +
               QString::number(query.numRowsAffected()));
    }
    ret = query.exec(
        "delete from profilecomment where commentdata not like x'7b25' and "
        "flags&1 = 0");
    if (ret == false) {
      QLOG_STR("Profile comment cleanup: " + query.lastError().text());
    } else {
      QLOG_STR("Cleaned away faulty comments " +
               QString::number(query.numRowsAffected()));
    }
  }

  if (db.isOpen()) {
    createTablesV2(db);
    createTablesV3(db);
    createTablesV4(db);
    QSqlQuery versionQuery("select sqlite_version()", db);
    if (versionQuery.exec() && versionQuery.next()) {
      QLOG_STR("Sqlite version " + versionQuery.value(0).toString());
    }
  }
  return db.isOpen();
}

QSqlError Model::lastError() {
  const QSqlDatabase db(dataBaseConnection());
  QSqlError e = db.lastError();
  QLOG_STR(e.text());
  return e;
}

bool Model::createTables(QSqlDatabase aDb) {
  bool ret = false;

  QSqlQuery query(aDb);
  ret = query.exec(
      "create table settings "
      "(datastore_version unsigned int primary key,"
      "node_listenport unsigned int,"
      "profile_maxrows unsigned int,"
      "private_message_maxrows unsigned int,"
      "binaryfile_maxrows unsigned int,"
      "classified_ad_maxrows unsigned int,"
      "profilecomment_maxrows unsigned int,"
      "node_maxrows unsigned int,"
      "node_key varchar(5000),"
      "node_cert varchar(5000),"
      "dns_name varchar(256))");
  if (ret) {
    QSqlQuery query(aDb);
    ret = query.exec("insert into settings (datastore_version) values (1)");
  }
  LOG_STR2("Table settings created 1 is ok, =  %d", ret);
  if (ret) {
    QSqlQuery q2(aDb);
    ret = q2.exec(
        "create table node "
        "(hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "listenport unsigned int,"
        "last_conn_time unsigned int,"
        "does_listen unsigned int,"
        "ipv4addr unsigned int,"
        "ipv6addr1 unsigned int,"
        "ipv6addr2 unsigned int,"
        "ipv6addr3 unsigned int,"
        "ipv6addr4 unsigned int,"
        "time_last_reference unsigned int,"
        "last_nodelist_time unsigned int,"
        "dns_name varchar(256),"
        "tor_name varchar(256),"
        "last_mutual_conn_time unsigned int)");
    LOG_STR2("Table node created, retval =  %d", ret);
  }
  if (ret) {
    QSqlQuery q3(aDb);
    // note how index is not unique. set of hash1+2+3+4+5 is
    // (fairly) unique but setting hash1 to be unique would
    // prevent storage of documents(nodes) where hash collides in
    // first 32 bits - that's fairly bigger chance compared
    // to hash collision in "full" 160 bits.
    ret = q3.exec("create index node_hash1_ind on node(hash1)");
    LOG_STR2("Index node(hash1) created, retval =  %d", ret);
  }
  if (ret) {
    QSqlQuery q4(aDb);
    ret = q4.exec(
        "create table privatekeys(prikey varchar(5000) not null,"
        "hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "last_msgpoll_time unsigned int,"
        "private_data blob)");
    LOG_STR2("Table for private keys created %d", ret);
  }
  if (ret) {
    QSqlQuery q5(aDb);
    ret = q5.exec(
        "create table profile(pubkey varchar(5000) not null,"
        "hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "profiledata blob,"
        "is_private int,"
        "time_last_reference unsigned int,"
        "signature varchar(1024),"
        "display_name varchar(160),"
        "time_of_publish unsigned int,"
        "recvd_from unsigned int,"
        "time_of_update_poll unsigned int)");
    LOG_STR2("Table for profile data created %d", ret);
  }
  if (ret) {
    QSqlQuery q6(aDb);
    ret = q6.exec("create index profile_hash1_ind on profile(hash1)");
    LOG_STR2("Index for profile data table created %d", ret);
  }
  if (ret) {
    QSqlQuery q7(aDb);
    // usage of the following table is this:
    // when new item (profile,ca,privmsg,binary blob) is published
    // it is inserted into this table.
    // networking module will take items (identified by hash1-5)
    // from this table one-by-one, connect to 5 nodes one-by-one
    // (marked by bangpath1-5 where each bangpath contains the
    // low-order bits of the noderef of node where item
    // has already been sent)
    // when 5th node has been filled, row is removed from table
    // and stuff is considered published
    // itemtype is one of enum ProtocolItemType from protocol.h
    //
    // some object types have actually 2 addresses and are
    // published twice: classified ads to content address and
    // the group controller address ; profile comments to
    // content address and profile address.
    ret = q7.exec(
        "create table publish(itemtype int not null,"
        "hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "secondary_addr_hash1 unsigned int ,"
        "secondary_addr_hash2 unsigned int ,"
        "secondary_addr_hash3 unsigned int ,"
        "secondary_addr_hash4 unsigned int ,"
        "secondary_addr_hash5 unsigned int ,"
        "bangpath1 unsigned int,"
        "bangpath2 unsigned int,"
        "bangpath3 unsigned int,"
        "bangpath4 unsigned int,"
        "bangpath5 unsigned int,"
        "hostcount int not null,"
        "last_time_tried unsigned int)");
    LOG_STR2("Table for publish data created %d", ret);
  }

  if (ret) {
    QSqlQuery q8(aDb);
    ret = q8.exec(
        "create table binaryfile("
        "hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "publisher_hash1 unsigned int not null,"
        "publisher_hash2 unsigned int not null,"
        "publisher_hash3 unsigned int not null,"
        "publisher_hash4 unsigned int not null,"
        "publisher_hash5 unsigned int not null,"
        "contentdata blob,"
        "is_private int,"
        "is_compressed int,"
        "time_last_reference unsigned int,"
        "signature varchar(1024),"
        "display_name varchar(160),"
        "time_of_publish unsigned int,"
        "metadata blob ,"
        "recvd_from unsigned int  )");

    LOG_STR2("Table for binary files created %d", ret);
  }
  if (ret) {
    QSqlQuery q9(aDb);
    ret = q9.exec("create index binaryfile_hash1_ind on binaryfile(hash1)");
    LOG_STR2("Index for binary blob data table created %d", ret);
  }
  if (ret) {
    QSqlQuery q10(aDb);
    ret = q10.exec(
        "create table classified_ad("
        "hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "group_hash1 unsigned int not null,"
        "group_hash2 unsigned int not null,"
        "group_hash3 unsigned int not null,"
        "group_hash4 unsigned int not null,"
        "group_hash5 unsigned int not null,"
        "reply_to unsigned int,"
        "time_last_reference unsigned int,"
        "time_added unsigned int not null,"
        "signature varchar(1024),"
        "display_name varchar(160),"
        "time_of_publish unsigned int,"
        "jsondata blob ,"
        "recvd_from unsigned int  )");

    LOG_STR2("Table for classified_ads created %d", ret);
  }
  if (ret) {
    QSqlQuery q11(aDb);
    ret = q11.exec("create index ca_hash1_ind on classified_ad(hash1)");
    LOG_STR2("Index for classified data table created %d", ret);
  }
  if (ret) {
    QSqlQuery q12(aDb);
    ret = q12.exec("create index ca_ghash1_ind on classified_ad(group_hash1)");
    LOG_STR2("Index for classified data table group hash created %d", ret);
  }

  if (ret) {
    QSqlQuery q13(aDb);
    ret = q13.exec(
        "create table private_message("
        "hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "dnode_hash1 unsigned int,"
        "dnode_hash2 unsigned int,"
        "dnode_hash3 unsigned int,"
        "dnode_hash4 unsigned int,"
        "dnode_hash5 unsigned int,"
        "recipient_hash1 unsigned int not null,"
        "recipient_hash2 unsigned int not null,"
        "recipient_hash3 unsigned int not null,"
        "recipient_hash4 unsigned int not null,"
        "recipient_hash5 unsigned int not null,"
        "sender_hash1 unsigned int,"
        "is_read int,"
        "time_last_reference unsigned int,"
        "signature varchar(1024),"
        "time_of_publish unsigned int,"
        "messagedata blob ,"
        "recvd_from unsigned int  )");

    LOG_STR2("Table for private messages created %d", ret);
  }
  if (ret) {
    QSqlQuery q14(aDb);
    ret = q14.exec("create index privmsg_hash1_ind on private_message(hash1)");
    QSqlQuery q15(aDb);
    ret = q15.exec(
        "create index privmsg_dhash1_ind on private_message(dnode_hash1)");
    QSqlQuery q16(aDb);
    ret = q16.exec(
        "create index privmsg_rhash1_ind on private_message(recipient_hash1)");
    LOG_STR2("Index for private message data table created %d", ret);
  }
  if (ret) {
    QSqlQuery q17(aDb);
    ret = q17.exec(
        "create table profilecomment("
        "hash1 unsigned int not null,"
        "hash2 unsigned int not null,"
        "hash3 unsigned int not null,"
        "hash4 unsigned int not null,"
        "hash5 unsigned int not null,"
        "profile_hash1 unsigned int,"
        "profile_hash2 unsigned int,"
        "profile_hash3 unsigned int,"
        "profile_hash4 unsigned int,"
        "profile_hash5 unsigned int,"
        "sender_hash1 unsigned int,"
        "time_last_reference unsigned int,"
        "signature varchar(1024),"
        "time_of_publish unsigned int,"
        "commentdata blob ,"
        "recvd_from unsigned int,"
        "flags unsigned int,"
        "display_name varchar(160) )");  // display name might be message
                                         // subject

    LOG_STR2("Table for profile comments created %d", ret);
  }
  if (ret) {
    QSqlQuery q18(aDb);
    ret =
        q18.exec("create index profcomment_hash1_ind on profilecomment(hash1)");
    QSqlQuery q19(aDb);
    ret = q19.exec(
        "create index profcomment_phash1_ind on profilecomment(profile_hash1)");
  }
  // in sqlite there is special table type for full text search.
  if (SearchModel::queryIfFTSSupported(aDb)) {
    SearchModel::createFTSTables(aDb);
  }
  return ret;
}

// method check datastore version. if is 1, upgrades to version 2.
bool Model::createTablesV2(QSqlDatabase aDb) {
  QSqlQuery query(aDb);
  bool ret;
  ret = query.prepare("select datastore_version from settings");
  ret = query.exec();

  if (!ret) {
    QLOG_STR("select datastore_version from settings: " +
             query.lastError().text());
    emit error(MController::DbTransactionError, query.lastError().text());
  } else {
    if (query.next() && !query.isNull(0)) {
      int datastoreVersion(query.value(0).toInt());
      if (datastoreVersion == 1) {
        // upgrade datastore to version 2
        QSqlQuery q(aDb);
        ret = q.exec(
            "alter table settings add column ringtone int not null default 0");
        if (ret) {
          QSqlQuery q3(aDb);
          ret = q3.exec(
              "alter table settings add column call_acceptance int not null "
              "default 0");
          if (!ret) {
            QLOG_STR("alter table settings: " + q3.lastError().text());
            emit error(MController::DbTransactionError, q3.lastError().text());
          }
        }
        if (ret) {
          QSqlQuery q2(aDb);
          q2.exec("update settings set datastore_version = 2");
        }
      }
    }
  }
  return ret;
}

// method check datastore version. if is 2, upgrades to version 3.
bool Model::createTablesV3(QSqlDatabase aDb) {
  QSqlQuery query(aDb);
  bool ret;
  ret = query.prepare("select datastore_version from settings");
  ret = query.exec();

  if (!ret) {
    QLOG_STR("select datastore_version from settings: " +
             query.lastError().text());
    emit error(MController::DbTransactionError, query.lastError().text());
  } else {
    if (query.next() && !query.isNull(0)) {
      int datastoreVersion(query.value(0).toInt());
      if (datastoreVersion == 2) {
        // upgrade datastore to version 3
        QSqlQuery q(aDb);
        ret = q.exec(
            "create table tclprogs "
            "(hash1 unsigned int not null,"
            "hash2 unsigned int not null,"
            "hash3 unsigned int not null,"
            "hash4 unsigned int not null,"
            "hash5 unsigned int not null,"
            "prog_name varchar(256) not null,"
            "prog_text blob not null,"
            "prog_settings blob,"
            "time_modified unsigned int not null)");
        if (ret) {
          QSqlQuery q2(aDb);
          q2.exec("update settings set datastore_version = 3");
        } else {
          QLOG_STR("alter table settings: " + q.lastError().text());
          emit error(MController::DbTransactionError, q.lastError().text());
        }
      }
    }
  }
  return ret;
}

bool Model::createTablesV4(QSqlDatabase aDb) {
  QLOG_STR("createTablesV4 in");
  QSqlQuery query(aDb);
  bool ret;
  ret = query.prepare("select datastore_version from settings");
  ret = query.exec();

  if (!ret) {
    QLOG_STR("select datastore_version from settings: " +
             query.lastError().text());
    emit error(MController::DbTransactionError, query.lastError().text());
  } else {
    if (query.next() && !query.isNull(0)) {
      int datastoreVersion(query.value(0).toInt());
      if (datastoreVersion == 3) {
        // upgrade datastore to version 4
        QSqlQuery q(aDb);
        ret = q.exec(
            "create table dbrecord ( hash1 unsigned int not null,"
            "hash2 unsigned int not null,"
            "hash3 unsigned int not null,"
            "hash4 unsigned int not null,"
            "hash5 unsigned int not null,"
            "collection_hash1 unsigned int not null,"
            "collection_hash2 unsigned int not null,"
            "collection_hash3 unsigned int not null,"
            "collection_hash4 unsigned int not null,"
            "collection_hash5 unsigned int not null,"
            "sender_hash1 unsigned int not null,"
            "sender_hash2 unsigned int not null,"
            "sender_hash3 unsigned int not null,"
            "sender_hash4 unsigned int not null,"
            "sender_hash5 unsigned int not null,"
            "time_last_reference unsigned int not null,"
            "signature varchar(1024) not null,"
            "time_of_publish unsigned int not null,"
            "data blob,"
            "recvd_from  unsigned int not null,"
            "searchstring varchar(4096),"
            "searchnumber bigint,"
            "isencrypted tinyint not null,"
            "issignatureverified tinyint not null);");  // will store boolean
                                                        // value
        if (!ret) {
          QLOG_STR("create table dbrecord: " + q.lastError().text());
          emit error(MController::DbTransactionError, q.lastError().text());
        }
        if (ret && SearchModel::queryIfFTSSupported(aDb)) {
          QSqlQuery ftsQ(aDb);
          ret = ftsQ.exec(
              "create virtual table dbrecord_search using fts3(searchstring)");
          if (!ret) {
            QLOG_STR("FTS3 table dbrecord_search creation: " +
                     ftsQ.lastError().text());
            return false;
          } else {
            QSqlQuery ftsQ2(aDb);
            ret = ftsQ2.exec(
                "CREATE TRIGGER dbrecord_search_del BEFORE DELETE ON dbrecord "
                "BEGIN"
                "  DELETE FROM dbrecord_search WHERE docid=old.hash1;"
                "END;");
            if (!ret) {
              QLOG_STR("CREATE TRIGGER dbrecord_search_del: " +
                       ftsQ2.lastError().text());
              emit error(MController::DbTransactionError,
                         ftsQ2.lastError().text());
            }
          }
        } else {
          QLOG_STR("FTS was not supported");
        }
        if (ret) {
          QSqlQuery q3(aDb);
          ret = q3.exec(
              "alter table settings add column dbrecord_maxrows int not null "
              "default 1000");
          if (!ret) {
            QLOG_STR("add dbrecord_maxrows: " + q3.lastError().text());
            emit error(MController::DbTransactionError, q3.lastError().text());
            return false;
          }
          QSqlQuery q4(aDb);
          ret = q4.exec("update settings set dbrecord_maxrows = 100000");
          if (!ret) {
            QLOG_STR("update dbrecord_maxrows: " + q4.lastError().text());
            emit error(MController::DbTransactionError, q4.lastError().text());
            return false;
          }
          QSqlQuery q5(aDb);
          ret = q5.exec("alter table tclprogs add column prog_data blob");
          if (!ret) {
            QLOG_STR("alter table tclprogs: " + q5.lastError().text());
            emit error(MController::DbTransactionError, q5.lastError().text());
            return false;
          }
        }
        if (ret) {
          QSqlQuery q2(aDb);
          q2.exec("update settings set datastore_version = 4");
        }
      }
    }
  }
  return ret;
}

void Model::initPseudoRandom() {
  SSL_load_error_strings();
  SSL_library_init();
  LOG_STR2("Openssl %s\n", SSLeay_version(SSLEAY_VERSION));
#ifdef WIN32
  OpenSSL_add_all_algorithms();
#endif
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
  LOG_STR("Loading OpenSSL legacy provider for blowfish supoprt");
  /* Load Multiple providers into the default (NULL) library context */
  iLegacyCryptoProvider = OSSL_PROVIDER_load(NULL, "legacy");
  if (iLegacyCryptoProvider == NULL) {
    emit error(MController::ContentEncryptionError, "OpenSSL failed to load Legacy provider");
    printf("Failed to load Legacy provider\n");
  }
  iDefaultCryptoProvider = OSSL_PROVIDER_load(NULL, "default");
  if (iDefaultCryptoProvider == NULL) {
    printf("Failed to load Default provider\n");
    emit error(MController::ContentEncryptionError, "OpenSSL failed to load Default provider");    
    OSSL_PROVIDER_unload(iLegacyCryptoProvider);
    iLegacyCryptoProvider = NULL;
  }
#endif
  // first initialize random numbah generator
  QString randomFileName("");
  if (QFile("/dev/urandom").exists()) {  // fast
    randomFileName = "/dev/urandom";
  } else if (QFile("/dev/arandom").exists()) {  // bsd?
    randomFileName = "/dev/arandom";
  } else if (QFile("/dev/random").exists()) {  // slow but good
    randomFileName = "/dev/random";
  } else {
#ifdef WIN32
#define RANDOM_LEN (15 * sizeof(quint32))
    // this code is from MSDN examples
    HCRYPTPROV hCryptProv = 0;
    BYTE pbData[RANDOM_LEN] = {0};

    LPCWSTR UserName = L"<NAME>";     // name of the key container
    if (CryptAcquireContext(&hCryptProv,    // handle to the CSP
                            UserName,       // container name
                            NULL,           // use the default provider
                            PROV_RSA_FULL,  // provider type
                            0)) {           // flag values
      QLOG_STR("Win32 seed: A cryptographic context has been acquired.");
    } else {
      if (CryptAcquireContext(&hCryptProv, UserName, NULL, PROV_RSA_FULL,
                              CRYPT_NEWKEYSET)) {
        QLOG_STR("Win32 seed: A new key container has been created.\n");
{'fraction_non_alphanumeric': 0.10733440821295778, 'fraction_numerical': 0.015136060017768418, 'mean_word_length': 2.654361810857933, 'pattern_counts': {'":': 0, '<': 51, '<?xml version=': 0, '>': 85, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 6, 'word_list_counts': {'cursed_substrings.json': 34, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7205707', 'n_tokens_mistral': 18472, 'n_tokens_neox': 17862, 'n_words': 5018}