$ git clone http://thingshare.ion.nu/thingshare.git
commit ca46826d07de83551cf2cb7be330a201e2fd7495
Author: Alicia <...>
Date:   Wed Jul 29 23:06:06 2020 +0200

    Added support for tags on things.

diff --git a/admin.php b/admin.php
index 93ad9de..4e69781 100644
--- a/admin.php
+++ b/admin.php
@@ -27,11 +27,13 @@ switch($path[2])
   case 'federation': include('admin_federation.php'); break;
   case 'filetypes': include('admin_filetypes.php'); break;
   case 'licenses': include('admin_licenses.php'); break;
+  case 'tags': include('admin_tags.php'); break;
   default:
     if($privileges&PRIV_PRIVILEGES){print('<a href="'.BASEURL.'/admin/privileges">Manage user privileges</a><br />');}
     if($privileges&PRIV_MODERATE){print('<a href="'.BASEURL.'/admin/moderate">Moderation options</a><br />');}
     if($privileges&PRIV_FEDERATION){print('<a href="'.BASEURL.'/admin/federation">Manage peer federation</a><br />');}
     if($privileges&PRIV_FILETYPES){print('<a href="'.BASEURL.'/admin/filetypes">Manage filetypes</a><br />');}
     if($privileges&PRIV_LICENSES){print('<a href="'.BASEURL.'/admin/licenses">Manage the license list</a><br />');}
+    if($privileges&PRIV_TAGS){print('<a href="'.BASEURL.'/admin/tags">Manage tags</a><br />');}
 }
 ?>
diff --git a/db.php b/db.php
index 1715fd6..5fca0d3 100644
--- a/db.php
+++ b/db.php
@@ -120,6 +120,8 @@ 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 tagmaps(tag integer, thing integer);');
 }
 
 function db_getuser($id)
diff --git a/editthing.php b/editthing.php
index e28d40d..1521bf7 100644
--- a/editthing.php
+++ b/editthing.php
@@ -107,6 +107,23 @@ if(isset($_POST['name']) && isset($_POST['description']) && checknonce())
       $name=mysqli_real_escape_string($db, $_FILES['files']['name'][$i]);
       insertfile($thingid, $name, $hash, $_POST['previewfile']==$i);
     }
+    // Save tags
+    foreach(explode(' ', $_POST['tags']) as $tag)
+    {
+      if($tag==''){continue;}
+      $tag=mysqli_real_escape_string($db, strtolower($tag));
+      $res=mysqli_query($db, 'select id, blacklist from tags where name="'.$tag.'"');
+      if(($res=mysqli_fetch_row($res)))
+      {
+        if($res[1]){continue;}
+        $tagid=$res[0];
+      }else{
+        if(!getoption('usertags', true)){continue;}
+        mysqli_query($db, 'insert into tags(name, blacklist) values("'.$tag.'", 0)');
+        $tagid=mysqli_insert_id($db);
+      }
+      mysqli_query($db, 'insert into tagmaps(tag, thing) values('.$tagid.', '.$thingid.')');
+    }
     header('Location: '.BASEURL.'/thing/'.$id.'@'.DOMAIN);
     system('php genpreviews.php > /dev/null &'); // Launch preview generation in the background
     exit();
@@ -117,6 +134,7 @@ $name='';
 $description='';
 $files='';
 $license='';
+$tags='';
 if($id!='new') // Load from DB when editing a preexisting thing
 {
   $res=mysqli_query($db, 'select id, name, description, license from things where thingid='.(int)$id.' and latest');
@@ -136,11 +154,18 @@ if($id!='new') // Load from DB when editing a preexisting thing
     $files.='<button onclick="this.parentNode.remove(); return false;">X</button></div>'."\n";
     ++$i;
   }
+  // Gather tags
+  $res=mysqli_query($db, 'select tags.name from tagmaps, tags where tags.id=tagmaps.tag and tagmaps.thing='.(int)$thingid);
+  while($row=mysqli_fetch_row($res))
+  {
+    $tags.=($tags==''?'':' ').$row[0];
+  }
 }
 // If saving was attempted, retain changes
 if(isset($_POST['name'])){$name=$_POST['name'];}
 if(isset($_POST['description'])){$description=$_POST['description'];}
 if(isset($_POST['license'])){$license=$_POST['license'];}
+if(isset($_POST['tags'])){$tags=$_POST['tags'];}
 
 // Gather license options
 $licenses='';
@@ -157,9 +182,9 @@ $selected=(($license=='other')?' selected':'');
 $licenses.='<option value="other"'.$selected.'>'._('Other (see description)').'</option>';
 
 $maxsize=-1;
-for(Array('upload_max_filesize','post_max_size') as $name)
+foreach(Array('upload_max_filesize','post_max_size') as $sname)
 {
-  $size=ini_get($name);
+  $size=ini_get($sname);
   // Translate to bytes for the MAX_FILE_SIZE input
   switch(strtoupper(substr($size,-1)))
   {
@@ -222,6 +247,10 @@ function morefiles(prev)
   <?=_('Description:')?><br />
   <textarea name="description" rows="15" style="width:100%;" onchange="document.getElementById('mdpreview').innerHTML='Markdown preview:<br />'+Mdjs.md2html(this.value.replace(/&/g,'&amp;amp;').replace(/</g,'&amp;lt;'));" onkeyup="this.onchange();"><?=htmlentities($description)?></textarea><br />
   <div id="mdpreview"></div>
+  <div class="paragraph">
+    <?=_('Tags')?>:
+    <input type="text" name="tags" value="<?=htmlentities($tags)?>" /> <?=_('(separate with spaces)')?>
+  </div>
   <div class="paragraph">
     <?=_('License')?>:
     <select name="license">
diff --git a/head.php b/head.php
index 5b28e6f..4784a3f 100644
--- a/head.php
+++ b/head.php
@@ -43,7 +43,12 @@ if(isset($_COOKIE['PHPSESSID'])) // TODO: See if there's a better way to check i
 }
 $loginlink=BASEURL.'/login?returnto='.urlencode($_SERVER['REQUEST_URI']);
 $logoutlink=BASEURL.'/logout?returnto='.urlencode($_SERVER['REQUEST_URI']);
-$search=htmlentities(isset($_GET['q'])?$_GET['q']:'');
+if($path[1]=='tag')
+{
+  $search='tag:'.htmlentities($path[2]);
+}else{
+  $search=htmlentities(isset($_GET['q'])?$_GET['q']:'');
+}
 ?>
 <!DOCTYPE html>
 <html lang="en">
diff --git a/index.php b/index.php
index 3f2b34c..b45be62 100644
--- a/index.php
+++ b/index.php
@@ -40,6 +40,7 @@ switch($path[1])
   case 'user': include('user.php'); break;
   case 'thing': include('thing.php'); break;
   case 'browse':
+  case 'tag':
   case 'search': include('search.php'); break;
   case 'license': include('license.php'); break;
   case 'editprofile': include('editprofile.php'); break;
diff --git a/rpc_search.php b/rpc_search.php
index 57b0a3f..2ce2b3e 100644
--- a/rpc_search.php
+++ b/rpc_search.php
@@ -21,7 +21,7 @@ include_once('db.php');
 include_once('files.php');
 // If you experience problem with slashes in searches you may need 'AllowEncodedSlashes NoDecode' in your Apache config (outside of <Directory>. More info at https://httpd.apache.org/docs/current/mod/core.html#allowencodedslashes )
 $words=explode(' ', urldecode($path[3]));
-$order=$path[4]; // TODO: Test (at time of writing there was only one 'thing')
+$order=$path[4];
 $count=(int)$path[5];
 $skip=(int)$path[6];
 if(!$count){$count=10;}
@@ -60,7 +60,8 @@ foreach($words as $word)
   // Negative matches
   $like='like';
   $or='or';
-  if(substr($word,0,1)=='-'){$like='not like'; $or='and'; $word=substr($word,1);}
+  $in='in';
+  if(substr($word,0,1)=='-'){$like='not like'; $or='and'; $in='not in'; $word=substr($word,1);}
   // Escape
   $word=addcslashes(mysqli_real_escape_string($db, $word), '%_');
   if($q!=''){$q.=' and ';}
@@ -68,6 +69,12 @@ foreach($words as $word)
   {
     case 'name': $q.='(name '.$like.' "%'.$word.'%")'; break;
     case 'description': $q.='(description '.$like.' "%'.$word.'%")'; break;
+    case 'tag':
+      $ids=Array();
+      $res=mysqli_query($db, 'select tagmaps.thing from tags, tagmaps where tagmaps.tag=tags.id and tags.name like "'.$word.'"');
+      while($row=mysqli_fetch_row($res)){$ids[]=$row[0];}
+      $q.='(id '.$in.' ('.implode(',', $ids).'))';
+      break;
     default: $q.='(name '.$like.' "%'.$word.'%" '.$or.' description '.$like.' "%'.$word.'%")'; break;
   }
 }
diff --git a/rpc_thing.php b/rpc_thing.php
index 3c4a854..760d58b 100644
--- a/rpc_thing.php
+++ b/rpc_thing.php
@@ -31,6 +31,7 @@ $thing=Array('id'=>$id,
              'files'=>Array(),
              'license'=>Array('name'=>$row['license']));
 $user=(int)$row['user'];
+$revid=(int)$row['id'];
 // List files
 $res=mysqli_query($db, 'select name, hash from files where thing='.(int)$row['id']);
 while($row=mysqli_fetch_assoc($res))
@@ -55,5 +56,12 @@ if($thing['license']['name']!='other')
   $res=mysqli_query($db, 'select simple from licenses where name="'.$name.'"');
   if($row=mysqli_fetch_row($res)){$thing['license']['simple']=$row[0];}
 }
+// Tags
+$thing['tags']=Array();
+$res=mysqli_query($db, 'select tags.name from tags, tagmaps where tags.id=tagmaps.tag and tagmaps.thing='.$revid);
+while($row=mysqli_fetch_row($res))
+{
+  $thing['tags'][]=$row[0];
+}
 print(json_encode($thing));
 ?>
diff --git a/search.php b/search.php
index 1b7f112..373c751 100644
--- a/search.php
+++ b/search.php
@@ -29,6 +29,7 @@ $page=(int)(isset($_GET['page'])?$_GET['page']:0);
 $perpeer=round($perpage/count($peers));
 $sortby=(isset($_GET['sort'])?$_GET['sort']:'new');
 if($path[1]=='browse'){$sortby=$path[2];} // Handle 'Browse' links
+else if($path[1]=='tag'){$_GET['q']='tag:'.$path[2];} // Handle 'Tag' links
 $res=rpc_search($peers, 'search/'.urlencode($_GET['q']).'/'.urlencode($sortby).'/'.$perpeer.'/'.($page*$perpage));
 // Construct navigation links for pages
 $pagefull=false;
diff --git a/setup.php b/setup.php
index e0684c6..c0d3b8c 100644
--- a/setup.php
+++ b/setup.php
@@ -33,7 +33,7 @@ if($sessiontime<3*3600)
 }
 // Max upload filesize
 $maxsize=-1;
-for(Array('upload_max_filesize','post_max_size') as $name)
+foreach(Array('upload_max_filesize','post_max_size') as $name)
 {
   $size=ini_get($name);
   switch(strtoupper(substr($size,-1)))
diff --git a/thing.php b/thing.php
index b22126e..25eb61a 100644
--- a/thing.php
+++ b/thing.php
@@ -59,8 +59,15 @@ foreach($thingobj['files'] as $file)
   if(isset($file['preview3d'])){$files.='<div class="boxtop" style="right:0px;"><a href="#" onclick="return threedview(this,'.PREVIEW_SIZE[0].','.PREVIEW_SIZE[1].');" data-file="https://'.$thing[1].$file['preview3d'].'">3D view</a></div>';}
   $files.='<a href="https://'.$thing[1].$file['path'].'" download="'.htmlentities($file['name']).'" type="'.$type.'"><div class="boxbottom">'.htmlentities($file['name']).'</div><img src="https://'.$thing[1].$file['preview'].'" /></a></div>';
 }
+$tags='';
+foreach($thingobj['tags'] as $tag)
+{
+  $tag=htmlentities($tag);
+  $tags.=' <a href="'.BASEURL.'/tag/'.$tag.'">'.$tag.'</a>';
+}
 ?>
 <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 />
 <?=$files?>