- Array of all blog posts or error message */ public function getBlogPosts(): array { $conn = dbConn(); $stmt = $conn->prepare("SELECT ID, title, DATE_FORMAT(dateCreated, '%Y-%m-%dT%TZ') AS dateCreated, DATE_FORMAT(dateModified, '%Y-%m-%dT%TZ') AS dateModified, featured, abstract, headerImg, body, bodyText, categories, keywords, folderID FROM blog ORDER BY featured DESC, dateCreated DESC;"); $stmt->execute(); // set the resulting array to associative $result = $stmt->fetchAll(PDO::FETCH_ASSOC); if ($result) { return $result; } return array("errorMessage" => "Error, blog data not found"); } /** * Get a blog post with the given ID * @param string $title - Title of the blog post * @return array - Array of blog post or error message */ public function getBlogPost(string $title): array { $conn = dbConn(); $stmt = $conn->prepare("SELECT ID, title, DATE_FORMAT(dateCreated, '%Y-%m-%dT%TZ') AS dateCreated, DATE_FORMAT(dateModified, '%Y-%m-%dT%TZ') AS dateModified, featured, abstract, headerImg, body, bodyText, categories, keywords, folderID FROM blog WHERE title = :title;"); $stmt->bindParam(":title", $title); $stmt->execute(); // set the resulting array to associative $result = $stmt->fetch(PDO::FETCH_ASSOC); if ($result) { return $result; } return array("errorMessage" => "Error, blog post could not found"); } /** * Get the latest blog post * @return array - Array of the latest blog post or error message */ public function getLatestBlogPost(): array { $conn = dbConn(); $stmt = $conn->prepare("SELECT ID, title, DATE_FORMAT(dateCreated, '%Y-%m-%dT%TZ') AS dateCreated, DATE_FORMAT(dateModified, '%Y-%m-%dT%TZ') AS dateModified, featured, abstract, headerImg, body, bodyText, categories, keywords, folderID FROM blog ORDER BY dateCreated DESC LIMIT 1;"); $stmt->execute(); // set the resulting array to associative $result = $stmt->fetch(PDO::FETCH_ASSOC); if ($result) { return $result; } return array("errorMessage" => "Error, blog post could not found"); } /** * Get featured blog post * @return array - Array of the featured blog post or error message */ public function getFeaturedBlogPost(): array { $conn = dbConn(); $stmt = $conn->prepare("SELECT ID, title, DATE_FORMAT(dateCreated, '%Y-%m-%dT%TZ') AS dateCreated, DATE_FORMAT(dateModified, '%Y-%m-%dT%TZ') AS dateModified, featured, abstract, headerImg, body, bodyText, categories, keywords, folderID FROM blog WHERE featured = 1;"); $stmt->execute(); $result = $stmt->fetch(PDO::FETCH_ASSOC); if ($result) { return $result; } return array("errorMessage" => "Error, blog post could not found"); } /** * Get all unique categories * @return string[] - Array of all categories or error message */ public function getCategories(): array { $conn = dbConn(); $stmt = $conn->prepare("SELECT DISTINCT categories FROM blog;"); $stmt->execute(); // set the resulting array to associative $result = $stmt->fetchAll(PDO::FETCH_ASSOC); if ($result) { return $result; } return array("errorMessage" => "Error, blog post could not found"); } /** * Delete a blog post with the given ID * @param int $ID - ID of the blog post to delete * @return string - Success or error message */ public function deletePost(int $ID): string { $conn = dbConn(); $stmtCheckPost = $conn->prepare("SELECT * FROM blog WHERE ID = :ID"); $stmtCheckPost->bindParam(":ID", $ID); $stmtCheckPost->execute(); $result = $stmtCheckPost->fetch(PDO::FETCH_ASSOC); if (!$result) { return "post not found"; } if ($result["featured"] === 1) { return "cannot delete"; } $stmt = $conn->prepare("DELETE FROM blog WHERE ID = :ID"); $stmt->bindParam(":ID", $ID); if ($stmt->execute()) { $imagUtils = new imgUtils(); $imagUtils->deleteDirectory("../blog/imgs/" . $result["title"] . "_" . $result["folderID"] . "/"); return "success"; } return "error"; } /** * Update the blog post with the given ID * @param int $ID - ID of the blog post to update * @param string $title - Title of the blog post * @param bool $featured - Whether the blog post is featured or not * @param string $abstract - Abstract of the blog post i.e. a short description * @param string $body - Body of the blog post * @param string $bodyText - Body of the blog post as plain text * @param string $dateModified - Date the blog post was modified * @param string $categories - Categories of the blog post * @return bool|string - Success or error message */ public function updatePost(int $ID, string $title, bool $featured, string $abstract, string $body, string $bodyText, string $dateModified, string $categories): bool|string { $conn = dbConn(); $stmtCheckPost = $conn->prepare("SELECT * FROM blog WHERE ID = :ID"); $stmtCheckPost->bindParam(":ID", $ID); $stmtCheckPost->execute(); $result = $stmtCheckPost->fetch(PDO::FETCH_ASSOC); if (!$result) { return "post not found"; } if (!$featured && $result["featured"] === 1) { return "unset feature"; } if ($featured) { $stmtUnsetFeatured = $conn->prepare("UPDATE blog SET featured = 0 WHERE featured = 1;"); $stmtUnsetFeatured->execute(); } $to = "../blog/imgs/" . $title . "_" . $result["folderID"] . "/"; if ($result["title"] !== $title) { $from = "../blog/imgs/" . $result["title"] . "_" . $result["folderID"] . "/"; mkdir($to, 0777, true); rename($result["headerImg"], $to . basename($result["headerImg"])); $body = $this->changeHTMLSrc($body, $to, $from); rmdir($from); } $from = "../blog/imgs/tmp/"; $newBody = $this->changeHTMLSrc($body, $to, $from); $keywords = implode(", ", RakePlus::create($bodyText)->keywords()); $stmt = $conn->prepare("UPDATE blog SET title = :title, featured = :featured, abstract = :abstract, body = :body, bodyText = :bodyText, dateModified = :dateModified, categories = :categories, keywords = :keywords WHERE ID = :ID;"); $stmt->bindParam(":ID", $ID); $stmt->bindParam(":title", $title); $stmt->bindParam(":featured", $featured); $stmt->bindParam(":abstract", $abstract); $stmt->bindParam(":body", $newBody); $stmt->bindParam(":bodyText", $bodyText); $stmt->bindParam(":dateModified", $dateModified); $stmt->bindParam(":categories", $categories); $stmt->bindParam(":keywords", $keywords); return $stmt->execute(); } /** * Creates a new post di rectory, uploads the header image and moves the images from the * temp folder to the new folder, then updates the post html to point to the new images, finally * it creates the post in the database * @param string $title - Title of the blog post * @param string $abstract - Abstract of the blog post i.e. a short description * @param string $body - Body of the blog post * @param string $bodyText - Body of the blog post as plain text * @param string $dateCreated - Date the blog post was created * @param bool $featured - Whether the blog post is featured or not * @param string $categories - Categories of the blog post * @param UploadedFileInterface|null $headerImg - Header image of the blog post * @return int|string - ID of the blog post or error message */ public function createPost(string $title, string $abstract, string $body, string $bodyText, string $dateCreated, bool $featured, string $categories, UploadedFileInterface|null $headerImg): int|string { $conn = dbConn(); $folderID = uniqid(); $targetFile = array("imgLocation" => "../blog/imgs/placeholder.png"); $targetDir = "../blog/imgs/" . $title . "_" . $folderID . "/"; mkdir($targetDir, 0777, true); if ($headerImg !== null) { $imagUtils = new imgUtils(); $targetFile = $imagUtils->uploadFile($targetDir, $headerImg); } if (!is_array($targetFile)) { return $targetFile; } $newBody = $this->changeHTMLSrc($body, $targetDir, "../blog/imgs/tmp/"); if ($featured) { $stmtMainProject = $conn->prepare("UPDATE blog SET featured = 0 WHERE featured = 1;"); $stmtMainProject->execute(); } $keywords = implode(", ", RakePlus::create($bodyText)->keywords()); $latest = $this->getLatestBlogPost(); $prevTitle = $latest["title"]; $prevAbstract = $latest["abstract"]; $prevHeaderImage = substr($latest["headerImg"], 10); $prevHeaderImage = str_ireplace("%2F", "/", $prevHeaderImage); $headerImage = rawurlencode("../" . $targetFile["imgLocation"]); $stmt = $conn->prepare("INSERT INTO blog (title, dateCreated, dateModified, featured, headerImg, abstract, body, bodyText, categories, keywords, folderID) VALUES (:title, :dateCreated, :dateModified, :featured, :headerImg, :abstract, :body, :bodyText, :categories, :keywords, :folderID);"); $stmt->bindParam(":title", $title); $stmt->bindParam(":dateCreated", $dateCreated); $stmt->bindParam(":dateModified", $dateCreated); // $isFeatured = $featured ? 1 : 0; $stmt->bindParam(":featured", $featured); $stmt->bindParam(":headerImg", $headerImage); $stmt->bindParam(":abstract", $abstract); $stmt->bindParam(":body", $newBody); $stmt->bindParam(":bodyText", $bodyText); $stmt->bindParam(":categories", $categories); $stmt->bindParam(":keywords", $keywords); $stmt->bindParam(":folderID", $folderID); if (!$stmt->execute()) { return "Error, couldn't create post"; } $stmtEmails = $conn->prepare("SELECT email FROM newsletter;"); $stmtEmails->execute(); $emails = $stmtEmails->fetchAll(PDO::FETCH_ASSOC); $headerImage = substr($headerImage, 10); $headerImage = str_ireplace("%2F", "/", $headerImage); $emailBody = << Rohit Pai's blog

Hey, I've got a new post!

latest post

header image of the latest post

$title

$abstract

See Post

in case you missed the previous post

header image of the previous post

$prevTitle

$prevAbstract

See Post
EOD; foreach ($emails as $email) { $emailFooter = <<
2023
EOD; $emailBody .= $emailFooter; $this->sendMail($email["email"], $emailBody, "Hey, Rohit's blog has a new post!"); } return intval($conn->lastInsertId()); } /** * Upload the images in the post to temp folder and return image location * @param UploadedFileInterface $img - Image to upload * @return string|array - String with error message or array with the location of the uploaded file */ public function uploadPostImage(UploadedFileInterface $img): string|array { $targetDir = "../blog/imgs/tmp/"; $imagUtils = new imgUtils(); $targetFile = $imagUtils->uploadFile($targetDir, $img); $file = $targetDir . basename($img->getClientFilename()); if (file_exists($file)) { return array("url" => $file); } if (!is_array($targetFile)) { return $targetFile; } if (file_exists($targetFile["imgLocation"])) { return array("url" => $targetFile["imgLocation"]); } return "Couldn't upload the image"; } /** * Upload the header image of the post and update the database * @param int $ID - ID of the post * @param UploadedFileInterface $img - Image to upload * @return string|array - String with error message or array with the location of the uploaded file */ public function uploadHeaderImage(int $ID, UploadedFileInterface $img): string|array { $conn = dbConn(); $stmt = $conn->prepare("SELECT * FROM blog WHERE ID = :ID;"); $stmt->bindParam(":ID", $ID); $stmt->execute(); $result = $stmt->fetch(PDO::FETCH_ASSOC); if (!$result) { return "Couldn't find the post"; } $targetDir = "../blog/imgs/" . $result["title"] . "_" . $result["folderID"] . "/"; $imagUtils = new imgUtils(); $targetFile = $imagUtils->uploadFile($targetDir, $img); if (!is_array($targetFile)) { return $targetFile; } if (file_exists($targetFile["imgLocation"])) { unlink($result["headerImg"]); $stmt = $conn->prepare("UPDATE blog SET headerImg = :headerImg WHERE ID = :ID;"); $stmt->bindParam(":ID", $ID); $location = urldecode("../" . $targetFile["imgLocation"]); $stmt->bindParam(":headerImg", $location); $stmt->execute(); if ($stmt->rowCount() > 0) { return $targetFile; } return "Couldn't update the post"; } return "Couldn't upload the image"; } /** * Change the HTML src of the images in the post to point to the new location * @param string $body - Body of the post * @param string $to - New location of the images * @param string $from - Old location of the images * @return string - Body of the post with the new image locations */ public function changeHTMLSrc(string $body, string $to, string $from): string { $htmlDoc = new DOMDocument(); // Load the raw HTML content into DOMDocument @$htmlDoc->loadHTML($body, LIBXML_NOERROR); // Get the body and process images $doc = $htmlDoc->getElementsByTagName('body')->item(0); $imgs = $doc->getElementsByTagName('img'); $srcList = array(); foreach ($imgs as $img) { $src = $img->getAttribute("src"); $src = urldecode($src); $srcList[] = $src; $fileName = basename($src); // Update the src attribute to the new location $img->setAttribute("src", substr($to, 2) . $fileName); } // Rename files and clean up old ones $files = scandir($from); foreach ($files as $file) { if ($file != "." && $file != "..") { if (!in_array($from . $file, $srcList)) { unlink($from . $file); continue; } rename($from . $file, $to . $file); } } // Process the HTML content for output $newBody = ''; foreach ($doc->childNodes as $node) { // Only convert text nodes to HTML entities if ($node->nodeType === XML_TEXT_NODE) { $newBody .= $this->convertToHtmlEntities($node->nodeValue); // Convert text nodes } else { $newBody .= $htmlDoc->saveHTML($node); // Keep HTML tags intact } } return $newBody; } /** * Convert all characters in a string to HTML entities while leaving HTML tags intact. * @param string $text - The text to convert * @return string - The converted text with HTML entities */ private function convertToHtmlEntities(string $text): string { // Convert characters to HTML entities using mb_encode_numericentity return htmlentities($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); } /** * Get all posts with the given category * @param string $category - Category of the post * @return array - Array of all posts with the given category or error message */ public function getPostsByCategory(string $category): array { $conn = dbConn(); $stmt = $conn->prepare("SELECT * FROM blog WHERE LOCATE(:category, categories) > 0;"); $stmt->bindParam(":category", $category); $stmt->execute(); return $stmt->fetchAll(PDO::FETCH_ASSOC); } /** * Search for a blog post with the given search term * @param string $searchTerm - Search term * @return array - Array of all posts with the given search term or error message */ public function searchBlog(string $searchTerm): array { $conn = dbConn(); $stmt = $conn->prepare("SELECT * FROM blog WHERE MATCH(title, bodyText) AGAINST(:searchTerm IN NATURAL LANGUAGE MODE);"); $stmt->bindParam(":searchTerm", $searchTerm); $stmt->execute(); $result = $stmt->fetchAll(PDO::FETCH_ASSOC); if ($result) { for ($i = 0; $i < count($result); $i++) { $result[$i]["abstract"] = $this->getShortPost($searchTerm, stripcslashes($result[$i]["bodyText"])); } return $result; } return array("errorMessage" => "Error, could not find posts"); } /** * Get the short post with the search term * @param string $searchTerm - Search term * @param $text - Body of the post as plain text * @return string - Short post with the search term */ private function getShortPost(string $searchTerm, $text): string { $pattern = '/([,:;!?.-]+)/u'; $parts = preg_split($pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); $cleanedParts = []; foreach ($parts as $part) { $part = trim($part); // Remove leading/trailing spaces and newline characters if (!empty($part)) { $cleanedParts[] = $part; } } $combinedParts = []; $currentPart = ''; foreach ($cleanedParts as $part) { if (preg_match('/[,:;!?.-]/u', $part)) { $currentPart .= $part; } else { if (!empty($currentPart)) { $combinedParts[] = trim($currentPart); } $currentPart = rtrim($part); } } if (!empty($currentPart)) { $combinedParts[] = trim($currentPart); } $result = ""; for ($i = 0; $i < count($combinedParts); $i++) { $part = $combinedParts[$i]; if (stripos($part, $searchTerm) !== false) { $before = ($i > 0) ? $combinedParts[$i - 1] : ""; $after = ($i < count($combinedParts) - 1) ? $combinedParts[$i + 1] : ""; if ($before === "" && $i > 0) { $before = $combinedParts[$i - 1]; } $result = $before . " " . $part . " " . $after; // If the search term is found, we don't need to continue checking subsequent parts break; } } return $result; } /** * Generate the XML feed * @param mixed $type - Type of feed * @return array|string - Error message or the XML feed */ private function generateXMLFeed(mixed $type): array|string { ob_start(); $feed = new FeedWriter($type); $feed->setTitle("Rohit Pai's Blog"); $feed->setLink('https://rohitpai.co.uk/blog'); $feed->setFeedURL('https://rohitpai.co.uk/api/blog/feed/atom'); $feed->setChannelElement('updated', date(DATE_ATOM, time())); $feed->setChannelElement('author', ['name' => 'Rohit Pai']); $posts = $this->getBlogPosts(); if (isset($posts["errorMessage"])) { return $posts; } foreach ($posts as $post) { $newItem = $feed->createNewItem(); $newItem->setTitle($post["title"]); $newItem->setLink("https://rohitpai.co.uk/blog/post/" . rawurlencode($post["title"]) . "#disqus_thread"); $newItem->setDate($post["dateModified"]); $newItem->setDescription($post["body"]); $feed->addItem($newItem); } $feed->generateFeed(); $atom = ob_get_contents(); ob_end_clean(); return $atom; } /** * Generate the JSON feed * @return array|array[] - Error message or the JSON feed */ private function generateJSONFeed(): array { $posts = $this->getBlogPosts(); if (isset($posts["errorMessage"])) { return $posts; } $json = array(); $json["version"] = "https://jsonfeed.org/version/1.1"; $json["title"] = "Rohit Pai's Blog"; $json["home_page_url"] = "https://rohitpai.co.uk/blog"; $json["feed_url"] = "https://rohitpai.co.uk/api/blog/feed/json"; $json["description"] = "Rohit Pai's personal blog on all things self hosting and various other tech topics"; $json["author"] = array( "name" => "Rohit Pai", "url" => "https://rohitpai.co.uk", "avatar" => "https://rohitpai.co.uk/imgs/profile.jpg" ); $items = array(); foreach ($posts as $post) { $items[] = array( "id" => strval($post["ID"]), "url" => "https://rohitpai.co.uk/blog/post/" . rawurlencode($post["title"]) . "#disqus_thread", "title" => $post["title"], "date_published" => date($post["dateCreated"]), "date_modified" => date($post["dateModified"]), "description" => $post["abstract"], "banner_image" => "https://rohitpai.co.uk/" . rawurlencode($post["headerImg"]), "content_html" => $post["body"] ); } $json["items"] = $items; return $json; } /** * Generate the RSS feed based on type * @param string $type - Type of feed * @return string|array - RSS feed or an error message */ public function getFeed(string $type): string|array { $feed = ""; if ($type == "atom") { $feed = $this->generateXMLFeed(ATOM); } if ($type == "rss") { $feed = $this->generateXMLFeed(RSS2); } if ($type == "json") { $feed = $this->generateJSONFeed(); } return $feed; } /** * Add an email to the newsletter and send welcome email * @param string $email - Email to add to the newsletter * @return string|array - Success or error message */ public function addNewsletterEmail(string $email): string|array { $conn = dbConn(); $stmtCheckEmail = $conn->prepare("SELECT * FROM newsletter WHERE email = :email;"); $stmtCheckEmail->bindParam(":email", $email); $stmtCheckEmail->execute(); $result = $stmtCheckEmail->fetch(PDO::FETCH_ASSOC); if ($result) { return "Email already exists"; } $stmt = $conn->prepare("INSERT INTO newsletter (email) VALUES (:email);"); $stmt->bindParam(":email", $email); $stmt->execute(); $body = << Rohit Pai's blog

hello from rohit

hey there, i'm rohit!

What to Expect

You'll get an email from me everytime I make a new post to my blog. Sometimes, you may get a special email on occasion, where I talk about something interesting that's not worth a full on post but I Still want to tell you about it.

Don't worry, I won't spam you with emails. Well, thanks for signing up to my newsletter, hopefully you'll hear from me soon!

P.S. Please consider adding this email address rohit@rohitpai.co.uk to your emails contact list so that my emails won't get sent to span, thanks.

EOD; return $this->sendMail($email, $body, "Hello from Rohit!"); } /** * Send an email to the given email address * @param string $email - Email address to send the email to * @param string $body - Body of the email * @param string $subject - Subject of the email * @return string|string[] */ public function sendMail(string $email, string $body, string $subject): string|array { $mail = new PHPMailer(true); try { $mail->isSMTP(); $mail->Host = "smtp.hostinger.com"; $mail->SMTPAuth = true; $mail->Username = "rohit@rohitpai.co.uk"; $mail->Password = getEmailPassword(); $mail->SMTPSecure = "tls"; $mail->Port = 587; $mail->setFrom("rohit@rohitpai.co.uk", "Rohit Pai"); $mail->addAddress($email); $mail->isHTML(); $mail->Subject = $subject; $mail->Body = $body; $mail->send(); return "success"; } catch (Exception $e) { return array("errorMessage" => "Error, couldn't send email because of " . $e); } } /** * @param string $email - Email to delete from the newsletter * @return string - Success or error message */ public function deleteNewsletterEmail(string $email): string { $conn = dbConn(); $stmtCheckEmail = $conn->prepare("SELECT * FROM newsletter WHERE email = :email;"); $stmtCheckEmail->bindParam(":email", $email); $stmtCheckEmail->execute(); $result = $stmtCheckEmail->fetch(PDO::FETCH_ASSOC); if (!$result) { return "email not found"; } $stmt = $conn->prepare("DELETE FROM newsletter WHERE email = :email;"); $stmt->bindParam(":email", $email); $stmt->execute();; return "success"; } /** * @param string $subject - Subject of the newsletter * @param string $message - Message content * @return array|string - Success or error message */ public function sendNewsletter(string $subject, string $message): array|string { $conn = dbConn(); $stmtEmails = $conn->prepare("SELECT email FROM newsletter;"); $stmtEmails->execute(); $emails = $stmtEmails->fetchAll(PDO::FETCH_ASSOC); $msg = ""; $body = << Rohit Pai's blog

a surprise hello from rohit

$subject

$message
EOD; foreach ($emails as $email) { $msg = $this->sendMail($email["email"], $body, $subject); if (is_array($msg)) { return $msg; } } return $msg; } }