<commit_before>// vim: fdm=marker et ts=2 sw=2 tw=80 fo=tcqwn list

package com.debiki.v0

import com.debiki.v0.Prelude._
import com.google.{common => guava}
import java.{util => ju}
import Dao._
import EmailNotfPrefs.EmailNotfPrefs


// SECURITY Todo: Only pass data to Dao via model class instances! (?)
// Never directly e.g. in a String.
// Let each model class validate itself, e.g. check that each String
// conforms to the required format (e.g. [a-z0-9_]+ for page ids).
// Search for "String" in this file -- no match must be found!
// Could actually add a build pipe step that searches this file for String
// and fails the build shoud any String be found?
// (Unless you use model classes, and only model classes, when passing
// data to/from the Dao, then eventually you will forget to validate
// and sanitaze input. That'd be an eventually inconsistent solution :-/ .)


/**
 * Constructs database DAO:s, implemented by service providers,
 * (currently only debiki-dao-pgsql, for Postgres) and used by debiki-app-play.
 */
abstract class DbDaoFactory {
  def systemDbDao: SystemDbDao
  def newTenantDbDao(quotaConsumers: QuotaConsumers): TenantDbDao
}



/**
 * Debiki's database Data Access Object (DAO).
 *
 * It's named "DbDao" not simply "Dao" because there are other kinds
 * of DAO:s, e.g. a cache DAO that reads stuff from the database, and
 * constructs objects and caches them in-memory / on disk. Perhaps there
 * could be a web service DAO as well.
 */
abstract class TenantDbDao {

  def quotaConsumers: QuotaConsumers

  def tenantId: String

  def loadTenant(): Tenant

  def createWebsite(name: String, address: String, ownerIp: String,
        ownerLoginId: String, ownerIdentity: IdentityOpenId, ownerRole: User)
        : Option[Tenant]

  def addTenantHost(host: TenantHost)

  def lookupOtherTenant(scheme: String, host: String): TenantLookup

  def saveLogin(loginReq: LoginRequest): LoginGrant

  def saveLogout(loginId: String, logoutIp: String)

  def createPage(page: PageStuff): PageStuff

  def loadPageMeta(pageId: String): Option[PageMeta]

  def movePages(pageIds: Seq[String], fromFolder: String, toFolder: String)

  def moveRenamePage(pageId: String,
        newFolder: Option[String], showId: Option[Boolean],
        newSlug: Option[String]): PagePath

  def loadTemplate(templPath: PagePath): Option[TemplateSrcHtml]

  def checkPagePath(pathToCheck: PagePath): Option[PagePath]

  def lookupPagePathByPageId(pageId: String): Option[PagePath]

  def listChildPages(parentPageId: String, sortBy: PageSortOrder,
        limit: Int, offset: Int): Seq[(PagePath, PageDetails)]

  def savePageActions[T <: Action](debateId: String, xs: List[T]): List[T]

  def loadPage(debateId: String): Option[Debate]

  def loadPageBodiesTitles(pagePaths: Seq[PagePath])
        : Seq[(PagePath, Option[Debate])]

  def loadRecentActionExcerpts(fromIp: Option[String],
        byIdentity: Option[String],
        pathRanges: PathRanges, limit: Int): (Seq[ViAc], People)

  def listPagePaths(
        pageRanges: PathRanges,
        include: List[PageStatus],
        sortBy: PageSortOrder,
        limit: Int,
        offset: Int
      ): Seq[(PagePath, PageDetails)]

  def loadIdtyAndUser(forLoginId: String): Option[(Identity, User)]

  def loadIdtyDetailsAndUser(forLoginId: String = null,
        forIdentity: Identity = null): Option[(Identity, User)]

  def loadPermsOnPage(reqInfo: RequestInfo): PermsOnPage

  def saveNotfs(notfs: Seq[NotfOfPageAction])

  def loadNotfsForRole(roleId: String): Seq[NotfOfPageAction]

  def loadNotfByEmailId(emailId: String): Option[NotfOfPageAction]

  def skipEmailForNotfs(notfs: Seq[NotfOfPageAction], debug: String): Unit

  def saveUnsentEmail(email: Email): Unit

  def saveUnsentEmailConnectToNotfs(email: Email,
        notfs: Seq[NotfOfPageAction]): Unit

  def updateSentEmail(email: Email): Unit

  def loadEmailById(emailId: String): Option[Email]

  def configRole(loginId: String, ctime: ju.Date,
                    roleId: String, emailNotfPrefs: EmailNotfPrefs)

  def configIdtySimple(loginId: String, ctime: ju.Date,
                       emailAddr: String, emailNotfPrefs: EmailNotfPrefs)

}


abstract class SystemDbDao {

  def close()  // remove? move to DbDaoFactory in some manner?


  // ----- Websites (a.k.a. tenants)

  /**
   * Creates the very first tenant, assigns it an id and and returns it.
   *
   * It's different from TenantDbDao.createWebsite(), because there cannot be
   * any creator of this tenant, because there are not yet any users or roles
   * (since there are on other tenants).
   */
  // COULD rename to createFirstWebsite
  def createTenant(name: String): Tenant

  // COULD rename to loadWebsitesByIds
  def loadTenants(tenantIds: Seq[String]): Seq[Tenant]

  // COULD rename to findWebsitesCanonicalHost
  def lookupTenant(scheme: String, host: String): TenantLookup


  // ----- Emails

  def loadNotfsToMailOut(delayInMinutes: Int, numToLoad: Int): NotfsToMail


  // ----- Quota

  def loadQuotaState(consumers: Seq[QuotaConsumer])
        : Map[QuotaConsumer, QuotaState]

  /**
   * Adds `delta.deltaQuota` and `.deltaResources` to the amount of quota
   * and resources used, for the specified consumers.
   * Also updates limits and timestamp.
   * Creates new consumer quota entries if needed.
   */
  def useMoreQuotaUpdateLimits(deltas: Map[QuotaConsumer, QuotaDelta])


  // ----- Misc

  def checkRepoVersion(): Option[String]

  /** Used as salt when hashing e.g. email and IP, before the hash
   *  is included in HTML. */
  def secretSalt(): String

}



/**
 * Debiki's Data Access Object, for tenant specific data.
 *
 * Delegates database requests to a TenantDbDao implementation.
 */
class TenantDao(
   private val _spi: TenantDbDao,
   protected val _quotaCharger: QuotaCharger) {

  import com.debiki.v0.{ResourceUse => ResUsg}
<commit_msg>Rename TenantDao to ChargingTenantDbDao, extend TenantDbDao<commit_after>// vim: fdm=marker et ts=2 sw=2 tw=80 fo=tcqwn list

package com.debiki.v0

import com.debiki.v0.Prelude._
import com.google.{common => guava}
import java.{util => ju}
import Dao._
import EmailNotfPrefs.EmailNotfPrefs


// SECURITY Todo: Only pass data to Dao via model class instances! (?)
// Never directly e.g. in a String.
// Let each model class validate itself, e.g. check that each String
// conforms to the required format (e.g. [a-z0-9_]+ for page ids).
// Search for "String" in this file -- no match must be found!
// Could actually add a build pipe step that searches this file for String
// and fails the build shoud any String be found?
// (Unless you use model classes, and only model classes, when passing
// data to/from the Dao, then eventually you will forget to validate
// and sanitaze input. That'd be an eventually inconsistent solution :-/ .)


/**
 * Constructs database DAO:s, implemented by service providers,
 * (currently only debiki-dao-pgsql, for Postgres) and used by debiki-app-play.
 */
abstract class DbDaoFactory {
  def systemDbDao: SystemDbDao
  def newTenantDbDao(quotaConsumers: QuotaConsumers): TenantDbDao
}



/**
 * Debiki's database Data Access Object (DAO).
 *
 * It's named "DbDao" not simply "Dao" because there are other kinds
 * of DAO:s, e.g. a cache DAO that reads stuff from the database, and
 * constructs objects and caches them in-memory / on disk. Perhaps there
 * could be a web service DAO as well.
 */
abstract class TenantDbDao {

  def quotaConsumers: QuotaConsumers

  def tenantId: String

  def loadTenant(): Tenant

  def createWebsite(name: String, address: String, ownerIp: String,
        ownerLoginId: String, ownerIdentity: IdentityOpenId, ownerRole: User)
        : Option[Tenant]

  def addTenantHost(host: TenantHost)

  def lookupOtherTenant(scheme: String, host: String): TenantLookup

  def saveLogin(loginReq: LoginRequest): LoginGrant

  def saveLogout(loginId: String, logoutIp: String)

  def createPage(page: PageStuff): PageStuff

  def loadPageMeta(pageId: String): Option[PageMeta]

  def movePages(pageIds: Seq[String], fromFolder: String, toFolder: String)

  def moveRenamePage(pageId: String,
        newFolder: Option[String] = None, showId: Option[Boolean] = None,
        newSlug: Option[String] = None): PagePath

  def loadTemplate(templPath: PagePath): Option[TemplateSrcHtml]

  def checkPagePath(pathToCheck: PagePath): Option[PagePath]

  def lookupPagePathByPageId(pageId: String): Option[PagePath]

  def listChildPages(parentPageId: String, sortBy: PageSortOrder,
        limit: Int, offset: Int = 0): Seq[(PagePath, PageDetails)]

  def savePageActions[T <: Action](debateId: String, xs: List[T]): List[T]

  def loadPage(debateId: String): Option[Debate]

  def loadPageBodiesTitles(pagePaths: Seq[PagePath])
        : Seq[(PagePath, Option[Debate])]

  def loadRecentActionExcerpts(
        fromIp: Option[String] = None,
        byIdentity: Option[String] = None,
        pathRanges: PathRanges = PathRanges.Anywhere,
        limit: Int): (Seq[ViAc], People)

  def listPagePaths(
        pageRanges: PathRanges,
        include: List[PageStatus],
        sortBy: PageSortOrder,
        limit: Int,
        offset: Int
      ): Seq[(PagePath, PageDetails)]

  def loadIdtyAndUser(forLoginId: String): Option[(Identity, User)]

  def loadIdtyDetailsAndUser(forLoginId: String = null,
        forIdentity: Identity = null): Option[(Identity, User)]

  def loadPermsOnPage(reqInfo: RequestInfo): PermsOnPage

  def saveNotfs(notfs: Seq[NotfOfPageAction])

  def loadNotfsForRole(roleId: String): Seq[NotfOfPageAction]

  def loadNotfByEmailId(emailId: String): Option[NotfOfPageAction]

  def skipEmailForNotfs(notfs: Seq[NotfOfPageAction], debug: String): Unit

  def saveUnsentEmail(email: Email): Unit

  def saveUnsentEmailConnectToNotfs(email: Email,
        notfs: Seq[NotfOfPageAction]): Unit

  def updateSentEmail(email: Email): Unit

  def loadEmailById(emailId: String): Option[Email]

  def configRole(loginId: String, ctime: ju.Date,
                    roleId: String, emailNotfPrefs: EmailNotfPrefs)

  def configIdtySimple(loginId: String, ctime: ju.Date,
                       emailAddr: String, emailNotfPrefs: EmailNotfPrefs)

}


abstract class SystemDbDao {

  def close()  // remove? move to DbDaoFactory in some manner?


  // ----- Websites (a.k.a. tenants)

  /**
   * Creates the very first tenant, assigns it an id and and returns it.
   *
   * It's different from TenantDbDao.createWebsite(), because there cannot be
   * any creator of this tenant, because there are not yet any users or roles
   * (since there are on other tenants).
   */
  // COULD rename to createFirstWebsite
  def createTenant(name: String): Tenant

  // COULD rename to loadWebsitesByIds
  def loadTenants(tenantIds: Seq[String]): Seq[Tenant]

  // COULD rename to findWebsitesCanonicalHost
  def lookupTenant(scheme: String, host: String): TenantLookup


  // ----- Emails

  def loadNotfsToMailOut(delayInMinutes: Int, numToLoad: Int): NotfsToMail


  // ----- Quota

  def loadQuotaState(consumers: Seq[QuotaConsumer])
        : Map[QuotaConsumer, QuotaState]

  /**
   * Adds `delta.deltaQuota` and `.deltaResources` to the amount of quota
   * and resources used, for the specified consumers.
   * Also updates limits and timestamp.
   * Creates new consumer quota entries if needed.
   */
  def useMoreQuotaUpdateLimits(deltas: Map[QuotaConsumer, QuotaDelta])


  // ----- Misc

  def checkRepoVersion(): Option[String]

  /** Used as salt when hashing e.g. email and IP, before the hash
   *  is included in HTML. */
  def secretSalt(): String

}



/**
 * Charges the tenants with some quota for each db request.
 *
 * (It delegates database requests to a TenantDbDao implementation.)
 */
class ChargingTenantDbDao(
  private val _spi: TenantDbDao,
  protected val _quotaCharger: QuotaCharger)
  extends TenantDbDao {

  import com.debiki.v0.{ResourceUse => ResUsg}

{'fraction_non_alphanumeric': 0.09870238593553789, 'fraction_numerical': 0.001590623691921306, 'mean_word_length': 3.8739290085679317, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1352849', 'n_tokens_mistral': 3883, 'n_tokens_neox': 3698, 'n_words': 1182}