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" { @@ -64,11 +64,9 @@ component displayname="translate" accessors="true" { } - private string function transFields(boolean itsEditor) { + private string function transFields(string editorClass) { - param name="arguments.itsEditor" default=0; - - local.itsEditor = ""; + local.editorClass = structKeyExists(arguments, "editorClass") ? arguments.editorClass : ""; local.transTable = variables.thisTable & "_trans"; // Loop over existing languages exept the default language @@ -76,14 +74,11 @@ component displayname="translate" accessors="true" { loop query = local.getLng { - if (arguments.itsEditor eq 1) { - local.itsEditor = "editor"; - } writeOutput("
#local.getLng.strLanguageEN#
"); if (isNumeric(variables.maxLength)) { - writeOutput(" + + + + + + +
+
+ +
+ + + + +
+
+
+ + +
+ Preview Image + + + +
+ + +
+
+
+ +
+
+
+

Blog Post

+
+
+ +
+
+
+ +
+ + + + +
+
+
+ +
+ + + + +
+
+
+
+
+ + +
+ Header Image + + + +
+ + +
+
+
+
+
+
+ + +
+
+ + + + + + + + +#getModal.args('blog_posts', 'strPreviewTitle', qPost.intBlogPostID).openModal('preview_title', cgi.path_info, 'Translate preview title')# +#getModal.args('blog_posts', 'strPreviewText', qPost.intBlogPostID).openModal('preview_text', cgi.path_info, 'Translate preview text')# +#getModal.args('blog_posts', 'strButtonText', qPost.intBlogPostID).openModal('button_text', cgi.path_info, 'Translate button text')# +#getModal.args('blog_posts', 'strPostTitle', qPost.intBlogPostID).openModal('post_title', cgi.path_info, 'Translate post title')# +#getModal.args('blog_posts', 'strPostIntro', qPost.intBlogPostID).openModal('post_intro', cgi.path_info, 'Translate post intro')# +#getModal.args('blog_posts', 'strPostContent', qPost.intBlogPostID).openModal('post_content', cgi.path_info, 'Translate post content', 'big-editor')# + + + \ No newline at end of file diff --git a/www/backend/core/views/sysadmin/invoice_edit.cfm b/www/backend/core/views/sysadmin/invoice_edit.cfm index 659139b0..02822c67 100644 --- a/www/backend/core/views/sysadmin/invoice_edit.cfm +++ b/www/backend/core/views/sysadmin/invoice_edit.cfm @@ -232,7 +232,7 @@
-