diff --git a/.gitignore b/.gitignore
index 006d4dc9..d98063e2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,10 +7,10 @@ www/userdata
### Config files ###
config.cfm
.env
+config/nginx/conf.d/*.conf
### Log files ###
+logs/*
www/logs
-.history
-
-### NGINX configs ###
-config/nginx/conf.d/*.conf
\ No newline at end of file
+filebrowser.db
+.history
\ No newline at end of file
diff --git a/compose-dev.yml b/compose-dev.yml
index 9afd5b4b..7f3b2357 100644
--- a/compose-dev.yml
+++ b/compose-dev.yml
@@ -3,7 +3,6 @@ services:
image: ${LUCEE_IMAGE}:${LUCEE_IMAGE_VERSION}
ports:
- "${LUCEE_PORT}:80"
- restart: always
container_name: ${LUCEE_CONTAINER_NAME}
environment:
- LUCEE_ADMIN_PASSWORD=${LUCEE_ADMIN_PASSWORD}
@@ -20,7 +19,6 @@ services:
depends_on:
- lucee
image: mysql:8.1
- restart: always
container_name: ${MYSQL_CONTAINER_NAME}
ports:
- "${MYSQL_PORT}:3306"
@@ -60,7 +58,6 @@ services:
inbucket:
image: inbucket/inbucket
container_name: ${INBUCKET_CONTAINER_NAME}
- restart: always
ports:
- "${INBUCKET_WEB_PORT}:9000"
- "${INBUCKET_SMTP_PORT}:2500"
diff --git a/compose.yml b/compose.yml
index d5150727..976bf666 100644
--- a/compose.yml
+++ b/compose.yml
@@ -13,6 +13,9 @@ services:
- ./www:/var/www
- ./config/nginx/conf.d:/etc/nginx/conf.d
- userdata_volume:/var/www/userdata
+ - ./logs/lucee:/opt/lucee/server/lucee-server/context/logs
+ - ./logs/nginx:/var/log/nginx
+ - ./logs/tomcat:/usr/local/tomcat/logs
networks:
- npm_network
@@ -31,6 +34,9 @@ services:
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
volumes:
- db_volume:/var/lib/mysql
+ - ./config/mysql/tuning.cnf:/etc/mysql/conf.d/tuning.cnf:ro
+ - ./logs/mysql:/var/log/mysql
+
networks:
- npm_network
healthcheck:
@@ -56,6 +62,19 @@ services:
condition: service_healthy
networks:
- npm_network
+
+ filebrowser:
+ image: filebrowser/filebrowser:latest
+ container_name: ${FILEBROWSER_CONTAINER_NAME}
+ restart: always
+ user: "0:0"
+ ports:
+ - "${FILEBROWSER_PORT}:80"
+ volumes:
+ - ./logs:/srv/logs
+ - ./filebrowser.db:/database/filebrowser.db
+ networks:
+ - npm_network
volumes:
userdata_volume:
diff --git a/config/db/core/V14__blog-posts.sql b/config/db/core/V14__blog-posts.sql
new file mode 100644
index 00000000..14b4261a
--- /dev/null
+++ b/config/db/core/V14__blog-posts.sql
@@ -0,0 +1,88 @@
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+INSERT IGNORE INTO system_mappings (strMapping, strPath, blnOnlyAdmin, blnOnlySuperAdmin, blnOnlySysAdmin)
+VALUES
+ ('sysadmin/blog-posts', 'backend/core/views/sysadmin/blog_posts.cfm', 0, 0, 1),
+ ('sysadmin/blog-posts/edit', 'backend/core/views/sysadmin/blog_posts_edit.cfm', 0, 0, 1),
+ ('sysadm/blog-posts', 'backend/core/handler/sysadmin/blog_posts.cfm', 0, 0, 1),
+ ('sysadmin/blog-posts/categories', 'backend/core/views/sysadmin/blog_posts_categories.cfm', 0, 0, 1);
+
+INSERT IGNORE INTO frontend_mappings (strMapping, strPath, strMetatitle, strMetadescription, strhtmlcodes, blnCreatedByApp)
+VALUES ('blog/overview', 'templates/blog/overview.cfm', '', '', '', 0);
+
+CREATE TABLE `blog_categories` (
+ `intBlogCategoryID` int NOT NULL AUTO_INCREMENT,
+ `strCategoryName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
+ `intPrio` tinyint NOT NULL DEFAULT 1,
+ PRIMARY KEY (`intBlogCategoryID`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+CREATE TABLE `blog_categories_trans` (
+ `intBlogCategoryTransID` int NOT NULL AUTO_INCREMENT,
+ `intBlogCategoryID` int NOT NULL,
+ `intLanguageID` int NOT NULL,
+ `strCategoryName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
+ PRIMARY KEY (`intBlogCategoryTransID`) USING BTREE,
+ INDEX `_intBlogCategoryID`(`intBlogCategoryID`) USING BTREE,
+ INDEX `_intLanguageID`(`intLanguageID`) USING BTREE,
+ CONSTRAINT `frn_bc_languages` FOREIGN KEY (`intLanguageID`) REFERENCES `languages` (`intLanguageID`) ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT `frn_blog_categories` FOREIGN KEY (`intBlogCategoryID`) REFERENCES `blog_categories` (`intBlogCategoryID`) ON DELETE CASCADE ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+CREATE TABLE `blog_post_categories` (
+ `intBlogPostCategoryID` int NOT NULL AUTO_INCREMENT,
+ `intBlogPostID` int NOT NULL,
+ `intBlogCategoryID` int NOT NULL,
+ PRIMARY KEY (`intBlogPostCategoryID`) USING BTREE,
+ UNIQUE INDEX `idx_unique`(`intBlogPostID`, `intBlogCategoryID`) USING BTREE,
+ INDEX `idx_blogpost`(`intBlogPostID`) USING BTREE,
+ INDEX `idx_postcategory`(`intBlogCategoryID`) USING BTREE,
+ CONSTRAINT `fk_bpc_categories` FOREIGN KEY (`intBlogCategoryID`) REFERENCES `blog_categories` (`intBlogCategoryID`) ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT `fk_bpc_posts` FOREIGN KEY (`intBlogPostID`) REFERENCES `blog_posts` (`intBlogPostID`) ON DELETE CASCADE ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+CREATE TABLE `blog_posts` (
+ `intBlogPostID` int NOT NULL AUTO_INCREMENT,
+ `dtmCreated` datetime NOT NULL,
+ `dtmUpdated` datetime NULL DEFAULT NULL,
+ `blnIsPublished` tinyint NOT NULL DEFAULT 0,
+ `dtePublishDate` date NULL DEFAULT NULL,
+ `dteUnpublishDate` date NULL DEFAULT NULL,
+ `blnShowPublishedDate` tinyint NOT NULL DEFAULT 1,
+ `blnShowTOC` tinyint NOT NULL DEFAULT 0,
+ `strAuthorName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
+ `blnShowAuthor` tinyint NOT NULL DEFAULT 0,
+ `strPreviewTitle` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
+ `strPreviewText` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
+ `strPreviewImage` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
+ `strButtonText` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
+ `strBlogHeaderImage` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
+ `strPostTitle` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
+ `strPostIntro` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
+ `strPostContent` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
+ `intFrontendMappingsID` int NULL DEFAULT NULL,
+ PRIMARY KEY (`intBlogPostID`) USING BTREE,
+ INDEX `idx_dtmCreated`(`dtmCreated`) USING BTREE,
+ INDEX `idx_blnIsPublished`(`blnIsPublished`) USING BTREE,
+ FULLTEXT INDEX `FulltextSearch`(`strPreviewTitle`, `strPreviewText`, `strButtonText`, `strPostContent`)
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
+
+CREATE TABLE `blog_posts_trans` (
+ `intBlogPostTransID` int NOT NULL AUTO_INCREMENT,
+ `intBlogPostID` int NOT NULL,
+ `intLanguageID` int NOT NULL,
+ `strPreviewTitle` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
+ `strPreviewText` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
+ `strButtonText` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
+ `strPostTitle` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
+ `strPostIntro` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
+ `strPostContent` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
+ PRIMARY KEY (`intBlogPostTransID`) USING BTREE,
+ UNIQUE INDEX `idx_unique_translation`(`intBlogPostID`, `intLanguageID`) USING BTREE,
+ INDEX `fk_blogpoststrans_language`(`intLanguageID`) USING BTREE,
+ CONSTRAINT `fk_blogpoststrans_blogposts` FOREIGN KEY (`intBlogPostID`) REFERENCES `blog_posts` (`intBlogPostID`) ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT `fk_blogpoststrans_language` FOREIGN KEY (`intLanguageID`) REFERENCES `languages` (`intLanguageID`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
+
+SET FOREIGN_KEY_CHECKS = 1;
\ No newline at end of file
diff --git a/config/db/core/V15__blog-updates.sql b/config/db/core/V15__blog-updates.sql
new file mode 100644
index 00000000..cc70d0fe
--- /dev/null
+++ b/config/db/core/V15__blog-updates.sql
@@ -0,0 +1,15 @@
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+ALTER TABLE blog_posts
+ DROP INDEX FulltextSearch,
+ ADD FULLTEXT INDEX FulltextSearch (
+ strPreviewTitle,
+ strPreviewText,
+ strPostTitle,
+ strPostIntro,
+ strPostContent
+ );
+
+ SET FOREIGN_KEY_CHECKS = 1;
\ No newline at end of file
diff --git a/config/mysql/tuning.cnf b/config/mysql/tuning.cnf
new file mode 100644
index 00000000..4340f1b9
--- /dev/null
+++ b/config/mysql/tuning.cnf
@@ -0,0 +1,26 @@
+[mysqld]
+# MEMORY & CACHING
+innodb_buffer_pool_size = 2G
+innodb_buffer_pool_instances = 2
+innodb_redo_log_capacity = 512M
+tmp_table_size = 64M
+max_heap_table_size = 64M
+sort_buffer_size = 4M
+read_rnd_buffer_size = 4M
+join_buffer_size = 4M
+
+# CONNECTION & TIMEOUTS
+max_connections = 100
+wait_timeout = 300
+interactive_timeout = 300
+connect_timeout = 10
+
+# INNODB IO THREADS
+innodb_flush_log_at_trx_commit = 2
+innodb_io_capacity = 200
+innodb_read_io_threads = 4
+innodb_write_io_threads = 4
+
+# FILEBROWSER MOUNT LOCATION
+slow_query_log = 1
+slow_query_log_file = /var/log/mysql/slow.log
\ No newline at end of file
diff --git a/readme.md b/readme.md
index c1fde3b4..57be9d2b 100644
--- a/readme.md
+++ b/readme.md
@@ -173,7 +173,7 @@ For more information or if you're unsure about something, feel free to open an i
- [Tabler](https://github.com/tabler/tabler/blob/main/LICENSE)
- [Dropify](https://github.com/JeremyFagis/dropify/blob/master/LICENCE.md)
-- [Trumbowyg](https://github.com/Alex-D/Trumbowyg/blob/develop/LICENSE)
+- [Hugerte](https://github.com/hugerte/hugerte/blob/main/LICENSE.TXT)
- [Bootstrap](https://github.com/twbs/bootstrap/blob/main/LICENSE)
- [MockDataCFC](https://github.com/Ortus-Solutions/MockDataCFC/blob/development/LICENSE)
- [Fontawesome](https://fontawesome.com/v4/license/)
@@ -181,3 +181,4 @@ For more information or if you're unsure about something, feel free to open an i
- [taffy](https://github.com/atuttle/Taffy)
+
diff --git a/www/backend/core/com/blog.cfc b/www/backend/core/com/blog.cfc
new file mode 100644
index 00000000..67ca3f05
--- /dev/null
+++ b/www/backend/core/com/blog.cfc
@@ -0,0 +1,832 @@
+component displayname="blogposts" output="false" {
+
+ // Create a new blog post using only the title
+ public struct function saveNewPost(required string title, required string language) {
+
+ local.title = arguments.title;
+
+ try {
+
+ // Set the mapping path based on the title
+ local.mapping = "blog/" & application.objGlobal.beautifyString(local.title);
+
+ // Save frontend mapping
+ queryExecute(
+ options = {datasource = application.datasource, result = "local.newMappingID"},
+ params = {
+ urlSlug: {type: "varchar", value: local.mapping},
+ metaTitle: {type: "nvarchar", value: local.title}
+ },
+ sql = "
+ INSERT INTO frontend_mappings (strMapping, strMetatitle, blnCreatedByApp)
+ VALUES (:urlSlug, :metaTitle, 1)
+ "
+ );
+
+ local.newMappingID = local.newMappingID.generated_key;
+
+ // Save post with only title and created date
+ queryExecute(
+ options = {datasource = application.datasource, result = "local.newPostID"},
+ params = {
+ title: {type: "varchar", value: local.title},
+ createdAt: {type: "timestamp", value: now()},
+ newMappingID: {type: "numeric", value: local.newMappingID}
+ },
+ sql = "
+ INSERT INTO blog_posts (dtmCreated, strPreviewTitle, strPostTitle, intFrontendMappingsID)
+ VALUES (:createdAt, :title, :title, :newMappingID)
+ "
+ );
+
+ local.newPostID = local.newPostID.generated_key;
+ local.path = "templates/blog/post.cfm?id=#local.newPostID#";
+
+ // Update the mapping path with the new post ID
+ queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ mappingID: {type: "numeric", value: local.newMappingID},
+ mappingPath: {type: "varchar", value: local.path}
+ },
+ sql = "
+ UPDATE frontend_mappings
+ SET strPath = :mappingPath
+ WHERE intFrontendMappingsID = :mappingID
+ "
+ );
+
+ } catch (any e) {
+
+ return {
+ success: false,
+ message: "Error creating post: " & e.message
+ };
+
+ }
+
+ return {
+ success: true,
+ postID: local.newPostID,
+ message: "Post created successfully. Add your content now.",
+ };
+
+ }
+
+
+ public query function getTotalPostsSearch(required string search) {
+
+ local.qTotalPosts = queryExecute(
+ options = {datasource = application.datasource},
+ sql = "
+ SELECT COUNT(intBlogPostID) as totalPosts
+ FROM blog_posts
+ WHERE MATCH (strPreviewTitle, strPreviewText, strPostTitle, strPostIntro, strPostContent)
+ #arguments.search#
+ "
+ );
+
+ return local.qTotalPosts;
+
+ }
+
+
+ public query function getTotalPosts() {
+
+ local.qTotalPosts = queryExecute(
+ options = {datasource = application.datasource},
+ sql = "
+ SELECT COUNT(intBlogPostID) as totalPosts
+ FROM blog_posts
+ "
+ );
+
+ return local.qTotalPosts;
+ }
+
+
+ public query function getPosts(required numeric start, required string sort){
+
+ local.entries = 10;
+
+ local.qPosts = queryExecute(
+ options = {datasource = application.datasource},
+ sql = "
+ SELECT *
+ FROM blog_posts
+ ORDER BY #arguments.sort#
+ LIMIT #arguments.start#, #local.entries#
+ "
+ );
+
+ return local.qPosts;
+
+ }
+
+ public query function getPostsSearch(required string search, required numeric start, required string sort) {
+
+ local.entries = 10;
+
+ local.qTotalPosts = queryExecute(
+ options = {datasource = application.datasource},
+ sql = "
+ SELECT *
+ FROM blog_posts
+ WHERE MATCH (strPreviewTitle, strPreviewText, strPostTitle, strPostIntro, strPostContent)
+ #arguments.search#
+ ORDER BY #arguments.sort#
+ LIMIT #arguments.start#, #local.entries#
+ "
+ );
+
+ return local.qTotalPosts;
+
+ }
+
+
+ public query function getPost(required numeric postID) {
+
+ local.qPost = queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ postID: {type: "numeric", value: postID}
+ },
+ sql = "
+ SELECT *,
+ (
+ SELECT strMapping
+ FROM frontend_mappings
+ WHERE intFrontendMappingsID = blog_posts.intFrontendMappingsID
+ ) AS strMapping
+ FROM blog_posts
+ WHERE intBlogPostID = :postID
+ "
+ );
+
+ return local.qPost;
+
+
+ }
+
+
+ public query function getBlogCategories() {
+
+ local.qBlogCategories = queryExecute(
+ options = {datasource = application.datasource},
+ sql = "
+ SELECT *
+ FROM blog_categories
+ ORDER BY intPrio
+ "
+ );
+
+ return local.qBlogCategories;
+
+ }
+
+
+ public struct function saveCategory(required string categoryName, required numeric categoryID) {
+
+ local.categoryName = arguments.categoryName;
+ local.categoryID = arguments.categoryID;
+
+ try {
+
+ queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ categoryName: {type: "varchar", value: local.categoryName},
+ categoryID: {type: "numeric", value: local.categoryID}
+ },
+ sql = "
+ UPDATE blog_categories
+ SET strCategoryName = :categoryName
+ WHERE intBlogCategoryID = :categoryID
+ "
+ );
+
+ } catch (any e) {
+
+ return {
+ success: false,
+ message: "Error updating category: " & e.message
+ };
+
+ }
+
+ return {
+ success: true,
+ message: "Category updated successfully"
+ };
+
+ }
+
+ public struct function deleteCategory(required numeric categoryID) {
+
+ local.categoryID = arguments.categoryID;
+
+ qCategory = queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ categoryID: {type: "numeric", value: local.categoryID}
+ },
+ sql = "
+ SELECT intPrio
+ FROM blog_categories
+ WHERE intBlogCategoryID = :categoryID
+ "
+ );
+
+ try {
+
+ queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ categoryID: {type: "numeric", value: local.categoryID},
+ currPrio: {type: "numeric", value: qCategory.intPrio}
+ },
+ sql = "
+ DELETE FROM blog_categories WHERE intBlogCategoryID = :categoryID;
+ UPDATE blog_categories SET intPrio = intPrio-1 WHERE intPrio > :currPrio
+ "
+ );
+
+ } catch (any e) {
+
+ return {
+ success: false,
+ message: "Error deleting category: " & e.message
+ };
+
+ }
+
+ return {
+ success: true,
+ message: "Category deleted successfully"
+ };
+
+ }
+
+ public struct function createCategory(required string categoryName) {
+
+ local.categoryName = arguments.categoryName;
+
+ qNexPrio = queryExecute(
+ options = {datasource = application.datasource},
+ sql = "
+ SELECT COALESCE(MAX(intPrio),0)+1 as nextPrio
+ FROM blog_categories
+ "
+ );
+
+ try {
+
+ queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ categoryName: {type: "varchar", value: local.categoryName},
+ nextPrio: {type: "numeric", value: qNexPrio.nextPrio}
+ },
+ sql = "
+ INSERT INTO blog_categories (strCategoryName, intPrio)
+ VALUES (:categoryName, :nextPrio)
+ "
+ );
+
+ } catch (any e) {
+
+ return {
+ success: false,
+ message: "Error creating category: " & e.message
+ };
+
+ }
+
+ return {
+ success: true,
+ message: "Category created successfully"
+ };
+
+ }
+
+
+ /* Get the list of connected blog categories */
+ public string function getConnectedCategories(required numeric postID) {
+
+ local.postID = arguments.postID;
+
+ local.qCategories = queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ postID: {type: "numeric", value: local.postID}
+ },
+ sql = "
+ SELECT *
+ FROM blog_post_categories
+ WHERE intBlogPostID = :postID
+ "
+ );
+
+ local.connectedCategories = "";
+ loop query="local.qCategories" {
+ local.connectedCategories = listAppend(local.connectedCategories, local.qCategories.intBlogCategoryID);
+ }
+
+ return local.connectedCategories;
+
+ }
+
+ public struct function savePost(required struct postStruct) {
+
+ local.postID = arguments.postStruct.postID;
+ local.updateDate = now();
+ local.isPublished = arguments.postStruct.is_published ?: 0;
+ local.publishDate = arguments.postStruct.publish_date ?: "";
+ local.unpublishDate = arguments.postStruct.unpublish_date ?: "";
+ local.showPublishedDate = arguments.postStruct.show_publish_date ?: 0;
+ local.showTOC = arguments.postStruct.show_toc ?: 0;
+ local.authorName = arguments.postStruct.author_name ?: "";
+ local.showAuthor = arguments.postStruct.show_author ?: 0;
+ local.previewTitle = arguments.postStruct.preview_title ?: "";
+ local.previewText = arguments.postStruct.preview_text ?: "";
+ local.previewImage = arguments.postStruct.preview_image ?: "";
+ local.buttonText = arguments.postStruct.button_text ?: "";
+ local.headerImage = arguments.postStruct.header_image ?: "";
+ local.postTitle = arguments.postStruct.post_title ?: "";
+ local.postIntro = arguments.postStruct.post_intro ?: "";
+ local.postContent = arguments.postStruct.content ?: "";
+ local.errorMessage = "";
+
+ local.connectedCategories = arguments.postStruct.categoryID ?: "";
+
+ if (len(local.publishDate)) {
+ local.publishType = "date";
+ } else {
+ local.publishType = "null";
+ }
+ if (len(local.unpublishDate)) {
+ local.unpublishType = "date";
+ } else {
+ local.unpublishType = "null";
+ }
+
+ if (len(trim(local.previewImage))) {
+
+ local.allowedFileTypes = ["jpg", "jpeg", "png", "gif", "webp"];
+
+ // Build folder path by date: /userdata/images/blog/YYYY/MM/DD
+ local.now = now();
+ local.year = dateFormat(local.now, "yyyy");
+ local.month = dateFormat(local.now, "mm");
+ local.day = dateFormat(local.now, "dd");
+ local.relPath = "/userdata/images/blog/" & local.year & "/" & local.month & "/" & local.day;
+ local.absPath = expandPath(local.relPath);
+
+ // Create the directory if it doesn't exist
+ if (!directoryExists(local.absPath)) {
+ directoryCreate(local.absPath, true);
+ }
+
+ local.uploadArgs = {
+ filePath: local.absPath,
+ fileNameOrig: "preview_image",
+ makeUnique: true
+ };
+
+ local.globalObj = new backend.core.com.global();
+ local.result = local.globalObj.uploadFile(local.uploadArgs, local.allowedFileTypes);
+
+ if (local.result.success) {
+ local.previewImage = local.relPath & "/" & local.result.fileName;
+ } else {
+ local.errorMessage = "Error uploading preview image: " & local.result.message;
+ }
+
+ } else {
+
+ // Get the image from table
+ local.qPost = queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ postID: {type: "numeric", value: local.postID}
+ },
+ sql = "
+ SELECT strPreviewImage
+ FROM blog_posts
+ WHERE intBlogPostID = :postID
+ "
+ );
+
+ if (local.qPost.recordCount) {
+ local.previewImage = local.qPost.strPreviewImage;
+ }
+
+ }
+
+ if (len(trim(local.headerImage))) {
+
+ local.allowedFileTypes = ["jpg", "jpeg", "png", "gif", "webp"];
+
+ // Build folder path by date: /userdata/images/blog/YYYY/MM/DD
+ local.now = now();
+ local.year = dateFormat(local.now, "yyyy");
+ local.month = dateFormat(local.now, "mm");
+ local.day = dateFormat(local.now, "dd");
+ local.relPath = "/userdata/images/blog/" & local.year & "/" & local.month & "/" & local.day;
+ local.absPath = expandPath(local.relPath);
+
+ // Create the directory if it doesn't exist
+ if (!directoryExists(local.absPath)) {
+ directoryCreate(local.absPath, true);
+ }
+
+ local.uploadArgs = {
+ filePath: local.absPath,
+ fileNameOrig: "header_image",
+ makeUnique: true
+ };
+
+ local.globalObj = new backend.core.com.global();
+ local.result = local.globalObj.uploadFile(local.uploadArgs, local.allowedFileTypes);
+
+ if (local.result.success) {
+ local.headerImage = local.relPath & "/" & local.result.fileName;
+ } else {
+ local.errorMessage = "Error uploading header image: " & local.result.message;
+ }
+
+ } else {
+
+ // Get the image from table
+ local.qPost = queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ postID: {type: "numeric", value: local.postID}
+ },
+ sql = "
+ SELECT strBlogHeaderImage
+ FROM blog_posts
+ WHERE intBlogPostID = :postID
+ "
+ );
+
+ if (local.qPost.recordCount) {
+ local.headerImage = local.qPost.strBlogHeaderImage;
+ }
+
+ }
+
+ try {
+
+ // Check if frontend mapping exists and if not, create it
+ local.qMapping = queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ postID: {type: "numeric", value: local.postID}
+ },
+ sql = "
+ SELECT bp.intFrontendMappingsID
+ FROM blog_posts bp
+ INNER JOIN frontend_mappings fm
+ ON bp.intFrontendMappingsID = fm.intFrontendMappingsID
+ WHERE bp.intBlogPostID = :postID
+ "
+ );
+
+ if (local.qMapping.recordCount) {
+
+ local.mappingID = local.qMapping.intFrontendMappingsID;
+
+ } else {
+
+ // Set the mapping path based on the title
+ local.mapping = "blog/" & application.objGlobal.beautifyString(local.previewTitle);
+ local.path = "templates/blog/post.cfm?id=#local.postID#";
+
+ // Save frontend mapping
+ queryExecute(
+ options = {datasource = application.datasource, result = "local.newMappingID"},
+ params = {
+ urlSlug: {type: "varchar", value: local.mapping},
+ metaTitle: {type: "nvarchar", value: local.previewTitle},
+ postPath: {type: "varchar", value: local.path}
+ },
+ sql = "
+ INSERT INTO frontend_mappings (strMapping, strMetatitle, strPath)
+ VALUES (:urlSlug, :metaTitle, :postPath)
+ "
+ );
+
+ local.mappingID = local.newMappingID.generated_key;
+
+ }
+
+ queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ postID: {type: "numeric", value: local.postID},
+ updateDate: {type: "datetime", value: local.updateDate},
+ isPublished: {type: "boolean", value: local.isPublished},
+ publishDate: {type: local.publishType, value: local.publishDate},
+ unpublishDate: {type: local.unpublishType, value: local.unpublishDate},
+ showPublishedDate: {type: "boolean", value: local.showPublishedDate},
+ showTOC: {type: "boolean", value: local.showTOC},
+ authorName: {type: "nvarchar", value: local.authorName},
+ showAuthor: {type: "boolean", value: local.showAuthor},
+ previewTitle: {type: "nvarchar", value: local.previewTitle},
+ previewText: {type: "nvarchar", value: local.previewText},
+ previewImage: {type: "varchar", value: local.previewImage},
+ buttonText: {type: "nvarchar", value: local.buttonText},
+ headerImage: {type: "varchar", value: local.headerImage},
+ postTitle: {type: "nvarchar", value: local.postTitle},
+ postIntro: {type: "nvarchar", value: local.postIntro},
+ postContent: {type: "nvarchar", value: local.postContent},
+ mappingID: {type: "numeric", value: local.mappingID}
+ },
+ sql = "
+ UPDATE blog_posts
+ SET dtmUpdated = :updateDate,
+ blnIsPublished = :isPublished,
+ dtePublishDate = :publishDate,
+ dteUnpublishDate = :unpublishDate,
+ blnShowPublishedDate = :showPublishedDate,
+ blnShowTOC = :showTOC,
+ strAuthorName = :authorName,
+ blnShowAuthor = :showAuthor,
+ strPreviewTitle = :previewTitle,
+ strPreviewText = :previewText,
+ strPreviewImage = :previewImage,
+ strButtonText = :buttonText,
+ strBlogHeaderImage = :headerImage,
+ strPostTitle = :postTitle,
+ strPostIntro = :postIntro,
+ strPostContent = :postContent,
+ intFrontendMappingsID = :mappingID
+ WHERE intBlogPostID = :postID
+ "
+ );
+
+ // Update categories
+ queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ postID: {type: "numeric", value: local.postID}
+ },
+ sql = "
+ DELETE FROM blog_post_categories WHERE intBlogPostID = :postID
+ "
+ );
+
+ if (len(trim(local.connectedCategories))) {
+ arrayOfCategories = listToArray(local.connectedCategories);
+ for (local.category in arrayOfCategories) {
+ queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ postID: {type: "numeric", value: local.postID},
+ categoryID: {type: "numeric", value: local.category}
+ },
+ sql = "
+ INSERT INTO blog_post_categories (intBlogPostID, intBlogCategoryID)
+ VALUES (:postID, :categoryID)
+ "
+ );
+ }
+ }
+
+ } catch (any e) {
+
+ return {
+ success: false,
+ message: "Error saving post: " & e.message
+ };
+
+ }
+
+ if (len(local.errorMessage)) {
+ return {
+ success: false,
+ message: local.errorMessage
+ };
+ }
+
+ return {
+ success: true,
+ message: "Post saved successfully"
+ };
+
+ }
+
+ // Delete post
+ public struct function deletePost(required numeric postID) {
+
+ local.postID = arguments.postID;
+
+ // Get all images associated with the post
+ // we need to scan the content for image URLs
+ local.qPost = queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ postID: {type: "numeric", value: local.postID}
+ },
+ sql = "
+ SELECT strPostContent, strPreviewImage, intFrontendMappingsID
+ FROM blog_posts
+ WHERE intBlogPostID = :postID
+ "
+ );
+
+ local.imageURLs = [];
+
+ // Scan the post content for image URLs that start with /userdata/images/blog
+ if (local.qPost.recordCount) {
+
+ local.postContent = local.qPost.strPostContent;
+
+ // Use reMatchNoCase to find all img tags with src attributes
+ local.imgTags = reMatchNoCase(']+src="[^"]+"', local.postContent);
+ local.imageURLs = [];
+
+ for (local.tag in local.imgTags) {
+
+ // Extract the src value from each img tag
+ local.srcMatch = reFindNoCase('src="([^"]+)"', local.tag, 1, true);
+ if (
+ arrayLen(local.srcMatch.pos) &&
+ local.srcMatch.len[2] > 0
+ ) {
+ local.imgUrl = mid(local.tag, local.srcMatch.pos[2], local.srcMatch.len[2]);
+ if (left(local.imgUrl, 21) eq "/userdata/images/blog") {
+ arrayAppend(local.imageURLs, local.imgUrl);
+ }
+ }
+
+ }
+
+ // Also check the preview image
+ if (len(trim(local.qPost.strPreviewImage)) && left(local.qPost.strPreviewImage, 21) eq "/userdata/images/blog") {
+ arrayAppend(local.imageURLs, local.qPost.strPreviewImage);
+ }
+
+ }
+
+ try {
+
+ queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ postID: {type: "numeric", value: local.postID},
+ mappingID: {type: "numeric", value: local.qPost.intFrontendMappingsID}
+ },
+ sql = "
+ DELETE FROM blog_posts WHERE intBlogPostID = :postID;
+ DELETE FROM frontend_mappings WHERE intFrontendMappingsID = :mappingID;
+ "
+ );
+
+ } catch (any e) {
+
+ return {
+ success: false,
+ message: "Error deleting post: " & e.message
+ };
+
+ }
+
+ // Delete associated images
+ for (local.imgUrl in local.imageURLs) {
+ local.absImgPath = expandPath(local.imgUrl);
+ if (fileExists(local.absImgPath)) {
+ fileDelete(local.absImgPath);
+ }
+ }
+
+ return {
+ success: true,
+ message: "Post deleted successfully"
+ };
+
+ }
+
+ // Delete preview image
+ public struct function deletePreviewImage(required numeric postID) {
+
+ local.postID = arguments.postID;
+
+ // Get the current preview image path
+ local.qPost = queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ postID: {type: "numeric", value: local.postID}
+ },
+ sql = "
+ SELECT strPreviewImage
+ FROM blog_posts
+ WHERE intBlogPostID = :postID
+ "
+ );
+
+ local.currentPreviewImage = local.qPost.strPreviewImage;
+
+ try {
+
+ queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ postID: {type: "numeric", value: local.postID}
+ },
+ sql = "
+ UPDATE blog_posts
+ SET strPreviewImage = ''
+ WHERE intBlogPostID = :postID
+ "
+ );
+
+ // Delete the preview image from the filesystem
+ if (len(trim(local.currentPreviewImage))) {
+
+ local.absImgPath = expandPath(local.currentPreviewImage);
+ if (fileExists(local.absImgPath)) {
+ fileDelete(local.absImgPath);
+ }
+
+ }
+
+ } catch (any e) {
+
+ return {
+ success: false,
+ message: "Error deleting preview image: " & e.message
+ };
+
+ }
+
+ return {
+ success: true,
+ message: "Preview image deleted successfully"
+ };
+
+ }
+
+
+ // Delete header image
+ public struct function deleteHeaderImage(required numeric postID) {
+
+ local.postID = arguments.postID;
+
+ // Get the current header image path
+ local.qPost = queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ postID: {type: "numeric", value: local.postID}
+ },
+ sql = "
+ SELECT strBlogHeaderImage
+ FROM blog_posts
+ WHERE intBlogPostID = :postID
+ "
+ );
+
+ local.currentHeaderImage = local.qPost.strBlogHeaderImage;
+
+ try {
+
+ queryExecute(
+ options = {datasource = application.datasource},
+ params = {
+ postID: {type: "numeric", value: local.postID}
+ },
+ sql = "
+ UPDATE blog_posts
+ SET strBlogHeaderImage = ''
+ WHERE intBlogPostID = :postID
+ "
+ );
+
+ // Delete the header image from the filesystem
+ if (len(trim(local.currentHeaderImage))) {
+
+ local.absImgPath = expandPath(local.currentHeaderImage);
+ if (fileExists(local.absImgPath)) {
+ fileDelete(local.absImgPath);
+ }
+
+ }
+
+ } catch (any e) {
+
+ return {
+ success: false,
+ message: "Error deleting header image: " & e.message
+ };
+
+ }
+
+ return {
+ success: true,
+ message: "Header image deleted successfully"
+ };
+
+ }
+
+}
diff --git a/www/backend/core/com/global.cfc b/www/backend/core/com/global.cfc
index b86ecfb4..4db18358 100644
--- a/www/backend/core/com/global.cfc
+++ b/www/backend/core/com/global.cfc
@@ -79,7 +79,32 @@ component displayname="globalFunctions" output="false" {
}
if (local.qCheckSEF.itsFrontend) {
+
local.returnStruct['thisPath'] = "frontend/" & application.activeTheme & "/" & local.qCheckSEF.strPath;
+
+ // Remove all url variables and append each as a struct value to the struct
+ if (find("?", local.returnStruct['thisPath'])) {
+
+ // Split path and query string
+ local.pathParts = listToArray(local.returnStruct['thisPath'], "?");
+ local.returnStruct['thisPath'] = local.pathParts[1];
+ local.returnStruct['urlVariables'] = {};
+
+ if (arrayLen(local.pathParts) > 1) {
+ local.queryString = local.pathParts[2];
+ local.pairs = listToArray(local.queryString, "&");
+ for (local.i = 1; local.i <= arrayLen(local.pairs); local.i++) {
+ local.pair = local.pairs[local.i];
+ local.eqPos = find("=", local.pair);
+ if (local.eqPos) {
+ local.key = left(local.pair, local.eqPos - 1);
+ local.value = mid(local.pair, local.eqPos + 1, len(local.pair) - local.eqPos);
+ local.returnStruct['urlVariables'][local.key] = local.value;
+ }
+ }
+ }
+ }
+
} else {
local.returnStruct['thisPath'] = local.qCheckSEF.strPath;
}
diff --git a/www/backend/core/com/language.cfc b/www/backend/core/com/language.cfc
index f503eb5e..7ab5227a 100644
--- a/www/backend/core/com/language.cfc
+++ b/www/backend/core/com/language.cfc
@@ -71,7 +71,7 @@ component displayname="language" output="false" {
} else if (structKeyExists(session, "lng")) {
local.thisLang = session.lng;
} else {
- local.thisLang = getDefaultLanguage().iso;
+ local.thisLang = application.objLanguage.getDefaultLanguage().iso;
}
local.searchString = structFindKey(application.langStruct[#local.thisLang#], arguments.stringToTrans, "one");
diff --git a/www/backend/core/com/sysadmin.cfc b/www/backend/core/com/sysadmin.cfc
index 711f5c55..385a7f08 100644
--- a/www/backend/core/com/sysadmin.cfc
+++ b/www/backend/core/com/sysadmin.cfc
@@ -299,7 +299,7 @@ component displayname="sysadmin" output="false" {
)
#arguments.search#
GROUP BY customers.intCustomerID
- ORDER BY #arguments.sort#
+ ORDER BY customers.#arguments.sort#
LIMIT #arguments.start#, #local.entries#
"
);
diff --git a/www/backend/core/com/translate.cfc b/www/backend/core/com/translate.cfc
index 5efce016..6fc032a3 100644
--- a/www/backend/core/com/translate.cfc
+++ b/www/backend/core/com/translate.cfc
@@ -17,10 +17,10 @@ component displayname="translate" accessors="true" {
// Open a modal
- public string function openModal(required string modalName, required string redirect, string modalTitle, boolean itsEditor) {
+ public string function openModal(required string modalName, required string redirect, string modalTitle, string editorClass) {
param name="arguments.modalTitle" default="Translate content";
- param name="arguments.itsEditor" default=0;
+ local.editorClass = structKeyExists(arguments, "editorClass") ? arguments.editorClass : "";
local.transTable = variables.thisTable & "_trans";
@@ -44,7 +44,7 @@ component displayname="translate" accessors="true" {