diff --git a/discover.php b/discover.php index 8796fc5..61997cf 100644 --- a/discover.php +++ b/discover.php @@ -28,8 +28,7 @@ function transformPasteRow(array $row) : array { 'time' => $row['created_at'], 'time_update' => $row['updated_at'], 'friendly_update_time' => friendlyDateDifference(new DateTime($row['updated_at']), new DateTime()), - 'friendly_time' => friendlyDateDifference(new DateTime($row['created_at']), new DateTime()), - 'tags' => $row['tagsys'] + 'friendly_time' => friendlyDateDifference(new DateTime($row['created_at']), new DateTime()) ]; } diff --git a/includes/DatabaseHandle.class.php b/includes/DatabaseHandle.class.php index 91be65a..44a474b 100644 --- a/includes/DatabaseHandle.class.php +++ b/includes/DatabaseHandle.class.php @@ -25,4 +25,20 @@ class DatabaseHandle { 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(); + } } diff --git a/includes/Tag.class.php b/includes/Tag.class.php new file mode 100644 index 0000000..ab0c978 --- /dev/null +++ b/includes/Tag.class.php @@ -0,0 +1,96 @@ +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)); + } +} diff --git a/includes/common.php b/includes/common.php index e7db980..8195d3c 100644 --- a/includes/common.php +++ b/includes/common.php @@ -27,7 +27,7 @@ function urlForMember(string $member_name) : string { /* Database functions */ function getSiteInfo() : array { - return require('config/site.php'); + return require(__DIR__ . '/../config/site.php'); } function getSiteAds(DatabaseHandle $conn) : array|bool { @@ -130,8 +130,8 @@ $noguests = $disableguest; // Prevent a potential LFI (you never know :p) $lang_file = "${default_lang}.php"; -if (in_array($lang_file, scandir('langs/'))) { - require_once("langs/${lang_file}"); +if (in_array($lang_file, scandir(__DIR__ . '/../langs/'))) { + require_once(__DIR__ . "/../langs/${lang_file}"); } // Check if IP is banned diff --git a/includes/functions.php b/includes/functions.php index 1c78118..bf9ea81 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -13,9 +13,17 @@ * 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 { $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 INNER JOIN pastes ON pastes.id = pins.paste_id WHERE pins.user_id = ?"); @@ -129,7 +137,7 @@ function getUserRecom(DatabaseHandle $conn, int $user_id) : array { function recentupdate($conn, $count) { $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 INNER JOIN users ON users.id = pastes.user_id WHERE visible = '0' ORDER BY updated_at DESC @@ -140,7 +148,7 @@ function recentupdate($conn, $count) { function monthpop($conn, $count) { $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 INNER JOIN users ON users.id = pastes.user_id 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) { $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 INNER JOIN users ON pastes.user_id = users.id WHERE visible = '0' @@ -201,7 +209,7 @@ function getRecentadmin($conn, $count = 5) { function getpopular(DatabaseHandle $conn, int $count) : array { $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 WHERE visible = '0' ORDER BY views DESC @@ -213,7 +221,7 @@ function getpopular(DatabaseHandle $conn, int $count) : array { function getrandom(DatabaseHandle $conn, int $count) : array { $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 INNER JOIN users ON users.id = pastes.user_id WHERE visible = '0' @@ -224,11 +232,10 @@ function getrandom(DatabaseHandle $conn, int $count) : array { } function getUserPastes(DatabaseHandle $conn, int $user_id) : array { - $query = $conn->prepare( - "SELECT id, title, visible, code, created_at, tagsys, user_id, views from pastes WHERE user_id = ? - ORDER by pastes.id DESC"); - $query->execute([$user_id]); - return $query->fetchAll(); + return $conn->query( + "SELECT id, title, visible, code, created_at, views FROM pastes + WHERE user_id = ? + ORDER by pastes.id DESC", [$user_id])->fetchAll(); } function getTotalPastes(DatabaseHandle $conn, int $user_id) : int { diff --git a/index.php b/index.php index 873aff7..452b292 100644 --- a/index.php +++ b/index.php @@ -25,6 +25,7 @@ define('IN_PONEPASTE', 1); require_once('includes/common.php'); require_once('includes/captcha.php'); require_once('includes/functions.php'); +require_once('includes/Tag.class.php'); function verifyCaptcha() : string|bool { global $cap_e; @@ -87,7 +88,7 @@ function validatePasteFields() : string|null { return $lang['empty_paste']; } elseif (!isset($_POST['title'])) { /* No paste title POSTed */ return $lang['error']; - } elseif (empty($_POST["tags"])) { /* No tags provided */ + } elseif (empty($_POST["tag_input"])) { /* No tags provided */ return $lang['notags']; } elseif (strlen($_POST["title"]) > 70) { /* Paste title too long */ return $lang['titlelen']; @@ -157,26 +158,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $editing = isset($_POST['edit']); - $p_title = Trim(htmlspecialchars($_POST['title'])); + $p_title = trim(htmlspecialchars($_POST['title'])); if (empty($p_title)) { $p_title = 'Untitled'; } $p_content = htmlspecialchars($_POST['paste_data']); - $p_visible = Trim(htmlspecialchars($_POST['visibility'])); - $p_code = Trim(htmlspecialchars($_POST['format'])); - $p_expiry = Trim(htmlspecialchars($_POST['paste_expire_date'])); - $p_tagsys = Trim(htmlspecialchars($_POST['tags'])); - $p_tagsys = rtrim($p_tagsys, ','); + $p_visible = trim(htmlspecialchars($_POST['visibility'])); + $p_code = trim(htmlspecialchars($_POST['format'])); + $p_expiry = trim(htmlspecialchars($_POST['paste_expire_date'])); $p_password = $_POST['pass']; - if ($p_password == "" || $p_password == null) { - $p_password = "NONE"; + + if (empty($p_password)) { + $p_password = null; } else { $p_password = password_hash($p_password, PASSWORD_DEFAULT); } + $p_encrypt = trim(htmlspecialchars($_POST['encrypted'])); + $tag_input = $_POST['tag_input']; + if (empty($p_encrypt)) { $p_encrypt = "0"; } else { @@ -190,45 +193,41 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Edit existing paste or create new? 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']); - $statement = $conn->prepare( - "UPDATE pastes SET title = ?, content = ?, visible = ?, code = ?, expiry = ?, password = ?, encrypt = ?,ip = ?, tagsys = ?, updated_at = NOW() - WHERE id = ?" + + $conn->query( + "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([ - $p_title, $p_content, $p_visible, $p_code, $expires, $p_password, $p_encrypt, $ip, $p_tagsys, $paste_id - ]); - $success = $paste_id; + Tag::replacePasteTags($conn, $paste_id, Tag::parseTagInput($tag_input)); } else { $error = $lang['loginwarning']; //"You must be logged in to do that." } } else { - if ($current_user['id'] == null) { - $paste_owner = "1"; - } else { - $paste_owner = $current_user ? $current_user['id'] : null; - } - $statement = $conn->prepare( - "INSERT INTO pastes (title, content, visible, code, expiry, password, encrypt, user_id, created_at, ip, views, tagsys) VALUES - (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, 0, ?)" + $paste_owner = $current_user ? $current_user->user_id : 1; /* 1 is the guest user's user ID */ + + $paste_id = $conn->queryInsert( + "INSERT INTO pastes (title, content, visible, code, expiry, password, encrypt, user_id, created_at, ip, views) VALUES + (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, 0)", + [$p_title, $p_content, $p_visible, $p_code, $expires, $p_password, $p_encrypt, $paste_owner, $ip] ); - $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') { 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 - if (isset($success)) { - $paste_url = urlForPaste($success); - header("Location: ${paste_url}"); + if (isset($paste_id)) { + header('Location: ' . urlForPaste($paste_id)); die(); } - } OutPut: diff --git a/paste.php b/paste.php index 66d189d..7bea66f 100644 --- a/paste.php +++ b/paste.php @@ -21,6 +21,8 @@ define('IN_PONEPASTE', 1); require_once('includes/common.php'); require_once('includes/geshi.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/ParsedownExtra.php'); @@ -36,13 +38,12 @@ $query->execute([$paste_id]); $fav_count = intval($query->fetch(PDO::FETCH_NUM)[0]); // 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 FROM pastes INNER JOIN users ON users.id = pastes.user_id - WHERE pastes.id = ?'); -$query->execute([$paste_id]); -$row = $query->fetch(); + WHERE pastes.id = ?', [$paste_id]); + // This is used in the theme files. $totalpastes = getSiteTotalPastes($conn); @@ -60,9 +61,9 @@ if (!$row) { 'updated_at' => (new DateTime($row['updated_at']))->format('jS F Y h:i:s A'), 'user_id' => $row['user_id'], 'member' => $row['member'], - 'tags' => $row['tagsys'], 'views' => $row['views'], - 'code' => $paste_code + 'code' => $paste_code, + 'tags' => getPasteTags($conn, $paste_id) ]; $p_content = $row['content']; $p_visible = $row['visible']; @@ -98,7 +99,7 @@ if (!$row) { // Download the paste 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); exit(); } else { @@ -117,7 +118,7 @@ if (!$row) { // Raw view if (isset($_GET['raw'])) { - if ($p_password == "NONE") { + if ($p_password == "NONE" || $p_password === null) { rawView($paste_id, $paste_title, $op_content, $paste_code); exit(); } else { @@ -176,7 +177,7 @@ if (!$row) { // Embed view after GeSHI is applied so that $p_code is syntax highlighted as it should be. 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); exit(); } else { @@ -195,7 +196,7 @@ if (!$row) { } require_once('theme/' . $default_theme . '/header.php'); -if ($p_password == "NONE") { +if ($p_password == "NONE" || $p_password === null) { // No password & diplay the paste // Set download URL diff --git a/scripts/convert_tags.php b/scripts/convert_tags.php new file mode 100644 index 0000000..f1bab2d --- /dev/null +++ b/scripts/convert_tags.php @@ -0,0 +1,34 @@ +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); + diff --git a/theme/bulma/main.php b/theme/bulma/main.php index 8f1faa6..f288ae4 100644 --- a/theme/bulma/main.php +++ b/theme/bulma/main.php @@ -242,10 +242,10 @@