<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* 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 3 of the License, or
* (at your option) any later version.
*
* 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, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\models;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\library\UrlParser;
use seekquarry\yioop\library\mail\DkimKey;
use seekquarry\yioop\models\datasources\DatasourceManager;
use seekquarry\yioop\models\datasources\Sqlite3Manager;
/**
* This is class is used to handle
* getting and saving the Profile.php of the current search engine instance
*
* @author Chris Pollett
*/
class ProfileModel extends Model
{
/**
* Canonical defaults for profile fields whose values must be
* non-empty when the admin lands on a settings form for the
* first time, or whose value would otherwise be clobbered by
* an empty nsdefine in Profile.php. Lives in code rather than
* the C\* config constants because Profile.php is included
* BEFORE Config.php's nsconddefine calls run -- which means
* once an empty value lands in the profile file, the
* conditional define in Config.php never gets a chance to
* supply a real default.
*
* @var array
*/
public static $profile_defaults = [
'AUTH_METHOD' => C\LOCAL_AUTHENTICATION,
'PASSWORD_MIN_LEN' => C\PASSWORD_MIN_LEN,
'PASSWORD_REQUIRE_LOWERCASE' => C\PASSWORD_REQUIRE_LOWERCASE,
'PASSWORD_REQUIRE_UPPERCASE' => C\PASSWORD_REQUIRE_UPPERCASE,
'PASSWORD_REQUIRE_DIGIT' => C\PASSWORD_REQUIRE_DIGIT,
'PASSWORD_REQUIRE_SYMBOL' => C\PASSWORD_REQUIRE_SYMBOL,
'LDAP_CONTROLLERS' => C\LDAP_CONTROLLERS,
'LDAP_ACCOUNT_SUFFIX' => C\LDAP_ACCOUNT_SUFFIX,
'LDAP_BASE_DN' => C\LDAP_BASE_DN,
'MAIL_SMTP_PORT' => 25,
'MAIL_SMTPS_PORT' => 465,
'MAIL_IMAP_PORT' => 143,
'MAIL_IMAPS_PORT' => 993,
'ALPN_PROTOCOLS' => 'h2,http/1.1',
'GIT_DEFAULT_BRANCH' => C\GIT_DEFAULT_BRANCH,
'GIT_MAX_BLOB_SIZE' => C\GIT_MAX_BLOB_SIZE,
'GIT_MAX_PUSH_SIZE' => C\GIT_MAX_PUSH_SIZE,
];
/**
* These are fields whose values might be set in a Yioop instance
* Profile.php file
* @var array
*/
public $profile_fields = ['AD_LOCATION', 'API_ACCESS', 'AUTH_KEY',
'AUTH_METHOD', 'PASSWORD_MIN_LEN', 'PASSWORD_REQUIRE_LOWERCASE',
'PASSWORD_REQUIRE_UPPERCASE', 'PASSWORD_REQUIRE_DIGIT',
'PASSWORD_REQUIRE_SYMBOL',
'LDAP_CONTROLLERS', 'LDAP_ACCOUNT_SUFFIX', 'LDAP_BASE_DN',
'AUTOLOGOUT', 'AUXILIARY_CSS', 'AUXILIARY_CSS_NAME',
'BACKGROUND_COLOR', 'BACKGROUND_IMAGE',
'CACHE_LINK', 'CLD_URL_BONUS', 'CONFIGURE_BOT',
'COOKIE_LIFETIME', 'CSRF_TOKEN', 'DEBUG_LEVEL',
'DEFAULT_CONTINUOUS_SCROLL', 'DB_HOST', 'DBMS', 'DB_NAME',
'DB_PASSWORD', 'DB_USER', 'DEFAULT_LOCALE', 'DIFFERENTIAL_PRIVACY',
'FAVICON', 'FOREGROUND_COLOR', 'GIT_DEFAULT_BRANCH',
'GIT_MAX_BLOB_SIZE', 'GIT_MAX_PUSH_SIZE',
'GLOBAL_ADSCRIPT', 'GROUP_ITEM',
'GROUP_ANALYTICS_MODE', 'HOST_KEYWORD_BONUS', 'HOST_URL_BONUS',
'IN_LINK', 'IP_LINK', 'LANDING_PAGE', 'LOGO_SMALL', 'LOGO_MEDIUM',
'LOGO_LARGE', 'MAIL_DOMAINS', 'MAIL_HOST_NAME',
'MAIL_IMAPS_PORT', 'MAIL_EXTERNAL',
'ACME_ON', 'SECURE_DOMAINS', 'WWW_USER', 'ALPN_PROTOCOLS',
'MAIL_IMAP_PORT', 'MAIL_LOG_ENABLED', 'MAIL_MODE',
'MAIL_DELIVERY_SECURITY', 'MAIL_DMARC_ENFORCE',
'MAIL_USE_STARTTLS',
'MAIL_PASSWORD', 'MAIL_SECURITY', 'MAIL_BOT_HANDLED',
'MAIL_SENDER', 'MAIL_SERVER', 'MAIL_SERVERPORT', 'MAIL_SMTPS_PORT',
'MAIL_SMTP_PORT', 'MAIL_SUBMISSION_PORT',
'MAIL_TEST_MODE',
'MAIL_TEST_MODE_FALLBACK_PORT', 'MAIL_USERNAME',
'MIN_RESULTS_TO_GROUP', 'MONETIZATION_TYPE', 'MORE_RESULT',
'MEDIA_MODE', 'MODERATION_FLAG_THRESHOLD', 'USE_MODERATION',
'NAME_SERVER', 'NUM_SLASHES_BONUS', 'PATH_KEYWORD_BONUS',
'PRIVATE_DB_NAME', 'PRIVATE_DB_HOST', 'PRIVATE_DBMS',
'PRIVATE_DB_PASSWORD', 'PRIVATE_DB_USER', 'PROXY_SERVERS',
'RECOVERY_MODE', 'REGISTRATION_TYPE', 'RESULT_SCORE',
'ROBOT_INSTANCE','RSS_ACCESS', 'SEARCH_ANALYTICS_MODE',
'SEARCHBAR_PATH', 'SEND_MAIL_MEDIA_UPDATER', 'SERP_FAVICONS',
'SESSION_NAME', 'SIDE_ADSCRIPT', 'SIDEBAR_COLOR', 'SIGNIN_LINK',
'SIMILAR_LINK', 'SITE_MOTTO', 'SITE_NAME', 'SUBSEARCH_LINK',
'TITLE_BONUS', 'TIMEZONE',
'TOPBAR_COLOR', 'TOP_ADSCRIPT','TOR_PROXY', 'USE_FILECACHE',
'USE_PROXY', 'USER_AGENT_SHORT', 'USER_RANK_BONUS',
'WEB_URI', 'WEB_ACCESS', 'WIKI_BONUS', 'WORD_CLOUD', 'WORD_SUGGEST'
];
/**
* Profile fields which are stored in wiki or in a flat file
* @var array
*/
public $file_fields = ["AUXILIARY_CSS", "ROBOT_DESCRIPTION"];
/**
* Associative array (table_name => SQL statement to create that table
* for the pubblic database)
* List is alphabetical and contains all Yioop tables. List is only
* initialized after an @see initializeSql call.
* @var array
*/
public $create_statements;
/**
* Associative array (table_name => SQL statement to create that table
* for the private database)
* List is alphabetical and contains all Yioop tables. List is only
* initialized after an @see initializeSql call.
* @var array
*/
public $private_create_statements;
/**
* {@inheritDoc}
*
* @param string $db_name the name of the database for the search engine
* @param bool $connect whether to connect to the database by default
* after making the datasource class
*/
public function __construct($db_name = C\DB_NAME, $connect = true)
{
parent::__construct($db_name, $connect);
$this->create_statements = [];
$this->private_create_statements = [];
}
/**
* Used to construct $this->create_statements, the list of all SQL
* CREATE statements needed to build a Yioop database
*
* @param object $dbm a datasource_manager object used to get strings
* for autoincrement and serial types for a given db
* @param array $dbinfo connect info for the database, also used in
* getting autoincrement and serial types
*/
public function initializeSql($dbm, $dbinfo)
{
$auto_increment = $dbm->autoIncrement($dbinfo);
$serial = $dbm->serialType($dbinfo);
$integer = $dbm->integerType($dbinfo);
$page_type = $dbm->pageType($dbinfo);
$scraper_text = (stristr($dbinfo['DBMS'], "mysql") !== false) ?
" TEXT " : " VARCHAR(" . (10 * C\MAX_URL_LEN) . ") ";
$user_session_text = (stristr($dbinfo['DBMS'], "mysql") !== false) ?
" TEXT " : " VARCHAR(". C\MAX_USER_SESSION_SIZE . ") ";
$appearance_text = (stristr($dbinfo['DBMS'], "mysql") !== false) ?
" TEXT " : " VARCHAR(" . (10 * C\MAX_URL_LEN) . ") ";
/**
* SQL statements used to create the Yioop database. Some of the
* these statements could use UNIQUE on some the columns that are
* later used in CREATE INDEX statements. However, because of
* restrictions on the number of bytes (not chars) in MYSQL for keys
* this has not been done.
*/
$this->create_statements = [
"ACTIVE_PROCESS" =>
"CREATE TABLE ACTIVE_PROCESS (NAME VARCHAR(".C\NAME_LEN.
"), ID $integer, TYPE VARCHAR(" . C\NAME_LEN . "))",
"ADVERTISEMENT" => "CREATE TABLE ADVERTISEMENT (ID
$serial PRIMARY KEY $auto_increment, USER_ID $integer,
NAME VARCHAR(". C\ADVERTISEMENT_NAME_LEN ."),
DESCRIPTION VARCHAR(". C\ADVERTISEMENT_TEXT_LEN ."),
DESTINATION VARCHAR(". C\ADVERTISEMENT_DESTINATION_LEN ."),
KEYWORDS VARCHAR(". C\ADVERTISEMENT_KEYWORD_LEN ."),
STATUS $integer, BUDGET $integer, CLICKS $integer,
IMPRESSIONS $integer, START_DATE VARCHAR(".
C\ADVERTISEMENT_DATE_LEN ."), END_DATE VARCHAR(".
C\ADVERTISEMENT_DATE_LEN ."))",
"ACCEPTED_AD_BIDS" => "CREATE TABLE ACCEPTED_AD_BIDS
(AD_ID $integer, KEYWORD VARCHAR(". C\ADVERTISEMENT_KEYWORD_LEN
."), BID_AMOUNT $integer, BID_DATE VARCHAR(".
C\ADVERTISEMENT_DATE_LEN ."))",
"AAD_KEYWORD_DATE_INDEX" => "CREATE INDEX
AAD_KEYWORD_DATE_INDEX ON ACCEPTED_AD_BIDS(KEYWORD, BID_DATE)",
"ACTIVITY" => "CREATE TABLE ACTIVITY (ACTIVITY_ID $serial
PRIMARY KEY $auto_increment, TRANSLATION_ID $integer,
METHOD_NAME VARCHAR(" . C\LONG_NAME_LEN . "))",
"ACTIVITY_TRANSLATION_ID_INDEX" => "CREATE INDEX
ACTIVITY_TRANSLATION_ID_INDEX ON ACTIVITY (TRANSLATION_ID)",
"CHAT_BOT" => "CREATE TABLE CHAT_BOT(
USER_ID $integer PRIMARY KEY, BOT_TOKEN CHAR(". C\TIMESTAMP_LEN.
") UNIQUE, CALLBACK_URL VARCHAR(" . C\MAX_URL_LEN . "))",
"CHAT_BOT_PATTERN" => "CREATE TABLE CHAT_BOT_PATTERN(
PATTERN_ID $serial PRIMARY KEY $auto_increment,
USER_ID $integer,
REQUEST VARCHAR(" . C\MAX_DESCRIPTION_LEN ."),
TRIGGER_STATE VARCHAR(" . C\NAME_LEN . "),
REMOTE_MESSAGE VARCHAR(" . C\MAX_DESCRIPTION_LEN . "),
RESULT_STATE VARCHAR(" . C\NAME_LEN . "),
RESPONSE VARCHAR(" . C\MAX_DESCRIPTION_LEN ."))",
"CRAWL_MIXES" => "CREATE TABLE CRAWL_MIXES (TIMESTAMP NUMERIC(".
C\TIMESTAMP_LEN.")
PRIMARY KEY, NAME VARCHAR(" . C\NAME_LEN .
"), OWNER_ID $integer,
PARENT NUMERIC(" . C\TIMESTAMP_LEN . "))",
"CM_OWNER_ID_INDEX" => "CREATE INDEX CM_OWNER_ID_INDEX ON
CRAWL_MIXES (OWNER_ID)",
"CM_PARENT_INDEX" => "CREATE INDEX CM_PARENT_INDEX ON
CRAWL_MIXES (PARENT)",
"CREDIT_LEDGER" => "CREATE TABLE CREDIT_LEDGER
(USER_ID $integer, AMOUNT $integer, TYPE VARCHAR(" .
C\NAME_LEN . "), BALANCE $integer, TIMESTAMP NUMERIC(" .
C\TIMESTAMP_LEN . "))",
"CL_USER_INDEX" => "CREATE INDEX CL_USER_INDEX ON
CREDIT_LEDGER (USER_ID)",
"CURRENT_WEB_INDEX" => "CREATE TABLE CURRENT_WEB_INDEX
(CRAWL_TIME NUMERIC(" . C\TIMESTAMP_LEN . ") PRIMARY KEY)",
"GROUP_ITEM" => "CREATE TABLE GROUP_ITEM (ID $serial PRIMARY KEY
$auto_increment, PARENT_ID $integer, GROUP_ID $integer,
USER_ID $integer, URL VARCHAR(" . C\TITLE_LEN
.") DEFAULT '', TITLE VARCHAR(" . C\TITLE_LEN
."), DESCRIPTION VARCHAR(".
C\MAX_GROUP_POST_LEN . "), PUBDATE NUMERIC(" .
C\TIMESTAMP_LEN . "),
EDIT_DATE NUMERIC(" . C\TIMESTAMP_LEN . "),
UPS $integer DEFAULT 0, DOWNS $integer DEFAULT 0,
FLAG $integer DEFAULT 0,
PARENT_ITEM_ID $integer DEFAULT 0,
TYPE $integer DEFAULT " . C\STANDARD_GROUP_ITEM . ")",
"GI_GROUP_ID_INDEX" => "CREATE INDEX GI_GROUP_ID_INDEX ON
GROUP_ITEM (GROUP_ID)",
"GI_PUBDATE_INDEX" => "CREATE INDEX
GI_PUBDATE_INDEX ON GROUP_ITEM (PUBDATE)",
"GI_GROUP_ID_PUBDATE_INDEX" => "CREATE INDEX
GI_GROUP_ID_PUBDATE_INDEX ON GROUP_ITEM (GROUP_ID, PUBDATE)",
"GI_USER_ID_INDEX" => "CREATE INDEX GI_USER_ID_INDEX ON
GROUP_ITEM (USER_ID)",
"GI_PARENT_ID_INDEX" => "CREATE INDEX GI_PARENT_ID_INDEX ON
GROUP_ITEM (PARENT_ID)",
"GI_PARENT_ID_PUBDATE_INDEX" => "CREATE INDEX
GI_PARENT_ID_PUBDATE_INDEX ON GROUP_ITEM (PARENT_ID, PUBDATE)",
"GROUP_CALL" => "CREATE TABLE GROUP_CALL(
CALL_HASH_ID $integer PRIMARY KEY, INITIATOR_ID $integer,
CALL_TIME NUMERIC(" . C\MICROSECOND_TIMESTAMP_LEN . "))",
"GROUP_CALL_EVENTS" => "CREATE TABLE GROUP_CALL_EVENTS(
CALL_HASH_ID $integer, SENDER_ID $integer, EVENT_TIME
NUMERIC(" . C\MICROSECOND_TIMESTAMP_LEN . "),
EVENT VARCHAR(". C\MAX_GROUP_POST_LEN . "),
PRIMARY KEY(CALL_HASH_ID, EVENT_TIME))",
"GROUP_ITEM_VOTE" => "CREATE TABLE GROUP_ITEM_VOTE(
USER_ID $integer, ITEM_ID $integer)",
"GROUP_ITEM_FLAG" => "CREATE TABLE GROUP_ITEM_FLAG(
USER_ID $integer, ITEM_ID $integer)",
"GROUP_ITEM_RECOMMENDATION" => "CREATE TABLE
GROUP_ITEM_RECOMMENDATION (USER_ID $integer NOT NULL,
ITEM_ID $integer NOT NULL, ITEM_TYPE $integer NOT NULL,
SCORE FLOAT, TIMESTAMP NUMERIC(" . C\TIMESTAMP_LEN . "))",
"GI_RECOMMENDATION_INDEX" => "CREATE INDEX GI_RECOMMENDATION_INDEX
ON GROUP_ITEM_RECOMMENDATION (USER_ID, ITEM_TYPE)",
"GROUP_PAGE" => "CREATE TABLE GROUP_PAGE (
ID $serial PRIMARY KEY $auto_increment, GROUP_ID $integer,
DISCUSS_THREAD $integer, TITLE VARCHAR(" . C\TITLE_LEN . "),
PAGE $page_type, LOCALE_TAG VARCHAR(" . C\NAME_LEN . "),
LAST_MODIFIED NUMERIC(" . C\TIMESTAMP_LEN . "))",
"GP_ID_INDEX" => "CREATE INDEX GP_ID_INDEX ON GROUP_PAGE
(GROUP_ID, TITLE, LOCALE_TAG)",
"GP_GROUP_ID_INDEX" => "CREATE INDEX GP_GROUP_ID_INDEX ON GROUP_PAGE
(GROUP_ID)",
"GROUP_PAGE_HISTORY" => "CREATE TABLE GROUP_PAGE_HISTORY(
PAGE_ID $integer, GROUP_ID $integer, EDITOR_ID $integer,
TITLE VARCHAR(" . C\TITLE_LEN . "), PAGE $page_type,
EDIT_COMMENT VARCHAR(" . C\SHORT_TITLE_LEN .
"), LOCALE_TAG VARCHAR(" . C\NAME_LEN . "),
PUBDATE NUMERIC(" . C\TIMESTAMP_LEN . "),
PRIMARY KEY(PAGE_ID, PUBDATE))",
"GROUP_PAGE_LINK" => "CREATE TABLE GROUP_PAGE_LINK(
LINK_TYPE_ID $integer, FROM_ID $integer,
TO_ID $integer,
PRIMARY KEY(LINK_TYPE_ID, FROM_ID, TO_ID))",
"GROUP_PAGE_PRE_LINK" => "CREATE TABLE GROUP_PAGE_PRE_LINK(
LINK_TYPE_ID $integer, FROM_ID $integer,
TO_GROUP_ID $integer, TO_PAGE_NAME VARCHAR(" . C\TITLE_LEN .
"), PRIMARY KEY(LINK_TYPE_ID, FROM_ID, TO_GROUP_ID,
TO_PAGE_NAME))",
"GPP_ID_INDEX" => "CREATE INDEX GP_PRE_INDEX ON GROUP_PAGE_PRE_LINK
(TO_GROUP_ID, TO_PAGE_NAME)",
"GROUP_RESOURCE_RECOMMENDATION" => "CREATE TABLE
GROUP_RESOURCE_RECOMMENDATION (USER_ID $integer NOT NULL,
GROUP_ID $integer NOT NULL, PAGE_ID $integer NOT NULL,
RESOURCE_PATH VARCHAR(". C\SHORT_TITLE_LEN.
"), SCORE FLOAT, TIMESTAMP NUMERIC(" .
C\TIMESTAMP_LEN . "), RESOURCE_ID $integer)",
"GR_RECOMMENDATION_INDEX" => "CREATE INDEX GR_RECOMMENDATION_INDEX
ON GROUP_RESOURCE_RECOMMENDATION (USER_ID)",
"SOCIAL_GROUPS" => "CREATE TABLE SOCIAL_GROUPS (
GROUP_ID $serial PRIMARY KEY $auto_increment,
GROUP_NAME VARCHAR(" . C\SHORT_TITLE_LEN
."), CREATED_TIME VARCHAR(" . C\MICROSECOND_TIMESTAMP_LEN . "),
OWNER_ID $integer, REGISTER_TYPE $integer,
MEMBER_ACCESS $integer, VOTE_ACCESS $integer DEFAULT ".
C\NON_VOTING_GROUP . ", POST_LIFETIME $integer DEFAULT ".
C\FOREVER . ", OPTIONS $integer DEFAULT 0,
RENDER_ENGINE $integer DEFAULT " . C\MEDIAWIKI_ENGINE . ")",
/* NOTE: We are not using singular name GROUP for SOCIAL_GROUPS as
GROUP is a reserved SQL keyword and the table used to be
called GROUPS until Mysql reserved that word too
*/
"GRP_OWNER_ID_INDEX" => "CREATE INDEX GRP_OWNER_ID_INDEX ON
SOCIAL_GROUPS (OWNER_ID)",
"GRP_MEMBER_ACCESS_INDEX" => "CREATE INDEX GRP_MEMBER_ACCESS_INDEX
ON SOCIAL_GROUPS(MEMBER_ACCESS)",
"ITEM_IMPRESSION_STAT" => "CREATE TABLE ITEM_IMPRESSION_STAT(
ITEM_ID $integer, ITEM_TYPE $integer, UPDATE_PERIOD $integer,
NUM_VIEWS $integer DEFAULT -1,
FUZZY_NUM_VIEWS $integer DEFAULT -1)",
"ITEM_IMPRESSION_SUMMARY" => "CREATE TABLE ITEM_IMPRESSION_SUMMARY(
USER_ID $integer, ITEM_ID $integer, ITEM_TYPE $integer,
UPDATE_PERIOD $integer,
UPDATE_TIMESTAMP NUMERIC(" . C\TIMESTAMP_LEN . "),
NUM_VIEWS $integer, FUZZY_NUM_VIEWS $integer DEFAULT -1,
TMP_NUM_VIEWS $integer DEFAULT -1,
PRIMARY KEY(USER_ID, ITEM_ID, ITEM_TYPE,
UPDATE_PERIOD, UPDATE_TIMESTAMP))",
"LOCALE" => "CREATE TABLE LOCALE(LOCALE_ID $serial PRIMARY KEY
$auto_increment, LOCALE_TAG VARCHAR(" . C\NAME_LEN . "),
LOCALE_NAME VARCHAR(" . C\LONG_NAME_LEN .
"), WRITING_MODE CHAR(" .
C\WRITING_MODE_LEN."), ACTIVE $integer DEFAULT 1)",
"LCL_LOCALE_TAG_INDEX" => "CREATE INDEX LCL_LOCALE_TAG_INDEX ON
LOCALE(LOCALE_TAG)",
"MACHINE" => "CREATE TABLE MACHINE (NAME VARCHAR(" . C\NAME_LEN
.") PRIMARY KEY,
URL VARCHAR(" . C\MAX_URL_LEN .
"), CHANNEL $integer,
NUM_FETCHERS $integer, PARENT VARCHAR(" . C\NAME_LEN . ") )",
"MAIL_ACCOUNT" => "CREATE TABLE MAIL_ACCOUNT (
ID $serial PRIMARY KEY $auto_increment,
USER_ID $integer,
DISPLAY_NAME VARCHAR(" . C\MAIL_DISPLAY_NAME_LEN . "),
PROVIDER VARCHAR(" . C\MAIL_PROVIDER_LEN . "),
HOST VARCHAR(" . C\MAIL_HOST_LEN . "),
PORT $integer,
USERNAME VARCHAR(" . C\MAIL_USERNAME_LEN . "),
PASSWORD_NONCE VARCHAR(" .
C\MAIL_PASSWORD_NONCE_LEN . "),
PASSWORD_CIPHERTEXT VARCHAR(" .
C\MAIL_PASSWORD_CIPHERTEXT_LEN . "),
TLS_MODE VARCHAR(" . C\MAIL_TLS_MODE_LEN . "),
DEFAULT_FOLDER VARCHAR(" .
C\MAIL_DISPLAY_NAME_LEN . "),
ALLOW_SELF_SIGNED $integer DEFAULT 0,
SMTP_HOST VARCHAR(" . C\MAIL_HOST_LEN . "),
SMTP_PORT $integer,
SMTP_USERNAME VARCHAR(" . C\MAIL_USERNAME_LEN . "),
SMTP_PASSWORD_NONCE VARCHAR(" .
C\MAIL_PASSWORD_NONCE_LEN . "),
SMTP_PASSWORD_CIPHERTEXT VARCHAR(" .
C\MAIL_PASSWORD_CIPHERTEXT_LEN . "),
SMTP_TLS_MODE VARCHAR(" . C\MAIL_TLS_MODE_LEN . "),
SORT_ORDER $integer DEFAULT 0,
CREATED_AT $integer,
UPDATED_AT $integer)",
"MAIL_ACCOUNT_USER_IDX" => "CREATE INDEX
MAIL_ACCOUNT_USER_IDX ON MAIL_ACCOUNT (USER_ID)",
"MAIL_SCHEDULED" => "CREATE TABLE MAIL_SCHEDULED (
ID $serial PRIMARY KEY $auto_increment,
USER_ID $integer,
ACCOUNT_ID $integer,
SCHEDULED_AT $integer,
CREATED_AT $integer,
STATUS VARCHAR(" . C\MAIL_TLS_MODE_LEN . "),
LAST_ERROR VARCHAR(" . C\MAIL_LAST_ERROR_LEN . "),
ATTEMPT_COUNT $integer DEFAULT 0,
SUBJECT VARCHAR(" . C\MAIL_SUBJECT_LEN . "),
TO_LIST TEXT,
CC_LIST TEXT,
BCC_LIST TEXT,
BODY_TEXT TEXT,
BODY_HTML TEXT,
IN_REPLY_TO VARCHAR(" . C\MAIL_SUBJECT_LEN . "),
REFERENCES_HEADER TEXT)",
"MAIL_SCHEDULED_USER_IDX" => "CREATE INDEX
MAIL_SCHEDULED_USER_IDX ON MAIL_SCHEDULED
(USER_ID, SCHEDULED_AT)",
"MAIL_SCHEDULED_DUE_IDX" => "CREATE INDEX
MAIL_SCHEDULED_DUE_IDX ON MAIL_SCHEDULED
(SCHEDULED_AT, STATUS)",
"MAIL_CLONE_JOB" => "CREATE TABLE MAIL_CLONE_JOB (
ID $serial PRIMARY KEY $auto_increment,
USER_ID $integer,
SRC_ACCOUNT $integer,
DESTINATION_USER VARCHAR(" . C\NAME_LEN . "),
MODE VARCHAR(" . C\MAIL_TLS_MODE_LEN . "),
STATUS VARCHAR(" . C\MAIL_TLS_MODE_LEN . "),
DRY_RUN $integer DEFAULT 0,
FOLDERS_DONE TEXT,
CURRENT_FOLDER VARCHAR(" .
C\MAIL_FOLDER_NAME_LEN . "),
CURRENT_UID $integer DEFAULT 0,
CURRENT_UID_PARTIAL_OFFSET $integer DEFAULT 0,
CURRENT_UID_PARTIAL_SIZE $integer DEFAULT 0,
IMPORTED $integer DEFAULT 0,
SKIPPED $integer DEFAULT 0,
FAILED $integer DEFAULT 0,
FOLDER_CAP $integer DEFAULT 0,
STARTED_AT $integer,
UPDATED_AT $integer)",
"MAIL_CLONE_JOB_USER_IDX" => "CREATE INDEX
MAIL_CLONE_JOB_USER_IDX ON MAIL_CLONE_JOB
(USER_ID, STATUS)",
"MAIL_CLONE_SEEN" => "CREATE TABLE MAIL_CLONE_SEEN (
JOB_ID $integer,
SRC_UIDVALIDITY $integer,
SRC_UID $integer,
PRIMARY KEY (JOB_ID, SRC_UIDVALIDITY, SRC_UID))",
"MAIL_CLONE_ERROR" => "CREATE TABLE MAIL_CLONE_ERROR (
ID $serial PRIMARY KEY $auto_increment,
JOB_ID $integer,
FOLDER VARCHAR(" . C\MAIL_FOLDER_NAME_LEN . "),
SRC_UIDVALIDITY $integer DEFAULT 0,
SRC_UID $integer DEFAULT 0,
ERROR_KIND VARCHAR(" .
C\MAIL_CLONE_ERROR_KIND_LEN . "),
MESSAGE VARCHAR(" .
C\MAIL_LAST_ERROR_LEN . "),
OCCURRED_AT $integer,
RESOLVED_AT $integer DEFAULT 0,
RESOLVED_VIA VARCHAR(" .
C\MAIL_CLONE_ERROR_KIND_LEN . "))",
"MAIL_CLONE_ERROR_JOB_IDX" => "CREATE INDEX
MAIL_CLONE_ERROR_JOB_IDX ON MAIL_CLONE_ERROR
(JOB_ID, RESOLVED_AT)",
"MAIL_CLONE_ERROR_UID_IDX" => "CREATE INDEX
MAIL_CLONE_ERROR_UID_IDX ON MAIL_CLONE_ERROR
(JOB_ID, SRC_UIDVALIDITY, SRC_UID)",
"MAIL_ALIAS" => "CREATE TABLE MAIL_ALIAS (
ID $serial PRIMARY KEY $auto_increment,
USER_ID $integer,
ALIAS VARCHAR(" . C\NAME_LEN . "),
DOMAIN VARCHAR(" . C\MAIL_HOST_LEN . "),
CREATED_AT $integer)",
"MAIL_ALIAS_NAME_IDX" => "CREATE UNIQUE INDEX
MAIL_ALIAS_NAME_IDX ON MAIL_ALIAS (ALIAS, DOMAIN)",
"MAIL_ALIAS_USER_IDX" => "CREATE INDEX
MAIL_ALIAS_USER_IDX ON MAIL_ALIAS (USER_ID)",
"DOMAIN_ROUTE" => "CREATE TABLE DOMAIN_ROUTE (
DOMAIN VARCHAR(" . C\MAIL_HOST_LEN . ") PRIMARY KEY,
GROUP_ID $integer)",
"DOMAIN_APPEARANCE" => "CREATE TABLE DOMAIN_APPEARANCE (
DOMAIN VARCHAR(" . C\MAIL_HOST_LEN . ") PRIMARY KEY,
APPEARANCE $appearance_text)",
"MAIL_SENDER_ALLOW" => "CREATE TABLE MAIL_SENDER_ALLOW (
ID $serial PRIMARY KEY $auto_increment,
USER_ID $integer,
SENDER VARCHAR(" . C\MAIL_USERNAME_LEN . "),
CREATED_AT $integer)",
"MAIL_SENDER_ALLOW_IDX" => "CREATE UNIQUE INDEX
MAIL_SENDER_ALLOW_IDX ON MAIL_SENDER_ALLOW
(USER_ID, SENDER)",
"MAIL_SUPPRESSION" => "CREATE TABLE MAIL_SUPPRESSION (
ID $serial PRIMARY KEY $auto_increment,
EMAIL VARCHAR(" . C\MAIL_USERNAME_LEN . "),
CREATED_AT $integer)",
"MAIL_SUPPRESSION_IDX" => "CREATE UNIQUE INDEX
MAIL_SUPPRESSION_IDX ON MAIL_SUPPRESSION (EMAIL)",
"MEDIA_SOURCE" => "CREATE TABLE MEDIA_SOURCE (
TIMESTAMP NUMERIC(" . C\TIMESTAMP_LEN . ") PRIMARY KEY,
NAME VARCHAR(" . C\LONG_NAME_LEN . "),
TYPE VARCHAR(" . C\NAME_LEN . "),
CATEGORY VARCHAR(" . C\NAME_LEN . ") DEFAULT 'NEWS',
SOURCE_URL VARCHAR(" . C\MAX_URL_LEN . "), AUX_INFO VARCHAR(".
C\MAX_URL_LEN . "), LANGUAGE VARCHAR(" . C\NAME_LEN . "))",
"MS_TYPE_INDEX" => "CREATE INDEX MS_TYPE_INDEX ON
MEDIA_SOURCE(TYPE)",
"MIX_COMPONENTS" => "CREATE TABLE MIX_COMPONENTS (
TIMESTAMP NUMERIC(" . C\TIMESTAMP_LEN . "),
FRAGMENT_ID $integer,
CRAWL_TIMESTAMP NUMERIC(".C\TIMESTAMP_LEN."), WEIGHT FLOAT,
DIRECTION $integer, KEYWORDS VARCHAR(" . C\TITLE_LEN . "),
PRIMARY KEY(TIMESTAMP, FRAGMENT_ID, CRAWL_TIMESTAMP) )",
"MIX_FRAGMENTS" => "CREATE TABLE MIX_FRAGMENTS (
TIMESTAMP NUMERIC(" . C\TIMESTAMP_LEN . "),FRAGMENT_ID $integer,
RESULT_BOUND $integer, PRIMARY KEY(TIMESTAMP, FRAGMENT_ID))",
"PAGE_RELATIONSHIP" => "CREATE TABLE PAGE_RELATIONSHIP (
ID $serial PRIMARY KEY $auto_increment, NAME VARCHAR(" .
C\NAME_LEN . ") UNIQUE)",
"QUERY_ITEM" => "CREATE TABLE QUERY_ITEM (ID $serial PRIMARY KEY
$auto_increment, QUERY_HASH CHAR(". C\TIMESTAMP_LEN .
") UNIQUE, QUERY VARCHAR(" . C\MAX_QUERY_LEN."),
AUX_HASH VARCHAR(". C\TIMESTAMP_LEN . ") DEFAULT '',
CREATION NUMERIC(" . C\TIMESTAMP_LEN . "))",
"QI_QUERY_HASH_INDEX" => "CREATE INDEX QI_QUERY_HASH_INDEX ON
QUERY_ITEM (QUERY_HASH)",
"QI_AUX_HASH_INDEX" => "CREATE INDEX QI_AUX_HASH_INDEX ON
QUERY_ITEM (AUX_HASH)",
"ROLE" => "CREATE TABLE ROLE (
ROLE_ID $serial PRIMARY KEY $auto_increment, NAME VARCHAR(".
C\NAME_LEN."))",
"ROLE_ACTIVITY" => "CREATE TABLE ROLE_ACTIVITY (ROLE_ID $integer,
ACTIVITY_ID $integer,
ALLOWED_ARGUMENTS VARCHAR(" . C\MAX_URL_LEN . ") DEFAULT 'all',
PRIMARY KEY(ROLE_ID, ACTIVITY_ID))",
"ROLE_LIMITS" => "CREATE TABLE ROLE_LIMITS (ROLE_ID $integer
PRIMARY KEY, MAX_GROUPS_OWNED $integer DEFAULT -1,
MAX_GROUP_MEMBERS $integer DEFAULT -1,
MAX_GROUP_WIKI_PAGES $integer DEFAULT -1,
MAX_PAGE_RESOURCE_MEMORY $integer DEFAULT -1,
MAX_GROUP_THREADS $integer DEFAULT -1,
MAX_THREAD_RESOURCES $integer DEFAULT -1,
MAX_THREAD_POSTS $integer DEFAULT -1,
ROLE_COST $integer DEFAULT 0,
CHARGE_FREQUENCY VARCHAR(" . C\NAME_LEN . ")
DEFAULT 'never')",
"RECOMMENDATION_ITEM_EMBEDDING" => "CREATE TABLE
RECOMMENDATION_ITEM_EMBEDDING (ID $integer NOT NULL,
ITEM_TYPE $integer NOT NULL, VECTOR VARCHAR(" .
32 * C\EMBEDDING_VECTOR_SIZE . "), PARENT_ID $integer,
PRIMARY KEY(ID, ITEM_TYPE))",
"RECOMMENDATION_TERM_EMBEDDING" => "CREATE TABLE
RECOMMENDATION_TERM_EMBEDDING (ID VARCHAR(16) NOT NULL,
ITEM_TYPE $integer NOT NULL, VECTOR VARCHAR(" .
32 * C\EMBEDDING_VECTOR_SIZE . "),
PRIMARY KEY(ID, ITEM_TYPE))",
"SCRAPER" =>
"CREATE TABLE SCRAPER (ID $serial PRIMARY KEY
$auto_increment, NAME VARCHAR(" . C\TITLE_LEN . "),
PRIORITY $integer DEFAULT 0,
SIGNATURE VARCHAR(" . C\MAX_URL_LEN . "),
TEXT_PATH VARCHAR(" . C\MAX_URL_LEN . ") DEFAULT '',
DELETE_PATHS $scraper_text, EXTRACT_FIELDS $scraper_text)",
"SUBSEARCH" => "CREATE TABLE SUBSEARCH (
LOCALE_STRING VARCHAR(" . C\LONG_NAME_LEN . ") PRIMARY KEY,
FOLDER_NAME VARCHAR(" . C\NAME_LEN."),
INDEX_IDENTIFIER CHAR(" . (strlen('m:') + C\TIMESTAMP_LEN) . "),
PER_PAGE $integer,
DEFAULT_QUERY VARCHAR(" . C\TITLE_LEN . ") DEFAULT ''
)",
"TRENDING_TERM" => "CREATE TABLE TRENDING_TERM (
TERM VARCHAR(" . C\TITLE_LEN . "),
OCCURRENCES FLOAT DEFAULT 0,
CATEGORY VARCHAR(" . C\TITLE_LEN . ") DEFAULT 'news',
UPDATE_PERIOD NUMERIC,
TIMESTAMP NUMERIC(" . C\TIMESTAMP_LEN . "),
LANGUAGE VARCHAR(" . C\NAME_LEN . ")
)",
"TRANSLATION" => "CREATE TABLE TRANSLATION (
TRANSLATION_ID $serial PRIMARY KEY
$auto_increment, IDENTIFIER_STRING VARCHAR(" . C\TITLE_LEN
."))",
"TRANS_IDENTIFIER_STRING_INDEX" => "CREATE INDEX
TRANS_IDENTIFIER_STRING_INDEX ON
TRANSLATION(IDENTIFIER_STRING)",
"TRANSLATION_LOCALE" => "CREATE TABLE TRANSLATION_LOCALE
(TRANSLATION_ID $integer, LOCALE_ID $integer,
TRANSLATION VARCHAR(" . C\MAX_GROUP_POST_LEN."),
PRIMARY KEY(TRANSLATION_ID, LOCALE_ID))",
"USERS" => "CREATE TABLE USERS(USER_ID $serial PRIMARY KEY
$auto_increment, FIRST_NAME VARCHAR(" . C\NAME_LEN."),
LAST_NAME VARCHAR(" . C\NAME_LEN . "), USER_NAME VARCHAR(" .
C\NAME_LEN .") UNIQUE, EMAIL VARCHAR(" . C\LONG_NAME_LEN . "),
PASSWORD VARCHAR(" . C\LONG_NAME_LEN . "), STATUS $integer,
HASH VARCHAR(" . C\LONG_NAME_LEN . "),
CREATION_TIME VARCHAR(" . C\MICROSECOND_TIMESTAMP_LEN .
"), UPS $integer DEFAULT 0, DOWNS $integer DEFAULT 0,
AD_CREDITS_SERVICE_ACCOUNT VARCHAR(" .
C\AD_CREDITS_SERVICE_ACCOUNT_LEN . "),
RECOVERY_QUESTION VARCHAR(" . C\RECOVERY_QUESTION_LEN .
") DEFAULT '', RECOVERY_ANSWER VARCHAR(" . C\LONG_NAME_LEN .
") DEFAULT '')",
"USRS_USER_NAME_INDEX" => "CREATE INDEX USRS_USER_NAME_INDEX ON
USERS(USER_NAME)",
"USER_GROUP" => "CREATE TABLE USER_GROUP (USER_ID $integer,
GROUP_ID $integer, STATUS $integer,
RECEIVE_MAIL $integer DEFAULT 1,
JOIN_DATE NUMERIC(" . C\TIMESTAMP_LEN . "),
PRIMARY KEY (GROUP_ID, USER_ID) )",
"THREAD_FOLLOW" => "CREATE TABLE THREAD_FOLLOW (
USER_ID $integer, THREAD_ID $integer,
PRIMARY KEY (THREAD_ID, USER_ID) )",
"USER_ROLE" => "CREATE TABLE USER_ROLE (USER_ID $integer,
ROLE_ID $integer, EXPIRES $integer DEFAULT " . C\FOREVER .
", WILL_RENEW $integer DEFAULT 1, " .
"PRIMARY KEY (ROLE_ID, USER_ID))",
"USER_SESSION" => "CREATE TABLE USER_SESSION (
USER_ID $integer PRIMARY KEY, SESSION $user_session_text)",
"AUTH_SESSION" => "CREATE TABLE AUTH_SESSION (
SESSION_ID VARCHAR(" . C\SESSION_ID_LEN . ") PRIMARY KEY,
USER_ID $integer, EXPIRES $integer)",
"SIGNIN_CODE" => "CREATE TABLE SIGNIN_CODE (
USER_ID $integer PRIMARY KEY,
CODE_HASH VARCHAR(" . C\LONG_NAME_LEN . "),
EXPIRES $integer, TRIES $integer)",
"VISITOR" => "CREATE TABLE VISITOR(ADDRESS VARCHAR(".
C\MAX_IP_ADDRESS_AS_STRING_LEN . "),
PAGE_NAME VARCHAR(" . C\NAME_LEN . "),
END_TIME $integer, DELAY $integer, FORGET_AGE $integer,
ACCESS_COUNT $integer,
PRIMARY KEY(ADDRESS, PAGE_NAME))",
"VERSION" => "CREATE TABLE VERSION(ID $integer PRIMARY KEY)",
];
}
/**
* Used to construct $this->private_create_statements, the list of all SQL
* CREATE statements needed to build Yioop private database
*
* @param object $dbm a datasource_manager object used to get strings
* for autoincrement and serial types for a given db
* @param array $dbinfo connect info for the database, also used in
* getting autoincrement and serial types
*/
public function initializeSqlPrivate($dbm, $dbinfo)
{
$auto_increment = $dbm->autoIncrement($dbinfo);
$serial = $dbm->serialType($dbinfo);
$integer = $dbm->integerType($dbinfo);
/**
* SQL statements used to create the Yioop database. Some of the
* these statements could use UNIQUE on some the columns that are
* later used in CREATE INDEX statements. However, because of
* restrictions on the number of bytes (not chars) in MYSQL for keys
* this has not been done. AES 256 = 8*32bits long. So need
* only 32 bytes to store a key. We give up to 64 (LONG_NAME_LEN)
* as base 64 encoding keys to keep postgres happy when using varchhar
*/
$this->private_create_statements = ["TYPE_KEYS" =>
"CREATE TABLE TYPE_KEYS (KEY_ID $serial PRIMARY KEY " .
"$auto_increment, TYPE_ID $integer, KEY_NAME VARCHAR(" .
C\LONG_NAME_LEN . "))",
"MAIL_SECRET" =>
"CREATE TABLE MAIL_SECRET (KEY_ID $integer PRIMARY KEY, " .
"KEY_VALUE VARCHAR(" . C\MAIL_SECRET_KEY_LEN . "))",
"GIT_APP_CODE" =>
"CREATE TABLE GIT_APP_CODE (USER_ID $integer PRIMARY KEY, " .
"APP_CODE VARCHAR(" . C\LONG_NAME_LEN . "), EXPIRES $integer)"];
}
/**
* Creates a folder to be used to maintain local information about this
* instance of the Yioop/SeekQuarry engine
*
* Creates the directory provides as well as subdirectories for crawls,
* locales, logging, and sqlite DBs.
*
* @param string $directory path and name of directory to create
* @return bool true if every required subdirectory exists or was
* created; false on the first createIfNecessaryDirectory
* failure
*/
public function makeWorkDirectory($directory)
{
$to_make_dirs = [$directory, "$directory/app",
"$directory/archives", "$directory/cache",
"$directory/classifiers", "$directory/data",
"$directory/data/domain_filters",
"$directory/app/locale", "$directory/log",
"$directory/mail", "$directory/prepare",
"$directory/schedules", "$directory/temp"];
$dir_status = [];
foreach ($to_make_dirs as $dir) {
$dir_status[$dir] = $this->createIfNecessaryDirectory($dir);
if ($dir_status[$dir] < 0) {
return false;
}
}
if ($dir_status["$directory/app/locale"] == 1) {
$this->db->copyRecursive(C\BASE_DIR . "/locale",
"$directory/app/locale");
}
if ($dir_status["$directory/data"] == 1) {
$this->db->copyRecursive(C\BASE_DIR . "/data", "$directory/data");
}
return true;
}
/**
* Outputs a Profile.php file in the given directory containing profile
* data based on new and old data sources
*
* This function creates a Profile.php file if it doesn't exist. A given
* field is output in the profile
* according to the precedence that a new value is preferred to an old
* value is preferred to the value that comes from a currently defined
* constant. It might be the case that a new value for a given field
* doesn't exist, etc.
*
* @param string $directory the work directory to output the Profile.php
* file
* @param array $new_profile_data fields and values containing at least
* some profile information (only $this->profile_fields
* fields of $new_profile_data will be considered).
* @param array $old_profile_data fields and values that come from
* presumably a previously existing profile
* @param bool $reset whether the new profile data is coming from a reset
* to factory settings or not
*/
public function updateProfile($directory, $new_profile_data,
$old_profile_data, $reset = false)
{
$n = [];
$n[] = <<<EOT
<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009-2012 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* 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 3 of the License, or
* (at your option) any later version.
*
* 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, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009-2012
* @filesource
*/
namespace seekquarry\yioop\configs;
/**
* Computer generated file giving the key defines of directory locations
* as well as database settings used to run the SeekQuarry/Yioop search engine
*/
EOT;
$base_url = C\baseUrl();
//make sure certain fields are not null
$not_null_fields = [
'AD_LOCATION' => 'none',
'AUTOLOGOUT' => C\ONE_HOUR,
'BACKGROUND_COLOR' => "#FFF",
'COOKIE_LIFETIME' => C\ONE_YEAR,
'DIFFERENTIAL_PRIVACY' => false,
'CSRF_TOKEN' => "YIOOP_TOKEN",
'FAVICON' => "favicon.ico",
'FOREGROUND_COLOR' => "#FFF",
'LOGO_SMALL' => "resources/yioop-small.png",
'LOGO_MEDIUM' => "resources/yioop-medium.png",
'LOGO_LARGE' => "resources/yioop-large.png",
'MEDIA_MODE' => "name_server",
'RECOVERY_MODE' => C\EMAIL_RECOVERY,
'SESSION_NAME' => "yioopbiscuit",
'SIDEBAR_COLOR' => "#F8F8F8",
'TIMEZONE' => 'America/Los_Angeles',
'TOPBAR_COLOR' => "#F5F5FF",
];
/*
Fields where 0 (or "0") is a meaningful, explicit setting and
must not be silently overwritten by the not-null fallback
(which fires on any falsy value). RECOVERY_MODE is one such:
NO_RECOVERY is the integer 0 and is a deliberate choice by
an admin that has to be honored across saves.
*/
$zero_is_valid_fields = ['RECOVERY_MODE'];
$not_null_keys = array_keys($not_null_fields);
$file_fields = $this->file_fields;
//now integrate the different profiles
foreach ($this->profile_fields as $field) {
if (isset($new_profile_data[$field])) {
if (!$reset && in_array($field,
['LOGO_SMALL', 'LOGO_MEDIUM', 'LOGO_LARGE',
'FAVICON', 'SEARCHBAR_PATH',
'BACKGROUND_IMAGE'])) {
if (isset($new_profile_data[$field]['name']) &&
isset($new_profile_data[$field]['tmp_name'])) {
if (empty($new_profile_data[$field]['data'])) {
move_uploaded_file(
$new_profile_data[$field]['tmp_name'], C\APP_DIR .
"/resources/". $new_profile_data[$field]['name']);
} else {
file_put_contents(C\APP_DIR . "/resources/".
$new_profile_data[$field]['name'],
$new_profile_data[$field]['data']);
}
if (C\REDIRECTS_ON) {
$profile[$field] = "wd/resources/" .
$new_profile_data[$field]['name'];
} else {
$profile[$field] =
"?c=resource&a=get&" .
"f=resources&n=" .
$new_profile_data[$field]['name'];
}
} elseif (isset($old_profile_data[$field])) {
$profile[$field] = $old_profile_data[$field];
} elseif (nsdefined($field)) {
$profile[$field] = constant(C\NS_CONFIGS . $field);
} else {
$profile[$field] = "";
}
} else {
$profile[$field] = $new_profile_data[$field];
}
} elseif (isset($old_profile_data[$field])) {
$profile[$field] = $old_profile_data[$field];
} elseif (C\nsdefined($field)) {
$profile[$field] = constant(C\NS_CONFIGS . $field);
} else {
$profile[$field] = "";
}
if (!$profile[$field] && isset($not_null_fields[$field])) {
if (!self::shouldKeepZero($field, $profile[$field],
$new_profile_data, $old_profile_data,
$zero_is_valid_fields)) {
$profile[$field] = $not_null_fields[$field];
}
}
//don't let the database systems be unspecified
if (empty($profile["DBMS"])) {
$profile["DBMS"] = 'Sqlite3';
$profile['DB_NAME'] = "public_default";
}
if (empty($profile["PRIVATE_DBMS"])) {
$profile["PRIVATE_DBMS"] = 'Sqlite3';
$profile['PRIVATE_DB_NAME'] = "private_default";
}
if ($field == "WEB_URI") {
$profile[$field] = UrlParser::getPath(C\baseUrl());
}
if (in_array($field, $file_fields)) {
continue;
}
$raw_value = $profile[$field];
if ($field != "DEBUG_LEVEL") {
$profile[$field] = "\"{$profile[$field]}\"";
}
$n[] = "nsdefine('$field', {$profile[$field]});";
C\p($field, $raw_value, true);
}
$out = implode("\n", $n);
if (file_put_contents($directory . C\PROFILE_FILE_NAME, $out)
!== false) {
set_error_handler(null);
@chmod($directory . C\PROFILE_FILE_NAME, 0777);
$css_name = trim($profile['AUXILIARY_CSS_NAME'] ?? "", '"');
if (isset($new_profile_data['AUXILIARY_CSS']) &&
!empty($css_name)) {
$this->saveThemeStylesheet($css_name,
$new_profile_data['AUXILIARY_CSS']);
}
restore_error_handler();
return true;
}
return false;
}
/**
* Writes one theme's stylesheet rules to their own file in the
* folder holding the site's themes, making the folder if it is not
* there yet. This is the one place a theme's rules are written, and
* every domain reads them from there.
*
* @param string $name the theme's name, which also names its file
* @param string $css the stylesheet rules to write
* @return bool whether the rules were written
*/
public function saveThemeStylesheet($name, $css)
{
$theme_name = $this->themeFileName($name);
if ($theme_name === "") {
return false;
}
$folder = $this->themeFolder();
set_error_handler(null);
foreach ([C\APP_DIR . "/css", $folder] as $make) {
if (!file_exists($make)) {
@mkdir($make);
@chmod($make, 0777);
}
}
$css_file = "$folder/$theme_name.css";
$wrote = file_put_contents($css_file, $css) !== false;
@chmod($css_file, 0777);
restore_error_handler();
return $wrote;
}
/**
* Returns the folder holding the site's CSS themes. Every theme lives
* here whichever domain uses it: a theme is a thing the site owns and
* any of its domains may be dressed in, so the list is one list and a
* domain picks from it rather than keeping a list of its own. It sits
* beside rather than among the stylesheets the site ships, so that
* what an admin has added is separable from what came with Yioop.
*
* @return string path of the folder holding the site's themes
*/
public function themeFolder()
{
return C\APP_DIR . "/css/" . C\DEFAULT_THEME_FOLDER;
}
/**
* Reduces a theme or domain name to the plain name its file or
* folder may carry, with any surrounding quotes, directory
* separators, and parent-folder steps removed, so a name read from a
* profile or a request can never reach outside the css folder.
*
* @param string $name theme or domain name to reduce
* @return string the name with anything that could change folder
* taken out
*/
public function themeFileName($name)
{
$clean_name = str_replace(["/", "\\", "..", "\0"], "",
trim((string)$name, '"'));
return trim($clean_name);
}
/**
* Decides whether to preserve a resolved-to-zero profile value
* across the not-null fallback in updateProfile. Returns true
* (keep the zero) when the field is listed in
* $zero_is_valid_fields AND the zero came from an explicit
* setting in either $new_profile_data or $old_profile_data
* (not just because the field was missing entirely). Pulled
* out as a static so the policy can be unit-tested without
* standing up a work directory.
*
* @param string $field profile field being resolved
* @param mixed $resolved the value already resolved for the
* field (only called when this is falsy)
* @param array $new_profile_data new profile data passed to
* updateProfile
* @param array $old_profile_data old profile data passed to
* updateProfile
* @param array $zero_is_valid_fields list of fields where 0
* is a meaningful, deliberate setting
* @return bool true if the zero should be kept (don't apply
* the not-null fallback); false if the fallback should
* apply
*/
public static function shouldKeepZero($field, $resolved,
$new_profile_data, $old_profile_data, $zero_is_valid_fields)
{
return in_array($field, $zero_is_valid_fields) &&
(isset($new_profile_data[$field]) ||
isset($old_profile_data[$field])) &&
(string)$resolved === "0";
}
/**
* Check if $dbinfo provided the connection details for a Yioop/SeekQuarry
* database. If it does provide a valid db connection but no data then try
* to recreate the database from the default copy stored in /data dir.
*
* @param array $dbinfo has fields for DBMS, DB_USER, DB_PASSWORD, DB_HOST
* and DB_NAME
* @param array $skip_list an array of table or index names not to bother
* creating or copying
* @return bool returns true if can connect to/create a valid database;
* returns false otherwise
*/
public function migrateDatabaseIfNecessary($dbinfo, $skip_list = [])
{
$test_dbm = $this->testDatabaseManager($dbinfo);
if ($test_dbm === false || $test_dbm === true) {
return $test_dbm;
}
$this->initializeSql($test_dbm, $dbinfo);
$copy_tables = array_diff(array_keys($this->create_statements),
$skip_list);
if (!($create_ok = $this->createDatabaseTables($test_dbm, $dbinfo))) {
return false;
}
$default_dbm = new Sqlite3Manager();
$default_dbm->connect("", "", "", C\BASE_DIR."/data/public_default.db");
if (!$default_dbm) {
return false;
}
foreach ($copy_tables as $table_or_index) {
if ($table_or_index != "CURRENT_WEB_INDEX" &&
stristr($table_or_index, "_INDEX")) {
continue;
}
if (!DatasourceManager::copyTable($table_or_index, $default_dbm,
$table_or_index, $test_dbm)) {
return false;
}
}
if (stristr($dbinfo["DB_HOST"], "pgsql") !== false) {
/* For postgres count initial values of SERIAL sequences
will be screwed up unless do
*/
$auto_tables = ["ACTIVITY" =>"ACTIVITY_ID",
"GROUP_ITEM" =>"ID",
"GROUP_PAGE" => "ID",
"SOCIAL_GROUPS" => "GROUP_ID", "LOCALE"=> "LOCALE_ID",
"ROLE" => "ROLE_ID", "TRANSLATION" => "TRANSLATION_ID",
"USERS" => "USER_ID"];
foreach ($auto_tables as $table => $auto_column) {
$sql = "SELECT MAX($auto_column) AS NUM FROM $table";
$result = $test_dbm->execute($sql);
$row = $test_dbm->fetchArray($result);
$next = $row['NUM'] ?? 1;
$sequence = strtolower("{$table}_{$auto_column}_seq");
$sql = "SELECT setval('$sequence', $next)";
$test_dbm->execute($sql);
}
}
return true;
}
/**
* On a blank database this method create all the tables necessary for
* Yioop less those on a skip list
*
* @param object $dbm a DatabaseManager open to some DBMS and with a
* blank database selected
* @param array $dbinfo name of database, host, user, and password
* @param array $skip_list an array of table or index names not to bother
* creating
* @return bool whether all of the creates were successful or not
*/
public function createDatabaseTables($dbm, $dbinfo, $skip_list = [])
{
$this->initializeSql($dbm, $dbinfo);
$create_statements = $this->create_statements;
foreach ($create_statements as $table_or_index => $statement) {
if (in_array($table_or_index, $skip_list) || empty($statement)) {
continue;
}
if (!$result = $dbm->execute($statement)) {
echo $statement." ERROR!";
return false;
}
}
/* Generate the DKIM signing key for a fresh install so
outbound mail can be signed; existing installs get it
from the version 101 upgrade migration instead. */
DkimKey::ensureKeyPair();
return true;
}
/**
* On a blank database this method create all the tables necessary for
* private Yioop less those on a skip list
*
* @param object $dbm a DatabaseManager open to some DBMS and with a
* blank database selected
* @param array $dbinfo name of database, host, user, and password
* @param array $skip_list an array of table or index names not to bother
* creating
* @return bool whether all of the creates were successful or not
*/
public function createDatabaseTablesPrivate($dbm, $dbinfo, $skip_list = [])
{
$this->initializeSqlPrivate($dbm, $dbinfo);
$private_create_statements = $this->private_create_statements;
foreach ($private_create_statements as $table_or_index => $statement) {
if (in_array($table_or_index, $skip_list)) {
continue;
}
if (!$result = $dbm->execute($statement)) {
echo $statement . " ERROR!";
return false;
}
}
return true;
}
/**
* Checks if $dbinfo provides info to connect to an working instance of
* app db.
*
* @param array $dbinfo has field for DBMS, DB_USER, DB_PASSWORD, DB_HOST
* and DB_NAME
* @return mixed returns true if can connect to DBMS with username and
* password, can select the given database name and that database
* seems to be of Yioop/SeekQuarry type. If the connection works
* but database isn't there it attempts to create it. If the
* database is there but no data, then it returns a resource for
* the database. Otherwise, it returns false.
*/
public function testDatabaseManager($dbinfo)
{
if (!isset($dbinfo['DBMS'])) {
return false;
}
$dbms_manager = C\NS_DATASOURCES . ucfirst($dbinfo['DBMS'])."Manager";
$test_dbm = new $dbms_manager();
$fields = ['DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'];
foreach ($fields as $field) {
if (!isset($dbinfo[$field])) {
$dbinfo[$field] = constant($field);
}
}
$host = $dbinfo['DB_HOST'];
// for postgres database needs to already exists
$host = str_ireplace("database=".$dbinfo['DB_NAME'], "",
$host); // informix, ibm (use connection string DSN)
$host = str_replace(";;", ";", $host);
$host = trim($host);
$conn = $test_dbm->connect($host, $dbinfo['DB_USER'],
$dbinfo['DB_PASSWORD'], "");
if ($conn === false) {
return false;
}
//check if can select db or if not create it
$q = "";
if (isset($test_dbm->special_quote)) {
$q = $test_dbm->special_quote;
}
set_error_handler(null);
$is_mysql = (stristr($dbinfo['DBMS'], 'Mysql') !== false);
$charset_clause = ($is_mysql) ? " CHARACTER SET utf8mb4 ": "";
@$test_dbm->execute("CREATE DATABASE $q" . $dbinfo['DB_NAME'] . "$q" .
$charset_clause);
$test_dbm->disconnect();
if (!$test_dbm->connect(
$dbinfo['DB_HOST'], $dbinfo['DB_USER'],
$dbinfo['DB_PASSWORD'], $dbinfo['DB_NAME'])) {
return false;
}
/* check if need to create db contents.
We check if any locale exists as proxy for contents being okay.
Temporarily disable more aggressive yioop error handler while do
this
*/
$sql = "SELECT LOCALE_ID FROM LOCALE";
$result = $test_dbm->execute($sql);
restore_error_handler();
if ($result !== false && $test_dbm->fetchArray($result) !== false) {
return true;
}
return $test_dbm;
}
/**
* Modifies the config.php file so the WORK_DIRECTORY define points at
* $directory
*
* @param string $directory folder that WORK_DIRECTORY should be defined to
* @return bool true on a successful rewrite of Config.php; false
* when the configurable section markers can't be found or
* the write fails
*/
public function setWorkDirectoryConfigFile($directory)
{
$dir_value = "'$directory'";
if ($directory == C\DEFAULT_WORK_DIRECTORY) {
$dir_value = "DEFAULT_WORK_DIRECTORY";
}
$config = file_get_contents(C\BASE_DIR . "/configs/Config.php");
$start_machine_section = strpos($config,
'/*+++ The next block of code');
if ($start_machine_section === false) {
return false;
}
$end_machine_section = strpos($config, '/*++++++*/');
if ($end_machine_section === false) {
return false;
}
$out = substr($config, 0, $start_machine_section);
$out .= "/*+++ The next block of code is machine edited, change at\n".
"your own risk, please use configure web page instead +++*/\n";
$out .= "nsdefine('WORK_DIRECTORY', $dir_value);\n";
$out .= substr($config, $end_machine_section);
if (file_put_contents(C\BASE_DIR."/configs/Config.php", $out)) {
return true;
}
return false;
}
/**
* Reads a profile from a Profile.php file in the provided directory
*
* @param string $work_directory directory to look for profile in
* @return array associate array of the profile fields and their values
*/
public function getProfile($work_directory)
{
$profile = [];
$profile_string = @file_get_contents($work_directory .
C\PROFILE_FILE_NAME);
$file_fields = $this->file_fields;
foreach ($this->profile_fields as $field) {
if (!in_array($field, $file_fields)) {
$profile[$field] = $this->matchDefine($field,
$profile_string);
if ($profile[$field] === "" &&
isset(self::$profile_defaults[$field])) {
$profile[$field] =
self::$profile_defaults[$field];
}
if ($field == "AD_LOCATION" && $profile[$field] == "") {
$profile[$field] = "none";
}
}
}
if (!empty($profile["AUXILIARY_CSS_NAME"])) {
$profile['AUXILIARY_CSS'] =
$this->getThemeStylesheet($profile["AUXILIARY_CSS_NAME"]);
}
return $profile;
}
/**
* Reads one theme's stylesheet rules from its file in the folder
* holding the site's themes. A theme whose file has gone missing
* reads as no rules at all rather than as an error, so a profile or
* a domain still loads when it names a theme that is no longer
* there.
*
* @param string $name the theme's name, which also names its file
* @return string the theme's stylesheet rules, empty when the theme
* has none or its file is gone
*/
public function getThemeStylesheet($name)
{
$theme_name = $this->themeFileName($name);
if ($theme_name === "") {
return "";
}
$css_file = $this->themeFolder() . "/$theme_name.css";
return (file_exists($css_file)) ? file_get_contents($css_file) : "";
}
/**
* Get a list of auxliary stylesheets which can be applied in
* addition to the default stylesheet across the site and which
* can be used to control the styling of individual wiki pages.
* The site keeps one list of themes, which every domain and every
* wiki page picks from.
*
* @return array list of the available css themes
*/
public function getThemeNames()
{
$folder_prefix = $this->themeFolder() . "/";
$folder_prefix_len = strlen($folder_prefix);
$pre_themes = glob("$folder_prefix*.css");
$theme_names = [];
foreach ($pre_themes as $pre_theme) {
$theme_names[] = substr($pre_theme, $folder_prefix_len, -4);
}
return $theme_names;
}
/**
* Deletes the CSS theme with the provided name from the folder
* holding the site's themes. The list is shared, so a theme deleted
* here is gone for every domain that was using it.
*
* @param string $theme_name name of the theme to delete
*/
public function deleteTheme($theme_name)
{
$name = $this->themeFileName($theme_name);
if ($name === "") {
return;
}
$theme_path = $this->themeFolder() . "/$name.css";
if (file_exists($theme_path)) {
unlink($theme_path);
}
}
/**
* Finds the first occurrence of define('$defined', something) in $string
* and returns something
*
* @param string $defined the constant being defined
* @param string $string the haystack string to search in
* @return string matched value of define if exists; empty string otherwise
*/
public function matchDefine($defined, $string)
{
preg_match("/nsdefine\((?:\"$defined\"|\'$defined\')\,([^\)]*)\)/",
$string, $match);
$match = (isset($match[1])) ? trim($match[1]) : "";
$len = strlen($match);
if ($len >=2 && ($match[0] == '"' || $match[0] == "'")) {
$match = substr($match, 1, strlen($match) - 2);
}
return $match;
}
}