Compare commits

...
4 Commits
Author SHA1 Message Date
danwin1210andGitHub 51c939d743 Merge pull request #168 from PhenixHartogh/master
New Features
2026-07-09 10:45:20 +02:00
Phenix Hartogh 742a0a1c28 New Features
- Like System
- Reply System
- Personal notes encrypted with user-defined passwords
2026-06-21 06:41:54 +10:00
danwin1210andGitHub e05ce9a5de Merge pull request #167 from PhenixHartogh/master
Add PGP-2FA
2026-06-09 13:59:57 +02:00
Phenix Hartogh c9f8a4abd3 Add PGP-2FA 2026-06-09 20:39:23 +10:00
2 changed files with 500 additions and 32 deletions
+1
View File
@@ -33,6 +33,7 @@ Features:
* Clean the whole room * Clean the whole room
* Plain text message filter * Plain text message filter
* Regex message filter * Regex message filter
* 2FA via PGP
* And more * And more
Installation Instructions: Installation Instructions:
+499 -32
View File
@@ -99,6 +99,10 @@ function route(): void
}elseif($_REQUEST['action']==='view'){ }elseif($_REQUEST['action']==='view'){
check_session(); check_session();
send_messages(); send_messages();
}elseif($_REQUEST['action']==='like'){
check_session();
toggle_like();
send_messages();
}elseif($_REQUEST['action']==='redirect' && !empty($_GET['url'])){ }elseif($_REQUEST['action']==='redirect' && !empty($_GET['url'])){
send_redirect($_GET['url']); send_redirect($_GET['url']);
}elseif($_REQUEST['action']==='wait'){ }elseif($_REQUEST['action']==='wait'){
@@ -122,6 +126,10 @@ function route(): void
check_login(); check_login();
show_fails(); show_fails();
send_frameset(); send_frameset();
}elseif($_REQUEST['action']==='pgp2fa'){
check_pgp2fa();
show_fails();
send_frameset();
}elseif($_REQUEST['action']==='controls'){ }elseif($_REQUEST['action']==='controls'){
check_session(); check_session();
send_controls(); send_controls();
@@ -469,6 +477,7 @@ function prepare_stylesheets(string $class): void
$styles['messages'] .= '.msg{max-height:180px;overflow-y:auto} #bottom_link{position:fixed;top:0.5em;right:0.5em} #top_link{position:fixed;bottom:0.5em;right:0.5em} '; $styles['messages'] .= '.msg{max-height:180px;overflow-y:auto} #bottom_link{position:fixed;top:0.5em;right:0.5em} #top_link{position:fixed;bottom:0.5em;right:0.5em} ';
$styles['messages'] .= '#chatters th,#chatters td{vertical-align:top} a img{width:15%} a:hover img{width:35%}'; $styles['messages'] .= '#chatters th,#chatters td{vertical-align:top} a img{width:15%} a:hover img{width:35%}';
$styles['messages'] .= '#messages{word-wrap:break-word}'; $styles['messages'] .= '#messages{word-wrap:break-word}';
$styles['messages'] .= '.msgactions form{display:inline}.msgactions input{font-size:smaller}.replyref{font-size:smaller}';
} }
$css=get_setting('css'); $css=get_setting('css');
$coltxt=get_setting('coltxt'); $coltxt=get_setting('coltxt');
@@ -2074,19 +2083,37 @@ function send_notes(int $type): void
echo '<h2>'._('Public notes').'</h2><p>'; echo '<h2>'._('Public notes').'</h2><p>';
$hiddendo=hidden('do', 'public'); $hiddendo=hidden('do', 'public');
} }
if(isset($_POST['text'])){ $notice='';
if(MSGENCRYPTED){ if(isset($_POST['save_notes']) && isset($_POST['text'])){
try { $save=true;
$_POST['text']=base64_encode(sodium_crypto_aead_aes256gcm_encrypt($_POST['text'], '', AES_IV, ENCRYPTKEY)); if($type===2){
} catch (SodiumException $e){ $password=$_POST['note_password'] ?? '';
send_error($e->getMessage()); $confirm=$_POST['note_password_confirm'] ?? '';
if($password===''){
$notice='<b>'._('Please enter a note password.').'</b> ';
$save=false;
}elseif($confirm!=='' && $password!==$confirm){
$notice='<b>'._('Password confirmation does not match!').'</b> ';
$save=false;
}else{
$_POST['text']=encrypt_personal_note($_POST['text'], $password);
} }
} }
$time=time(); if($save){
$stmt=$db->prepare('INSERT INTO ' . PREFIX . 'notes (type, lastedited, editedby, text) VALUES (?, ?, ?, ?);'); if(MSGENCRYPTED){
$stmt->execute([$type, $time, $U['nickname'], $_POST['text']]); try {
echo '<b>'._('Notes saved!').'</b> '; $_POST['text']=base64_encode(sodium_crypto_aead_aes256gcm_encrypt($_POST['text'], '', AES_IV, ENCRYPTKEY));
} catch (SodiumException $e){
send_error($e->getMessage());
}
}
$time=time();
$stmt=$db->prepare('INSERT INTO ' . PREFIX . 'notes (type, lastedited, editedby, text) VALUES (?, ?, ?, ?);');
$stmt->execute([$type, $time, $U['nickname'], $_POST['text']]);
$notice='<b>'._('Notes saved!').'</b> ';
}
} }
echo $notice;
$dateformat=get_setting('dateformat'); $dateformat=get_setting('dateformat');
if(($type!==2) && ($type !==3)){ if(($type!==2) && ($type !==3)){
$stmt=$db->prepare('SELECT COUNT(*) FROM ' . PREFIX . 'notes WHERE type=?;'); $stmt=$db->prepare('SELECT COUNT(*) FROM ' . PREFIX . 'notes WHERE type=?;');
@@ -2120,9 +2147,30 @@ function send_notes(int $type): void
send_error($e->getMessage()); send_error($e->getMessage());
} }
} }
if($type===2){
if(is_personal_note_encrypted($note['text'])){
if(isset($_POST['note_password']) && $_POST['note_password']!==''){
$decrypted=decrypt_personal_note($note['text'], $_POST['note_password']);
if($decrypted===false){
echo '<b>'._('Wrong note password.').'</b> ';
$note['text']='';
}else{
$note['text']=$decrypted;
}
}else{
echo '<b>'._('Enter your note password to view or edit encrypted personal notes.').'</b> ';
$note['text']='';
}
}
}
echo "</p>".form('notes'); echo "</p>".form('notes');
echo "$hiddendo<textarea name=\"text\">".htmlspecialchars($note['text']).'</textarea><br>'; echo "$hiddendo<textarea name=\"text\">".htmlspecialchars($note['text']).'</textarea><br>';
echo submit(_('Save notes')).'</form><br>'; if($type===2){
echo '<label>'._('Note password').': <input type="password" name="note_password"></label> ';
echo '<label>'._('Confirm password').': <input type="password" name="note_password_confirm"></label><br>';
echo submit(_('View notes'), 'name="view_notes"').' ';
}
echo submit(_('Save notes'), 'name="save_notes"').'</form><br>';
if($num[0]>1){ if($num[0]>1){
echo '<br><table><tr><td>'._('Revisions:').'</td>'; echo '<br><table><tr><td>'._('Revisions:').'</td>';
if($revision<$num[0]-1){ if($revision<$num[0]-1){
@@ -2138,6 +2186,57 @@ function send_notes(int $type): void
print_end(); print_end();
} }
function encrypt_personal_note(string $text, string $password): string
{
if(!extension_loaded('sodium')){
send_fatal_error(sprintf(_('The %s extension of PHP is required for encrypted personal notes. Please install it first.'), 'sodium'));
}
try {
$salt=random_bytes(SODIUM_CRYPTO_PWHASH_SALTBYTES);
$nonce=random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$key=sodium_crypto_pwhash(SODIUM_CRYPTO_SECRETBOX_KEYBYTES, $password, $salt, SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE);
$cipher=sodium_crypto_secretbox($text, $nonce, $key);
sodium_memzero($key);
return 'LCNOTE1:'.base64_encode(json_encode([
'salt'=>base64_encode($salt),
'nonce'=>base64_encode($nonce),
'cipher'=>base64_encode($cipher),
]));
} catch (SodiumException $e){
send_error($e->getMessage());
} catch (Exception $e){
send_error($e->getMessage());
}
return '';
}
function decrypt_personal_note(string $payload, string $password)
{
if(!extension_loaded('sodium')){
send_fatal_error(sprintf(_('The %s extension of PHP is required for encrypted personal notes. Please install it first.'), 'sodium'));
}
if(!is_personal_note_encrypted($payload)){
return $payload;
}
$data=json_decode(base64_decode(substr($payload, 8)), true);
if(!is_array($data) || !isset($data['salt'], $data['nonce'], $data['cipher'])){
return false;
}
try {
$key=sodium_crypto_pwhash(SODIUM_CRYPTO_SECRETBOX_KEYBYTES, $password, base64_decode($data['salt']), SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE);
$plain=sodium_crypto_secretbox_open(base64_decode($data['cipher']), base64_decode($data['nonce']), $key);
sodium_memzero($key);
return $plain;
} catch (SodiumException $e){
return false;
}
}
function is_personal_note_encrypted(string $text): bool
{
return strpos($text, 'LCNOTE1:')===0;
}
function send_approve_waiting(): void function send_approve_waiting(): void
{ {
global $db; global $db;
@@ -2248,8 +2347,16 @@ function send_post(string $rejected=''): void
if(!isset($_REQUEST['sendto'])){ if(!isset($_REQUEST['sendto'])){
$_REQUEST['sendto']=''; $_REQUEST['sendto']='';
} }
$reply_to=get_valid_reply_id($_REQUEST['reply_to'] ?? 0);
if($reply_to>0 && $_REQUEST['sendto']===''){
$_REQUEST['sendto']=get_reply_sendto($reply_to);
}
echo '<table><tr><td>'.form('post'); echo '<table><tr><td>'.form('post');
echo hidden('postid', $U['postid']); echo hidden('postid', $U['postid']);
if($reply_to>0){
echo hidden('reply_to', (string) $reply_to);
echo '<div class="replyref">'.sprintf(_('Replying to message #%d'), $reply_to).'</div>';
}
if(isset($_POST['multi'])){ if(isset($_POST['multi'])){
echo hidden('multi', 'on'); echo hidden('multi', 'on');
} }
@@ -2419,6 +2526,14 @@ function send_profile(string $arg=''): void
while($tmp=$stmt->fetch(PDO::FETCH_ASSOC)){ while($tmp=$stmt->fetch(PDO::FETCH_ASSOC)){
$ignored[]=htmlspecialchars($tmp['ign']); $ignored[]=htmlspecialchars($tmp['ign']);
} }
$pgpkey='';
if($U['status']>=2){
$stmt=$db->prepare('SELECT pgpkey FROM ' . PREFIX . 'members WHERE nickname=?;');
$stmt->execute([$U['nickname']]);
if($tmp=$stmt->fetch(PDO::FETCH_NUM)){
$pgpkey=$tmp[0] ?? '';
}
}
if(count($ignored)>0){ if(count($ignored)>0){
echo '<tr><td><table id="unignore"><tr><th>'._("Don't ignore anymore").'</th><td>'; echo '<tr><td><table id="unignore"><tr><th>'._("Don't ignore anymore").'</th><td>';
echo '<select name="unignore" size="1"><option value="">'._('(choose)').'</option>'; echo '<select name="unignore" size="1"><option value="">'._('(choose)').'</option>';
@@ -2544,6 +2659,11 @@ function send_profile(string $arg=''): void
echo '<tr><td>&nbsp;</td><td>'._('Confirm new password:').'</td><td><input type="password" name="confirmpass" size="20" autocomplete="new-password"></td></tr>'; echo '<tr><td>&nbsp;</td><td>'._('Confirm new password:').'</td><td><input type="password" name="confirmpass" size="20" autocomplete="new-password"></td></tr>';
echo '</table></td></tr></table></td></tr>'; echo '</table></td></tr></table></td></tr>';
thr(); thr();
echo '<tr><td><table id="pgp2fa"><tr><th>'._('PGP two-factor authentication').'</th></tr>';
echo '<tr><td><textarea name="pgpkey" rows="8" cols="72" placeholder="'._('Paste your PGP public key here to require PGP 2FA at login.').'">'.htmlspecialchars($pgpkey).'</textarea></td></tr>';
echo '<tr><td><label><input type="checkbox" name="clearpgpkey" value="on"> '._('Remove PGP two-factor authentication').'</label></td></tr>';
echo '</table></td></tr>';
thr();
echo '<tr><td><table id="changenick"><tr><th>'._('Change Nickname').'</th><td><table>'; echo '<tr><td><table id="changenick"><tr><th>'._('Change Nickname').'</th><td><table>';
echo '<tr><td>&nbsp;</td><td>'._('New nickname:').'</td><td><input type="text" name="newnickname" size="20" autocomplete="username">'; echo '<tr><td>&nbsp;</td><td>'._('New nickname:').'</td><td><input type="text" name="newnickname" size="20" autocomplete="username">';
echo '</table></td></tr></table></td></tr>'; echo '</table></td></tr></table></td></tr>';
@@ -2668,6 +2788,22 @@ function send_colours(): void
print_end(); print_end();
} }
function send_pgp2fa(string $nickname, string $encrypted_challenge): void
{
print_start('pgp2fa');
echo '<h1 id="chatname">'.get_setting('chatname').'</h1>';
echo '<h2>'._('PGP two-factor authentication').'</h2>';
echo '<p>'._('Decrypt this message with your PGP private key, then enter the verification code.').'</p>';
echo '<textarea rows="12" cols="72" readonly>'.htmlspecialchars($encrypted_challenge).'</textarea>';
echo form_target('_parent', 'pgp2fa');
echo hidden('nick', htmlspecialchars($nickname));
echo '<table>';
echo '<tr><td>'._('Verification code:').'</td><td><input type="text" name="pgpcode" size="20" autocomplete="one-time-code" autofocus></td></tr>';
echo '<tr><td colspan="2">'.submit(_('Enter Chat')).'</td></tr></table></form>';
echo form_target('_parent', '').submit(_('Back to the login page.'), 'class="backbutton"').'</form>';
print_end();
}
function send_login(): void function send_login(): void
{ {
$ga=(int) get_setting('guestaccess'); $ga=(int) get_setting('guestaccess');
@@ -2836,6 +2972,9 @@ function create_session(bool $setup, string $nickname, string $password): void
if($setup && $U['status']>=7){ if($setup && $U['status']>=7){
$U['incognito']=1; $U['incognito']=1;
} }
if(!$setup && !empty(trim($U['pgpkey'] ?? ''))){
start_pgp2fa();
}
$U['entry']=$U['lastpost']=time(); $U['entry']=$U['lastpost']=time();
}else{ }else{
add_user_defaults($password); add_user_defaults($password);
@@ -2865,6 +3004,189 @@ function create_session(bool $setup, string $nickname, string $password): void
write_new_session($password); write_new_session($password);
} }
function pgp_encrypt_message(string $public_key, string $message) : string
{
try {
$encrypted=pgp_try_encrypt_message($public_key, $message);
} catch(Throwable $e) {
send_error($e->getMessage());
}
return $encrypted;
}
function pgp_try_encrypt_message(string $public_key, string $message) : string
{
$public_key=str_replace("\r\n", "\n", trim($public_key));
if(extension_loaded('gnupg')){
$gpg=new gnupg();
$info=$gpg->import($public_key);
$fingerprint=pgp_import_fingerprint($info);
if($fingerprint!==''){
$gpg->addencryptkey($fingerprint);
$encrypted=$gpg->encrypt($message);
if($encrypted!==false){
return $encrypted;
}
}
}
return pgp_try_encrypt_message_with_gpg($public_key, $message);
}
function pgp_import_fingerprint($info) : string
{
if(!is_array($info)){
return '';
}
if(!empty($info['fingerprint']) && is_string($info['fingerprint'])){
return $info['fingerprint'];
}
if(!empty($info['fingerprints']) && is_array($info['fingerprints'])){
foreach($info['fingerprints'] as $fingerprint){
if(is_string($fingerprint) && $fingerprint!==''){
return $fingerprint;
}
}
}
if(!empty($info[0]) && is_string($info[0])){
return $info[0];
}
return '';
}
function pgp_try_encrypt_message_with_gpg(string $public_key, string $message) : string
{
if(!function_exists('exec')){
throw new RuntimeException(_('The PHP exec function or the gnupg extension is required for PGP two-factor authentication.'));
}
$gpg=pgp_find_gpg_binary();
$base=rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'lechatpgp-'.bin2hex(random_bytes(8));
if(!mkdir($base, 0700)){
throw new RuntimeException(_('Could not create a temporary directory for PGP two-factor authentication.'));
}
$keyfile=$base.DIRECTORY_SEPARATOR.'key.asc';
$messagefile=$base.DIRECTORY_SEPARATOR.'message.txt';
$outputfile=$base.DIRECTORY_SEPARATOR.'message.asc';
file_put_contents($keyfile, $public_key);
file_put_contents($messagefile, $message);
try {
$result=pgp_run_command([$gpg, '--batch', '--homedir', $base, '--import', $keyfile]);
if($result['code']!==0){
throw new RuntimeException(_('Invalid PGP public key.').' '.htmlspecialchars($result['output']));
}
$result=pgp_run_command([$gpg, '--batch', '--homedir', $base, '--with-colons', '--fingerprint', '--list-keys']);
$fingerprint='';
foreach(explode("\n", $result['output']) as $line){
if(preg_match('/^fpr:::::::::([0-9A-F]+):/i', $line, $match)){
$fingerprint=$match[1];
break;
}
}
if($fingerprint===''){
throw new RuntimeException(_('Invalid PGP public key.'));
}
$result=pgp_run_command([$gpg, '--batch', '--yes', '--trust-model', 'always', '--homedir', $base, '--armor', '--encrypt', '--recipient', $fingerprint, '--output', $outputfile, $messagefile]);
if($result['code']!==0 || !is_file($outputfile)){
throw new RuntimeException(_('Could not encrypt the PGP two-factor authentication challenge.').' '.htmlspecialchars($result['output']));
}
return file_get_contents($outputfile);
} finally {
pgp_remove_directory($base);
}
}
function pgp_find_gpg_binary() : string
{
if(defined('GPG_BINARY')){
return GPG_BINARY;
}
foreach(['gpg', '/usr/bin/gpg', '/usr/local/bin/gpg', '/opt/homebrew/bin/gpg'] as $gpg){
$result=pgp_run_command([$gpg, '--version']);
if($result['code']===0){
return $gpg;
}
}
throw new RuntimeException(_('The gpg command or the gnupg extension is required for PGP two-factor authentication.'));
}
function pgp_run_command(array $args) : array
{
$cmd=implode(' ', array_map('escapeshellarg', $args));
$output=[];
$code=0;
exec($cmd.' 2>&1', $output, $code);
return ['code' => $code, 'output' => implode("\n", $output)];
}
function pgp_remove_directory(string $dir): void
{
if(!is_dir($dir)){
return;
}
foreach(scandir($dir) as $file){
if($file==='.' || $file==='..'){
continue;
}
$path=$dir.DIRECTORY_SEPARATOR.$file;
if(is_dir($path)){
pgp_remove_directory($path);
}else{
unlink($path);
}
}
rmdir($dir);
}
function start_pgp2fa(): void
{
global $U, $db;
try {
$code=strtoupper(bin2hex(random_bytes(4)));
} catch(Exception $e) {
send_error($e->getMessage());
}
$message=sprintf(_("Your verification code for %s is: %s"), get_setting('chatname'), $code);
$encrypted=pgp_encrypt_message($U['pgpkey'], $message);
$stmt=$db->prepare('UPDATE ' . PREFIX . 'members SET pgpchallengehash=?, pgpchallengeexpires=? WHERE nickname=?;');
$stmt->execute([hash('sha256', $code), time()+300, $U['nickname']]);
send_pgp2fa($U['nickname'], $encrypted);
}
function check_pgp2fa(): void
{
global $U, $db;
if(empty($_POST['nick']) || empty($_POST['pgpcode'])){
send_login();
}
$nick=preg_replace('/\s/', '', $_POST['nick']);
$stmt=$db->prepare('SELECT * FROM ' . PREFIX . 'members WHERE nickname=?;');
$stmt->execute([$nick]);
if(!$member=$stmt->fetch(PDO::FETCH_ASSOC)){
send_error(_('Invalid PGP two-factor authentication challenge.'));
}
$hash=$member['pgpchallengehash'] ?? '';
$expires=(int) ($member['pgpchallengeexpires'] ?? 0);
$code=strtoupper(preg_replace('/[^0-9A-F]/i', '', $_POST['pgpcode']));
if($hash==='' || $expires<time() || !hash_equals($hash, hash('sha256', $code))){
$stmt=$db->prepare('UPDATE ' . PREFIX . 'members SET loginfails=? WHERE nickname=?;');
$stmt->execute([$member['loginfails']+1, $member['nickname']]);
send_error(_('Invalid or expired PGP two-factor authentication code.'));
}
$stmt=$db->prepare('UPDATE ' . PREFIX . 'members SET pgpchallengehash=?, pgpchallengeexpires=?, lastlogin=? WHERE nickname=?;');
$stmt->execute(['', 0, time(), $member['nickname']]);
$U=$member;
$U['entry']=$U['lastpost']=time();
$U['exiting']=0;
try {
$U[ 'postid' ] = bin2hex( random_bytes( 3 ) );
} catch(Exception $e) {
send_error($e->getMessage());
}
write_new_session('', true);
if($U['status']==1 && in_array((int) get_setting('guestaccess'), [2, 3], true)){
send_waiting_room();
}
}
function check_captcha(string $challenge, string $captcha_code): void function check_captcha(string $challenge, string $captcha_code): void
{ {
global $db, $memcached; global $db, $memcached;
@@ -2920,14 +3242,14 @@ function set_secure_cookie(string $name, string $value): void
} }
} }
function write_new_session(string $password): void function write_new_session(string $password, bool $authenticated=false): void
{ {
global $U, $db, $session; global $U, $db, $session;
$stmt=$db->prepare('SELECT * FROM ' . PREFIX . 'sessions WHERE nickname=?;'); $stmt=$db->prepare('SELECT * FROM ' . PREFIX . 'sessions WHERE nickname=?;');
$stmt->execute([$U['nickname']]); $stmt->execute([$U['nickname']]);
if($temp=$stmt->fetch(PDO::FETCH_ASSOC)){ if($temp=$stmt->fetch(PDO::FETCH_ASSOC)){
// check whether alrady logged in // check whether alrady logged in
if(password_verify($password, $temp['passhash'])){ if(($authenticated && hash_equals($U['passhash'], $temp['passhash'])) || password_verify($password, $temp['passhash'])){
$U=$temp; $U=$temp;
check_kicked(); check_kicked();
set_secure_cookie(COOKIENAME, $U['session']); set_secure_cookie(COOKIENAME, $U['session']);
@@ -3432,8 +3754,18 @@ function save_profile() : string {
$stmt=$db->prepare('UPDATE ' . PREFIX . 'sessions SET refresh=?, style=?, bgcolour=?, timestamps=?, embed=?, incognito=?, nocache=?, tz=?, eninbox=?, sortupdown=?, hidechatters=? WHERE session=?;'); $stmt=$db->prepare('UPDATE ' . PREFIX . 'sessions SET refresh=?, style=?, bgcolour=?, timestamps=?, embed=?, incognito=?, nocache=?, tz=?, eninbox=?, sortupdown=?, hidechatters=? WHERE session=?;');
$stmt->execute([$U['refresh'], $U['style'], $U['bgcolour'], $U['timestamps'], $U['embed'], $U['incognito'], $U['nocache'], $U['tz'], $U['eninbox'], $U['sortupdown'], $U['hidechatters'], $U['session']]); $stmt->execute([$U['refresh'], $U['style'], $U['bgcolour'], $U['timestamps'], $U['embed'], $U['incognito'], $U['nocache'], $U['tz'], $U['eninbox'], $U['sortupdown'], $U['hidechatters'], $U['session']]);
if($U['status']>=2){ if($U['status']>=2){
$stmt=$db->prepare('UPDATE ' . PREFIX . 'members SET refresh=?, bgcolour=?, timestamps=?, embed=?, incognito=?, style=?, nocache=?, tz=?, eninbox=?, sortupdown=?, hidechatters=? WHERE nickname=?;'); $pgpkey=trim($_POST['pgpkey'] ?? '');
$stmt->execute([$U['refresh'], $U['bgcolour'], $U['timestamps'], $U['embed'], $U['incognito'], $U['style'], $U['nocache'], $U['tz'], $U['eninbox'], $U['sortupdown'], $U['hidechatters'], $U['nickname']]); if(isset($_POST['clearpgpkey'])){
$pgpkey='';
}elseif($pgpkey!==''){
try {
pgp_try_encrypt_message($pgpkey, 'test');
} catch(Throwable $e) {
return $e->getMessage();
}
}
$stmt=$db->prepare('UPDATE ' . PREFIX . 'members SET refresh=?, bgcolour=?, timestamps=?, embed=?, incognito=?, style=?, nocache=?, tz=?, eninbox=?, sortupdown=?, hidechatters=?, pgpkey=?, pgpchallengehash=?, pgpchallengeexpires=? WHERE nickname=?;');
$stmt->execute([$U['refresh'], $U['bgcolour'], $U['timestamps'], $U['embed'], $U['incognito'], $U['style'], $U['nocache'], $U['tz'], $U['eninbox'], $U['sortupdown'], $U['hidechatters'], $pgpkey, '', 0, $U['nickname']]);
} }
if(!empty($_POST['unignore'])){ if(!empty($_POST['unignore'])){
$stmt=$db->prepare('DELETE FROM ' . PREFIX . 'ignored WHERE ign=? AND ignby=?;'); $stmt=$db->prepare('DELETE FROM ' . PREFIX . 'ignored WHERE ign=? AND ignby=?;');
@@ -3641,7 +3973,8 @@ function validate_input() : string {
$message=sprintf(get_setting('msgattache'), "<a class=\"attachement\" href=\"$_SERVER[SCRIPT_NAME]?action=download&amp;id=$hash\" target=\"_blank\">$name</a>", $message); $message=sprintf(get_setting('msgattache'), "<a class=\"attachement\" href=\"$_SERVER[SCRIPT_NAME]?action=download&amp;id=$hash\" target=\"_blank\">$name</a>", $message);
} }
} }
if(add_message($message, $recipient, $U['nickname'], (int) $U['status'], $poststatus, $displaysend, $U['style'])){ $reply_to=get_valid_reply_id($_POST['reply_to'] ?? 0);
if(add_message($message, $recipient, $U['nickname'], (int) $U['status'], $poststatus, $displaysend, $U['style'], $reply_to)){
$U['lastpost']=time(); $U['lastpost']=time();
try { try {
$U[ 'postid' ] = bin2hex( random_bytes( 3 ) ); $U[ 'postid' ] = bin2hex( random_bytes( 3 ) );
@@ -3814,7 +4147,7 @@ function apply_mention(string $message) : string {
}, $message); }, $message);
} }
function add_message(string $message, string $recipient, string $poster, int $delstatus, int $poststatus, string $displaysend, string$style) : bool { function add_message(string $message, string $recipient, string $poster, int $delstatus, int $poststatus, string $displaysend, string $style, int $reply_to=0) : bool {
global $db; global $db;
if($message===''){ if($message===''){
return false; return false;
@@ -3825,11 +4158,12 @@ function add_message(string $message, string $recipient, string $poster, int $de
'poster' =>$poster, 'poster' =>$poster,
'recipient' =>$recipient, 'recipient' =>$recipient,
'text' =>"<span class=\"usermsg\">$displaysend".style_this($message, $style).'</span>', 'text' =>"<span class=\"usermsg\">$displaysend".style_this($message, $style).'</span>',
'delstatus' =>$delstatus 'delstatus' =>$delstatus,
'reply_to' =>$reply_to
]; ];
//prevent posting the same message twice, if no other message was posted in-between. //prevent posting the same message twice, if no other message was posted in-between.
$stmt=$db->prepare('SELECT id FROM ' . PREFIX . 'messages WHERE poststatus=? AND poster=? AND recipient=? AND text=? AND id IN (SELECT * FROM (SELECT id FROM ' . PREFIX . 'messages ORDER BY id DESC LIMIT 1) AS t);'); $stmt=$db->prepare('SELECT id FROM ' . PREFIX . 'messages WHERE poststatus=? AND poster=? AND recipient=? AND text=? AND reply_to=? AND id IN (SELECT * FROM (SELECT id FROM ' . PREFIX . 'messages ORDER BY id DESC LIMIT 1) AS t);');
$stmt->execute([$newmessage['poststatus'], $newmessage['poster'], $newmessage['recipient'], $newmessage['text']]); $stmt->execute([$newmessage['poststatus'], $newmessage['poster'], $newmessage['recipient'], $newmessage['text'], $newmessage['reply_to']]);
if($stmt->fetch(PDO::FETCH_NUM)){ if($stmt->fetch(PDO::FETCH_NUM)){
return false; return false;
} }
@@ -3849,7 +4183,8 @@ function add_system_message(string $mes, string $doer): void
'poster' =>'', 'poster' =>'',
'recipient' =>'', 'recipient' =>'',
'text' =>"$mes", 'text' =>"$mes",
'delstatus' =>4 'delstatus' =>4,
'reply_to' =>0
]; ];
} else { } else {
@@ -3859,7 +4194,8 @@ function add_system_message(string $mes, string $doer): void
'poster' =>'', 'poster' =>'',
'recipient' =>'', 'recipient' =>'',
'text' =>"$mes ($doer)", 'text' =>"$mes ($doer)",
'delstatus' =>4 'delstatus' =>4,
'reply_to' =>0
]; ];
} }
write_message($sysmessage); write_message($sysmessage);
@@ -3876,7 +4212,8 @@ function add_system_pm_message(string $mes, string $recipient, string $doer): vo
'poster' =>'System', 'poster' =>'System',
'recipient' => $recipient, 'recipient' => $recipient,
'text' =>"$mes", 'text' =>"$mes",
'delstatus' =>4 'delstatus' =>4,
'reply_to' =>0
]; ];
} else { } else {
@@ -3886,7 +4223,8 @@ function add_system_pm_message(string $mes, string $recipient, string $doer): vo
'poster' =>'System', 'poster' =>'System',
'recipient' => $recipient, 'recipient' => $recipient,
'text' =>"$mes ($doer)", 'text' =>"$mes ($doer)",
'delstatus' =>4 'delstatus' =>4,
'reply_to' =>0
]; ];
} }
write_message($sysmessage); write_message($sysmessage);
@@ -3901,8 +4239,11 @@ function write_message(array $message): void
send_error($e->getMessage()); send_error($e->getMessage());
} }
} }
$stmt=$db->prepare('INSERT INTO ' . PREFIX . 'messages (postdate, poststatus, poster, recipient, text, delstatus) VALUES (?, ?, ?, ?, ?, ?);'); if(!isset($message['reply_to'])){
$stmt->execute([$message['postdate'], $message['poststatus'], $message['poster'], $message['recipient'], $message['text'], $message['delstatus']]); $message['reply_to']=0;
}
$stmt=$db->prepare('INSERT INTO ' . PREFIX . 'messages (postdate, poststatus, poster, recipient, text, delstatus, reply_to) VALUES (?, ?, ?, ?, ?, ?, ?);');
$stmt->execute([$message['postdate'], $message['poststatus'], $message['poster'], $message['recipient'], $message['text'], $message['delstatus'], $message['reply_to']]);
if($message['poststatus']<9 && get_setting('sendmail')){ if($message['poststatus']<9 && get_setting('sendmail')){
$subject='New Chat message'; $subject='New Chat message';
$headers='From: '.get_setting('mailsender')."\r\nX-Mailer: PHP/".phpversion()."\r\nContent-Type: text/html; charset=UTF-8\r\n"; $headers='From: '.get_setting('mailsender')."\r\nX-Mailer: PHP/".phpversion()."\r\nContent-Type: text/html; charset=UTF-8\r\n";
@@ -4008,7 +4349,7 @@ function print_messages(int $delstatus=0): void
} }
echo '<div id="messages">'; echo '<div id="messages">';
if($delstatus>0){ if($delstatus>0){
$stmt=$db->prepare('SELECT postdate, id, text FROM ' . PREFIX . 'messages WHERE '. $stmt=$db->prepare('SELECT postdate, id, text, reply_to FROM ' . PREFIX . 'messages WHERE '.
"(poststatus<? AND delstatus<?) OR ((poster=? OR recipient=?) AND postdate>=?) ORDER BY id $direction;"); "(poststatus<? AND delstatus<?) OR ((poster=? OR recipient=?) AND postdate>=?) ORDER BY id $direction;");
$stmt->execute([$U['status'], $delstatus, $U['nickname'], $U['nickname'], $entry]); $stmt->execute([$U['status'], $delstatus, $U['nickname'], $U['nickname'], $entry]);
while($message=$stmt->fetch(PDO::FETCH_ASSOC)){ while($message=$stmt->fetch(PDO::FETCH_ASSOC)){
@@ -4020,7 +4361,7 @@ function print_messages(int $delstatus=0): void
echo " $message[text]</label></div>"; echo " $message[text]</label></div>";
} }
}else{ }else{
$stmt=$db->prepare('SELECT id, postdate, poststatus, text FROM ' . PREFIX . 'messages WHERE (poststatus<=? OR poststatus=4 OR '. $stmt=$db->prepare('SELECT id, postdate, poststatus, poster, recipient, text, reply_to FROM ' . PREFIX . 'messages WHERE (poststatus<=? OR poststatus=4 OR '.
'(poststatus=9 AND ( (poster=? AND recipient NOT IN (SELECT ign FROM ' . PREFIX . 'ignored WHERE ignby=?) ) OR recipient=?) AND postdate>=?)'. '(poststatus=9 AND ( (poster=? AND recipient NOT IN (SELECT ign FROM ' . PREFIX . 'ignored WHERE ignby=?) ) OR recipient=?) AND postdate>=?)'.
') AND poster NOT IN (SELECT ign FROM ' . PREFIX . "ignored WHERE ignby=?) ORDER BY id $direction;"); ') AND poster NOT IN (SELECT ign FROM ' . PREFIX . "ignored WHERE ignby=?) ORDER BY id $direction;");
$stmt->execute([$U['status'], $U['nickname'], $U['nickname'], $U['nickname'], $entry, $U['nickname']]); $stmt->execute([$U['status'], $U['nickname'], $U['nickname'], $U['nickname'], $entry, $U['nickname']]);
@@ -4033,13 +4374,47 @@ function print_messages(int $delstatus=0): void
if ($message['poststatus']==4) { if ($message['poststatus']==4) {
echo '<span class="sysmsg" title="'._('system message').'">'.get_setting('sysmessagetxt')."$message[text]</span></div>"; echo '<span class="sysmsg" title="'._('system message').'">'.get_setting('sysmessagetxt')."$message[text]</span></div>";
} else { } else {
echo "$message[text]</div>"; print_reply_reference((int) $message['reply_to']);
echo "$message[text]";
print_message_actions($message);
echo '</div>';
} }
} }
} }
echo '</div>'; echo '</div>';
} }
function print_reply_reference(int $reply_to): void
{
global $db;
if($reply_to<1 || !can_view_message($reply_to)){
return;
}
$stmt=$db->prepare('SELECT poster FROM ' . PREFIX . 'messages WHERE id=?;');
$stmt->execute([$reply_to]);
if($message=$stmt->fetch(PDO::FETCH_ASSOC)){
echo '<div class="replyref">'.sprintf(_('Reply to #%1$d by %2$s'), $reply_to, htmlspecialchars($message['poster'])).'</div>';
}
}
function print_message_actions(array $message): void
{
global $U;
$liked=is_message_liked((int) $message['id']);
$likes=count_message_likes((int) $message['id']);
echo '<div class="msgactions">';
echo form('like').hidden('mid', (string) $message['id']);
echo submit(($liked ? _('Unlike') : _('Like'))." ($likes)").'</form> ';
echo form_target('post', 'post');
echo hidden('reply_to', (string) $message['id']);
echo hidden('sendto', htmlspecialchars(get_reply_sendto((int) $message['id'])));
if($U['sortupdown']){
echo hidden('sort', '1');
}
echo submit(_('Reply')).'</form>';
echo '</div>';
}
function prepare_message_print(array &$message, bool $removeEmbed): void function prepare_message_print(array &$message, bool $removeEmbed): void
{ {
if(MSGENCRYPTED){ if(MSGENCRYPTED){
@@ -4058,6 +4433,76 @@ function prepare_message_print(array &$message, bool $removeEmbed): void
} }
} }
function toggle_like(): void
{
global $U, $db;
$message_id=(int) ($_POST['mid'] ?? 0);
if($message_id<1 || !can_view_message($message_id)){
return;
}
$stmt=$db->prepare('SELECT id FROM ' . PREFIX . 'likes WHERE message_id=? AND nickname=?;');
$stmt->execute([$message_id, $U['nickname']]);
if($like=$stmt->fetch(PDO::FETCH_ASSOC)){
$stmt=$db->prepare('DELETE FROM ' . PREFIX . 'likes WHERE id=?;');
$stmt->execute([$like['id']]);
}else{
$stmt=$db->prepare('INSERT INTO ' . PREFIX . 'likes (message_id, nickname) VALUES (?, ?);');
$stmt->execute([$message_id, $U['nickname']]);
}
}
function count_message_likes(int $message_id): int
{
global $db;
$stmt=$db->prepare('SELECT COUNT(*) FROM ' . PREFIX . 'likes WHERE message_id=?;');
$stmt->execute([$message_id]);
return (int) $stmt->fetchColumn();
}
function is_message_liked(int $message_id): bool
{
global $U, $db;
$stmt=$db->prepare('SELECT null FROM ' . PREFIX . 'likes WHERE message_id=? AND nickname=?;');
$stmt->execute([$message_id, $U['nickname']]);
return (bool) $stmt->fetch(PDO::FETCH_NUM);
}
function get_valid_reply_id($reply_to): int
{
$reply_to=(int) $reply_to;
if($reply_to<1 || !can_view_message($reply_to)){
return 0;
}
return $reply_to;
}
function get_reply_sendto(int $reply_to): string
{
global $U, $db;
$stmt=$db->prepare('SELECT poststatus, poster, recipient FROM ' . PREFIX . 'messages WHERE id=?;');
$stmt->execute([$reply_to]);
if($message=$stmt->fetch(PDO::FETCH_ASSOC)){
if((int) $message['poststatus']===9 && $message['poster']!==$U['nickname']){
return $message['poster'];
}
}
return 's *';
}
function can_view_message(int $message_id): bool
{
global $U, $db;
if($message_id<1){
return false;
}
$entry=$U['status']>1 ? 0 : $U['entry'];
$stmt=$db->prepare('SELECT null FROM ' . PREFIX . 'messages WHERE id=? AND (poststatus<=? OR poststatus=4 OR '.
'(poststatus=9 AND ( (poster=? AND recipient NOT IN (SELECT ign FROM ' . PREFIX . 'ignored WHERE ignby=?) ) OR recipient=?) AND postdate>=?)'.
') AND poster NOT IN (SELECT ign FROM ' . PREFIX . 'ignored WHERE ignby=?);');
$stmt->execute([$message_id, $U['status'], $U['nickname'], $U['nickname'], $U['nickname'], $entry, $U['nickname']]);
return (bool) $stmt->fetch(PDO::FETCH_NUM);
}
// this and that // this and that
function send_headers(): void function send_headers(): void
@@ -4342,6 +4787,12 @@ function cron(): void
while($tmp=$result->fetch(PDO::FETCH_NUM)){ while($tmp=$result->fetch(PDO::FETCH_NUM)){
$stmt->execute($tmp); $stmt->execute($tmp);
} }
// delete likes that do not belong to any message
$result=$db->query('SELECT id FROM ' . PREFIX . 'likes WHERE message_id NOT IN (SELECT id FROM ' . PREFIX . 'messages);');
$stmt=$db->prepare('DELETE FROM ' . PREFIX . 'likes WHERE id=?;');
while($tmp=$result->fetch(PDO::FETCH_NUM)){
$stmt->execute($tmp);
}
// delete old notes // delete old notes
$limit=get_setting('numnotes'); $limit=get_setting('numnotes');
$to_keep = []; $to_keep = [];
@@ -4452,16 +4903,20 @@ function init_chat(): void
$db->exec('CREATE TABLE ' . PREFIX . "ignored (id $primary, ign varchar(50) NOT NULL, ignby varchar(50) NOT NULL)$diskengine$charset;"); $db->exec('CREATE TABLE ' . PREFIX . "ignored (id $primary, ign varchar(50) NOT NULL, ignby varchar(50) NOT NULL)$diskengine$charset;");
$db->exec('CREATE INDEX ' . PREFIX . 'ign ON ' . PREFIX . 'ignored(ign);'); $db->exec('CREATE INDEX ' . PREFIX . 'ign ON ' . PREFIX . 'ignored(ign);');
$db->exec('CREATE INDEX ' . PREFIX . 'ignby ON ' . PREFIX . 'ignored(ignby);'); $db->exec('CREATE INDEX ' . PREFIX . 'ignby ON ' . PREFIX . 'ignored(ignby);');
$db->exec('CREATE TABLE ' . PREFIX . "members (id $primary, nickname varchar(50) NOT NULL UNIQUE, passhash varchar(255) NOT NULL, status smallint NOT NULL, refresh smallint NOT NULL, bgcolour char(6) NOT NULL, regedby varchar(50) DEFAULT '', lastlogin integer DEFAULT 0, loginfails integer unsigned NOT NULL DEFAULT 0, timestamps smallint NOT NULL, embed smallint NOT NULL, incognito smallint NOT NULL, style varchar(255) NOT NULL, nocache smallint NOT NULL, tz varchar(255) NOT NULL, eninbox smallint NOT NULL, sortupdown smallint NOT NULL, hidechatters smallint NOT NULL, nocache_old smallint NOT NULL)$diskengine$charset;"); $db->exec('CREATE TABLE ' . PREFIX . "members (id $primary, nickname varchar(50) NOT NULL UNIQUE, passhash varchar(255) NOT NULL, status smallint NOT NULL, refresh smallint NOT NULL, bgcolour char(6) NOT NULL, regedby varchar(50) DEFAULT '', lastlogin integer DEFAULT 0, loginfails integer unsigned NOT NULL DEFAULT 0, timestamps smallint NOT NULL, embed smallint NOT NULL, incognito smallint NOT NULL, style varchar(255) NOT NULL, nocache smallint NOT NULL, tz varchar(255) NOT NULL, eninbox smallint NOT NULL, sortupdown smallint NOT NULL, hidechatters smallint NOT NULL, nocache_old smallint NOT NULL, pgpkey text, pgpchallengehash varchar(64) NOT NULL DEFAULT '', pgpchallengeexpires integer NOT NULL DEFAULT 0)$diskengine$charset;");
$db->exec('CREATE TABLE ' . PREFIX . "inbox (id $primary, postdate integer NOT NULL, postid integer NOT NULL UNIQUE, poster varchar(50) NOT NULL, recipient varchar(50) NOT NULL, text text NOT NULL, FOREIGN KEY (recipient) REFERENCES " . PREFIX . "members(nickname) ON DELETE CASCADE ON UPDATE CASCADE)$diskengine$charset;"); $db->exec('CREATE TABLE ' . PREFIX . "inbox (id $primary, postdate integer NOT NULL, postid integer NOT NULL UNIQUE, poster varchar(50) NOT NULL, recipient varchar(50) NOT NULL, text text NOT NULL, FOREIGN KEY (recipient) REFERENCES " . PREFIX . "members(nickname) ON DELETE CASCADE ON UPDATE CASCADE)$diskengine$charset;");
$db->exec('CREATE INDEX ' . PREFIX . 'inbox_poster ON ' . PREFIX . 'inbox(poster);'); $db->exec('CREATE INDEX ' . PREFIX . 'inbox_poster ON ' . PREFIX . 'inbox(poster);');
$db->exec('CREATE INDEX ' . PREFIX . 'inbox_recipient ON ' . PREFIX . 'inbox(recipient);'); $db->exec('CREATE INDEX ' . PREFIX . 'inbox_recipient ON ' . PREFIX . 'inbox(recipient);');
$db->exec('CREATE TABLE ' . PREFIX . "linkfilter (id $primary, filtermatch varchar(255) NOT NULL, filterreplace varchar(255) NOT NULL, regex smallint NOT NULL)$diskengine$charset;"); $db->exec('CREATE TABLE ' . PREFIX . "linkfilter (id $primary, filtermatch varchar(255) NOT NULL, filterreplace varchar(255) NOT NULL, regex smallint NOT NULL)$diskengine$charset;");
$db->exec('CREATE TABLE ' . PREFIX . "messages (id $primary, postdate integer NOT NULL, poststatus smallint NOT NULL, poster varchar(50) NOT NULL, recipient varchar(50) NOT NULL, text text NOT NULL, delstatus smallint NOT NULL)$diskengine$charset;"); $db->exec('CREATE TABLE ' . PREFIX . "messages (id $primary, postdate integer NOT NULL, poststatus smallint NOT NULL, poster varchar(50) NOT NULL, recipient varchar(50) NOT NULL, text text NOT NULL, delstatus smallint NOT NULL, reply_to integer NOT NULL DEFAULT 0)$diskengine$charset;");
$db->exec('CREATE INDEX ' . PREFIX . 'poster ON ' . PREFIX . 'messages (poster);'); $db->exec('CREATE INDEX ' . PREFIX . 'poster ON ' . PREFIX . 'messages (poster);');
$db->exec('CREATE INDEX ' . PREFIX . 'recipient ON ' . PREFIX . 'messages(recipient);'); $db->exec('CREATE INDEX ' . PREFIX . 'recipient ON ' . PREFIX . 'messages(recipient);');
$db->exec('CREATE INDEX ' . PREFIX . 'postdate ON ' . PREFIX . 'messages(postdate);'); $db->exec('CREATE INDEX ' . PREFIX . 'postdate ON ' . PREFIX . 'messages(postdate);');
$db->exec('CREATE INDEX ' . PREFIX . 'poststatus ON ' . PREFIX . 'messages(poststatus);'); $db->exec('CREATE INDEX ' . PREFIX . 'poststatus ON ' . PREFIX . 'messages(poststatus);');
$db->exec('CREATE INDEX ' . PREFIX . 'reply_to ON ' . PREFIX . 'messages(reply_to);');
$db->exec('CREATE TABLE ' . PREFIX . "likes (id $primary, message_id integer NOT NULL, nickname varchar(50) NOT NULL)$diskengine$charset;");
$db->exec('CREATE INDEX ' . PREFIX . 'likes_message_id ON ' . PREFIX . 'likes(message_id);');
$db->exec('CREATE UNIQUE INDEX ' . PREFIX . 'likes_message_nickname ON ' . PREFIX . 'likes(message_id, nickname);');
$db->exec('CREATE TABLE ' . PREFIX . "notes (id $primary, type smallint NOT NULL, lastedited integer NOT NULL, editedby varchar(50) NOT NULL, text text NOT NULL)$diskengine$charset;"); $db->exec('CREATE TABLE ' . PREFIX . "notes (id $primary, type smallint NOT NULL, lastedited integer NOT NULL, editedby varchar(50) NOT NULL, text text NOT NULL)$diskengine$charset;");
$db->exec('CREATE INDEX ' . PREFIX . 'notes_type ON ' . PREFIX . 'notes(type);'); $db->exec('CREATE INDEX ' . PREFIX . 'notes_type ON ' . PREFIX . 'notes(type);');
$db->exec('CREATE INDEX ' . PREFIX . 'notes_editedby ON ' . PREFIX . 'notes(editedby);'); $db->exec('CREATE INDEX ' . PREFIX . 'notes_editedby ON ' . PREFIX . 'notes(editedby);');
@@ -4952,6 +5407,18 @@ function update_db(): void
if($dbversion<51){ if($dbversion<51){
$db->exec('INSERT INTO ' . PREFIX . "settings (setting,value) VALUES ('captchagdfont', '');"); $db->exec('INSERT INTO ' . PREFIX . "settings (setting,value) VALUES ('captchagdfont', '');");
} }
if($dbversion<52){
$db->exec('ALTER TABLE ' . PREFIX . 'members ADD COLUMN pgpkey text;');
$db->exec('ALTER TABLE ' . PREFIX . "members ADD COLUMN pgpchallengehash varchar(64) NOT NULL DEFAULT '';");
$db->exec('ALTER TABLE ' . PREFIX . 'members ADD COLUMN pgpchallengeexpires integer NOT NULL DEFAULT 0;');
}
if($dbversion<53){
$db->exec('ALTER TABLE ' . PREFIX . 'messages ADD COLUMN reply_to integer NOT NULL DEFAULT 0;');
$db->exec('CREATE INDEX ' . PREFIX . 'reply_to ON ' . PREFIX . 'messages(reply_to);');
$db->exec('CREATE TABLE ' . PREFIX . "likes (id $primary, message_id integer NOT NULL, nickname varchar(50) NOT NULL)$diskengine$charset;");
$db->exec('CREATE INDEX ' . PREFIX . 'likes_message_id ON ' . PREFIX . 'likes(message_id);');
$db->exec('CREATE UNIQUE INDEX ' . PREFIX . 'likes_message_nickname ON ' . PREFIX . 'likes(message_id, nickname);');
}
update_setting('dbversion', DBVERSION); update_setting('dbversion', DBVERSION);
if($msgencrypted!==MSGENCRYPTED){ if($msgencrypted!==MSGENCRYPTED){
if(!extension_loaded('sodium')){ if(!extension_loaded('sodium')){
@@ -5154,7 +5621,7 @@ function load_lang(): void
function load_config(): void function load_config(): void
{ {
define('VERSION', '1.24.1'); // Script version define('VERSION', '1.24.1'); // Script version
define('DBVERSION', 51); // Database layout version define('DBVERSION', 53); // Database layout version
define('MSGENCRYPTED', false); // Store messages encrypted in the database to prevent other database users from reading them - true/false - visit the setup page after editing! define('MSGENCRYPTED', false); // Store messages encrypted in the database to prevent other database users from reading them - true/false - visit the setup page after editing!
define('ENCRYPTKEY_PASS', 'MY_SECRET_KEY'); // Recommended length: 32. Encryption key for messages define('ENCRYPTKEY_PASS', 'MY_SECRET_KEY'); // Recommended length: 32. Encryption key for messages
define('AES_IV_PASS', '012345678912'); // Recommended length: 12. AES Encryption IV define('AES_IV_PASS', '012345678912'); // Recommended length: 12. AES Encryption IV