$ git clone http://thingshare.ion.nu/thingshare.git
commit 6f1d70b603f5c7d3a4ddef1dfb7c74a225d7f990
Author: Alicia <...>
Date:   Thu Dec 16 19:02:31 2021 +0100

    Long overdue tag opt-in feature.

diff --git a/admin_tags.php b/admin_tags.php
index 23d40b0..8624dcb 100644
--- a/admin_tags.php
+++ b/admin_tags.php
@@ -2,7 +2,7 @@
 /*
     This file is part of Thingshare, a federated system for sharing data for home manufacturing (e.g. 3D models to 3D print)
     https://thingshare.ion.nu/
-    Copyright (C) 2020  Alicia <...>
+    Copyright (C) 2020-2021  Alicia <...>
 
     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
@@ -44,7 +44,7 @@ if(checknonce()) // Save changes
       }
     }
     mysqli_query($db, 'delete from tags where name="'.$tag.'"');
-    mysqli_query($db, 'insert into tags(name, blacklist) values("'.$tag.'", '.$blacklist.')');
+    mysqli_query($db, 'insert into tags(name, blacklist, optin) values("'.$tag.'", '.$blacklist.', "")');
   }
   if(isset($_POST['deletetag']))
   {
@@ -52,19 +52,46 @@ if(checknonce()) // Save changes
     mysqli_query($db, 'delete from tagmaps where tag="'.$tag.'"');
     mysqli_query($db, 'delete from tags where id="'.$tag.'"');
   }
+  if(isset($_POST['tag_optin']))
+  {
+    $id=(int)$path[3];
+    $optin=mysqli_real_escape_string($db, $_POST['tag_optin']);
+    mysqli_query($db, 'update tags set optin="'.$optin.'" where id='.$id);
+    header('Location: '.BASEURL.'/admin/tags');
+  }
 }
 if($error!=''){$error='<span class="error">'.$error.'</span>';}
 
 // Load current
-$tags='';
-$res=mysqli_query($db, 'select name, blacklist, id from tags order by name asc');
-while($row=mysqli_fetch_assoc($res))
+if(!isset($path[3]))
 {
-  $name=htmlentities($row['name']);
-  if($row['blacklist']){$name='<span class="blacklist">'.$name.'</span>';}
-  $tags.=$name.'<button name="deletetag" value="'.$row['id'].'">X</button><br />';
+  $tags='';
+  $res=mysqli_query($db, 'select name, blacklist, id from tags order by name asc');
+  while($row=mysqli_fetch_assoc($res))
+  {
+    $name=htmlentities($row['name']);
+    if($row['blacklist']){$name='<span class="blacklist">'.$name.'</span>';}
+    $tags.='<a href="'.BASEURL.'/admin/tags/'.$row['id'].'">'.$name.'</a> <button name="deletetag" value="'.$row['id'].'">X</button><br />';
+  }
+  $usertagscheck=(getoption('usertags', true)?' checked':'');
+}else{ // Individual tag
+  $id=(int)$path[3];
+  $res=mysqli_query($db, 'select name, blacklist, optin from tags where id='.$id);
+  $res=mysqli_fetch_assoc($res);
+  $name=$res['name'];
+  if($res['blacklist'])
+  {
+    $optin='';
+    $placeholder='"'._('Blacklisted').'" disabled';
+  }else{
+    $optin=htmlentities($res['optin']);
+    $placeholder='"'._('No opt-in required').'"';
+  }
+  $res=mysqli_query($db, 'select count(*) from tagmaps where tag='.$id);
+  $numtags=mysqli_fetch_row($res)[0];
 }
-$usertagscheck=(getoption('usertags', true)?' checked':'');
+if(!isset($path[3]))
+{
 ?>
 <?=$error?>
 <form method="post"><?=nonce()?>
@@ -77,3 +104,13 @@ $usertagscheck=(getoption('usertags', true)?' checked':'');
 <?=$tags?>
   <input type="text" name="newtag" /><button name="addtag"><?=_('Add tag')?></button><button name="blacklisttag" title="<?=_('Don\'t allow this tag to be added')?>"><?=_('Add tag to blacklist')?></button><br />
 </form>
+<?php }else{ /* Individual tag */ ?>
+<h2>Tag '<?=$name?>'</h2>
+<p><?=sprintf(_('%s things tagged on this node.'), $numtags)?></p>
+<form method="post"><?=nonce()?>
+  <?=_('Opt-in text:')?><br />
+  <textarea name="tag_optin" rows="12" style="width:100%;" placeholder=<?=$placeholder?>><?=$optin?></textarea><br />
+  <input type="submit" value="<?=_('Save')?>" />
+</form>
+<a href="<?=BASEURL?>/admin/tags"><button><?=_('Back')?></button></a>
+<?php } ?>
diff --git a/db.php b/db.php
index ffdd2b4..27c3b48 100644
--- a/db.php
+++ b/db.php
@@ -120,8 +120,9 @@ function db_create_tables()
     latest boolean);');
   mysqli_query($db, 'create table userblocks(user integer, blocked text);');
   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 tags(id integer primary key auto_increment, name text, blacklist boolean, optin text);');
   mysqli_query($db, 'create table tagmaps(tag integer, thing integer);');
+  mysqli_query($db, 'create table tag_optins(tag integer, user integer);');
   mysqli_query($db, 'create table comments(id integer primary key auto_increment,
     thing integer,
     sender text,
diff --git a/docs/RPCs b/docs/RPCs
index fa56ebc..86c5118 100644
--- a/docs/RPCs
+++ b/docs/RPCs
@@ -48,7 +48,10 @@ Return format:
     "by": {
       "displayname": <User's display name>,
       "name": <Username, without node>
-    }
+    },
+    "tags": [
+      <Tag names>
+    ]
   },
   <More entries in the same format as the first. One for every result>
 ]
diff --git a/rpc_search.php b/rpc_search.php
index 2ce2b3e..a58128e 100644
--- a/rpc_search.php
+++ b/rpc_search.php
@@ -2,7 +2,7 @@
 /*
     This file is part of Thingshare, a federated system for sharing data for home manufacturing (e.g. 3D models to 3D print)
     https://thingshare.ion.nu/
-    Copyright (C) 2020  Alicia <...>
+    Copyright (C) 2020-2021  Alicia <...>
 
     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
@@ -87,16 +87,24 @@ while($row=mysqli_fetch_assoc($res))
   $thing=Array('id'=>$row['thingid'],
                'name'=>$row['name'],
                'description'=>$row['description'],
-               'date'=>$row['posted']);
+               'date'=>$row['posted'],
+               'tags'=>Array());
   $user=$row['user'];
+  $revid=(int)$row['id'];
   // Grab preview from the chosen file, or the first file if none is chosen for preview
-  $res2=mysqli_query($db, 'select name, hash from files where thing='.(int)$row['id'].' order by preview desc limit 1');
+  $res2=mysqli_query($db, 'select name, hash from files where thing='.$revid.' order by preview desc limit 1');
   $row=mysqli_fetch_assoc($res2);
   $thing['preview']=getpreview($row['name'], $row['hash']);
   // Designer
   $res2=mysqli_query($db, 'select displayname, name from users where id='.$user);
   $row=mysqli_fetch_assoc($res2);
   $thing['by']=$row;
+  // Tags
+  $res2=mysqli_query($db, 'select tags.name from tags, tagmaps where tags.id=tagmaps.tag and tagmaps.thing='.$revid);
+  while($row=mysqli_fetch_row($res2))
+  {
+    $thing['tags'][]=$row[0];
+  }
   $obj[]=$thing;
 }
 print(json_encode($obj));
diff --git a/search.php b/search.php
index 1561f21..eefadd2 100644
--- a/search.php
+++ b/search.php
@@ -28,6 +28,18 @@ while($row=mysqli_fetch_row($res)){$peers[]=$row[0];}
 $filtertags='';
 $res=mysqli_query($db, 'select tag from filtertags');
 while($row=mysqli_fetch_row($res)){$filtertags.=' tag:-'.$row[0];}
+// Get list of tags to hide content for (e.g. 'nsfw')
+$hiddentags=Array();
+$res=mysqli_query($db, 'select name, id from tags where optin!=""');
+while($row=mysqli_fetch_assoc($res))
+{
+  if(isset($_SESSION['id'])) // Check if user opted in
+  {
+    $res2=mysqli_query($db, 'select tag from tag_optins where tag='.(int)$row['id'].' and user='.(int)$_SESSION['id']);
+    if(mysqli_fetch_row($res2)){continue;}
+  }
+  $hiddentags[]=$row['name'];
+}
 // Pagination
 $perpage=(isset($_GET['perpage'])?(int)$_GET['perpage']:20);
 $page=(int)(isset($_GET['page'])?$_GET['page']:0);
diff --git a/thing.php b/thing.php
index 3bdff6f..5ab7052 100644
--- a/thing.php
+++ b/thing.php
@@ -36,22 +36,34 @@ if(isset($_SESSION['id']))
   $blocked=mysqli_fetch_row($res);
   $info='';
   $error='';
-  if(isset($_POST['msg']) && checknonce())
+  if(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']))
+    if(isset($_POST['msg']))
+    {
+      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'];
+          if(isset($_POST['asyncrequest'])){exit($error);}
+        }else{
+          rpc_cache($thing[1], 'comments/'.$thing[0], false); // Invalidate cache to show new comments
+          if(isset($_POST['asyncrequest'])){exit('ok:'.$data['id']);}
+          $info=_('Comment posted');
+        }
+      }
+    }
+    if(isset($_POST['tag_optin']))
+    {
+      $tag_name=mysqli_real_escape_string($db, $_POST['tag_optin']);
+      $res=mysqli_query($db, 'select id from tags where optin!="" and name="'.$tag_name.'"');
+      if($res=mysqli_fetch_row($res))
       {
-        $error=$data['error'];
-        if(isset($_POST['asyncrequest'])){exit($error);}
-      }else{
-        rpc_cache($thing[1], 'comments/'.$thing[0], false); // Invalidate cache to show new comments
-        if(isset($_POST['asyncrequest'])){exit('ok:'.$data['id']);}
-        $info=_('Comment posted');
+        mysqli_query($db, 'insert into tag_optins(tag, user) values('.(int)$res[0].', '.(int)$_SESSION['id'].')');
       }
     }
   }
@@ -98,6 +110,34 @@ foreach($thingobj['files'] as $file)
 $tags='';
 foreach($thingobj['tags'] as $tag)
 {
+  if(!isset($_GET['show_'.strtolower($tag).'_content']) || $_GET['show_'.strtolower($tag).'_content']!='true')
+  {
+    // Check if tag requires optin
+    $tagname=mysqli_real_escape_string($db, $tag);
+    $res=mysqli_query($db, 'select id, optin from tags where name="'.$tagname.'"');
+    $optin=mysqli_fetch_assoc($res);
+    if($optin['optin']!='' && isset($_SESSION['id'])) // Check if user is already opted in
+    {
+      $res=mysqli_query($db, 'select tag from tag_optins where tag='.(int)$optin['id'].' and user='.(int)$_SESSION['id']);
+      if(mysqli_fetch_row($res)){$optin['optin']='';} // Just act like the optin doesn't exist
+    }
+    if($optin['optin']!='')
+    {
+      $showurl=$_SERVER['REQUEST_URI'];
+      $showurl.=(substr_count($showurl, '?')?'&':'?').'show_'.strtolower($tag).'_content=true';
+      print('<center>'.$optin['optin'].'<br />');
+      print('<a href="'.$showurl.'"><button>'.sprintf(_('Show %s content'), htmlentities($tag)).'</button></a>');
+      if(isset($_SESSION['id']))
+      {
+        print('<form method="post" style="display:inline;">'.nonce());
+        print('<button name="tag_optin" value="'.htmlentities($tag).'">'.sprintf(_('Always show %s content'), htmlentities($tag)).'</button>');
+        print('</form>');
+      }
+      print('</center>');
+      include_once('footer.php');
+      exit();
+    }
+  }
   $tag=htmlentities($tag);
   $tags.=' <a href="'.BASEURL.'/tag/'.$tag.'">'.$tag.'</a>';
 }