Lots of new tagging stuff.

This commit is contained in:
Floorb 2021-08-05 08:18:32 -04:00
parent 45dd14fd3e
commit 0bffb397d6
10 changed files with 218 additions and 68 deletions

View file

@ -28,8 +28,7 @@ function transformPasteRow(array $row) : array {
'time' => $row['created_at'], 'time' => $row['created_at'],
'time_update' => $row['updated_at'], 'time_update' => $row['updated_at'],
'friendly_update_time' => friendlyDateDifference(new DateTime($row['updated_at']), new DateTime()), 'friendly_update_time' => friendlyDateDifference(new DateTime($row['updated_at']), new DateTime()),
'friendly_time' => friendlyDateDifference(new DateTime($row['created_at']), new DateTime()), 'friendly_time' => friendlyDateDifference(new DateTime($row['created_at']), new DateTime())
'tags' => $row['tagsys']
]; ];
} }

View file

@ -25,4 +25,20 @@ class DatabaseHandle {
return $stmt; return $stmt;
} }
public function querySelectOne(string $query, array $params = null) : array | null {
$stmt = $this->query($query, $params);
if ($row = $stmt->fetch()) {
return $row;
}
return null;
}
public function queryInsert(string $query, array $params = null) : int {
$this->query($query, $params);
return (int) $this->conn->lastInsertId();
}
} }

96
includes/Tag.class.php Normal file
View file

@ -0,0 +1,96 @@
<?php
class Tag {
public int $id;
public string $name;
public string $slug;
public function __construct(array $row) {
$this->id = (int) $row['id'];
$this->name = $row['name'];
$this->slug = $row['slug'];
}
public static function getOrCreateByName(DatabaseHandle $conn, string $name) : Tag {
$name = Tag::cleanTagName($name);
if ($row = $conn->querySelectOne('SELECT id, name, slug FROM tags WHERE name = ?', [$name])) {
return new Tag($row);
}
$new_slug = Tag::encodeSlug($name);
$new_tag_id = $conn->queryInsert('INSERT INTO tags (name, slug) VALUES (?, ?)', [$name, $new_slug]);
return new Tag([
'id' => $new_tag_id,
'name' => $name,
'slug' => $new_slug
]);
}
public static function findBySlug(DatabaseHandle $conn, string $slug) : Tag | null {
if ($row = $conn->querySelectOne('SELECT id, name, slug FROM tags WHERE slug = ?', [$slug])) {
return new Tag($row);
}
return null;
}
public static function replacePasteTags(DatabaseHandle $conn, int $pasteId, array $tags) {
$conn->query('DELETE FROM paste_taggings WHERE paste_id = ?', [$pasteId]);
foreach ($tags as $tagName) {
$tag = Tag::getOrCreateByName($conn, $tagName);
$conn->query('INSERT INTO paste_taggings (paste_id, tag_id) VALUES (?, ?)', [$pasteId, $tag->id]);
}
// FIXME: We need to get rid of tagsys.
$conn->query('UPDATE pastes SET tagsys = ? WHERE id = ?', [implode(',', $tags), $pasteId]);
}
/**
* Normalize a tag name, which involves downcasing it, normalizing smart quotes, trimming the string, and
* normalizing runs of whitespace to a single space.
*
* @param string $name User-input tag name, for example "I'm gay ".
* @return string Cleaned tag name, for example "i'm gay".
*/
public static function cleanTagName(string $name) : string {
/* Downcase */
$name = trim(strtolower($name));
/* Smart quotes to regular quotes */
$name = preg_replace("[\u{00b4}\u{2018}\u{2019}\u{201a}\u{201b}\u{2032}]", "'", $name);
$name = preg_replace("[\u{00b4}\u{201c}\u{201d}\u{201e}\u{201f}\u{2033}]", '"', $name);
/* Collapse whitespace */
return preg_replace('/\s+/', ' ', $name);
}
public static function parseTagInput(string $tagInput) : array {
$cleanTags = [];
foreach (explode(',', $tagInput) as $tagName) {
$cleanName = Tag::cleanTagName($tagName);
if (!empty($cleanName)) {
array_push($cleanTags, $cleanName);
}
}
return array_unique($cleanTags);
}
private static function encodeSlug(string $name) : string {
/* This one's a doozy. */
$name = str_replace(
['-', '/', '\\', ':', '.', '+'],
['-dash-', '-fwslash-', '-bwslash-', '-colon-', '-dot-', '-plus-'],
$name
);
/* urlencode it. for URLs, dipshit. */
return str_replace('%20', '+', urlencode($name));
}
}

View file

@ -27,7 +27,7 @@ function urlForMember(string $member_name) : string {
/* Database functions */ /* Database functions */
function getSiteInfo() : array { function getSiteInfo() : array {
return require('config/site.php'); return require(__DIR__ . '/../config/site.php');
} }
function getSiteAds(DatabaseHandle $conn) : array|bool { function getSiteAds(DatabaseHandle $conn) : array|bool {
@ -130,8 +130,8 @@ $noguests = $disableguest;
// Prevent a potential LFI (you never know :p) // Prevent a potential LFI (you never know :p)
$lang_file = "${default_lang}.php"; $lang_file = "${default_lang}.php";
if (in_array($lang_file, scandir('langs/'))) { if (in_array($lang_file, scandir(__DIR__ . '/../langs/'))) {
require_once("langs/${lang_file}"); require_once(__DIR__ . "/../langs/${lang_file}");
} }
// Check if IP is banned // Check if IP is banned

View file

@ -13,9 +13,17 @@
* GNU General Public License in GPL.txt for more details. * GNU General Public License in GPL.txt for more details.
*/ */
function getPasteTags(DatabaseHandle $conn, int $paste_id) : array {
return $conn->query(
'SELECT name, slug FROM tags
INNER JOIN paste_taggings ON paste_taggings.tag_id = tags.id
WHERE paste_taggings.paste_id = ?',
[$paste_id])->fetchAll();
}
function getUserFavs(DatabaseHandle $conn, int $user_id) : array { function getUserFavs(DatabaseHandle $conn, int $user_id) : array {
$query = $conn->prepare( $query = $conn->prepare(
"SELECT pins.f_time, pastes.id, pins.paste_id, pastes.title, pastes.created_at, pastes.tagsys, pastes.updated_at "SELECT pins.f_time, pastes.id, pins.paste_id, pastes.title, pastes.created_at, pastes.updated_at
FROM pins FROM pins
INNER JOIN pastes ON pastes.id = pins.paste_id INNER JOIN pastes ON pastes.id = pins.paste_id
WHERE pins.user_id = ?"); WHERE pins.user_id = ?");
@ -129,7 +137,7 @@ function getUserRecom(DatabaseHandle $conn, int $user_id) : array {
function recentupdate($conn, $count) { function recentupdate($conn, $count) {
$query = $conn->prepare( $query = $conn->prepare(
"SELECT pastes.id AS id, visible, title, created_at, updated_at, users.username AS member, tagsys "SELECT pastes.id AS id, visible, title, created_at, updated_at, users.username AS member
FROM pastes FROM pastes
INNER JOIN users ON users.id = pastes.user_id INNER JOIN users ON users.id = pastes.user_id
WHERE visible = '0' ORDER BY updated_at DESC WHERE visible = '0' ORDER BY updated_at DESC
@ -140,7 +148,7 @@ function recentupdate($conn, $count) {
function monthpop($conn, $count) { function monthpop($conn, $count) {
$query = $conn->prepare( $query = $conn->prepare(
"SELECT pastes.id AS id, views, title, created_at, updated_at, visible, tagsys, users.username AS member "SELECT pastes.id AS id, views, title, created_at, updated_at, visible, users.username AS member
FROM pastes FROM pastes
INNER JOIN users ON users.id = pastes.user_id INNER JOIN users ON users.id = pastes.user_id
WHERE MONTH(created_at) = MONTH(NOW()) AND visible = '0' ORDER BY views DESC LIMIT ?"); WHERE MONTH(created_at) = MONTH(NOW()) AND visible = '0' ORDER BY views DESC LIMIT ?");
@ -178,7 +186,7 @@ function decrypt(string $value) : string {
function getRecent($conn, $count) { function getRecent($conn, $count) {
$query = $conn->prepare(" $query = $conn->prepare("
SELECT pastes.id, visible, title, created_at, updated_at, users.username AS member, tagsys SELECT pastes.id, visible, title, created_at, updated_at, users.username AS member
FROM pastes FROM pastes
INNER JOIN users ON pastes.user_id = users.id INNER JOIN users ON pastes.user_id = users.id
WHERE visible = '0' WHERE visible = '0'
@ -201,7 +209,7 @@ function getRecentadmin($conn, $count = 5) {
function getpopular(DatabaseHandle $conn, int $count) : array { function getpopular(DatabaseHandle $conn, int $count) : array {
$query = $conn->prepare(" $query = $conn->prepare("
SELECT pastes.id AS id, visible, title, pastes.created_at AS created_at, updated_at, views, users.username AS member, tagsys SELECT pastes.id AS id, visible, title, pastes.created_at AS created_at, updated_at, views, users.username AS member
FROM pastes INNER JOIN users ON users.id = pastes.user_id FROM pastes INNER JOIN users ON users.id = pastes.user_id
WHERE visible = '0' WHERE visible = '0'
ORDER BY views DESC ORDER BY views DESC
@ -213,7 +221,7 @@ function getpopular(DatabaseHandle $conn, int $count) : array {
function getrandom(DatabaseHandle $conn, int $count) : array { function getrandom(DatabaseHandle $conn, int $count) : array {
$query = $conn->prepare(" $query = $conn->prepare("
SELECT pastes.id, visible, title, created_at, updated_at, views, users.username AS member, tagsys SELECT pastes.id, visible, title, created_at, updated_at, views, users.username AS member
FROM pastes FROM pastes
INNER JOIN users ON users.id = pastes.user_id INNER JOIN users ON users.id = pastes.user_id
WHERE visible = '0' WHERE visible = '0'
@ -224,11 +232,10 @@ function getrandom(DatabaseHandle $conn, int $count) : array {
} }
function getUserPastes(DatabaseHandle $conn, int $user_id) : array { function getUserPastes(DatabaseHandle $conn, int $user_id) : array {
$query = $conn->prepare( return $conn->query(
"SELECT id, title, visible, code, created_at, tagsys, user_id, views from pastes WHERE user_id = ? "SELECT id, title, visible, code, created_at, views FROM pastes
ORDER by pastes.id DESC"); WHERE user_id = ?
$query->execute([$user_id]); ORDER by pastes.id DESC", [$user_id])->fetchAll();
return $query->fetchAll();
} }
function getTotalPastes(DatabaseHandle $conn, int $user_id) : int { function getTotalPastes(DatabaseHandle $conn, int $user_id) : int {

View file

@ -25,6 +25,7 @@ define('IN_PONEPASTE', 1);
require_once('includes/common.php'); require_once('includes/common.php');
require_once('includes/captcha.php'); require_once('includes/captcha.php');
require_once('includes/functions.php'); require_once('includes/functions.php');
require_once('includes/Tag.class.php');
function verifyCaptcha() : string|bool { function verifyCaptcha() : string|bool {
global $cap_e; global $cap_e;
@ -87,7 +88,7 @@ function validatePasteFields() : string|null {
return $lang['empty_paste']; return $lang['empty_paste'];
} elseif (!isset($_POST['title'])) { /* No paste title POSTed */ } elseif (!isset($_POST['title'])) { /* No paste title POSTed */
return $lang['error']; return $lang['error'];
} elseif (empty($_POST["tags"])) { /* No tags provided */ } elseif (empty($_POST["tag_input"])) { /* No tags provided */
return $lang['notags']; return $lang['notags'];
} elseif (strlen($_POST["title"]) > 70) { /* Paste title too long */ } elseif (strlen($_POST["title"]) > 70) { /* Paste title too long */
return $lang['titlelen']; return $lang['titlelen'];
@ -157,26 +158,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$editing = isset($_POST['edit']); $editing = isset($_POST['edit']);
$p_title = Trim(htmlspecialchars($_POST['title'])); $p_title = trim(htmlspecialchars($_POST['title']));
if (empty($p_title)) { if (empty($p_title)) {
$p_title = 'Untitled'; $p_title = 'Untitled';
} }
$p_content = htmlspecialchars($_POST['paste_data']); $p_content = htmlspecialchars($_POST['paste_data']);
$p_visible = Trim(htmlspecialchars($_POST['visibility'])); $p_visible = trim(htmlspecialchars($_POST['visibility']));
$p_code = Trim(htmlspecialchars($_POST['format'])); $p_code = trim(htmlspecialchars($_POST['format']));
$p_expiry = Trim(htmlspecialchars($_POST['paste_expire_date'])); $p_expiry = trim(htmlspecialchars($_POST['paste_expire_date']));
$p_tagsys = Trim(htmlspecialchars($_POST['tags']));
$p_tagsys = rtrim($p_tagsys, ',');
$p_password = $_POST['pass']; $p_password = $_POST['pass'];
if ($p_password == "" || $p_password == null) {
$p_password = "NONE"; if (empty($p_password)) {
$p_password = null;
} else { } else {
$p_password = password_hash($p_password, PASSWORD_DEFAULT); $p_password = password_hash($p_password, PASSWORD_DEFAULT);
} }
$p_encrypt = trim(htmlspecialchars($_POST['encrypted'])); $p_encrypt = trim(htmlspecialchars($_POST['encrypted']));
$tag_input = $_POST['tag_input'];
if (empty($p_encrypt)) { if (empty($p_encrypt)) {
$p_encrypt = "0"; $p_encrypt = "0";
} else { } else {
@ -190,45 +193,41 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Edit existing paste or create new? // Edit existing paste or create new?
if ($editing) { if ($editing) {
if ($current_user && $current_user['id'] === $paste_id) { if ($current_user &&
$current_user->user_id === (int) $conn->querySelectOne('SELECT user_id FROM pastes WHERE id = ?', [$_POST['paste_id']])['user_id']) {
$paste_id = intval($_POST['paste_id']); $paste_id = intval($_POST['paste_id']);
$statement = $conn->prepare(
"UPDATE pastes SET title = ?, content = ?, visible = ?, code = ?, expiry = ?, password = ?, encrypt = ?,ip = ?, tagsys = ?, updated_at = NOW() $conn->query(
WHERE id = ?" "UPDATE pastes SET title = ?, content = ?, visible = ?, code = ?, expiry = ?, password = ?, encrypt = ?, ip = ?, updated_at = NOW()
WHERE id = ?",
[$p_title, $p_content, $p_visible, $p_code, $expires, $p_password, $p_encrypt, $ip, $paste_id]
); );
$statement->execute([ Tag::replacePasteTags($conn, $paste_id, Tag::parseTagInput($tag_input));
$p_title, $p_content, $p_visible, $p_code, $expires, $p_password, $p_encrypt, $ip, $p_tagsys, $paste_id
]);
$success = $paste_id;
} else { } else {
$error = $lang['loginwarning']; //"You must be logged in to do that." $error = $lang['loginwarning']; //"You must be logged in to do that."
} }
} else { } else {
if ($current_user['id'] == null) { $paste_owner = $current_user ? $current_user->user_id : 1; /* 1 is the guest user's user ID */
$paste_owner = "1";
} else { $paste_id = $conn->queryInsert(
$paste_owner = $current_user ? $current_user['id'] : null; "INSERT INTO pastes (title, content, visible, code, expiry, password, encrypt, user_id, created_at, ip, views) VALUES
} (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, 0)",
$statement = $conn->prepare( [$p_title, $p_content, $p_visible, $p_code, $expires, $p_password, $p_encrypt, $paste_owner, $ip]
"INSERT INTO pastes (title, content, visible, code, expiry, password, encrypt, user_id, created_at, ip, views, tagsys) VALUES
(?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, 0, ?)"
); );
$statement->execute([$p_title, $p_content, $p_visible, $p_code, $expires, $p_password, $p_encrypt, $paste_owner, $ip, $p_tagsys]);
$paste_id = intval($conn->lastInsertId()); /* returns the last inserted ID as per the query above */ Tag::replacePasteTags($conn, $paste_id, Tag::parseTagInput($tag_input));
if ($p_visible == '0') { if ($p_visible == '0') {
addToSitemap($paste_id, $priority, $changefreq, $mod_rewrite); addToSitemap($paste_id, $priority, $changefreq, $mod_rewrite);
} }
$success = $paste_id;
} }
// Redirect to paste on successful entry, or on successful edit redirect back to edited paste // Redirect to paste on successful entry, or on successful edit redirect back to edited paste
if (isset($success)) { if (isset($paste_id)) {
$paste_url = urlForPaste($success); header('Location: ' . urlForPaste($paste_id));
header("Location: ${paste_url}");
die(); die();
} }
} }
OutPut: OutPut:

View file

@ -21,6 +21,8 @@ define('IN_PONEPASTE', 1);
require_once('includes/common.php'); require_once('includes/common.php');
require_once('includes/geshi.php'); require_once('includes/geshi.php');
require_once('includes/functions.php'); require_once('includes/functions.php');
require_once('includes/Tag.class.php');
require_once('includes/passwords.php');
require_once('includes/Parsedown/Parsedown.php'); require_once('includes/Parsedown/Parsedown.php');
require_once('includes/Parsedown/ParsedownExtra.php'); require_once('includes/Parsedown/ParsedownExtra.php');
@ -36,13 +38,12 @@ $query->execute([$paste_id]);
$fav_count = intval($query->fetch(PDO::FETCH_NUM)[0]); $fav_count = intval($query->fetch(PDO::FETCH_NUM)[0]);
// Get paste info // Get paste info
$query = $conn->prepare( $row = $conn->querySelectOne(
'SELECT title, content, visible, code, expiry, pastes.password AS password, created_at, updated_at, encrypt, views, tagsys, users.username AS member, users.id AS user_id 'SELECT title, content, visible, code, expiry, pastes.password AS password, created_at, updated_at, encrypt, views, tagsys, users.username AS member, users.id AS user_id
FROM pastes FROM pastes
INNER JOIN users ON users.id = pastes.user_id INNER JOIN users ON users.id = pastes.user_id
WHERE pastes.id = ?'); WHERE pastes.id = ?', [$paste_id]);
$query->execute([$paste_id]);
$row = $query->fetch();
// This is used in the theme files. // This is used in the theme files.
$totalpastes = getSiteTotalPastes($conn); $totalpastes = getSiteTotalPastes($conn);
@ -60,9 +61,9 @@ if (!$row) {
'updated_at' => (new DateTime($row['updated_at']))->format('jS F Y h:i:s A'), 'updated_at' => (new DateTime($row['updated_at']))->format('jS F Y h:i:s A'),
'user_id' => $row['user_id'], 'user_id' => $row['user_id'],
'member' => $row['member'], 'member' => $row['member'],
'tags' => $row['tagsys'],
'views' => $row['views'], 'views' => $row['views'],
'code' => $paste_code 'code' => $paste_code,
'tags' => getPasteTags($conn, $paste_id)
]; ];
$p_content = $row['content']; $p_content = $row['content'];
$p_visible = $row['visible']; $p_visible = $row['visible'];
@ -98,7 +99,7 @@ if (!$row) {
// Download the paste // Download the paste
if (isset($_GET['download'])) { if (isset($_GET['download'])) {
if ($p_password == "NONE") { if ($p_password == "NONE" || $p_password === null) {
doDownload($paste_id, $paste_title, $p_member, $op_content, $paste_code); doDownload($paste_id, $paste_title, $p_member, $op_content, $paste_code);
exit(); exit();
} else { } else {
@ -117,7 +118,7 @@ if (!$row) {
// Raw view // Raw view
if (isset($_GET['raw'])) { if (isset($_GET['raw'])) {
if ($p_password == "NONE") { if ($p_password == "NONE" || $p_password === null) {
rawView($paste_id, $paste_title, $op_content, $paste_code); rawView($paste_id, $paste_title, $op_content, $paste_code);
exit(); exit();
} else { } else {
@ -176,7 +177,7 @@ if (!$row) {
// Embed view after GeSHI is applied so that $p_code is syntax highlighted as it should be. // Embed view after GeSHI is applied so that $p_code is syntax highlighted as it should be.
if (isset($_GET['embed'])) { if (isset($_GET['embed'])) {
if ($p_password == "NONE") { if ($p_password == "NONE" || $p_password === null) {
embedView($paste_id, $paste_title, $p_content, $paste_code, $title, $baseurl, $ges_style, $lang); embedView($paste_id, $paste_title, $p_content, $paste_code, $title, $baseurl, $ges_style, $lang);
exit(); exit();
} else { } else {
@ -195,7 +196,7 @@ if (!$row) {
} }
require_once('theme/' . $default_theme . '/header.php'); require_once('theme/' . $default_theme . '/header.php');
if ($p_password == "NONE") { if ($p_password == "NONE" || $p_password === null) {
// No password & diplay the paste // No password & diplay the paste
// Set download URL // Set download URL

34
scripts/convert_tags.php Normal file
View file

@ -0,0 +1,34 @@
<?php
define('IN_PONEPASTE', 1);
require_once('../includes/common.php');
require_once('../includes/Tag.class.php');
function upgrade_tagsys(DatabaseHandle $conn) {
$result = $conn->query('SELECT id, tagsys FROM pastes')
->fetchAll(PDO::FETCH_NUM);
foreach ($result as $row) {
list($paste_id, $tagsys) = $row;
$tag_names = explode(',', $tagsys);
foreach ($tag_names as $tag_name) {
$tag_name = html_entity_decode($tag_name);
if (Tag::cleanTagName($tag_name) === '') continue;
$tag = Tag::getOrCreateByName($conn, $tag_name);
try {
$conn->queryInsert('INSERT INTO paste_taggings (paste_id, tag_id) VALUES (?, ?)', [$paste_id, $tag->id]);
} catch (Exception $e) {
if (str_contains($e->getMessage(), 'Duplicate entry')) {
var_dump($e);
} else throw $e;
}
}
}
}
echo 'hi';
upgrade_tagsys($conn);

View file

@ -242,10 +242,10 @@
<div class="field"> <div class="field">
<label class="label">Tags</label> <label class="label">Tags</label>
<div class="control"> <div class="control">
<input id="tags-with-source" name="tags" class="input" data-max-tags="10" <input id="tags-with-source" name="tag_input" class="input" data-max-tags="10"
data-max-chars="40" type="text" data-item-text="name" data-max-chars="40" type="text" data-item-text="name"
data-case-sensitive="false" placeholder="10 Tags Maximum" data-case-sensitive="false" placeholder="10 Tags Maximum"
value="<?php echo (isset($_POST['tags'])) ? $_POST['tags'] : ''; // Pre-populate if we come here on an error" ?>"> value="<?php echo (isset($_POST['tag_input'])) ? $_POST['tag_input'] : ''; // Pre-populate if we come here on an error" ?>">
</div> </div>
</div> </div>

View file

@ -220,13 +220,11 @@ $selectedloader = "$bg[$i]"; // set variable equal to which random filename was
<!-- Tag display --> <!-- Tag display -->
<div class="columns is-desktop is-centered"> <div class="columns is-desktop is-centered">
<?php <?php
$tagDisplay = htmlentities($paste['tags'], ENT_QUOTES, 'UTF-8'); $tags = $paste['tags'];
$tagDisplay = rtrim($tagDisplay); if (count($tags) != 0) {
$tagArray = explode(',', $tagDisplay); foreach ($tags as $tag) {
if (strlen($tagDisplay) > 0) { $tagName = ucfirst(pp_html_escape($tag['name']));
foreach ($tagArray as $tag_Array) { echo '<a href="/archive?q=' . $tagName . '"><span class="tag is-info">' . $tagName . '</span></a>';
$tag_Array = ucfirst($tag_Array);
echo '<a href="/archive?q=' . trim($tag_Array) . '"><span class="tag is-info">' . $tag_Array . '</span></a>';
} }
} else { } else {
echo ' <span class="tag is-warning">No tags</span>'; echo ' <span class="tag is-warning">No tags</span>';
@ -356,7 +354,7 @@ $selectedloader = "$bg[$i]"; // set variable equal to which random filename was
data-max-tags="10" data-max-chars="40" type="text" data-max-tags="10" data-max-chars="40" type="text"
data-item-text="name" data-case-sensitive="false" data-item-text="name" data-case-sensitive="false"
placeholder="10 Tags Maximum" placeholder="10 Tags Maximum"
value="<?php echo $p_tagsys; ?>"> value="<?php echo pp_html_encode(join(',', $paste['tags'])); ?>">
</div> </div>
</div> </div>
</div> </div>