All checks were successful
		
		
	
	🚀 Deploy website on push / 🎉 Deploy (push) Successful in 21s
				
			Signed-off-by: rodude123 <rodude123@gmail.com>
		
			
				
	
	
		
			1501 lines
		
	
	
		
			53 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			1501 lines
		
	
	
		
			53 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"><https://rohitpai.co.uk/blog></a></li>
 | 
						|
                            <li><a href="https://rohitpai.co.uk/blog/unsubscribe/$email"><Unsubscribe></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();
 | 
						|
        $htmlDoc->loadHTML($body, LIBXML_NOERROR);
 | 
						|
        $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);
 | 
						|
 | 
						|
            $img->setAttribute("src", substr($to, 2) . $fileName);
 | 
						|
        }
 | 
						|
 | 
						|
        $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);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        $newBody = '';
 | 
						|
        foreach ($doc->childNodes as $node)
 | 
						|
        {
 | 
						|
            $newBody .= $htmlDoc->saveHTML($node);
 | 
						|
        }
 | 
						|
        return $newBody;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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"><https://rohitpai.co.uk/blog></a></li>
 | 
						|
                            <li><a href="https://rohitpai.co.uk/blog/unsubscribe/$email"><Unsubscribe></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"><https://rohitpai.co.uk/blog></a></li>
 | 
						|
                            <li><a href="https://rohitpai.co.uk/blog/unsubscribe"><Unsubscribe></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;
 | 
						|
    }
 | 
						|
} |