my-portfolio/dist/api/blog/blogData.php
rodude123 a55ba6ce2f
All checks were successful
🚀 Deploy website on push / 🎉 Deploy (push) Successful in 20s
Fixed small tiny issue with the carousel having less the maximum number of visible items i.e. none in the hidden div
Signed-off-by: rodude123 <rodude123@gmail.com>
2024-11-04 22:39:04 +00:00

1529 lines
54 KiB
PHP

<?php
namespace api\blog;
use api\utils\feedGenerator\FeedWriter;
use api\utils\imgUtils;
use DOMDocument;
use PDO;
use Psr\Http\Message\UploadedFileInterface;
use DonatelloZa\RakePlus\RakePlus;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
use function api\utils\dbConn;
use function api\utils\getEmailPassword;
use const api\utils\feedGenerator\ATOM;
use const api\utils\feedGenerator\RSS2;
require_once __DIR__ . "/../utils/config.php";
require_once __DIR__ . "/../utils/imgUtils.php";
require_once __DIR__ . "/../utils/feedGenerator/FeedWriter.php";
/**
* Blog Data Class
* Define all functions which either get, update, create or delete posts
*/
class blogData
{
/**
* Get all blog posts
* @return array<array> - 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 = <<<EOD
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rohit Pai's blog</title>
<style>
@font-face {
font-family: 'Noto Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/notosans/v34/o-0NIpQlx3QUlC5A4PNjXhFVZNyB.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Share Tech Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/sharetechmono/v15/J7aHnp1uDWRBEqV98dVQztYldFcLowEF.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
*{
box-sizing: border-box;
}
body, html {
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: Noto Sans KR, sans-serif;
font-style: normal;
font-weight: 500;
font-size: 22px;
line-height: 1.625rem;
min-height: 100%;
}
main, header, footer {
max-width: 768px;
margin: 0 auto;
}
a {
text-decoration: none;
text-transform: lowercase;
}
a:visited {
color: inherit;
}
h1, h2 {
font-family: Share Tech Mono, monospace;
font-style: normal;
font-weight: normal;
line-height: 2.5625rem;
text-transform: lowercase;
}
h1, nav {
font-size: 40px;
}
h2 {
font-size: 28px;
}
h3 {
font-size: 22px;
}
header {
background: hsla(80, 60%, 50%, 1);
color: #FFFFFF;
border-bottom: 5px solid hsla(0, 0%, 75%, 1);
}
header div.title, header div.byLine {
padding: 0 3em 1em;
}
header div.title h1 {
margin: 0;
padding: 1em 0 0;
font-size: 42px;
}
header div.byLine {
border-top: 5px solid hsla(80, 60%, 30%, 1);
}
a.btn {
background-color: hsla(80, 60%, 50%, 1);
text-decoration: none;
display: inline-flex;
padding: 1em 2em;
border-radius: 0.625em;
border: 0.3215em solid hsla(80, 60%, 50%, 1);
color: #FFFFFF;
text-align: center;
align-items: center;
max-height: 4em;
}
div.postContainer, div.container {
padding: 1em 2em 0;
margin: 0 auto 1em;
max-width: 768px;
}
div.postContainer ~ div.postContainer {
border-top: 5px solid hsla(0, 0%, 75%, 1);
}
div.postContainer > *, div.container > * {
margin: 0 auto;
max-width: 768px;
}
div.postContainer div.post h2, div.container div.content h2 {
margin: 0;
padding: 0;
}
div.postContainer div.image, div.container div.image {
width: 50%;
max-width: 100%;
height: auto;
}
div.postContainer div.image img {
-webkit-border-radius: 0.5em;
-moz-border-radius: 0.5em;
border-radius: 0.5em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.container div.image img {
-webkit-border-radius: 10em;
-moz-border-radius: 10em;
border-radius: 10em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.postContainer div.image img, div.container div.image img {
min-width: 100%;
width: 100%;
}
footer {
background-color: hsla(80, 60%, 50%, 1);
margin-top: auto;
padding: 3em;
display: block;
color: #FFFFFF;
}
footer .nav {
width: 75%;
float: left;
}
footer .nav ul {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
}
footer .nav ul li {
display: inline-block;
margin: 0 1em;
}
footer .nav ul a {
color: #FFFFFF;
}
footer .date {
width: 25%;
float: right;
}
</style>
</head>
<body>
<header>
<div class="title">
<h1>Hey, I've got a new post!</h1>
</div>
<div class="byLine">
<p>Hello I'm Rohit an avid full-stack developer and home lab enthusiast. I love anything and everything
to do with full stack development, home labs, coffee and generally anything to do with tech.</p>
</div>
</header>
<main>
<div class="postContainer">
<h2>latest post</h2>
<div class="image">
<img src="https://rohitpai.co.uk/$headerImage" alt="header image of the latest post">
</div>
<div class="post">
<h3>$title</h3>
<p>$abstract</p>
<a href="https://rohitpai.co.uk/blog/post/$title" class="btn">See Post</a>
</div>
</div>
<div class="postContainer">
<h2>in case you missed the previous post</h2>
<div class="image">
<img src="https://rohitpai.co.uk/$prevHeaderImage" alt="header image of the previous post">
</div>
<div class="post">
<h3>$prevTitle</h3>
<p>$prevAbstract</p>
<a href="https://rohitpai.co.uk/blog/post/$prevTitle" class="btn">See Post</a>
</div>
</div>
</main>
EOD;
foreach ($emails as $email)
{
$emailFooter = <<<EOD
<footer>
<div class="nav">
<ul>
<li><a href="https://rohitpai.co.uk/blog">&lt;https://rohitpai.co.uk/blog&gt;</a></li>
<li><a href="https://rohitpai.co.uk/blog/unsubscribe/$email">&lt;Unsubscribe&gt;</a></li>
</ul>
</div>
<div class="date">2023</div>
</footer>
</body>
</html>
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> - 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> - 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 = <<<EOD
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rohit Pai's blog</title>
<style>
@font-face {
font-family: 'Noto Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/notosans/v34/o-0NIpQlx3QUlC5A4PNjXhFVZNyB.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Share Tech Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/sharetechmono/v15/J7aHnp1uDWRBEqV98dVQztYldFcLowEF.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
*{
box-sizing: border-box;
}
body, html {
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: Noto Sans KR, sans-serif;
font-style: normal;
font-weight: 500;
font-size: 22px;
line-height: 1.625rem;
min-height: 100%;
}
main, header, footer {
max-width: 768px;
margin: 0 auto;
}
a {
text-decoration: none;
text-transform: lowercase;
}
a:visited {
color: inherit;
}
h1, h2 {
font-family: Share Tech Mono, monospace;
font-style: normal;
font-weight: normal;
line-height: 2.5625rem;
text-transform: lowercase;
}
h1, nav {
font-size: 40px;
}
h2 {
font-size: 28px;
}
h3 {
font-size: 22px;
}
header {
background: hsla(80, 60%, 50%, 1);
color: #FFFFFF;
border-bottom: 5px solid hsla(0, 0%, 75%, 1);
}
header div.title, header div.byLine {
padding: 0 3em 1em;
}
header div.title h1 {
margin: 0;
padding: 1em 0 0;
font-size: 42px;
}
header div.byLine {
border-top: 5px solid hsla(80, 60%, 30%, 1);
}
a.btn {
background-color: hsla(80, 60%, 50%, 1);
text-decoration: none;
display: inline-flex;
padding: 1em 2em;
border-radius: 0.625em;
border: 0.3215em solid hsla(80, 60%, 50%, 1);
color: #FFFFFF;
text-align: center;
align-items: center;
max-height: 4em;
}
div.postContainer, div.container {
padding: 1em 2em 0;
margin: 0 auto 1em;
max-width: 768px;
}
div.postContainer ~ div.postContainer {
border-top: 5px solid hsla(0, 0%, 75%, 1);
}
div.postContainer > *, div.container > * {
margin: 0 auto;
max-width: 768px;
}
div.postContainer div.post h2, div.container div.content h2 {
margin: 0;
padding: 0;
}
div.postContainer div.image, div.container div.image {
width: 50%;
max-width: 100%;
height: auto;
}
div.postContainer div.image img {
-webkit-border-radius: 0.5em;
-moz-border-radius: 0.5em;
border-radius: 0.5em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.container div.image img {
-webkit-border-radius: 50em;
-moz-border-radius: 50em;
border-radius: 50em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.postContainer div.image img, div.container div.image img {
min-width: 100%;
width: 100%;
}
footer {
background-color: hsla(80, 60%, 50%, 1);
margin-top: auto;
padding: 3em;
display: block;
color: #FFFFFF;
}
footer .nav {
width: 75%;
float: left;
}
footer .nav ul {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
}
footer .nav ul li {
display: inline-block;
margin: 0 1em;
}
footer .nav ul a {
color: #FFFFFF;
}
footer .date {
width: 25%;
float: right;
}
</style>
</head>
<body>
<header>
<div class="title">
<h1>hello from rohit</h1>
</div>
<div class="byLine">
<p>Hello I'm Rohit an avid full-stack developer and home lab enthusiast. I love anything and everything
to do with full stack development, home labs, coffee and generally anything to do with tech.</p>
</div>
</header>
<main>
<div class="container">
<h2>hey there, i'm rohit!</h2>
<div class="image"><img src="https://rohitpai.co.uk/imgs/profile.jpg" alt=""></div>
<div class="content">
<h3>What to Expect</h3>
<p>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.</p>
<p>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>
<p>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.</p>
</div>
</div>
</main>
<footer>
<div class="nav">
<ul>
<li><a href="https://rohitpai.co.uk/blog">&lt;https://rohitpai.co.uk/blog&gt;</a></li>
<li><a href="https://rohitpai.co.uk/blog/unsubscribe/$email">&lt;Unsubscribe&gt;</a></li>
</ul>
</div>
<div class="date">2023</div>
</footer>
</body>
</html>
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 = <<<EOD
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rohit Pai's blog</title>
<style>
@font-face {
font-family: 'Noto Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/notosans/v34/o-0NIpQlx3QUlC5A4PNjXhFVZNyB.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Share Tech Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/sharetechmono/v15/J7aHnp1uDWRBEqV98dVQztYldFcLowEF.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
*{
box-sizing: border-box;
}
body, html {
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: Noto Sans KR, sans-serif;
font-style: normal;
font-weight: 500;
font-size: 22px;
line-height: 1.625rem;
min-height: 100%;
}
main, header, footer {
max-width: 768px;
margin: 0 auto;
}
a {
text-decoration: none;
text-transform: lowercase;
}
a:visited {
color: inherit;
}
h1, h2 {
font-family: Share Tech Mono, monospace;
font-style: normal;
font-weight: normal;
line-height: 2.5625rem;
text-transform: lowercase;
}
h1, nav {
font-size: 40px;
}
h2 {
font-size: 28px;
}
h3 {
font-size: 22px;
}
header {
background: hsla(80, 60%, 50%, 1);
color: #FFFFFF;
border-bottom: 5px solid hsla(0, 0%, 75%, 1);
}
header div.title, header div.byLine {
padding: 0 3em 1em;
}
header div.title h1 {
margin: 0;
padding: 1em 0 0;
font-size: 42px;
}
header div.byLine {
border-top: 5px solid hsla(80, 60%, 30%, 1);
}
a.btn {
background-color: hsla(80, 60%, 50%, 1);
text-decoration: none;
display: inline-flex;
padding: 1em 2em;
border-radius: 0.625em;
border: 0.3215em solid hsla(80, 60%, 50%, 1);
color: #FFFFFF;
text-align: center;
align-items: center;
max-height: 4em;
}
div.postContainer, div.container {
padding: 1em 2em 0;
margin: 0 auto 1em;
max-width: 768px;
}
div.postContainer ~ div.postContainer {
border-top: 5px solid hsla(0, 0%, 75%, 1);
}
div.postContainer > *, div.container > * {
margin: 0 auto;
max-width: 768px;
}
div.postContainer div.post h2, div.container div.content h2 {
margin: 0;
padding: 0;
}
div.postContainer div.image, div.container div.image {
width: 50%;
max-width: 100%;
height: auto;
}
div.postContainer div.image img {
-webkit-border-radius: 0.5em;
-moz-border-radius: 0.5em;
border-radius: 0.5em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.container div.image img {
-webkit-border-radius: 50em;
-moz-border-radius: 50em;
border-radius: 50em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.postContainer div.image img, div.container div.image img {
min-width: 100%;
width: 100%;
}
footer {
background-color: hsla(80, 60%, 50%, 1);
margin-top: auto;
padding: 3em;
display: block;
color: #FFFFFF;
}
footer .nav {
width: 75%;
float: left;
}
footer .nav ul {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
}
footer .nav ul li {
display: inline-block;
margin: 0 1em;
}
footer .nav ul a {
color: #FFFFFF;
}
footer .date {
width: 25%;
float: right;
}
</style>
</head>
<body>
<header>
<div class="title">
<h1>a surprise hello from rohit</h1>
</div>
<div class="byLine">
<p>Hello I'm Rohit an avid full-stack developer and home lab enthusiast. I love anything and everything
to do with full stack development, home labs, coffee and generally anything to do with tech.</p>
</div>
</header>
<main>
<div class="container">
<h2>$subject</h2>
<div class="content">
$message
</div>
</div>
</main>
<footer>
<div class="nav">
<ul>
<li><a href="https://rohitpai.co.uk/blog">&lt;https://rohitpai.co.uk/blog&gt;</a></li>
<li><a href="https://rohitpai.co.uk/blog/unsubscribe">&lt;Unsubscribe&gt;</a></li>
</ul>
</div>
<div class="date">2023</div>
</footer>
</body>
</html>
EOD;
foreach ($emails as $email)
{
$msg = $this->sendMail($email["email"], $body, $subject);
if (is_array($msg))
{
return $msg;
}
}
return $msg;
}
}