Skip to content
This repository was archived by the owner on Apr 21, 2026. It is now read-only.

Commit 68f4958

Browse files
bpamiriclaude
andcommitted
fix: Remove val() BIGINT precision loss, scope tag uniqueness, add admin unpublish
CFML's val() returns Java double (53-bit mantissa), silently corrupting 19-digit snowflake-style BIGINT IDs in SQL WHERE clauses. Removed val() wrappers from ~20 queries across Controller.cfc, BlogController.cfc, and AdminController.cfc. Scoped Tag uniqueness validation to per-blog (scope="blogId") so tags like "wheels" can be reused across different posts. Added Unpublish/Revert to Draft action in admin blog panel: sets statusId to Draft, clears approval status and publishedAt. Includes route, controller action, verifies exception, and dropdown buttons in both blog.cfm and _blogs.cfm partial. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 75e9dcc commit 68f4958

7 files changed

Lines changed: 119 additions & 27 deletions

File tree

app/controllers/Controller.cfc

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ component extends="wheels.Controller" {
115115

116116
function getCategoriesByBlogid(required numeric id) {
117117
return model("BlogCategory").findAll(
118-
where = "blogId = #val(arguments.id)#",
118+
where = "blogId = #arguments.id#",
119119
include = "Blog,Category",
120120
cache = 10
121121
);
@@ -386,11 +386,34 @@ component extends="wheels.Controller" {
386386
newTag.blogId = blogId;
387387
newTag.createdAt = now();
388388
newTag.updatedAt = now();
389-
newTag.save();
389+
if (!newTag.save()) {
390+
model("Log").log(
391+
category = "wheels.blog.tags",
392+
level = "ERROR",
393+
message = "Failed to save tag '#trim(tagName)#' for blogId: #blogId#",
394+
details = {
395+
"blog_id": blogId,
396+
"tag_name": trim(tagName),
397+
"errors": newTag.allErrors()
398+
},
399+
userId = GetSignedInUserId()
400+
);
401+
}
390402
}
391403
}
392404
} catch (any e) {
393-
local.exception = e;
405+
model("Log").log(
406+
category = "wheels.blog.tags",
407+
level = "ERROR",
408+
message = "Failed to save blog tags for blogId: #blogId#",
409+
details = {
410+
"blog_id": blogId,
411+
"error_message": e.message,
412+
"error_detail": e.detail,
413+
"error_type": e.type
414+
},
415+
userId = GetSignedInUserId()
416+
);
394417
}
395418
}
396419

@@ -399,7 +422,7 @@ component extends="wheels.Controller" {
399422
try {
400423
if (blogId > 0) {
401424
// direct delete approach
402-
model("Tag").deleteAll(where="blogId = #val(arguments.blogId)#");
425+
model("Tag").deleteAll(where="blogId = #arguments.blogId#");
403426

404427
return true;
405428
}
@@ -458,7 +481,7 @@ component extends="wheels.Controller" {
458481
try {
459482
if (blogId > 0) {
460483
// Find all category associations for this blog post
461-
model("BlogCategory").deleteAll(where="blogId = #val(arguments.blogId)#");
484+
model("BlogCategory").deleteAll(where="blogId = #arguments.blogId#");
462485

463486
return true;
464487
}
@@ -513,7 +536,7 @@ component extends="wheels.Controller" {
513536

514537
// Check if a blog with the same title/slug exists (that isn't this one)
515538
var existingBlog = model("Blog").findFirst(
516-
where="title = '#params.title#' AND slug = '#params.slug#' AND id != #val(blogId)#"
539+
where="title = '#params.title#' AND slug = '#params.slug#' AND id != #blogId#"
517540
);
518541

519542
if (isObject(existingBlog)) {

app/controllers/admin/AdminController.cfc

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ component extends="app.Controllers.Controller" {
33

44
function config() {
55
super.config();
6-
verifies(except="index,dashboard,checkAdminAccess,blog,editBlog,deleteBlog,update,blogList,blogApprove,rejectBlog,showBlog,commentsPublish,unpublishComment,comments,blogBulkApprove,blogBulkReject,viewComments,publishblog,closeComments", params="key", paramsTypes="integer");
6+
verifies(except="index,dashboard,checkAdminAccess,blog,editBlog,deleteBlog,update,blogList,blogApprove,rejectBlog,unpublishBlog,showBlog,commentsPublish,unpublishComment,comments,blogBulkApprove,blogBulkReject,viewComments,publishblog,closeComments", params="key", paramsTypes="integer");
77
usesLayout(template="/admin/AdminController/layout");
88
filters(through="checkAdminAccess");
99
}
@@ -37,8 +37,8 @@ component extends="app.Controllers.Controller" {
3737
// Get categories and tags for the form
3838
var categories = model("Category").findAll(order="name ASC");
3939
var postTypes = model("PostType").findAll(order="name ASC");
40-
var blogCategories = model("BlogCategory").findAll(where="blogId = #val(blog.id)#");
41-
var blogTags = model("Tag").findAll(where="blogId = #val(blog.id)#");
40+
var blogCategories = model("BlogCategory").findAll(where="blogId = #blog.id#");
41+
var blogTags = model("Tag").findAll(where="blogId = #blog.id#");
4242

4343
// Prepare data for the view
4444
var selectedCategories = [];
@@ -202,6 +202,16 @@ component extends="app.Controllers.Controller" {
202202
}
203203
}
204204

205+
function unpublishBlog() {
206+
try {
207+
blogUnpublish(params.id);
208+
renderText('<span class="badge bg-warning text-dark">Pending</span>');
209+
} catch (any e) {
210+
cfheader(statusCode=500);
211+
renderText(e.message);
212+
}
213+
}
214+
205215
function blogBulkApprove(){
206216
if (!structKeyExists(params, "selectedBlogIds") || !isArray(params.selectedBlogIds) || arrayLen(params.selectedBlogIds) == 0) {
207217
blogs = getAllBlogs();
@@ -742,6 +752,33 @@ component extends="app.Controllers.Controller" {
742752
};
743753
}
744754

755+
private function blogUnpublish(id){
756+
var blog = model("Blog").findByKey(id);
757+
758+
if (isObject(blog)) {
759+
blog.statusId = 1;
760+
blog.status = "";
761+
blog.publishedAt = "";
762+
if (blog.save()) {
763+
return {
764+
success = true,
765+
message = "Blog unpublished successfully!"
766+
};
767+
} else {
768+
return {
769+
success = false,
770+
errors = blog.allErrors(),
771+
message = "Failed to unpublish blog!"
772+
};
773+
}
774+
}
775+
776+
return {
777+
success = false,
778+
message = "Blog not found"
779+
};
780+
}
781+
745782
public void function importData() {
746783

747784
// 1) Load & parse JSON
@@ -949,8 +986,8 @@ component extends="app.Controllers.Controller" {
949986
postMap[wpId] = existingPost.id;
950987

951988
// Delete existing categories and tags for this post
952-
model("BlogCategory").deleteAll(where="blogId = #val(existingPost.id)#");
953-
model("Tag").deleteAll(where="blogId = #val(existingPost.id)#");
989+
model("BlogCategory").deleteAll(where="blogId = #existingPost.id#");
990+
model("Tag").deleteAll(where="blogId = #existingPost.id#");
954991

955992
// Process taxonomies (categories and tags)
956993
processTaxonomies(item, existingPost.id, arguments.categoryMap, arguments.tagMap);

app/controllers/web/BlogController.cfc

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ component extends="app.Controllers.Controller" {
118118
// Try to get both stats in one query if your database supports it
119119
try {
120120
return {
121-
"totalPosts": model("Blog").count(where="createdBy = #val(arguments.authorId)#"),
122-
"totalComments": model("Comment").count(where="authorId = #val(arguments.authorId)#")
121+
"totalPosts": model("Blog").count(where="createdBy = #arguments.authorId#"),
122+
"totalComments": model("Comment").count(where="authorId = #arguments.authorId#")
123123
};
124124
} catch (any e) {
125125
model("Log").log(
@@ -221,8 +221,8 @@ component extends="app.Controllers.Controller" {
221221
// Get categories and tags for the form
222222
categories = model("Category").findAll(order="name ASC");
223223
postTypes = model("PostType").findAll(order="name ASC");
224-
var blogCategories = model("BlogCategory").findAll(where="blogId = #val(blog.id)#");
225-
var blogTags = model("Tag").findAll(where="blogId = #val(blog.id)#");
224+
var blogCategories = model("BlogCategory").findAll(where="blogId = #blog.id#");
225+
var blogTags = model("Tag").findAll(where="blogId = #blog.id#");
226226

227227
// Prepare data for the view
228228
var selectedCategories = [];
@@ -264,7 +264,7 @@ component extends="app.Controllers.Controller" {
264264
private function getBlogsByAuthor(required authorId, numeric page=1, numeric perPage=6, boolean isInfiniteScroll=false) {
265265
var result = {
266266
query = model("Blog").findAll(
267-
where="blog_posts.statusId <> 1 AND blog_posts.status = 'Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.createdBy = #val(arguments.authorId)#",
267+
where="blog_posts.statusId <> 1 AND blog_posts.status = 'Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.createdBy = #arguments.authorId#",
268268
include="User",
269269
order="COALESCE(post_created_date, blog_posts.createdat) DESC",
270270
page = arguments.page,
@@ -275,7 +275,7 @@ component extends="app.Controllers.Controller" {
275275
};
276276

277277
result.totalCount = model("Blog").count(
278-
where="blog_posts.statusId <> 1 AND blog_posts.status = 'Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.createdBy = #val(arguments.authorId)#"
278+
where="blog_posts.statusId <> 1 AND blog_posts.status = 'Approved' AND blog_posts.publishedAt IS NOT NULL AND blog_posts.createdBy = #arguments.authorId#"
279279
);
280280
result.hasMore = (page * perPage) < result.totalCount;
281281

@@ -648,7 +648,7 @@ component extends="app.Controllers.Controller" {
648648
var whereClause = "title = '#form.title#'";
649649

650650
if(structKeyExists(form, "id") && isNumeric(form.id) && form.id > 0) {
651-
whereClause &= " AND id != #val(form.id)#";
651+
whereClause &= " AND id != #form.id#";
652652
}
653653

654654
var blogModel = model("Blog").findAll(where=whereClause);
@@ -857,15 +857,15 @@ component extends="app.Controllers.Controller" {
857857
if (!isObject(category)) return {query=queryNew(""), hasMore=false, totalCount=0};
858858

859859
var blogCategoryQuery = model("BlogCategory")
860-
.findAll(where="categoryId = #val(category.id)#", returnAs="query");
860+
.findAll(where="categoryId = #category.id#", returnAs="query");
861861
if (blogCategoryQuery.recordCount == 0) return {query=queryNew(""), hasMore=false, totalCount=0};
862862

863863
var blogIds = blogCategoryQuery.columnData("blogId");
864864
var blogIdList = arrayToList(blogIds);
865865

866866
var result = {
867867
query = model("Blog").findAll(
868-
where="blog_posts.id IN (#blogIdList#) AND categoryId = #val(category.id)# AND blog_posts.status ='Approved' AND blog_posts.publishedAt IS NOT NULL",
868+
where="blog_posts.id IN (#blogIdList#) AND categoryId = #category.id# AND blog_posts.status ='Approved' AND blog_posts.publishedAt IS NOT NULL",
869869
order="createdAt DESC",
870870
include="User,BlogCategory",
871871
returnAs="query",
@@ -877,7 +877,7 @@ component extends="app.Controllers.Controller" {
877877
};
878878

879879
result.totalCount = model("Blog").count(
880-
where="blog_posts.id IN (#blogIdList#) AND categoryId = #val(category.id)# AND blog_posts.status ='Approved' AND blog_posts.publishedAt IS NOT NULL",
880+
where="blog_posts.id IN (#blogIdList#) AND categoryId = #category.id# AND blog_posts.status ='Approved' AND blog_posts.publishedAt IS NOT NULL",
881881
include="User,BlogCategory"
882882
);
883883
result.hasMore = (page * perPage) < result.totalCount;
@@ -908,7 +908,7 @@ component extends="app.Controllers.Controller" {
908908

909909
private function getBlogById(required numeric id) {
910910
return model("Blog").findOne(
911-
where="blog_posts.id = #val(arguments.id)#",
911+
where="blog_posts.id = #arguments.id#",
912912
include="User, PostStatus"
913913
);
914914
}
@@ -993,7 +993,18 @@ component extends="app.Controllers.Controller" {
993993
}
994994
newBlog.save();
995995

996-
response.blogId = newBlog.id;
996+
// Retrieve the generated ID by looking up the just-created blog by slug.
997+
// Wheels' PostgreSQL adapter does not always return the auto-generated
998+
// primary key after INSERT (e.g. when the column uses a BIGINT default
999+
// or trigger-based ID generation rather than SERIAL).
1000+
if (!len(trim(newBlog.id))) {
1001+
var createdBlog = model("Blog").findOne(where="slug = '#blogData.slug#'");
1002+
if (isObject(createdBlog)) {
1003+
response.blogId = createdBlog.id;
1004+
}
1005+
} else {
1006+
response.blogId = newBlog.id;
1007+
}
9971008
response.message = "Blog post created successfully.";
9981009
} else {
9991010
response.message = "A blog post with the same title already exists.";
@@ -1056,7 +1067,7 @@ component extends="app.Controllers.Controller" {
10561067
}
10571068

10581069
function getAllCommentsByBlogid(required numeric id) {
1059-
var comments = model("Comment").findAll(include="User", where="isPublished = 1 AND blogid = #val(arguments.id)# AND commentParentId IS NULL", cache=5);
1070+
var comments = model("Comment").findAll(include="User", where="isPublished = 1 AND blogid = #arguments.id# AND commentParentId IS NULL", cache=5);
10601071

10611072
return comments;
10621073
}
@@ -1106,7 +1117,7 @@ component extends="app.Controllers.Controller" {
11061117
response = saveComment(params);
11071118
}
11081119
if(structKeyExists(response, "Id")){
1109-
comments = commentModel.findAll(include="User", where="id = #val(response.Id)# AND isPublished = 1");
1120+
comments = commentModel.findAll(include="User", where="id = #response.Id# AND isPublished = 1");
11101121
renderPartial(partial="partials/comment");
11111122
}
11121123
} catch (any e) {
@@ -1172,7 +1183,7 @@ component extends="app.Controllers.Controller" {
11721183
throw("Invalid blog ID", "InvalidRequest");
11731184
}
11741185

1175-
var blogId = val(params.id);
1186+
var blogId = params.id;
11761187
var blog = model("Blog").findByKey(blogId);
11771188
var user = model("User").findByKey(currentUserId);
11781189

app/models/Tag.cfc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ component extends="app.Models.Model" {
1515
belongsTo(name="Blog", foreignKey="blogId");
1616

1717
validatesPresenceOf(property="name");
18-
validatesUniquenessOf(property="name");
18+
validatesUniquenessOf(property="name", scope="blogId");
1919
}
2020

2121
// fetch all tags

app/views/admin/AdminController/blog.cfm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@
101101
hx-confirm="Are you sure you want to reject this blog?"
102102
>Reject</button>
103103
</li>
104+
<li>
105+
<button
106+
class="dropdown-item text-warning fs-16"
107+
hx-post="unpublish"
108+
hx-vals='{"id": "#blogs.id[i]#", "authenticityToken": "#authenticityToken()#"}'
109+
hx-target=".approval-status-#blogId#"
110+
hx-swap="innerHTML"
111+
hx-confirm="Are you sure you want to unpublish this blog? It will revert to draft status."
112+
>Unpublish</button>
113+
</li>
104114
<li>
105115
<button class="dropdown-item text-danger fs-16"
106116
hx-post="/admin/blog/delete"

app/views/admin/AdminController/partials/_blogs.cfm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@
9292
hx-confirm="Are you sure you want to reject this blog?"
9393
>Reject</button>
9494
</li>
95+
<li>
96+
<button
97+
class="dropdown-item text-warning fs-16"
98+
hx-post="unpublish"
99+
hx-vals='{"id": "#blogs.id[i]#", "authenticityToken": "#authenticityToken()#"}'
100+
hx-target=".approval-status-#blogId#"
101+
hx-swap="innerHTML"
102+
hx-confirm="Are you sure you want to unpublish this blog? It will revert to draft status."
103+
>Unpublish</button>
104+
</li>
95105
</ul>
96106
</div>
97107
</td>

config/routes.cfm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
.post(name = "blog-approve", pattern = "approve", to = "AdminController##blogApprove")
142142
.post(name = "bulk-approve", pattern = "bulkApprove", to = "AdminController##blogBulkApprove")
143143
.post(name = "blog-reject", pattern = "reject", to = "AdminController##rejectBlog")
144+
.post(name = "blog-unpublish-admin", pattern = "unpublish", to = "AdminController##unpublishBlog")
144145
.post(name = "bulk-reject", pattern = "bulkReject", to = "AdminController##blogBulkReject")
145146
.post(name = "comment-publish", pattern = "publish", to = "AdminController##commentsPublish")
146147
.post(name = "comment-unpublish", pattern = "hide", to = "AdminController##unpublishComment")

0 commit comments

Comments
 (0)