$ git clone https://thingshare.ion.nu/thingshare.git
commit 5123de7cca2dca7a48fc28442d61ce6987d92bd0
Author: Alicia <...>
Date: Sun Nov 1 17:32:51 2020 +0100
Added support for comments.
diff --git a/admin_moderate.php b/admin_moderate.php
index 9d8e4d6..91f8b62 100644
--- a/admin_moderate.php
+++ b/admin_moderate.php
@@ -30,6 +30,7 @@ if(isset($_POST['report']) && isset($_POST['action']) && checknonce())
$row=mysqli_fetch_assoc($res);
if(!$row){die('<div class="error"><h1>Report not found</h1></div>');}
$target=$row['target'];
+ $splittarget=explode('/', $target);
$reason=$row['reason'];
$action='';
switch($_POST['action'])
@@ -41,9 +42,17 @@ if(isset($_POST['report']) && isset($_POST['action']) && checknonce())
mysqli_query($db, 'update things set removed=true where thingid='.$id);
$action='deleted thing';
break;
+ case 'deletecomment':
+ if($splittarget[0]!='comment' || !is_numeric($splittarget[1]) || !is_numeric($splittarget[2])){die('<div class="error"><h1>Not a comment</h1></div>');}
+ $id=(int)$splittarget[2];
+ // Non-destructive removal
+ mysqli_query($db, 'update comments set removed=true where id='.$id);
+ $action='deleted comment';
+ break;
case 'ban':
// Track down user
if(substr($target,0,6)!='thing/' || !is_numeric(substr($target,6))){die('<div class="error"><h1>Not a thing</h1></div>');} // TODO: Handle comments
+ // The problem with comments is commenters can be from other nodes and we can't ban on other nodes. maybe we need a remotebans table? In the meantime user-level blocking might be enough
$id=(int)substr($target,6);
$res=mysqli_query($db, 'select user from things where id='.$id);
$user=(int)mysqli_fetch_row($res)[0];
@@ -77,7 +86,14 @@ $res=mysqli_query($db, 'select user, timestamp, action, target from moderationlo
while($row=mysqli_fetch_assoc($res))
{
// TODO: Figure out timezones
- $modlog.=$row['timestamp'].' <...>'.db_getuser($row['user']).'</a> '.$row['action'].' on <...>'.$row['target'].'</a><br />';
+ $linksplit=explode('/', $row['target']);
+ if($linksplit[0]=='comment')
+ {
+ $link=htmlentities('thing/'.$linksplit[1].'@'.DOMAIN.'#comment'.$linksplit[2]);
+ }else{
+ $link=htmlentities(implode('/', $linksplit).'@'.DOMAIN);
+ }
+ $modlog.=$row['timestamp'].' <...>'.db_getuser($row['user']).'</a> '.$row['action'].' on <a href="'.BASEURL.'/'.$link.'">'.$row['target'].'</a><br />';
}
if(mysqli_num_rows($res)==20){$modlog.='TODO: Paging?<br />';}
@@ -89,16 +105,28 @@ while($row=mysqli_fetch_assoc($res))
$id=$row['id'];
$user=htmlentities($row['user']);
$subject=htmlentities($row['target']);
+ $linksplit=explode('/', $row['target']);
+ if($linksplit[0]=='comment')
+ {
+ $link=htmlentities('thing/'.$linksplit[1].'@'.DOMAIN.'#comment'.$linksplit[2]);
+ }else{
+ $link=htmlentities(implode('/', $linksplit).'@'.DOMAIN);
+ }
$reason=htmlentities($row['reason']);
$timestamp=htmlentities($row['timestamp']);
$reports.='<tr><td><a href="'.BASEURL.'/user/'.$user.'">'.$user.'</a></td>';
- $reports.='<td><...>'.$subject.'</a></td>';
+ $reports.='<td><a href="'.BASEURL.'/'.$link.'">'.$subject.'</a></td>';
$reports.='<td>'.$reason.'</td>';
$reports.='<td>'.$timestamp.'</td>';
$reports.='<td><form method="post">'.nonce();
$reports.='<input type="hidden" name="report" value="'.$id.'" />';
- $reports.='<button name="action" value="deletething">'._('Delete thing').'</button>';
- $reports.='<button name="action" value="ban">'._('Ban user').'</button>';
+ if($linksplit[0]=='comment')
+ {
+ $reports.='<button name="action" value="deletecomment">'._('Delete comment').'</button>';
+ }else{
+ $reports.='<button name="action" value="deletething">'._('Delete thing').'</button>';
+ $reports.='<button name="action" value="ban">'._('Ban user').'</button>';
+ }
$reports.='<button name="action" value="deletereport">X</button>';
$reports.='</form></td></tr>';
}
diff --git a/db.php b/db.php
index 5fca0d3..22e7d16 100644
--- a/db.php
+++ b/db.php
@@ -122,6 +122,13 @@ function db_create_tables()
mysqli_query($db, 'create table loginfails(ip varchar(256), timestamp datetime);');
mysqli_query($db, 'create table tags(id integer primary key auto_increment, name text, blacklist boolean);');
mysqli_query($db, 'create table tagmaps(tag integer, thing integer);');
+ mysqli_query($db, 'create table comments(id integer primary key auto_increment,
+ thing integer,
+ sender text,
+ replyto integer,
+ message text,
+ sent datetime,
+ removed boolean);');
}
function db_getuser($id)
diff --git a/index.php b/index.php
index b45be62..082f6a1 100644
--- a/index.php
+++ b/index.php
@@ -71,6 +71,7 @@ switch($path[1])
case 'report': include('rpc_report.php'); exit();
case 'messages': include('rpc_messages.php'); exit();
case 'peers': include('rpc_peers.php'); exit();
+ case 'comments': include('rpc_comments.php'); exit();
}
header('HTTP/1.1 404 Not found');
die('{"httpresponse":404,"error":"Not found"}');
diff --git a/messages.php b/messages.php
index d0592d2..74a98a0 100644
--- a/messages.php
+++ b/messages.php
@@ -23,17 +23,6 @@ if(!isset($_SESSION['id'])){header('Location: '.BASEURL.'/login?returnto='.urlen
include_once('db.php');
include_once('nonce.php');
include_once('rpc.php');
-function getdisplayname($username)
-{
- static $cache=Array();
- if(isset($cache[$username])){return $cache[$username];}
- $user=explode('@', $username);
- if(count($user)!=2){return $username;} // Invalid username, fall back on username
- $obj=rpc_get($user[1], 'user/'.$user[0]);
- if(isset($obj['error'])){return $username;} // RPC error, fall back on username
- $cache[$username]=$obj['displayname'];
- return $obj['displayname'];
-}
$error='';
$info='';
// Resolve chain ID (and username of second party)
diff --git a/report.php b/report.php
index 74a88ac..54ceead 100644
--- a/report.php
+++ b/report.php
@@ -23,10 +23,9 @@ if(!isset($_SESSION['id'])){header('Location: '.BASEURL.'/login?returnto='.urlen
include_once('nonce.php');
include_once('rpc.php');
$target=$path[2];
-// TODO: See if this works well for comments too, only things and comments need reports
-$item=explode('@',$path[3]);
-$path[3]=$item[0];
for($i=3; $i<count($path); ++$i){$target.='/'.$path[$i];}
+$item=explode('@',$target);
+$target=$item[0];
if(checknonce() && isset($_POST['reason']))
{
$reason=$_POST['reason'];
@@ -45,8 +44,8 @@ if(checknonce() && isset($_POST['reason']))
}
// Get the name of the target
-$obj=rpc_get($item[1], $path[2].'/'.$item[0]);
-$targetname=htmlentities($target.'@'.$item[1].' ('.$obj['name'].')');
+$obj=rpc_get($item[1], 'thing/'.explode('@',$path[3])[0]);
+$targetname=htmlentities($target.'@'.$item[1].' ('.($path[2]=='thing'?'':'on ').$obj['name'].')');
?>
<form method="post">
<?=nonce()?>
diff --git a/rpc.php b/rpc.php
index 68997ec..795e7c0 100644
--- a/rpc.php
+++ b/rpc.php
@@ -54,8 +54,9 @@ function rpc_cache($domain, $rpc, $content)
global $db;
$timestamp=mysqli_real_escape_string($db, date('Y-m-d H:i:s'));
$hash=hash(HASH, $domain.'/'.$rpc);
- $content=mysqli_real_escape_string($db, $content);
mysqli_query($db, 'delete from rpccache where hash="'.$hash.'"'); // Erase duplicate (avoids primary key duplication)
+ if($content===false){return;}
+ $content=mysqli_real_escape_string($db, $content);
mysqli_query($db, 'insert into rpccache(hash, timestamp, cache) values("'.$hash.'", "'.$timestamp.'", "'.$content.'")');
}
@@ -225,4 +226,16 @@ function rpc_verifypost(&$peer)
}
return $obj;
}
+
+function getdisplayname($username)
+{
+ static $cache=Array();
+ if(isset($cache[$username])){return $cache[$username];}
+ $user=explode('@', $username);
+ if(count($user)!=2){return $username;} // Invalid username, fall back on username
+ $obj=rpc_get($user[1], 'user/'.$user[0]);
+ if(isset($obj['error'])){return $username;} // RPC error, fall back on username
+ $cache[$username]=$obj['displayname'];
+ return $obj['displayname'];
+}
?>
diff --git a/rpc_comments.php b/rpc_comments.php
new file mode 100644
index 0000000..37ac376
--- /dev/null
+++ b/rpc_comments.php
@@ -0,0 +1,95 @@
+<...>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+include_once('rpc.php');
+include_once('db.php');
+$thing=(int)$path[3];
+if(isset($_POST['data'])) // Posting a comment
+{
+ $obj=rpc_verifypost($peer);
+ // Find thing's user (and check that thing exists)
+ $res=mysqli_query($db, 'select user from things where thingid='.$thing.' limit 1');
+ $res=mysqli_fetch_row($res);
+ if(!$res){header('HTTP/1.1 404 Not found'); die('{"error":"Thing not found"}');}
+ // Prepare values for DB
+ $user=(int)$res[0];
+ $sender=mysqli_real_escape_string($db, $obj['from'].'@'.$peer);
+ $msg=mysqli_real_escape_string($db, $obj['message']);
+ $replyto=(int)$obj['replyto'];
+ $timestamp=mysqli_real_escape_string($db, date('Y-m-d H:i:s'));
+ // Check that $user isn't blocking $sender
+ $res=mysqli_query($db, 'select user from userblocks where user='.$user.' and blocked="'.$sender.'" limit 1');
+ if(mysqli_fetch_row($res))
+ {
+ die('{"error":"Communication blocked by user"}');
+ }
+ if($replyto) // Check that the replied-to comment exists (if it's a reply)
+ {
+ $res=mysqli_query($db, 'select id from comments where id='.$replyto);
+ if(!mysqli_fetch_row($res))
+ {
+ header('HTTP/1.1 404 Not found');
+ die('{"error":"Replied-to comment not found"}');
+ }
+ }
+ // Store message
+ $q='insert into comments(thing, sender, replyto, message, sent, removed) ';
+ $q.='values('.$thing.', "'.$sender.'", '.$replyto.', "'.$msg.'", "'.$timestamp.'", false)';
+ if(mysqli_query($db, $q))
+ {
+ print('{"status":"OK"}');
+ }else{
+ print('{"error":"Database error"}');
+ }
+}else{ // Get comments
+ $comments=Array();
+ $replies=Array();
+ function resolvereplies($id)
+ {
+ global $replies;
+ $r=Array();
+ foreach($replies as $i)
+ {
+ if($i['replyto']==$id)
+ {
+ $i['replies']=resolvereplies($i['id']);
+ $r[]=$i;
+ }
+ }
+ return $r;
+ }
+ $res=mysqli_query($db, 'select id, sender, replyto, message, sent, removed from comments where thing='.$thing.' order by sent asc');
+ while($row=mysqli_fetch_assoc($res))
+ {
+ if($row['removed'])
+ {
+ $row['sender']='removed';
+ $row['message']='removed';
+ }
+ if($row['replyto']==0)
+ {
+ $comments[]=$row;
+ }else{
+ $replies[]=$row;
+ }
+ }
+ foreach($comments as $i=>$c){$comments[$i]['replies']=resolvereplies($comments[$i]['id']);}
+ print(json_encode($comments));
+}
+?>
diff --git a/thing.php b/thing.php
index 25eb61a..f435ddd 100644
--- a/thing.php
+++ b/thing.php
@@ -19,6 +19,7 @@
*/
include_once('head.php');
include_once('db.php');
+include_once('nonce.php');
include_once('rpc.php');
$thing=explode('@',$path[2]);
$thingobj=rpc_get($thing[1], 'thing/'.$thing[0]);
@@ -65,9 +66,101 @@ foreach($thingobj['tags'] as $tag)
$tag=htmlentities($tag);
$tags.=' <a href="'.BASEURL.'/tag/'.$tag.'">'.$tag.'</a>';
}
+// Check if we're blocking the user (can't comment on things of people you're blocking)
+$by_esc=mysqli_real_escape_string($db, $thingobj['by']['name']);
+$res=mysqli_query($db, 'select user from userblocks where user='.(int)$_SESSION['id'].' and blocked="'.$by_esc.'" limit 1');
+$blocked=mysqli_fetch_row($res);
+$info='';
+$error='';
+if(isset($_POST['msg']) && checknonce())
+{
+ if($blocked){$error=_('Cannot send messages to blocked users');}else{
+ // Send it to thing's node
+ $msg=Array('from'=>$_SESSION['name'],
+ 'message'=>$_POST['msg'],
+ 'replyto'=>$_POST['replyto']);
+ $data=rpc_post($thing[1], 'comments/'.$thing[0], $msg);
+ if(isset($data['error']))
+ {
+ $error=$data['error'];
+ }else{
+ rpc_cache($thing[1], 'comments/'.$thing[0], false); // Invalidate cache to show new comments
+ $info=_('Comment posted');
+ }
+ }
+}
+$comments=rpc_get($thing[1], 'comments/'.$thing[0]);
+include_once('parsedown/Parsedown.php');
+$md=new Parsedown();
+function commenthasreply($comment) // Search for undeleted replies
+{
+ foreach($comment['replies'] as $reply)
+ {
+ if(substr_count($reply['sender'], '@')>0){return true;}
+ if(commenthaschild($reply)){return true;}
+ }
+ return false;
+}
+function printcomments($comments, $level=0)
+{
+ global $md;
+ global $thing;
+ foreach($comments as $comment)
+ {
+ $sender=htmlentities($comment['sender']);
+ $senderhref='href="'.BASEURL.'/user/'.$sender.'"';
+ if(substr_count($sender, '@')>0)
+ {
+ $displayname=htmlentities(getdisplayname($comment['sender']));
+ }else{
+ if(!commenthasreply($comment)){continue;} // Don't print fully deleted chains
+ $senderhref='';
+ $displayname=$sender;
+ }
+ $msg=$md->text(htmlentities($comment['message']));
+ $time=htmlentities($comment['sent']);
+ print('<div class="message" style="margin-left:'.($level*15).'px;" data-id="'.$comment['id'].'"><div class="message_sender"><a '.$senderhref.' title="'.$sender.'" name="comment'.$comment['id'].'">'.$displayname.'</a> <span class="time">'.$time.'</span></div><span>'.$msg.'</span><p>');
+ if(substr_count($sender, '@')>0 && isset($_SESSION['id']))
+ {
+ print('<a href="#" onclick="replyto(this.parentElement.parentElement); return false;">'._('Reply').'</a>');
+ print(' - <...>Report</a>');
+ }
+ print('</p></div>');
+ printcomments($comment['replies'], $level+1);
+ }
+}
+if($error!=''){$info='<span class="error">'.$error.'</span>';}
?>
+<script>
+<!--
+function replyto(comment)
+{
+ document.getElementById('replyto').value=comment.dataset.id;
+ document.getElementById('replyindicator').innerHTML='Replying to:<br />'+comment.children[1].innerHTML;
+ document.getElementById('commentmsg').focus();
+}
+// -->
+</script>
<h1><?=$name?> <small class="subheader">by <?=$by?></small></h1>
<small><?=sprintf(_('Published on %s under the license %s'), '<span class="time">'.htmlentities($thingobj['date']).'</span>', $license)?></small><br />
<?=$description?><br />
-Tags: <?=$tags?><br />
+<?=_('Tags:')?> <?=$tags?><br />
<?=$files?>
+<h2><?=_('Comments')?></h2>
+<?=$info?>
+<?=printcomments($comments)?>
+<?php if(isset($_SESSION['id'])){ ?>
+<script src="<?=BASEURL?>/mdjs/mdjs.js"></script>
+<form method="post" action="<?=BASEURL?>/thing/<?=$path[2]?>">
+ <p>
+ <?=nonce()?>
+ <?php if(!$blocked){ ?>
+ <input type="hidden" id="replyto" name="replyto" value="" />
+ <div id="replyindicator"></div>
+ <textarea rows="4" style="width:100%;" id="commentmsg" name="msg" onchange="document.getElementById('mdpreview').innerHTML='Markdown preview:<br />'+Mdjs.md2html(this.value.replace(/&/g,'&amp;').replace(/</g,'&lt;'));" onkeyup="this.onchange();"></textarea>
+ <div id="mdpreview"></div>
+ <button><?=_('Send')?></button>
+ <?php } ?>
+ </p>
+</form>
+<?php } ?>