From ea91c7736528cd233804b2fa68aa46a2f0acc69b Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen <115699497+zainforbjs@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:44:01 +0500 Subject: [PATCH 1/2] Support embed/autolink in blog content Add embedding and autolinking helpers and apply them to blog posts. Controller.cfc: introduce extractYouTubeId, getEmbedHtml, isEmbeddableUrl and embedAndAutoLink to convert plain URLs into embed iframes (YouTube/Twitter) or anchor tags. BlogController.cfc: process blog.content with embedAndAutoLink before rendering so embeddable links are displayed as embeds. public/javascripts/createBlog.js: minor client-side updates to form flow and post-save behavior (adds redirect to /blog on successful save and other JS tweaks). --- app/controllers/Controller.cfc | 109 +++++++++++++++++++++++++ app/controllers/web/BlogController.cfc | 51 ++++++------ public/javascripts/createBlog.js | 2 +- 3 files changed, 137 insertions(+), 25 deletions(-) diff --git a/app/controllers/Controller.cfc b/app/controllers/Controller.cfc index 709b774..8450966 100644 --- a/app/controllers/Controller.cfc +++ b/app/controllers/Controller.cfc @@ -709,4 +709,113 @@ component extends="wheels.Controller" { rethrow; } } + + /** + * Extracts YouTube video ID from various YouTube URL formats + * Supports: https://youtube.com/watch?v=xyz, https://youtu.be/xyz, https://www.youtube.com/embed/xyz + */ + string function extractYouTubeId(required string url) { + var id = ""; + // youtu.be format + if (findNoCase("youtu.be/", arguments.url)) { + id = listLast(arguments.url, "/"); + // Remove any query parameters + if (findNoCase("?", id)) { + id = listFirst(id, "?"); + } + } + // youtube.com/watch?v= format + else if (findNoCase("youtube.com", arguments.url) && findNoCase("v=", arguments.url)) { + var params = listLast(arguments.url, "?"); + var paramList = listToArray(params, "&"); + for (var param in paramList) { + if (findNoCase("v=", param)) { + id = listLast(param, "="); + break; + } + } + } + // Already embed format + else if (findNoCase("youtube.com/embed/", arguments.url)) { + id = listLast(listFirst(arguments.url, "?"), "/"); + } + return trim(id); + } + + /** + * Detects if a URL is embeddable and returns embed HTML + * Supports: YouTube, Twitter + */ + string function getEmbedHtml(required string url, string width="100%", string height="400") { + var embedHtml = ""; + var youtubeId = ""; + var vimeoId = ""; + var trimmedUrl = trim(arguments.url); + + // YouTube + if (findNoCase("youtube.com", trimmedUrl) || findNoCase("youtu.be", trimmedUrl)) { + youtubeId = extractYouTubeId(trimmedUrl); + if (len(youtubeId)) { + embedHtml = ''; + } + } + // Twitter/X + else if (findNoCase("twitter.com", trimmedUrl) || findNoCase("x.com", trimmedUrl)) { + embedHtml = '
'; + } + + return embedHtml; + } + + /** + * Checks if a URL is embeddable + */ + boolean function isEmbeddableUrl(required string url) { + var embeddableDomains = ["youtube.com", "youtu.be", "twitter.com", "x.com"]; + var trimmedUrl = lcase(trim(arguments.url)); + + for (var domain in embeddableDomains) { + if (findNoCase(domain, trimmedUrl)) { + return true; + } + } + return false; + } + + /** + * Converts plain text URLs and embeddable links into HTML + * For embeddable URLs (YouTube, Vimeo, etc.), creates embed iframes + * For other URLs, creates anchor tags + */ + string function embedAndAutoLink(required string content, string class="text--primary", string target="_blank") { + var result = arguments.content; + var urlPattern = "(https?://[^\s<""'`]+)"; + var matches = reMatch(urlPattern, result); + + // Remove duplicates + var uniqueUrls = {}; + for (var match in matches) { + var cleanUrl = trim(match); + // Skip if it's already part of an href or src + if (!findNoCase("href='#cleanUrl#", result) && !findNoCase('href="' & cleanUrl & '"', result) && !findNoCase("src='#cleanUrl#", result) && !findNoCase('src="' & cleanUrl & '"', result)) { + uniqueUrls[cleanUrl] = cleanUrl; + } + } + + // Replace each unique URL + for (var link in uniqueUrls) { + if (isEmbeddableUrl(link)) { + var embedCode = getEmbedHtml(link); + if (len(embedCode)) { + result = replace(result, link, embedCode, "all"); + } + } else { + // Regular link + var linkHtml = '' & link & ''; + result = replace(result, link, linkHtml, "all"); + } + } + + return result; + } } diff --git a/app/controllers/web/BlogController.cfc b/app/controllers/web/BlogController.cfc index 719d9a0..519a67a 100644 --- a/app/controllers/web/BlogController.cfc +++ b/app/controllers/web/BlogController.cfc @@ -23,10 +23,10 @@ component extends="app.Controllers.Controller" { perPage = structKeyExists(params, "perPage") ? max(1, min(24, val(sanitizeParam(params.perPage, 6)))) : 6; isInfiniteScroll = structKeyExists(params, "infiniteScroll") ? params.infiniteScroll : false; userId = GetSignedInUserId(); - + try { var result = getBlogData(filterType, filterValue, page, perPage, isInfiniteScroll); - + if (result.query.recordCount == 0) { // Show fallback blogs var fallback = getBlogData("", "", 1, perPage, isInfiniteScroll); @@ -39,14 +39,14 @@ component extends="app.Controllers.Controller" { // Set template variables hasMore = result.hasMore; totalCount = result.totalCount; - + // Set author info if filtering by author if (structKeyExists(result, "author")) { author = result.author; } - + renderPartial(partial="partials/blogList"); - + } catch (any e) { handleBlogError(e, userId, page, perPage, isInfiniteScroll); } @@ -63,35 +63,35 @@ component extends="app.Controllers.Controller" { // Main data retrieval logic private struct function getBlogData(filterType, filterValue, page, perPage, isInfiniteScroll) { var result = {}; - + // Handle special cases first if (len(arguments.filterType) && len(arguments.filterValue)) { // Normalize filter value (convert hyphens to dots) arguments.filterValue = replace(arguments.filterValue, "-", ".", "all"); - + // Handle date archive filtering (year/month) if (isNumeric(arguments.filterType) && isNumeric(arguments.filterValue)) { return getBlogsByMonthYear(arguments.filterType, arguments.filterValue, arguments.page, arguments.perPage, arguments.isInfiniteScroll); } - + // Handle content filtering switch (lcase(arguments.filterType)) { case "category": return getBlogsByCategory(arguments.filterValue, arguments.page, arguments.perPage, arguments.isInfiniteScroll); - + case "tag": return getAllByTag(arguments.filterValue, arguments.page, arguments.perPage, arguments.isInfiniteScroll); - + case "author": return getBlogsWithAuthorData(arguments.filterValue, arguments.page, arguments.perPage, arguments.isInfiniteScroll); - + default: // Invalid filter type, fallback to all blogs logInvalidFilter(arguments.filterType, arguments.filterValue); return getAllBlogs(arguments.page, arguments.perPage, arguments.isInfiniteScroll); } } - + // Default: return all blogs return getAllBlogs(arguments.page, arguments.perPage, arguments.isInfiniteScroll); } @@ -100,16 +100,16 @@ component extends="app.Controllers.Controller" { private struct function getBlogsWithAuthorData(authorIdentifier, page, perPage, isInfiniteScroll) { var authorId = getBlogAuthorId(arguments.authorIdentifier); var result = getBlogsByAuthor(authorId, arguments.page, arguments.perPage, arguments.isInfiniteScroll); - + // Get author statistics in a single optimized query if possible var authorStats = getAuthorStatistics(authorId); - + result.author = { "id": authorId, "totalposts": authorStats.totalPosts, "totalcomments": authorStats.totalComments }; - + return result; } @@ -172,18 +172,18 @@ component extends="app.Controllers.Controller" { }, userId = arguments.userId ); - + // Provide fallback data try { var fallbackResult = getAllBlogs(1, arguments.perPage, arguments.isInfiniteScroll); blogs = fallbackResult.query; hasMore = fallbackResult.hasMore; totalCount = fallbackResult.totalCount; - + // Set error flag for template hasError = true; errorMessage = "We're experiencing some technical difficulties. Showing recent posts instead."; - + } catch (any fallbackError) { // If even fallback fails, create empty result blogs = queryNew("id,title,slug,createdby,postDate,fullName,username,profilePicture", "integer,varchar,varchar,integer,date,varchar,varchar,varchar"); @@ -192,7 +192,7 @@ component extends="app.Controllers.Controller" { hasError = true; errorMessage = "Unable to load blog posts at this time. Please try again later."; } - + renderPartial(partial="partials/blogList"); } @@ -499,6 +499,9 @@ component extends="app.Controllers.Controller" { throw("Blog not found"); } + // Process embeds in content + blog.content = embedAndAutoLink(blog.content); + // Set blog post data for layout meta tags (avoids DB query in view) request.blogPostForMeta = blog; @@ -910,20 +913,20 @@ component extends="app.Controllers.Controller" { private function getAllByTag(required string tag, numeric page=1, numeric perPage=6, boolean isInfiniteScroll=false) { // First, find the tag by name var targetTag = model("Tag").findOne(where="name = '#arguments.tag#'"); - + if (!isObject(targetTag)) { return {query: queryNew(""), hasMore: false, totalCount: 0}; } - + // Get all blog_ids associated with this tag var blogTags = model("BlogTag").findAll(where="tagId = #targetTag.id#", returnAs="query"); - + if (blogTags.recordCount == 0) { return {query: queryNew(""), hasMore: false, totalCount: 0}; } - + var blogIds = valueList(blogTags.blogId); - + var result = { query = model("Blog").findAll( where="blog_posts.id IN (#blogIds#) AND blog_posts.status ='Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#now()#'", diff --git a/public/javascripts/createBlog.js b/public/javascripts/createBlog.js index ad517de..dfaae61 100644 --- a/public/javascripts/createBlog.js +++ b/public/javascripts/createBlog.js @@ -1 +1 @@ -document.addEventListener("DOMContentLoaded",(function(){const e=new EasyMDE({element:document.getElementById("editor"),spellChecker:!1,status:!1,autofocus:!0,toolbar:["bold","italic","heading","|","quote","unordered-list","ordered-list","|","link","image","code","|","preview","side-by-side","fullscreen","|","guide"],uploadImage:!0,imageUploadEndpoint:"/blog/upload",imageMaxSize:3145728,imageAccept:"image/png, image/jpeg, image/gif, image/webp",imageTexts:{sbInit:"Upload an image",sbOnDragEnter:"Drop image here",sbOnDrop:"Uploading...",sbProgress:"Uploading...",sbOnUploaded:"Uploaded!",sizeUnits:"b,Kb,Mb"},previewRender:function(e){return marked.parse(e)},theme:"github-light",sideBySideFullscreen:!1,maxHeight:"500px",minHeight:"500px",placeholder:"Write your blog post here...",shortcuts:{toggleSideBySide:"Ctrl-Alt-P",toggleFullScreen:"Ctrl-Alt-F",togglePreview:"Ctrl-Alt-V",toggleBold:"Ctrl-B",toggleItalic:"Ctrl-I",drawLink:"Ctrl-K",drawImage:"Ctrl-G",drawTable:"Ctrl-T",toggleHeadingSmaller:"Ctrl-H",toggleHeadingBigger:"Ctrl-Shift-H",toggleUnorderedList:"Ctrl-L",toggleOrderedList:"Ctrl-Alt-L",toggleCodeBlock:"Ctrl-Alt-C",toggleBlockquote:"Ctrl-Q",togglePreview:"Ctrl-P"}});function t(){const t=e.value();document.getElementById("content").value=t}t(),e.codemirror.on("change",(function(){const t=e.value();document.getElementById("content").value=t})),document.addEventListener("htmx:configRequest",(function(e){"blogForm"===e.detail.elt.id&&(t(),e.detail.parameters.content=document.getElementById("content").value)})),document.getElementById("blogForm").addEventListener("htmx:beforeRequest",(function(e){t()})),document.getElementById("saveDraftBtn").addEventListener("click",(function(){e&&e.codemirror.save(),t(),document.getElementById("isDraft").value="1",document.getElementById("blogForm").requestSubmit()})),document.getElementById("blogForm").addEventListener("submit",(function(n){t();let i=document.getElementById("content").value;var o=!0;const l=document.getElementById("title"),r=document.getElementById("categoryId"),d=document.getElementById("posttypeId"),a=document.getElementById("postTags"),s=document.getElementById("editor"),c=document.getElementById("tagInput"),u="1"===document.getElementById("isDraft").value;[l,r,d,c].forEach((e=>e.classList.remove("is-invalid"))),s.classList.remove("border-danger");const m=document.getElementById("titleExists");if(m&&"1"!==m.value&&""!==l.value.trim()){n.preventDefault();const t=document.getElementById("id").value||"",i=new FormData;i.append("title",l.value.trim()),t&&i.append("id",t);var g=document.querySelector('input[name="authenticityToken"]');return g&&i.append("authenticityToken",g.value),fetch("/blog/check-title",{method:"POST",body:i}).then((e=>e.text())).then((t=>{document.getElementById("title-message").innerHTML=t;const n=document.getElementById("titleExists");if(n&&"1"===n.value)return l.classList.add("is-invalid"),notifier.show("Error!","A blog post already exist with this title.","","/img/high_priority-48.png",4e3),!1;t(),e.submit()})),!1}return m&&"1"===m.value&&(o=!1,l.classList.add("is-invalid")),o?(u||(""===l.value.trim()&&(o=!1,l.classList.add("is-invalid")),""===r.value.trim()&&(o=!1,r.classList.add("is-invalid")),""===d.value.trim()&&(o=!1,d.classList.add("is-invalid")),""===a.value.trim()&&(o=!1,c.classList.add("is-invalid"),c.classList.remove("border-0")),""===i.trim()&&(o=!1,s.classList.add("border-danger"))),o?void t():(n.preventDefault(),notifier.show("Error!","Please fill out all required fields.","","/img/high_priority-48.png",4e3),!1)):(n.preventDefault(),n.stopPropagation(),notifier.show("Error!","A blog post already exist with this title.","","/img/high_priority-48.png",4e3),!1)})),document.addEventListener("htmx:afterSwap",(function(e){"categoryId"===e.target.id&&$("#categoryId").select2({placeholder:"Select Categories",theme:"bootstrap-5",width:$(this).data("width")?$(this).data("width"):($(this).hasClass("w-100"),"100%"),maximumSelectionLength:5})})),$("#categoryId").select2({placeholder:"Select Categories",theme:"bootstrap-5",width:$(this).data("width")?$(this).data("width"):($(this).hasClass("w-100"),"100%"),maximumSelectionLength:5});const n=document.getElementById("tagContainer"),i=document.getElementById("tagInput"),o=document.getElementById("postTags");let l=[];function r(e){const t=document.createElement("span");t.classList.add("tag","cursor-pointer"),t.innerHTML=`${e} `,t.querySelector(".remove-tag").addEventListener("click",(function(){!function(e){l=l.filter((t=>t!==e)),a(),d()}(e)})),n.insertBefore(t,i)}function d(){n.querySelectorAll(".tag").forEach((e=>e.remove())),l.forEach((e=>r(e)))}function a(){o.value=l.join(",")}o.value&&(l=o.value.split(","),d()),i.addEventListener("keydown",(function(e){if((","===e.key||"Enter"===e.key)&&l.length<5){e.preventDefault();let t=i.value.trim().replace(/,/g,"");""===t||l.includes(t)||(l.push(t),r(t),a(),i.value="")}else l.length>=5&&e.preventDefault()}));const s=document.getElementById("title");s&&s.addEventListener("blur",(function(){const e=this.value.trim();if(e){const n=document.getElementById("id").value||"",i=new FormData;i.append("title",e),n&&i.append("id",n);var t=document.querySelector('input[name="authenticityToken"]');t&&i.append("authenticityToken",t.value),fetch("/blog/check-title",{method:"POST",body:i}).then((e=>{if(!e.ok)throw new Error("Server error: "+e.status);return e.text()})).then((e=>{document.getElementById("title-message").innerHTML=e;const t=document.getElementById("titleExists");t&&"1"===t.value?s.classList.add("is-invalid"):s.classList.remove("is-invalid")})).catch((e=>{console.error("Error checking title:",e),document.getElementById("title-message").innerHTML='Error checking title availability'}))}else document.getElementById("title-message").innerHTML=""})),document.body.addEventListener("htmx:afterRequest",(function(e){const t=e.detail.xhr;500===t.status&&t.responseURL.includes("/blog/store")&¬ifier.show("Error","Something went wrong!","danger","",5e3),200===t.status&&t.responseURL.includes("/blog/store")&&(notifier.show("Success",t.responseText,"success","",5e3),setTimeout((function(){}),4e3))}))})),document.addEventListener("DOMContentLoaded",(function(){let e=document.getElementById("blogForm"),t=document.getElementById("editLoader");e.addEventListener("htmx:configRequest",(function(){t&&(t.style.display="block")})),document.body.addEventListener("htmx:afterRequest",(function(e){let n=e.detail.xhr;if(t&&(t.style.display="none"),200===n.status&&n.responseText.trim()){let e;try{e=JSON.parse(n.responseText)}catch(t){e={}}notifier.show("Success!","Blog Updated successfully.","success","",2e3),e.REDIRECTURL&&setTimeout((()=>{window.location.href=e.REDIRECTURL}),2e3)}else notifier.show("Error!","Something went wrong.","danger","",5e3)}))})); \ No newline at end of file +document.addEventListener("DOMContentLoaded",(function(){const e=new EasyMDE({element:document.getElementById("editor"),spellChecker:!1,status:!1,autofocus:!0,toolbar:["bold","italic","heading","|","quote","unordered-list","ordered-list","|","link","image","code","|","preview","side-by-side","fullscreen","|","guide"],uploadImage:!0,imageUploadEndpoint:"/blog/upload",imageMaxSize:3145728,imageAccept:"image/png, image/jpeg, image/gif, image/webp",imageTexts:{sbInit:"Upload an image",sbOnDragEnter:"Drop image here",sbOnDrop:"Uploading...",sbProgress:"Uploading...",sbOnUploaded:"Uploaded!",sizeUnits:"b,Kb,Mb"},previewRender:function(e){return marked.parse(e)},theme:"github-light",sideBySideFullscreen:!1,maxHeight:"500px",minHeight:"500px",placeholder:"Write your blog post here...",shortcuts:{toggleSideBySide:"Ctrl-Alt-P",toggleFullScreen:"Ctrl-Alt-F",togglePreview:"Ctrl-Alt-V",toggleBold:"Ctrl-B",toggleItalic:"Ctrl-I",drawLink:"Ctrl-K",drawImage:"Ctrl-G",drawTable:"Ctrl-T",toggleHeadingSmaller:"Ctrl-H",toggleHeadingBigger:"Ctrl-Shift-H",toggleUnorderedList:"Ctrl-L",toggleOrderedList:"Ctrl-Alt-L",toggleCodeBlock:"Ctrl-Alt-C",toggleBlockquote:"Ctrl-Q",togglePreview:"Ctrl-P"}});function t(){const t=e.value();document.getElementById("content").value=t}t(),e.codemirror.on("change",(function(){const t=e.value();document.getElementById("content").value=t})),document.addEventListener("htmx:configRequest",(function(e){"blogForm"===e.detail.elt.id&&(t(),e.detail.parameters.content=document.getElementById("content").value)})),document.getElementById("blogForm").addEventListener("htmx:beforeRequest",(function(e){t()})),document.getElementById("saveDraftBtn").addEventListener("click",(function(){e&&e.codemirror.save(),t(),document.getElementById("isDraft").value="1",document.getElementById("blogForm").requestSubmit()})),document.getElementById("blogForm").addEventListener("submit",(function(n){t();let i=document.getElementById("content").value;var o=!0;const l=document.getElementById("title"),r=document.getElementById("categoryId"),d=document.getElementById("posttypeId"),a=document.getElementById("postTags"),s=document.getElementById("editor"),c=document.getElementById("tagInput"),u="1"===document.getElementById("isDraft").value;[l,r,d,c].forEach((e=>e.classList.remove("is-invalid"))),s.classList.remove("border-danger");const m=document.getElementById("titleExists");if(m&&"1"!==m.value&&""!==l.value.trim()){n.preventDefault();const t=document.getElementById("id").value||"",i=new FormData;i.append("title",l.value.trim()),t&&i.append("id",t);var g=document.querySelector('input[name="authenticityToken"]');return g&&i.append("authenticityToken",g.value),fetch("/blog/check-title",{method:"POST",body:i}).then((e=>e.text())).then((t=>{document.getElementById("title-message").innerHTML=t;const n=document.getElementById("titleExists");if(n&&"1"===n.value)return l.classList.add("is-invalid"),notifier.show("Error!","A blog post already exist with this title.","","/img/high_priority-48.png",4e3),!1;t(),e.submit()})),!1}return m&&"1"===m.value&&(o=!1,l.classList.add("is-invalid")),o?(u||(""===l.value.trim()&&(o=!1,l.classList.add("is-invalid")),""===r.value.trim()&&(o=!1,r.classList.add("is-invalid")),""===d.value.trim()&&(o=!1,d.classList.add("is-invalid")),""===a.value.trim()&&(o=!1,c.classList.add("is-invalid"),c.classList.remove("border-0")),""===i.trim()&&(o=!1,s.classList.add("border-danger"))),o?void t():(n.preventDefault(),notifier.show("Error!","Please fill out all required fields.","","/img/high_priority-48.png",4e3),!1)):(n.preventDefault(),n.stopPropagation(),notifier.show("Error!","A blog post already exist with this title.","","/img/high_priority-48.png",4e3),!1)})),document.addEventListener("htmx:afterSwap",(function(e){"categoryId"===e.target.id&&$("#categoryId").select2({placeholder:"Select Categories",theme:"bootstrap-5",width:$(this).data("width")?$(this).data("width"):($(this).hasClass("w-100"),"100%"),maximumSelectionLength:5})})),$("#categoryId").select2({placeholder:"Select Categories",theme:"bootstrap-5",width:$(this).data("width")?$(this).data("width"):($(this).hasClass("w-100"),"100%"),maximumSelectionLength:5});const n=document.getElementById("tagContainer"),i=document.getElementById("tagInput"),o=document.getElementById("postTags");let l=[];function r(e){const t=document.createElement("span");t.classList.add("tag","cursor-pointer"),t.innerHTML=`${e} `,t.querySelector(".remove-tag").addEventListener("click",(function(){!function(e){l=l.filter((t=>t!==e)),a(),d()}(e)})),n.insertBefore(t,i)}function d(){n.querySelectorAll(".tag").forEach((e=>e.remove())),l.forEach((e=>r(e)))}function a(){o.value=l.join(",")}o.value&&(l=o.value.split(","),d()),i.addEventListener("keydown",(function(e){if((","===e.key||"Enter"===e.key)&&l.length<5){e.preventDefault();let t=i.value.trim().replace(/,/g,"");""===t||l.includes(t)||(l.push(t),r(t),a(),i.value="")}else l.length>=5&&e.preventDefault()}));const s=document.getElementById("title");s&&s.addEventListener("blur",(function(){const e=this.value.trim();if(e){const n=document.getElementById("id").value||"",i=new FormData;i.append("title",e),n&&i.append("id",n);var t=document.querySelector('input[name="authenticityToken"]');t&&i.append("authenticityToken",t.value),fetch("/blog/check-title",{method:"POST",body:i}).then((e=>{if(!e.ok)throw new Error("Server error: "+e.status);return e.text()})).then((e=>{document.getElementById("title-message").innerHTML=e;const t=document.getElementById("titleExists");t&&"1"===t.value?s.classList.add("is-invalid"):s.classList.remove("is-invalid")})).catch((e=>{console.error("Error checking title:",e),document.getElementById("title-message").innerHTML='Error checking title availability'}))}else document.getElementById("title-message").innerHTML=""})),document.body.addEventListener("htmx:afterRequest",(function(e){const t=e.detail.xhr;500===t.status&&t.responseURL.includes("/blog/store")&¬ifier.show("Error","Something went wrong!","danger","",5e3),200===t.status&&t.responseURL.includes("/blog/store")&&(notifier.show("Success",t.responseText,"success","",5e3),setTimeout((function(){window.location.href="/blog"}),4e3))}))})),document.addEventListener("DOMContentLoaded",(function(){let e=document.getElementById("blogForm"),t=document.getElementById("editLoader");e.addEventListener("htmx:configRequest",(function(){t&&(t.style.display="block")})),document.body.addEventListener("htmx:afterRequest",(function(e){let n=e.detail.xhr;if(t&&(t.style.display="none"),200===n.status&&n.responseText.trim()){let e;try{e=JSON.parse(n.responseText)}catch(t){e={}}notifier.show("Success!","Blog Updated successfully.","success","",2e3),e.REDIRECTURL&&setTimeout((()=>{window.location.href=e.REDIRECTURL}),2e3)}else notifier.show("Error!","Something went wrong.","danger","",5e3)}))})); \ No newline at end of file From cd0aa0459b46684cd1db9312502e93dd06c35b50 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen <115699497+zainforbjs@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:52:12 +0500 Subject: [PATCH 2/2] Update BlogController.cfc --- app/controllers/web/BlogController.cfc | 2563 ++++++++++++------------ 1 file changed, 1303 insertions(+), 1260 deletions(-) diff --git a/app/controllers/web/BlogController.cfc b/app/controllers/web/BlogController.cfc index 519a67a..d171461 100644 --- a/app/controllers/web/BlogController.cfc +++ b/app/controllers/web/BlogController.cfc @@ -1,1273 +1,1316 @@ // Frontend blog page component extends="app.Controllers.Controller" { - // Configuration function - function config() { - super.config(); - verifies(except="index,create,store,show,update,destroy,loadCategories,loadStatuses,loadPostTypes,Categories,blogs,comment,feed,error,checkTitle,edit,update,AuthorProfileBlogs,blogSearch,commentsFeed,unpublish", params="key", paramsTypes="integer", handler="index"); - filters(through="restrictAccess", only="create,store,comment,edit,update"); - usesLayout("/layout"); - } - - // Function to list all blogs - public void function index() { - isEditor = hasEditorAccess(); - } - - public void function blogs() { - - // Initialize default values and sanitize inputs - filterType = sanitizeParam(structKeyExists(params, "filterType") ? params.filterType : "", ""); - filterValue = sanitizeParam(structKeyExists(params, "filterValue") ? params.filterValue : "", ""); - page = structKeyExists(params, "page") ? max(1, val(sanitizeParam(params.page, 1))) : 1; - perPage = structKeyExists(params, "perPage") ? max(1, min(24, val(sanitizeParam(params.perPage, 6)))) : 6; - isInfiniteScroll = structKeyExists(params, "infiniteScroll") ? params.infiniteScroll : false; - userId = GetSignedInUserId(); - - try { - var result = getBlogData(filterType, filterValue, page, perPage, isInfiniteScroll); - - if (result.query.recordCount == 0) { - // Show fallback blogs - var fallback = getBlogData("", "", 1, perPage, isInfiniteScroll); - blogs = fallback.query; - isFallback = true; - } else { - blogs = result.query; - isFallback = false; - } - // Set template variables - hasMore = result.hasMore; - totalCount = result.totalCount; - - // Set author info if filtering by author - if (structKeyExists(result, "author")) { - author = result.author; - } - - renderPartial(partial="partials/blogList"); - - } catch (any e) { - handleBlogError(e, userId, page, perPage, isInfiniteScroll); - } - } - - // Helper function to sanitize parameters - private string function sanitizeParam(param, defaultValue) { - if (!structKeyExists(arguments, "param") || !len(trim(arguments.param))) { - return arguments.defaultValue; - } - return trim(arguments.param); - } - - // Main data retrieval logic - private struct function getBlogData(filterType, filterValue, page, perPage, isInfiniteScroll) { - var result = {}; - - // Handle special cases first - if (len(arguments.filterType) && len(arguments.filterValue)) { - // Normalize filter value (convert hyphens to dots) - arguments.filterValue = replace(arguments.filterValue, "-", ".", "all"); - - // Handle date archive filtering (year/month) - if (isNumeric(arguments.filterType) && isNumeric(arguments.filterValue)) { - return getBlogsByMonthYear(arguments.filterType, arguments.filterValue, arguments.page, arguments.perPage, arguments.isInfiniteScroll); - } - - // Handle content filtering - switch (lcase(arguments.filterType)) { - case "category": - return getBlogsByCategory(arguments.filterValue, arguments.page, arguments.perPage, arguments.isInfiniteScroll); - - case "tag": - return getAllByTag(arguments.filterValue, arguments.page, arguments.perPage, arguments.isInfiniteScroll); - - case "author": - return getBlogsWithAuthorData(arguments.filterValue, arguments.page, arguments.perPage, arguments.isInfiniteScroll); - - default: - // Invalid filter type, fallback to all blogs - logInvalidFilter(arguments.filterType, arguments.filterValue); - return getAllBlogs(arguments.page, arguments.perPage, arguments.isInfiniteScroll); - } - } - - // Default: return all blogs - return getAllBlogs(arguments.page, arguments.perPage, arguments.isInfiniteScroll); - } - - // Optimized author blog retrieval with author data - private struct function getBlogsWithAuthorData(authorIdentifier, page, perPage, isInfiniteScroll) { - var authorId = getBlogAuthorId(arguments.authorIdentifier); - var result = getBlogsByAuthor(authorId, arguments.page, arguments.perPage, arguments.isInfiniteScroll); - - // Get author statistics in a single optimized query if possible - var authorStats = getAuthorStatistics(authorId); - - result.author = { - "id": authorId, - "totalposts": authorStats.totalPosts, - "totalcomments": authorStats.totalComments - }; - - return result; - } - - // author statistics retrieval - private struct function getAuthorStatistics(authorId) { - // Try to get both stats in one query if your database supports it - try { - return { - "totalPosts": model("Blog").count(where="createdBy = #arguments.authorId#"), - "totalComments": model("Comment").count(where="authorId = #arguments.authorId#") - }; - } catch (any e) { - model("Log").log( - category = "wheels.blog", - level = "ERROR", - message = "Error retrieving author statistics", - details = { - "error_message": e.message, - "author_id": arguments.authorId, - "ip_address": cgi.REMOTE_ADDR - }, - userId = GetSignedInUserId() - ); - return { - "totalPosts": 0, - "totalComments": 0 - }; - } - } - - // Log invalid filter attempts - private void function logInvalidFilter(filterType, filterValue) { - model("Log").log( - category = "wheels.blog", - level = "WARN", - message = "Invalid filter type attempted", - details = { - "invalid_filter_type": arguments.filterType, - "filter_value": arguments.filterValue, - "ip_address": cgi.REMOTE_ADDR - }, - userId = GetSignedInUserId() - ); - } - - // Centralized error handling - private void function handleBlogError(error, userId, page, perPage, isInfiniteScroll) { - // Log the error - model("Log").log( - category = "wheels.blog", - level = "ERROR", - message = "Error in blogs(): #arguments.error.message#", - details = { - "error_message": arguments.error.message, - "error_detail": arguments.error.detail ?: "", - "error_type": arguments.error.type ?: "", - "stack_trace": arguments.error.stackTrace ?: "", - "ip_address": cgi.REMOTE_ADDR, - "timestamp": now() - }, - userId = arguments.userId - ); - - // Provide fallback data - try { - var fallbackResult = getAllBlogs(1, arguments.perPage, arguments.isInfiniteScroll); - blogs = fallbackResult.query; - hasMore = fallbackResult.hasMore; - totalCount = fallbackResult.totalCount; - - // Set error flag for template - hasError = true; - errorMessage = "We're experiencing some technical difficulties. Showing recent posts instead."; - - } catch (any fallbackError) { - // If even fallback fails, create empty result - blogs = queryNew("id,title,slug,createdby,postDate,fullName,username,profilePicture", "integer,varchar,varchar,integer,date,varchar,varchar,varchar"); - hasMore = false; - totalCount = 0; - hasError = true; - errorMessage = "Unable to load blog posts at this time. Please try again later."; - } - - renderPartial(partial="partials/blogList"); - } - - // Function to edit a blog post - public void function edit() { - try { - // Check if user is logged in - if (!hasEditorAccess()) { - redirectTo(route="blog"); - return; - } - - // Get the blog post - blog = model("Blog").findByKey(key=params.id, include="User,PostStatus"); - - // Check if blog exists - if (!isObject(blog)) { - throw("Blog not found", "BlogNotFound"); - } - - // Check if user has permission to edit this post - if (!hasEditorAccess() && blog.createdBy != session.userID) { - throw("You don't have permission to edit this post", "UnauthorizedAccess"); - } - - // Get categories and tags for the form - categories = model("Category").findAll(order="name ASC"); - postTypes = model("PostType").findAll(order="name ASC"); - var blogCategories = model("BlogCategory").findAll(where="blogId = #blog.id#"); - var blogTags = model("BlogTag").findAll(where="blogId = #blog.id#", include="Tag"); - - // Prepare data for the view - var selectedCategories = []; - for (var cat in blogCategories) { - arrayAppend(selectedCategories, val(cat.categoryId)); - } - - var selectedTags = []; - for (var blogTag in blogTags) { - arrayAppend(selectedTags, blogTag.name); - } - - // Set view variables - blog.categories = selectedCategories; - blog.tags = arrayToList(selectedTags, ","); - isEdit = true; - - renderView(template="create"); - - } catch (any e) { - // Log the error - model("Log").log( - category = "wheels.blog", - level = "ERROR", - message = "Error editing blog post: #e.message#", - details = { - "error": e, - "blog_id": structKeyExists(params, "key") ? params.key : "", - "user_id": GetSignedInUserId(), - "ip_address": cgi.REMOTE_ADDR - } - ); - - // Redirect with error message - redirectTo(route="blog"); - } - } - - private function getBlogsByAuthor(required authorId, numeric page=1, numeric perPage=6, boolean isInfiniteScroll=false) { - var result = { - query = model("Blog").findAll( - where="blog_posts.statusId <> 1 AND blog_posts.status = 'Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#now()#' AND blog_posts.createdBy = #arguments.authorId#", - include="User", - order="COALESCE(post_created_date, blog_posts.createdat) DESC", - page = arguments.page, - perPage = arguments.perPage - ), - hasMore = false, - totalCount = 0 - }; - - result.totalCount = model("Blog").count( - where="blog_posts.statusId <> 1 AND blog_posts.status = 'Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#now()#' AND blog_posts.createdBy = #arguments.authorId#" - ); - result.hasMore = (page * perPage) < result.totalCount; - - if (result.query.recordCount == 0) { - redirectTo(route="blog"); - } - - return result; - } - - private function getBlogAuthorId(required authorParam) { + // Configuration function + function config() { + super.config(); + verifies( + except = "index,create,store,show,update,destroy,loadCategories,loadStatuses,loadPostTypes,Categories,blogs,comment,feed,error,checkTitle,edit,update,AuthorProfileBlogs,blogSearch,commentsFeed,unpublish,myPosts", + params = "key", + paramsTypes = "integer", + handler = "index" + ); + filters(through = "restrictAccess", only = "create,store,comment,edit,update,myPosts"); + usesLayout("/layout"); + } + + // Function to list all blogs + public void function index() { + isEditor = hasEditorAccess(); + } + + public void function myPosts() { + var userId = getSignedInUserId(); + var validStatuses = "all,draft,published,pending"; + statusFilter = (StructKeyExists(params, "status") && ListFindNoCase(validStatuses, params.status)) + ? params.status + : "all"; + + // Expose statuses to view (controller methods aren't available in views) + statuses = blogStatuses(); + + var statusCountsQuery = model("Blog").findAll( + select = "statusId, COUNT(*) as cnt", + where = "createdBy = #userId# AND blog_posts.statusId <> #statuses.TRASH#", + group = "statusId", + returnAs = "query" + ); + + statusCounts = {"all" = 0, "draft" = 0, "published" = 0, "pending" = 0}; + for (var row in statusCountsQuery) { + statusCounts.all += row.cnt; + if (row.statusId == statuses.DRAFT) statusCounts.draft = row.cnt; + else if (row.statusId == statuses.POSTED) statusCounts.published = row.cnt; + else if (row.statusId == statuses.PENDING_REVIEW) statusCounts.pending = row.cnt; + } + + var where = "createdBy = #userId# AND blog_posts.statusId <> #statuses.TRASH#"; + if (statusFilter == "draft") { + where &= " AND blog_posts.statusId = #statuses.DRAFT#"; + } else if (statusFilter == "published") { + where &= " AND blog_posts.statusId = #statuses.POSTED#"; + } else if (statusFilter == "pending") { + where &= " AND blog_posts.statusId = #statuses.PENDING_REVIEW#"; + } + + myBlogs = model("Blog").findAll( + select = "id, title, slug, statusId, createdAt, publishedAt, updatedAt", + where = where, + order = "updatedAt DESC" + ); + } + + public void function blogs() { + // Initialize default values and sanitize inputs + filterType = sanitizeParam(StructKeyExists(params, "filterType") ? params.filterType : "", ""); + filterValue = sanitizeParam(StructKeyExists(params, "filterValue") ? params.filterValue : "", ""); + page = StructKeyExists(params, "page") ? Max(1, Val(sanitizeParam(params.page, 1))) : 1; + perPage = StructKeyExists(params, "perPage") ? Max(1, Min(24, Val(sanitizeParam(params.perPage, 6)))) : 6; + isInfiniteScroll = StructKeyExists(params, "infiniteScroll") ? params.infiniteScroll : false; + userId = getSignedInUserId(); + + try { + var result = getBlogData(filterType, filterValue, page, perPage, isInfiniteScroll); + + if (result.query.recordCount == 0) { + // Show fallback blogs + var fallback = getBlogData("", "", 1, perPage, isInfiniteScroll); + blogs = fallback.query; + isFallback = true; + } else { + blogs = result.query; + isFallback = false; + } + // Set template variables + hasMore = result.hasMore; + totalCount = result.totalCount; + + // Set author info if filtering by author + if (StructKeyExists(result, "author")) { + author = result.author; + } + + renderPartial(partial = "partials/blogList"); + } catch (any e) { + handleBlogError(e, userId, page, perPage, isInfiniteScroll); + } + } + + // Helper function to sanitize parameters + private string function sanitizeParam(param, defaultValue) { + if (!StructKeyExists(arguments, "param") || !Len(Trim(arguments.param))) { + return arguments.defaultValue; + } + return Trim(arguments.param); + } + + // Main data retrieval logic + private struct function getBlogData(filterType, filterValue, page, perPage, isInfiniteScroll) { + var result = {}; + + // Handle special cases first + if (Len(arguments.filterType) && Len(arguments.filterValue)) { + // Normalize filter value (convert hyphens to dots) + arguments.filterValue = Replace(arguments.filterValue, "-", ".", "all"); + + // Handle date archive filtering (year/month) + if (IsNumeric(arguments.filterType) && IsNumeric(arguments.filterValue)) { + return getBlogsByMonthYear( + arguments.filterType, + arguments.filterValue, + arguments.page, + arguments.perPage, + arguments.isInfiniteScroll + ); + } + + // Handle content filtering + switch (LCase(arguments.filterType)) { + case "category": + return getBlogsByCategory( + arguments.filterValue, + arguments.page, + arguments.perPage, + arguments.isInfiniteScroll + ); + case "tag": + return getAllByTag(arguments.filterValue, arguments.page, arguments.perPage, arguments.isInfiniteScroll); + case "author": + return getBlogsWithAuthorData( + arguments.filterValue, + arguments.page, + arguments.perPage, + arguments.isInfiniteScroll + ); + default: + // Invalid filter type, fallback to all blogs + logInvalidFilter(arguments.filterType, arguments.filterValue); + return getAllBlogs(arguments.page, arguments.perPage, arguments.isInfiniteScroll); + } + } + + // Default: return all blogs + return getAllBlogs(arguments.page, arguments.perPage, arguments.isInfiniteScroll); + } + + // Optimized author blog retrieval with author data + private struct function getBlogsWithAuthorData(authorIdentifier, page, perPage, isInfiniteScroll) { + var authorId = getBlogAuthorId(arguments.authorIdentifier); + var result = getBlogsByAuthor(authorId, arguments.page, arguments.perPage, arguments.isInfiniteScroll); + + // Get author statistics in a single optimized query if possible + var authorStats = getAuthorStatistics(authorId); + + result.author = { + "id" = authorId, + "totalposts" = authorStats.totalPosts, + "totalcomments" = authorStats.totalComments + }; + + return result; + } + + // author statistics retrieval + private struct function getAuthorStatistics(authorId) { + // Try to get both stats in one query if your database supports it + try { + return { + "totalPosts" = model("Blog").count(where = "createdBy = #arguments.authorId#"), + "totalComments" = model("Comment").count(where = "authorId = #arguments.authorId#") + }; + } catch (any e) { + model("Log").log( + category = "wheels.blog", + level = "ERROR", + message = "Error retrieving author statistics", + details = {"error_message" = e.message, "author_id" = arguments.authorId, "ip_address" = cgi.REMOTE_ADDR}, + userId = getSignedInUserId() + ); + return {"totalPosts" = 0, "totalComments" = 0}; + } + } + + // Log invalid filter attempts + private void function logInvalidFilter(filterType, filterValue) { + model("Log").log( + category = "wheels.blog", + level = "WARN", + message = "Invalid filter type attempted", + details = { + "invalid_filter_type" = arguments.filterType, + "filter_value" = arguments.filterValue, + "ip_address" = cgi.REMOTE_ADDR + }, + userId = getSignedInUserId() + ); + } + + // Centralized error handling + private void function handleBlogError(error, userId, page, perPage, isInfiniteScroll) { + // Log the error + model("Log").log( + category = "wheels.blog", + level = "ERROR", + message = "Error in blogs(): #arguments.error.message#", + details = { + "error_message" = arguments.error.message, + "error_detail" = arguments.error.detail ?: "", + "error_type" = arguments.error.type ?: "", + "stack_trace" = arguments.error.stackTrace ?: "", + "ip_address" = cgi.REMOTE_ADDR, + "timestamp" = Now() + }, + userId = arguments.userId + ); + + // Provide fallback data + try { + var fallbackResult = getAllBlogs(1, arguments.perPage, arguments.isInfiniteScroll); + blogs = fallbackResult.query; + hasMore = fallbackResult.hasMore; + totalCount = fallbackResult.totalCount; + + // Set error flag for template + hasError = true; + errorMessage = "We're experiencing some technical difficulties. Showing recent posts instead."; + } catch (any fallbackError) { + // If even fallback fails, create empty result + blogs = QueryNew( + "id,title,slug,createdby,publishedAt,fullName,username,profilePicture", + "integer,varchar,varchar,integer,date,varchar,varchar,varchar" + ); + hasMore = false; + totalCount = 0; + hasError = true; + errorMessage = "Unable to load blog posts at this time. Please try again later."; + } + + renderPartial(partial = "partials/blogList"); + } + + // Function to edit a blog post + public void function edit() { + try { + // Check if user is logged in + if (!hasEditorAccess()) { + redirectTo(route = "blog"); + return; + } + + // Get the blog post + blog = model("Blog").findByKey(key = params.id, include = "User,PostStatus"); + + // Check if blog exists + if (!IsObject(blog)) { + Throw("Blog not found", "BlogNotFound"); + } + + // Check if user has permission to edit this post + if (!hasEditorAccess() && blog.createdBy != session.userID) { + Throw("You don't have permission to edit this post", "UnauthorizedAccess"); + } + + // Get categories and tags for the form + categories = model("Category").findAll(order = "name ASC"); + postTypes = model("PostType").findAll(order = "name ASC"); + var blogCategories = model("BlogCategory").findAll(where = "blogId = #blog.id#"); + var blogTags = model("BlogTag").findAll(where = "blogId = #blog.id#", include = "Tag"); + + // Prepare data for the view + var selectedCategories = []; + for (var cat in blogCategories) { + ArrayAppend(selectedCategories, Val(cat.categoryId)); + } + + var selectedTags = []; + for (var blogTag in blogTags) { + ArrayAppend(selectedTags, blogTag.name); + } + + // Set view variables + blog.categories = selectedCategories; + blog.tags = ArrayToList(selectedTags, ","); + isEdit = true; + + renderView(template = "create"); + } catch (any e) { + // Log the error + model("Log").log( + category = "wheels.blog", + level = "ERROR", + message = "Error editing blog post: #e.message#", + details = { + "error" = e, + "blog_id" = StructKeyExists(params, "key") ? params.key : "", + "user_id" = getSignedInUserId(), + "ip_address" = cgi.REMOTE_ADDR + } + ); + + // Redirect with error message + redirectTo(route = "blog"); + } + } + + private function getBlogsByAuthor( + required authorId, + numeric page = 1, + numeric perPage = 6, + boolean isInfiniteScroll = false + ) { + var result = { + query = model("Blog").findAll( + where = "blog_posts.statusId <> #blogStatuses().DRAFT# AND blog_posts.status = 'Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#Now()#' AND blog_posts.createdBy = #arguments.authorId#", + include = "User", + order = "publishedAt DESC", + page = arguments.page, + perPage = arguments.perPage + ), + hasMore = false, + totalCount = 0 + }; + + result.totalCount = model("Blog").count( + where = "blog_posts.statusId <> #blogStatuses().DRAFT# AND blog_posts.status = 'Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#Now()#' AND blog_posts.createdBy = #arguments.authorId#" + ); + result.hasMore = (page * perPage) < result.totalCount; + + if (result.query.recordCount == 0) { + redirectTo(route = "blog"); + } + + return result; + } + + private function getBlogAuthorId(required authorParam) { // Check if authorParam is numeric (ID) or string (username) - if (isNumeric(authorParam)) { - return val(authorParam); + if (IsNumeric(authorParam)) { + return Val(authorParam); } else { // Lookup user by username - var user = model("user").findOne(where="username = '#arguments.authorParam#'"); - if (isObject(user)) { + var user = model("user").findOne(where = "username = '#arguments.authorParam#'"); + if (IsObject(user)) { return user.id; } else { // User not found, redirect - redirectTo(route="blog"); + redirectTo(route = "blog"); return false; } } - } - - function blogSearch() { - param name="params.searchTerm" default=""; - param name="params.page" default="1"; - param name="params.perPage" default="6"; - param name="params.infiniteScroll" default="false"; - param name="params.isSearched" default="false"; - - searchTerm = params.searchTerm; - page = params.page; - perPage = params.perPage; - isInfiniteScroll = params.infiniteScroll; - - if (len(trim(searchTerm))) { - var searchPattern = "%#searchTerm#%"; - var query = model("blog").findAll( - where="blog_posts.status ='Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#now()#' AND (blog_posts.slug LIKE '#searchPattern#' OR blog_posts.title LIKE '#searchPattern#' OR blog_posts.content LIKE '#searchPattern#' OR fullname LIKE '#searchPattern#' OR email LIKE '#searchPattern#')", - include="User, PostStatus, PostType", - order = "COALESCE(post_created_date, blog_posts.createdat) DESC", - page = page, - perPage = perPage - ); - - if (isInfiniteScroll) { - totalCount = model("blog").count( - include="User, PostStatus, PostType", - where="blog_posts.status ='Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#now()#' AND (blog_posts.slug LIKE '#searchPattern#' OR blog_posts.title LIKE '#searchPattern#' OR blog_posts.content LIKE '#searchPattern#' OR fullname LIKE '#searchPattern#' OR email LIKE '#searchPattern#')" - ); - hasMore = (page * perPage) < totalCount; - isSearched = true; - - query.addColumn("hasMore", "boolean"); - query.addColumn("totalCount", "integer"); - } - - blogs = query; - if(blogs.recordCount == 0) { - isFallBack = true; - result = getAllBlogs(page, perPage, isInfiniteScroll); - blogs = result.query; - } - renderPartial(partial="partials/blogList"); - } else { - // return all publish blogs with pagination - result = getAllBlogs(page, perPage, isInfiniteScroll); - blogs = result.query; - hasMore = result.hasMore; - totalCount = result.totalCount; - renderPartial(partial="partials/blogList"); - } - } - // Function to load categories for the blog list - function categories() { - categorylist = model("Category").findAll(where="isActive = 1", cache=60); - renderPartial(partial="partials/categorylist"); - } - - // Function to show the create blog form - function create() { - if (!hasEditorAccess()) { - redirectTo(route="blog"); - return; - } - categories = model("Category").findAll(order="name ASC"); - postTypes = model("PostType").findAll(order="name ASC"); - model("Log").log( - category = "wheels.blog", - level = "INFO", - message = "Blog creation form accessed", - details = { - "ip_address": cgi.REMOTE_ADDR - }, - userId = GetSignedInUserId() - ); - saveRedirectUrl(cgi.script_name & "?" & cgi.query_string); - isEdit = false; - } - - // Function to store a new blog - public void function store() { - // Get request parameters - var blogModel = model("Blog"); - try { - // Check if user has editor access - if (!hasEditorAccess()) { - throw("You don't have permission to create a blog post", "UnauthorizedAccess"); - } - model("Log").log( - category = "wheels.blog", - level = "INFO", - message = "New blog post creation attempted", - details = { - "title": params.title, - "ip_address": cgi.REMOTE_ADDR - }, - userId = GetSignedInUserId() - ); - - params.coverImagePath = ""; - var uploadPath = expandPath("/files/"); // Define the upload directory - - if (!directoryExists(uploadPath)) { - directoryCreate(uploadPath); - } - - // Handle file upload - if (structKeyExists(params, "attachment") && isDefined("params.attachment")) { - var uploadedFile = fileUpload(uploadPath, "attachment"); - - if (!structIsEmpty(uploadedFile) && structKeyExists(uploadedFile, "serverFile")) { - var originalFileName = uploadedFile.serverFile; - var fileExtension = lcase(listLast(originalFileName, ".")); - var allowedExtensions = "jpg,jpeg,png,gif,webp,pdf,doc,docx"; - var allowedContentTypes = "image/jpeg,image/png,image/gif,image/webp,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"; - var maxFileSizeBytes = 10 * 1024 * 1024; // 10MB - - // Validate file extension - if (!listFindNoCase(allowedExtensions, fileExtension)) { - fileDelete(uploadedFile.serverDirectory & "/" & originalFileName); - throw("Invalid file type. Allowed types: #allowedExtensions#", "InvalidFileType"); - } - - // Validate MIME content type - if (structKeyExists(uploadedFile, "contentType") && structKeyExists(uploadedFile, "contentSubType")) { - var detectedContentType = uploadedFile.contentType & "/" & uploadedFile.contentSubType; - if (!listFindNoCase(allowedContentTypes, detectedContentType)) { - fileDelete(uploadedFile.serverDirectory & "/" & originalFileName); - throw("Invalid file content type. The uploaded file does not match allowed types.", "InvalidContentType"); - } - } - - // Validate file size - if (uploadedFile.fileSize > maxFileSizeBytes) { - fileDelete(uploadedFile.serverDirectory & "/" & originalFileName); - throw("File size exceeds the 10MB limit.", "FileTooLarge"); - } - - var uniqueFileName = createUUID() & "." & fileExtension; - - // Rename file to unique name - var newFilePath = uploadPath & "/" & uniqueFileName; - fileMove(uploadedFile.serverDirectory & "/" & originalFileName, newFilePath); - - // Store the relative file path - params.coverImagePath = "/files/" & uniqueFileName; - } - } - - response = saveBlog(params); - saveTags(params, response.blogId); - saveCategories(params, response.blogId); - - model("Log").log( - category = "wheels.blog", - level = "INFO", - message = "New blog post created successfully", - details = { - "blog_id": response.blogId, - "title": params.title, - "ip_address": cgi.REMOTE_ADDR - }, - userId = GetSignedInUserId() - ); - - renderText(response.message); - } catch (any e) { - model("Log").log( - category = "wheels.blog", - level = "ERROR", - message = "Failed to save blog post", - details = { - "error_message": e.message, - "error_detail": e.detail, - "error_type": e.type, - "title": params.title, - "ip_address": cgi.REMOTE_ADDR - }, - userId = GetSignedInUserId() - ); - // Handle error - redirectTo(action="error", errorMessage="Failed to save blog post."); - } - } - - // Function to show a specific blog - function show() { - try { - blogModel = model("Blog"); - - // Get the blog by its slug - blog = getBlogBySlug(params.slug); - - // If no blog is found, throw an error to be caught - if (!structKeyExists(blog, "id")) { - throw("Blog not found"); - } - - // Process embeds in content - blog.content = embedAndAutoLink(blog.content); - - // Set blog post data for layout meta tags (avoids DB query in view) - request.blogPostForMeta = blog; - - // Get other necessary data - tags = getTagsByBlogid(blog.id); - categories = getCategoriesByBlogid(blog.id); - attachments = getAttachmentsByBlogid(blog.id); - comments = getAllCommentsByBlogid(blog.id); - allBlogComments = model("Comment").findAll(include="User", where="isPublished = 1 AND blogid = #blog.id#", order="commentParentId, createdAt", cache=5); - - // Track reading history - if (StructKeyExists(session, "userID")) { - history = model("ReadingHistory").findOne( - where="userId = #session.userID# AND blogId = #blog.id#", - includeSoftDeletes=true - ); - if (IsObject(history)) { - if (history.deletedAt != "") { - history.update(lastReadAt=Now(), deletedAt=""); - } else { - history.update(lastReadAt=Now()); - } - } else { - history = model("ReadingHistory").create( - userId=session.userID, - blogId=blog.id, - lastReadAt=Now() - ); - } - - // Check if bookmarked - isBookmarked = model("Bookmark").exists( - where="userId = #session.userID# AND blogId = #blog.id#" - ); - } else { - isBookmarked = false; - } - - } catch (any e) { - model("Log").log( - category = "wheels.blog", - level = "ERROR", - message = "Blog post not found", - details = { - "error_message": e.message, - "error_detail": e.detail, - "slug": params.slug, - "ip_address": cgi.REMOTE_ADDR - }, - userId = GetSignedInUserId() - ); - redirectTo(action="index"); - return; - } - } - - // Function to update an existing blog - public function update() { - var blogId = params.id; - result = { success: false, message: "", blogId: blogId }; - try { - model("Log").log( - category = "wheels.blog", - level = "INFO", - message = "Blog post update attempted", - details = { - "blog_id": blogId, - "title": params.title, - "ip_address": cgi.REMOTE_ADDR - }, - userId = GetSignedInUserId() - ); - params.isDraft = isNumeric(params.isDraft) ? params.isDraft : 0; - - // Allow title change and check uniqueness - if (structKeyExists(params, "title")) { - var existingBlog = model("Blog").findFirst(where="title = '#params.title#' AND id != #blogId#"); - if (isObject(existingBlog)) { - result.success = false; - result.message = "A blog post with this title already exists."; - model("Log").log( - category = "wheels.blog", - level = "DEBUG", - message = "[UPDATE] Duplicate title found", - details = { "blog_id": blogId, "title": params.title }, - userId = GetSignedInUserId() - ); - renderWith(data=result, hideDebugInformation=true, layout='/responseLayout'); - return; - } - } - - transaction { - result = updateBlog(params, blogId); - - if (!result.success) { - throw(type="BlogUpdateFailed", message=result.message); - } - - deleteBlogTags(blogId); - deleteBlogCategories(blogId); - - if (structKeyExists(params, "postTags") && len(trim(params.postTags))) { - params.tags = params.postTags; - saveTags(params, blogId); - } - - if (structKeyExists(params, "categoryId") && len(trim(params.categoryId))) { - saveCategories(params, blogId); - } - } - - model("Log").log( - category = "wheels.blog", - level = "INFO", - message = "Blog post updated successfully", - details = { - "blog_id": blogId, - "title": params.title, - "ip_address": cgi.REMOTE_ADDR - }, - userId = GetSignedInUserId() - ); - result.success = true; - if (!structKeyExists(result, "message") || !len(result.message)) { - result.message = "Blog post updated successfully."; - } - // Add redirectUrl to show page - result.redirectUrl = urlFor(action="show", slug=params.slug); - } catch (any e) { - model("Log").log( - category = "wheels.blog", - level = "ERROR", - message = "Failed to update blog post", - details = { - "blog_id": blogId, - "error_message": e.message, - "error_detail": e.detail, - "error_type": e.type, - "title": params.title, - "ip_address": cgi.REMOTE_ADDR - }, - userId = GetSignedInUserId() - ); - result.success = false; - result.message = "Failed to update blog post."; - } - renderWith(data=result, hideDebugInformation=true, layout='/responseLayout'); - return; - } - - // function to check title is unique - function checkTitle() { - try { - model("Log").log( - category = "wheels.blog", - level = "DEBUG", - message = "Title uniqueness check", - details = { - "title": form.title, - "id": structKeyExists(form, "id") ? form.id : 0, - "ip_address": cgi.REMOTE_ADDR - }, - userId = GetSignedInUserId() - ); - - if(structKeyExists(form, "title")) { - var whereClause = "title = '#form.title#'"; - - if(structKeyExists(form, "id") && isNumeric(form.id) && form.id > 0) { - whereClause &= " AND id != #form.id#"; - } - - var blogModel = model("Blog").findAll(where=whereClause); - - if(blogModel.recordCount != 0) { - renderText('A blog already exists with this title!'); - } else { - renderText('Title is available'); - } - } - } catch (any e) { - model("Log").log( - category = "wheels.blog", - level = "ERROR", - message = "Error checking title uniqueness", - details = { - "error_message": e.message, - "error_detail": e.detail, - "title": form.title, - "id": structKeyExists(form, "id") ? form.id : 0, - "ip_address": cgi.REMOTE_ADDR - }, - userId = GetSignedInUserId() - ); - // Handle error - renderText('Error checking title. Please try again.'); - } - } - - // Function to delete a blog - function destroy() { - var blogModel = model("Blog"); // Get model instance - try { - model("Log").log( - category = "wheels.blog", - level = "INFO", - message = "Blog post deletion attempted", - details = { - "blog_id": params.id, - "ip_address": cgi.REMOTE_ADDR - }, - userId = GetSignedInUserId() - ); - - var message = deleteBlog(params.id); - - model("Log").log( - category = "wheels.blog", - level = "INFO", - message = "Blog post deleted successfully", - details = { - "blog_id": params.id, - "ip_address": cgi.REMOTE_ADDR - }, - userId = GetSignedInUserId() - ); - - redirectTo(action="index", success="#message#"); - } catch (any e) { - model("Log").log( - category = "wheels.blog", - level = "ERROR", - message = "Failed to delete blog post", - details = { - "error_message": e.message, - "error_detail": e.detail, - "blog_id": params.id, - "ip_address": cgi.REMOTE_ADDR - }, - userId = GetSignedInUserId() - ); - // Handle error - redirectTo(action="index", errorMessage="Failed to delete blog post."); - } - } - - // Function to load categories for the dropdown - function loadCategories() { - categories = model("Category").getAll(); - renderPartial(partial="partials/categories"); - } - - // Function to load statuses for the dropdown - function loadStatuses() { - statuses = model("PostStatus").getAll(); - renderPartial(partial="partials/statuses"); - } - - // Function to load post types for the dropdown - function loadPostTypes() { - postTypes = model("PostType").getAll(); - renderPartial(partial="partials/postTypes"); - } - - function error() { - renderPartial(partial="partials/_error"); - } - - public function feed() { - // Fetch all blogs - blogPosts = model("Blog").findAll( - where="blog_posts.status = 'Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#now()#'", - include="User", - order="postDate DESC", - cache=10 - ); - - // Render the feed view - renderPartial(partial="partials/feed"); - } - - public function commentsFeed() { - // Get recent comments with related blog post - comments = model("Comment").findAll( - where = "isPublished = 1", - include = "Blog", - order = "createdAt DESC", - limit = 20, - returnAs = "structs" - ); - - // Collect unique authorIds - authorIds = []; - for (key in comments) { - comment = comments[key]; - if (isStruct(comment) and structKeyExists(comment, "authorId")) { - if (!arrayContains(authorIds, comment.authorId)) { - arrayAppend(authorIds, comment.authorId); - } - } - } - - // Fetch all related users at once - var authorIdList = arrayToList(authorIds); - authors = model("User").findAll(where="id IN (#authorIdList#)", returnAs="structs"); - - // Map authors by ID for quick lookup - authorMap = {}; - for (key in authors) { - author = authors[key]; - authorMap[author.id] = author; - } - - renderPartial(partial="partials/commentsFeed", locals={ - comments = comments, - authorMap = authorMap - }); - - } - - // Business Logic - - private function getAllBlogs(numeric page=1, numeric perPage=6, boolean isInfiniteScroll=false) { - var result = { - query = model("Blog").findAll( - where="blog_posts.statusId <> 1 AND blog_posts.status = 'Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#now()#'", - include="User", - order="COALESCE(post_created_date, blog_posts.createdat) DESC", - page = arguments.page, - perPage = arguments.perPage, - cache = 5 - ), - hasMore = false, - totalCount = 0 - }; - - result.totalCount = model("Blog").count( - where="blog_posts.statusId <> 1 AND blog_posts.status = 'Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#now()#'", - cache = 5 - ); - result.hasMore = (page * perPage) < result.totalCount; - - return result; - } - - private function getBlogsByMonthYear(required numeric year, required string month, numeric page=1, numeric perPage=6, boolean isInfiniteScroll=false) { - // Create start and end date for the selected month - var startdate = "#year#-#NumberFormat(month, '00')#-01 00:00:00"; - var enddate = "#year#-#NumberFormat(month, '00')#-#DaysInMonth('#year#-#NumberFormat(month, '00')#-01')# 23:59:59"; - - var result = { - query = model("Blog").findAll( - where="blog_posts.post_created_date BETWEEN '#startdate#' AND '#enddate#' AND blog_posts.status='Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#now()#'", - order="postCreatedDate DESC", - include="User", - returnAs="query", - page = arguments.page, - perPage = arguments.perPage - ), - hasMore = false, - totalCount = 0 - }; - - result.totalCount = model("Blog").count( - where="blog_posts.post_created_date BETWEEN '#startdate#' AND '#enddate#' AND blog_posts.status='Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#now()#'" - ); - result.hasMore = (page * perPage) < result.totalCount; - - return result; - } - - // Fetch Blogs by Category - public function getBlogsByCategory(required string categoryName, numeric page=1, numeric perPage=6, boolean isInfiniteScroll=false) { - // Get category ID from name - var category = model("Category").findOne(where="name = '#arguments.categoryName#'"); - if (!isObject(category)) return {query=queryNew(""), hasMore=false, totalCount=0}; - - var blogCategoryQuery = model("BlogCategory") - .findAll(where="categoryId = #category.id#", returnAs="query"); - if (blogCategoryQuery.recordCount == 0) return {query=queryNew(""), hasMore=false, totalCount=0}; - - var blogIds = blogCategoryQuery.columnData("blogId"); - var blogIdList = arrayToList(blogIds); - - var result = { - query = model("Blog").findAll( - where="blog_posts.id IN (#blogIdList#) AND categoryId = #category.id# AND blog_posts.status ='Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#now()#'", - order="createdAt DESC", - include="User,BlogCategory", - returnAs="query", - page = arguments.page, - perPage = arguments.perPage - ), - hasMore = false, - totalCount = 0 - }; - - result.totalCount = model("Blog").count( - where="blog_posts.id IN (#blogIdList#) AND categoryId = #category.id# AND blog_posts.status ='Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#now()#'", - include="User,BlogCategory" - ); - result.hasMore = (page * perPage) < result.totalCount; - - return result; - } - - // Fetch Blogs by Tag - private function getAllByTag(required string tag, numeric page=1, numeric perPage=6, boolean isInfiniteScroll=false) { - // First, find the tag by name - var targetTag = model("Tag").findOne(where="name = '#arguments.tag#'"); - - if (!isObject(targetTag)) { - return {query: queryNew(""), hasMore: false, totalCount: 0}; - } - - // Get all blog_ids associated with this tag - var blogTags = model("BlogTag").findAll(where="tagId = #targetTag.id#", returnAs="query"); - - if (blogTags.recordCount == 0) { - return {query: queryNew(""), hasMore: false, totalCount: 0}; - } - - var blogIds = valueList(blogTags.blogId); - - var result = { - query = model("Blog").findAll( - where="blog_posts.id IN (#blogIds#) AND blog_posts.status ='Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#now()#'", - order="createdAt DESC", - include="User", - returnAs="query", - page = arguments.page, - perPage = arguments.perPage - ), - hasMore = false, - totalCount = 0 - }; - - result.totalCount = model("Blog").count(where="blog_posts.id IN (#blogIds#) AND blog_posts.status ='Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.publishedAt <= '#now()#'"); - result.hasMore = (page * perPage) < result.totalCount; - - return result; - } - - private function getBlogById(required numeric id) { - return model("Blog").findOne( - where="blog_posts.id = #arguments.id#", - include="User, PostStatus" - ); - } - - private function saveBlog(required struct blogData) { - var response = { "message": "", "blogId": 0 }; - - // Generate slug - var slug = rereplace(lcase(blogData.title), "[^a-z0-9]", "-", "all"); // Replace non-alphanumeric with "-" - slug = rereplace(slug, "-+", "-", "all"); - blogData.slug = slug; - - - // Determine status based on draft flag and user role - if (blogData.isdraft eq 1) { - blogData.statusId = 1; // Draft - blogData.status = ""; - blogData.publishedAt = ""; - } else if (isUserAdmin()) { - // Auto-approve and publish for admin users - blogData.statusId = 2; - blogData.status = "Approved"; - if (structKeyExists(blogData, "postCreatedDate") && !isNull(blogData.postCreatedDate) && !isEmpty(blogData.postCreatedDate)) { - blogData.publishedAt = blogData.postCreatedDate; - } else { - blogData.publishedAt = now(); - blogData.postCreatedDate = now(); - } - } else { - blogData.statusId = 2; // Under Review - blogData.status = ""; - blogData.publishedAt = ""; - } - - try { - // Check if the blog ID is greater than 0 (editing an existing post) - if (structKeyExists(blogData, "id") && blogData.id > 0) { - var blog = model("Blog").findByKey(blogData.id); - - if (isObject(blog)) { - // Update the existing blog post - blog.title = blogData.title; - blog.content = blogData.content; - blog.statusId = blogData.statusId; - blog.postTypeId = blogData.postTypeId; - blog.slug = blogData.slug; - blog.updatedAt = now(); - blog.updatedBy = GetSignedInUserId(); - blog.save(); - - response.blogId = blog.id; - response.message = "Blog post updated successfully."; - } else { - response.message = "Blog post not found for editing."; - } - } else { - // Check if a blog post with the same title already exists - var existingBlog = model("Blog").findFirst( - where="title = '#blogData.title#' AND slug = '#blogData.slug#'" - ); - - if (!isObject(existingBlog)) { - // Create a new blog post - var newBlog = model("Blog").new(); - newBlog.title = blogData.title; - newBlog.content = blogData.content; - newBlog.slug = blogData.slug; - newBlog.statusId = blogData.statusId; - newBlog.postTypeId = blogData.postTypeId; - newBlog.coverImagePath = blogData.coverImagePath; - newBlog.createdAt = now(); - newBlog.updatedAt = now(); - newBlog.createdBy = GetSignedInUserId(); - - // Set approval status fields for admin auto-approval - if (structKeyExists(blogData, "status")) { - newBlog.status = blogData.status; - } - if (structKeyExists(blogData, "postCreatedDate") && !isNull(blogData.postCreatedDate) && !isEmpty(blogData.postCreatedDate)) { - newBlog.publishedAt = blogData.postCreatedDate; - newBlog.postCreatedDate = blogData.postCreatedDate; - } else { - newBlog.postCreatedDate = now(); - } - newBlog.save(); - - // Retrieve the generated ID by looking up the just-created blog by slug. - // Wheels' PostgreSQL adapter does not always return the auto-generated - // primary key after INSERT (e.g. when the column uses a BIGINT default - // or trigger-based ID generation rather than SERIAL). - if (!len(trim(newBlog.id))) { - var createdBlog = model("Blog").findOne(where="slug = '#blogData.slug#'"); - if (isObject(createdBlog)) { - response.blogId = createdBlog.id; - } - } else { - response.blogId = newBlog.id; - } - response.message = "Blog post created successfully."; - } else { - response.message = "A blog post with the same title already exists."; - } - } - } catch (any e) { - response.message = "Error: " & e.message; - } - - return response; - } - - // updateBlog() is inherited from Controller.cfc - - private function deleteBlog(required numeric id) { - var message = ""; - - try { - var blog = model("Blog").findByKey(id); - - if (isObject(blog)) { - blog.isDeleted = true; - blog.updatedAt = now(); - blog.updatedBy = GetSignedInUserId(); - blog.save(); - message = "Blog post deleted successfully."; - } else { - message = "Blog post not found for deletion."; - } - } catch (any e) { - // Catch any errors and store the message - message = "Error: " & e.message; - } - - // Return the message - return message; - } - - // Tags - function getAllTags() { - return model("Tag").findAll(); - } - - // saveTags() is inherited from Controller.cfc - // deleteBlogTags() is inherited from Controller.cfc - - // Categories - - function getAllCategories() { - return model("Category").findAll(); - } - - // saveCategories() is inherited from Controller.cfc - // deleteBlogCategories() is inherited from Controller.cfc - - //Attachement - - function getAllAttachments() { - return model("Attachment").findAll(); - } - - function getAllCommentsByBlogid(required numeric id) { - var comments = model("Comment").findAll(include="User", where="isPublished = 1 AND blogid = #arguments.id# AND commentParentId IS NULL", cache=5); - - return comments; - } - - public struct function uploadFile(file) { - var uploadPath = expandPath("/public/files"); - var filePath = uploadPath & file.serverFileName; - - // Ensure the upload directory exists - if (!directoryExists(uploadPath)) { - directoryCreate(uploadPath); - } - - // Move the uploaded file to the upload directory - fileWrite(filePath, file.fileContent); - - return { - filePath: filePath, - fileName: file.serverFileName - }; - } - - // Function to store comment - public void function comment() { - var commentModel = model("Comment"); - try { - // Check if user can comment using helper function - if (!canUserComment()) { - model("Log").log( - category = "wheels.blog", - level = "WARN", - message = "Unauthorized comment attempt", - details = { - "userId": GetSignedInUserId(), - "ip_address": cgi.REMOTE_ADDR - } - ); - // Do not allow commenting - renderText("Comments are closed"); - return; - } - - blog = getBlogById(params.blogId); - if (params.content.trim() == "" || params.content.trim() == "
" || params.content.trim() == "
" || params.content.trim() == "