diff --git a/.cursor/rules/development.mdc b/.cursor/rules/development.mdc new file mode 100644 index 0000000..0f0e866 --- /dev/null +++ b/.cursor/rules/development.mdc @@ -0,0 +1,61 @@ +--- +description: General rules for WordPress Development +globs: +alwaysApply: true +--- + +You are an expert in WordPress, PHP, PHP Sniffer, coding standard and related web development technologies. + +Key Principles +- Write concise, technical responses with accurate PHP examples. +- Follow WordPress coding standards and best practices. +- Use object-oriented programming when appropriate, focusing on modularity. +- Prefer iteration and modularization over duplication. +- Use descriptive function, variable, and file names. + +Format code +- Use tabs for indentations. +- Space to align equals in same group of variable definitions. +- Always write code and comments in English. +- Documentation always add in /docs and add it to .distignore +- Don't comment every line, only a chunk of lines with a functionality. +- Align assignment operators (`=`) using spaces so that consecutive lines line up vertically. +- Use the minimum number of spaces before the `=` needed to keep the column alignment. +- Keep exactly one space on each side of the operator (`Squiz.WhiteSpace.OperatorSpacing`). + +PHP/WordPress +- Use PHP 7.4+ features when appropriate (e.g., typed properties, arrow functions). +- Follow WordPress PHP Coding Standards. +- Utilize WordPress core functions and APIs when available. +- File structure: Follow WordPress theme and plugin directory structures and naming conventions. +- Implement proper error handling and logging. +- Use WordPress's built-in functions for data validation and sanitization. +- Use prepare() statements for secure database queries. +- Use Yoda conditions ALWAYS. +- PHP inline comments must start with capital letter and end with period character. +- PHP inline comments should be concise. + +JavaScript +- Don't use jQuery. Better Vanilla JavaScript. + +Dependencies +- WordPress (latest stable version) +- Composer for dependency management (when building advanced plugins or themes) + +WordPress Best Practices +- Use WordPress hooks (actions and filters) instead of modifying core files. +- Implement proper theme functions using functions.php. +- Use WordPress's built-in user roles and capabilities system. +- Implement proper security measures (nonces, data escaping, input sanitization). + +Key Conventions +1. Follow WordPress's plugin API for extending functionality. +2. Use WordPress's template hierarchy for theme development. +3. Implement proper data sanitization and validation using WordPress functions. +4. Use WordPress's template tags and conditional tags in themes. +5. Implement proper database queries using $wpdb or WP_Query. +6. Use WordPress's authentication and authorization functions. +7. Implement proper AJAX handling using admin-ajax.php or REST API. +8. Use WordPress's hook system for modular and extensible code. +9. Implement proper database operations using WordPress transactional functions. +10. Use WordPress's WP_Cron API for scheduling tasks. \ No newline at end of file diff --git a/.distignore b/.distignore new file mode 100644 index 0000000..38aa1c8 --- /dev/null +++ b/.distignore @@ -0,0 +1,19 @@ +.distignore +.editorconfig +.git +.gitignore +.travis.yml +circle.yml +.DS_Store +.github +.cursor +composer.lock +.wordpress-org +*.sql +*.tar.gz +*.zip +phpstan.neon.dist +.phpcs.xml.dist +.phplint.yml +tests/ +languages/ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..02bd6d3 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,18 @@ +name: Deploy to WordPress.org +on: + push: + tags: + - "*" +jobs: + tag: + name: New tag + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@master + - run: "composer install --no-dev" + - name: WordPress Plugin Deploy + uses: 10up/action-wordpress-plugin-deploy@stable + env: + SVN_PASSWORD: ${{ secrets.SVN_PASSWORD_CTECH }} + SVN_USERNAME: ${{ secrets.SVN_USERNAME_CTECH }} \ No newline at end of file diff --git a/.github/workflows/php-lint.yml b/.github/workflows/php-lint.yml new file mode 100644 index 0000000..8917772 --- /dev/null +++ b/.github/workflows/php-lint.yml @@ -0,0 +1,65 @@ +name: PHP Code Linting + +on: + push: + branches: + - main + - 'release/**' + # Only run if PHP-related files changed. + paths: + - '.github/workflows/php-lint.yml' + - '**.php' + - '.phpcs.xml.dist' + - 'phpstan.neon.dist' + - 'composer.json' + - 'composer.lock' + pull_request: + branches: + - main + - 'release/**' + - 'feature/**' + # Only run if PHP-related files changed. + paths: + - '.github/workflows/php-lint.yml' + - '**.php' + - '.phpcs.xml.dist' + - 'phpstan.neon.dist' + - 'composer.json' + - 'composer.lock' + types: + - opened + - reopened + - synchronize + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/trunk' }} +jobs: + php-lint: + name: PHP + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + + - name: Validate Composer configuration + run: composer validate + + - name: Install PHP dependencies + uses: ramsey/composer-install@a2636af0004d1c0499ffca16ac0b4cc94df70565 + with: + composer-options: '--prefer-dist' + + - name: PHP Lint + run: composer lint + + - name: PHP PHPStan + run: composer phpstan + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f838fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# WordPress +vendor/ +node_modules/ + +# Numerous always-ignore extensions +*.log +*.zip +.DS_Store + +# OS or Editor folders +._*.cache + diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000..ea8fe35 --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,47 @@ + + + Generally-applicable sniffs for WordPress plugins. + + . + /tests/ + /vendor/ + /node_modules/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.phplint.yml b/.phplint.yml new file mode 100644 index 0000000..4b1178c --- /dev/null +++ b/.phplint.yml @@ -0,0 +1,4 @@ +path: . +jobs: 10 +extensions: + - php \ No newline at end of file diff --git a/.wordpress-org/blueprints/blueprint.json b/.wordpress-org/blueprints/blueprint.json new file mode 100644 index 0000000..fe37347 --- /dev/null +++ b/.wordpress-org/blueprints/blueprint.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://playground.wordpress.net/blueprint-schema.json", + "landingPage": "/wp-admin/index.php", + "steps": [ + { + "step": "installPlugin", + "pluginZipFile": { + "resource": "wordpress.org/plugins", + "slug": "knowledge-base-chatbot" + } + }, + { + "step": "login", + "username": "admin", + "password": "password" + } + ] +} diff --git a/.wordpress-org/screenshot-1.png b/.wordpress-org/screenshot-1.png new file mode 100644 index 0000000..feb0107 Binary files /dev/null and b/.wordpress-org/screenshot-1.png differ diff --git a/assets/admin.js b/assets/admin.js new file mode 100644 index 0000000..bd87757 --- /dev/null +++ b/assets/admin.js @@ -0,0 +1,86 @@ +/** + * Multichats Admin Script + * + * @package CLOSE\KnowledgeBaseChatbot + * @author Closemarketing + * @copyright 2025 Closemarketing + */ + +(function($) { + 'use strict'; + + $(document).ready(function() { + let fileFrame; + const i18n = (typeof knowledgeBaseChatbotSettings !== 'undefined' && knowledgeBaseChatbotSettings.i18n) ? knowledgeBaseChatbotSettings.i18n : {}; + + // Handle icon upload button. + $(document).on('click', '.knowledge-base-chatbot-upload-icon', function(e) { + e.preventDefault(); + + const button = $(this); + const fieldId = button.data('field-id'); + const fileInput = $('#' + fieldId); + const preview = button.closest('.knowledge-base-chatbot-icon-field').find('.knowledge-base-chatbot-icon-preview'); + const removeBtn = button.closest('.knowledge-base-chatbot-icon-field').find('.knowledge-base-chatbot-remove-icon'); + + // If the media frame already exists, reopen it. + if (fileFrame) { + fileFrame.open(); + return; + } + + // Create the media frame. + fileFrame = wp.media({ + title: i18n.mediaTitle || 'Select Icon (SVG or PNG)', + button: { + text: i18n.mediaButton || 'Use this icon', + }, + multiple: false, + library: { + type: ['image/svg+xml', 'image/png'], + }, + }); + + // When a file is selected, run a callback. + fileFrame.on('select', function() { + const attachment = fileFrame.state().get('selection').first().toJSON(); + + // Validate that it's an SVG or PNG file. + const allowedMimes = ['image/svg+xml', 'image/png']; + if (attachment.mime && !allowedMimes.includes(attachment.mime)) { + alert(i18n.svgOrPngOnly || 'Please select only SVG or PNG files.'); + return; + } + + // Check file extension as fallback. + const filename = attachment.filename ? attachment.filename.toLowerCase() : ''; + if (filename && !filename.endsWith('.svg') && !filename.endsWith('.png')) { + alert(i18n.svgOrPngOnly || 'Please select only SVG or PNG files.'); + return; + } + + fileInput.val(attachment.id); + preview.html('' + (i18n.chatIconAlt || 'Chat icon') + ''); + removeBtn.show(); + }); + + // Open the modal. + fileFrame.open(); + }); + + // Handle icon remove button. + $(document).on('click', '.knowledge-base-chatbot-remove-icon', function(e) { + e.preventDefault(); + + const button = $(this); + const fieldId = button.data('field-id'); + const fileInput = $('#' + fieldId); + const preview = button.closest('.knowledge-base-chatbot-icon-field').find('.knowledge-base-chatbot-icon-preview'); + + fileInput.val(''); + preview.html('

' + (i18n.noIconSelected || 'No icon selected. Default icon will be used.') + '

'); + button.hide(); + }); + }); +})(jQuery); + diff --git a/assets/generate.css b/assets/generate.css new file mode 100644 index 0000000..d2b530f --- /dev/null +++ b/assets/generate.css @@ -0,0 +1,306 @@ +/* Generate Page Styles */ + +.kbcb-generate-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 30px; + margin-top: 30px; +} + +@media (max-width: 1200px) { + .kbcb-generate-container { + grid-template-columns: 1fr; + } +} + +/* Selection Section */ +.kbcb-selection-section { + background: #fff; + padding: 20px; + border: 1px solid #ccd0d4; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); +} + +.kbcb-selection-section h2 { + margin-top: 0; + margin-bottom: 20px; + font-size: 18px; +} + +/* Tabs */ +.kbcb-tabs .nav-tab-wrapper { + margin-bottom: 0; + border-bottom: 1px solid #ccd0d4; +} + +.kbcb-tab-content { + display: none; + padding: 20px 0; +} + +.kbcb-tab-content.kbcb-tab-active { + display: block; +} + +/* Items List */ +.kbcb-items-list { + max-height: 400px; + overflow-y: auto; + border: 1px solid #ddd; + padding: 10px; + margin-bottom: 15px; + background: #f9f9f9; +} + +.kbcb-item { + display: flex; + align-items: center; + padding: 12px; + margin-bottom: 5px; + background: #fff; + border: 1px solid #e5e5e5; + border-radius: 3px; + cursor: pointer; + transition: all 0.2s; +} + +.kbcb-item:hover { + background: #f0f0f0; + border-color: #0073aa; +} + +.kbcb-item.kbcb-item-selected { + background: #e8f5e9; + border-color: #4caf50; +} + +.kbcb-item input[type="checkbox"] { + margin: 0 10px 0 0; + flex-shrink: 0; +} + +.kbcb-item-title { + font-weight: 600; + color: #23282d; + flex: 1; +} + +.kbcb-item-type { + font-size: 12px; + color: #666; + padding: 2px 8px; + background: #f0f0f0; + border-radius: 3px; + margin-left: 10px; +} + +/* Controls */ +.kbcb-controls { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +/* Selected Section */ +.kbcb-selected-section { + background: #fff; + padding: 20px; + border: 1px solid #ccd0d4; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); +} + +.kbcb-selected-section h2 { + margin-top: 0; + margin-bottom: 10px; + font-size: 18px; +} + +.kbcb-selected-section > .description { + margin-bottom: 20px; + color: #666; +} + +/* Selected List */ +.kbcb-selected-list { + min-height: 200px; + max-height: 500px; + overflow-y: auto; + border: 1px solid #ddd; + padding: 10px; + margin-bottom: 20px; + background: #f9f9f9; +} + +.kbcb-empty-message { + text-align: center; + color: #666; + padding: 40px 20px; + font-style: italic; +} + +.kbcb-selected-item { + display: flex; + align-items: center; + padding: 12px; + margin-bottom: 8px; + background: #fff; + border: 1px solid #e5e5e5; + border-radius: 3px; + transition: all 0.2s; +} + +.kbcb-selected-item:hover { + border-color: #0073aa; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.kbcb-drag-handle { + cursor: move; + color: #999; + margin-right: 12px; + font-size: 18px; +} + +.kbcb-drag-handle:hover { + color: #0073aa; +} + +.kbcb-selected-item-info { + flex: 1; + display: flex; + flex-direction: column; +} + +.kbcb-selected-item-title { + font-weight: 600; + color: #23282d; + margin-bottom: 4px; +} + +.kbcb-selected-item-type { + font-size: 12px; + color: #666; +} + +.wp-core-ui .button.kbcb-remove-item { + padding: 10px 8px 0; + min-width: auto; + height: auto; + background: transparent; + border: none; + color: #dc3232; + cursor: pointer; + transition: all 0.2s; +} + +.kbcb-remove-item:hover { + color: #a00; + background: #ffe5e5; +} + +.kbcb-remove-item .dashicons { + font-size: 18px; + width: 18px; + height: 18px; +} + +/* Sortable Placeholder */ +.kbcb-selected-item-placeholder { + background: #e3f2fd; + border: 2px dashed #2196f3; + margin-bottom: 8px; + height: 50px; + border-radius: 3px; +} + +/* Actions */ +.kbcb-actions { + display: flex; + gap: 10px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +/* File Info */ +.kbcb-file-info { + background: #f0f6fc; + border: 1px solid #c3e0f7; + border-radius: 4px; + padding: 15px; + margin-bottom: 20px; +} + +.kbcb-file-info h3 { + margin: 0 0 10px 0; + font-size: 14px; + color: #0073aa; +} + +.kbcb-file-url { + display: flex; + gap: 10px; + margin-bottom: 10px; +} + +.kbcb-file-url input[type="text"] { + flex: 1; + padding: 8px 12px; + border: 1px solid #ccd0d4; + border-radius: 3px; + background: #fff; + font-family: monospace; + font-size: 13px; +} + +.wp-core-ui .button.kbcb-copy-url { + padding: 12px 12px 0; + min-width: auto; +} + +.wp-core-ui .button.kbcb-copy-url .dashicons { + font-size: 16px; + width: 16px; + height: 16px; +} + +.kbcb-file-date { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: rgba(0, 115, 170, 0.05); + border-radius: 3px; + font-size: 13px; +} + +.kbcb-file-date .dashicons { + color: #0073aa; + font-size: 16px; + width: 16px; + height: 16px; +} + +.kbcb-file-date strong { + color: #23282d; +} + +.kbcb-file-date-utc { + color: #666; + font-size: 11px; + text-transform: uppercase; +} + +/* Message */ +.kbcb-message { + margin-top: 15px; +} + +.kbcb-message p { + margin: 0.5em 0; +} + +/* Loading States */ +button[disabled] { + opacity: 0.6; + cursor: not-allowed !important; +} diff --git a/assets/generate.js b/assets/generate.js new file mode 100644 index 0000000..6ec908b --- /dev/null +++ b/assets/generate.js @@ -0,0 +1,323 @@ +jQuery(document).ready(function ($) { + const i18n = (typeof kbcbGenerate !== 'undefined' && kbcbGenerate.i18n) ? kbcbGenerate.i18n : {}; + + // Tab switching + $('.nav-tab-wrapper .nav-tab').on('click', function (e) { + e.preventDefault(); + const tab = $(this).data('tab'); + + // Update tab navigation. + $('.nav-tab-wrapper .nav-tab').removeClass('nav-tab-active'); + $(this).addClass('nav-tab-active'); + + // Update tab content. + $('.kbcb-tab-content').removeClass('kbcb-tab-active'); + $('#kbcb-tab-' + tab).addClass('kbcb-tab-active'); + }); + + // Select all items. + $('.kbcb-select-all').on('click', function () { + const type = $(this).data('type'); + $('.kbcb-item-checkbox[data-type="' + type + '"]').not(':disabled').prop('checked', true); + }); + + // Deselect all items. + $('.kbcb-deselect-all').on('click', function () { + const type = $(this).data('type'); + $('.kbcb-item-checkbox[data-type="' + type + '"]').prop('checked', false); + }); + + // Add selected items. + $('.kbcb-add-selected').on('click', function () { + const type = $(this).data('type'); + const selectedItems = $('.kbcb-item-checkbox[data-type="' + type + '"]:checked') + .map(function () { + return $(this).val(); + }) + .get(); + + if (selectedItems.length === 0) { + showMessage(i18n.selectAtLeastOne || 'Please select at least one item.', 'error'); + return; + } + + addPages(selectedItems); + }); + + // Remove item from selected list. + $(document).on('click', '.kbcb-remove-item', function () { + const postId = $(this).data('id'); + removePage(postId); + }); + + // Save order button. + $('#kbcb-save-order').on('click', function () { + saveOrder(); + }); + + // Regenerate button. + $('#kbcb-regenerate').on('click', function () { + regenerateFile(); + }); + + // Copy URL button. + $(document).on('click', '.kbcb-copy-url', function () { + const $input = $(this).prev('input'); + $input.select(); + document.execCommand('copy'); + showMessage(i18n.urlCopied || 'URL copied to clipboard.', 'success'); + }); + + // Make selected list sortable. + if ($('#kbcb-selected-list').length) { + $('#kbcb-selected-list').sortable({ + handle: '.kbcb-drag-handle', + placeholder: 'kbcb-selected-item-placeholder', + update: function () { + // Optional: Auto-save on reorder. + // saveOrder(); + } + }); + } + + /** + * Add pages to selected list + * + * @param {Array} postIds Array of post IDs. + */ + function addPages(postIds) { + const $button = $('.kbcb-add-selected'); + const originalText = $button.text(); + $button.prop('disabled', true).text(i18n.adding || 'Adding...'); + + $.ajax({ + url: kbcbGenerate.ajaxUrl, + type: 'POST', + data: { + action: 'kbcb_add_pages', + nonce: kbcbGenerate.nonce, + post_ids: postIds + }, + success: function (response) { + if (response.success) { + showMessage(response.data.message, 'success'); + + // Update file info if provided. + updateFileInfo(response.data.fileUrl, response.data.fileDate); + + // Reload page to update selected list. + setTimeout(function() { + location.reload(); + }, 1000); + } else { + showMessage(response.data.message || (i18n.addError || 'Error adding items.'), 'error'); + } + }, + error: function () { + showMessage(i18n.serverError || 'Communication error with the server.', 'error'); + }, + complete: function () { + $button.prop('disabled', false).text(originalText); + } + }); + } + + /** + * Remove page from selected list + * + * @param {number} postId Post ID. + */ + function removePage(postId) { + if (!confirm(i18n.removeConfirm || 'Are you sure you want to remove this item from the list?')) { + return; + } + + $.ajax({ + url: kbcbGenerate.ajaxUrl, + type: 'POST', + data: { + action: 'kbcb_remove_page', + nonce: kbcbGenerate.nonce, + post_id: postId + }, + success: function (response) { + if (response.success) { + // Remove item from DOM. + $('.kbcb-selected-item[data-id="' + postId + '"]').fadeOut(function () { + $(this).remove(); + + // Show empty message if no items left. + if ($('.kbcb-selected-item').length === 0) { + $('#kbcb-selected-list').html('

' + (i18n.emptySelectedItems || 'No items selected. Add items from the tabs above.') + '

'); + } + }); + + // Uncheck checkbox if exists. + $('.kbcb-item-checkbox[value="' + postId + '"]').prop('checked', false).closest('.kbcb-item').removeClass('kbcb-item-selected'); + + // Update file info. + updateFileInfo(response.data.fileUrl, response.data.fileDate); + + showMessage(response.data.message, 'success'); + } else { + showMessage(response.data.message || (i18n.removeError || 'Error removing item.'), 'error'); + } + }, + error: function () { + showMessage(i18n.serverError || 'Communication error with the server.', 'error'); + } + }); + } + + /** + * Save order of selected pages + */ + function saveOrder() { + const order = []; + $('.kbcb-selected-item').each(function () { + order.push($(this).data('id')); + }); + + if (order.length === 0) { + showMessage(i18n.noItemsToSave || 'There are no items to save.', 'error'); + return; + } + + const $button = $('#kbcb-save-order'); + const originalText = $button.text(); + $button.prop('disabled', true).text(i18n.saving || 'Saving...'); + + $.ajax({ + url: kbcbGenerate.ajaxUrl, + type: 'POST', + data: { + action: 'kbcb_save_order', + nonce: kbcbGenerate.nonce, + order: order + }, + success: function (response) { + if (response.success) { + showMessage(response.data.message, 'success'); + } else { + showMessage(response.data.message || (i18n.saveOrderError || 'Error saving order.'), 'error'); + } + }, + error: function () { + showMessage(i18n.serverError || 'Communication error with the server.', 'error'); + }, + complete: function () { + $button.prop('disabled', false).text(originalText); + } + }); + } + + /** + * Regenerate markdown file + */ + function regenerateFile() { + if (!confirm(i18n.regenerateConfirm || 'Are you sure you want to regenerate the file? This will overwrite the existing file.')) { + return; + } + + const $button = $('#kbcb-regenerate'); + const originalText = $button.text(); + $button.prop('disabled', true).text(i18n.generating || 'Generating...'); + + $.ajax({ + url: kbcbGenerate.ajaxUrl, + type: 'POST', + data: { + action: 'kbcb_regenerate', + nonce: kbcbGenerate.nonce + }, + success: function (response) { + if (response.success) { + showMessage(response.data.message, 'success'); + + // Update file info. + updateFileInfo(response.data.fileUrl, response.data.fileDate); + } else { + showMessage(response.data.message || (i18n.generateFileError || 'Error generating file.'), 'error'); + } + }, + error: function () { + showMessage(i18n.serverError || 'Communication error with the server.', 'error'); + }, + complete: function () { + $button.prop('disabled', false).text(originalText); + } + }); + } + + /** + * Update file info + * + * @param {string} fileUrl File URL. + * @param {string} fileDate File date. + */ + function updateFileInfo(fileUrl, fileDate) { + if (!fileUrl) { + return; + } + + // Update or create file info section. + if ($('.kbcb-file-info').length) { + $('.kbcb-file-url input').val(fileUrl); + if (fileDate && $('#kbcb-file-date-value').length) { + $('#kbcb-file-date-value').text(fileDate); + } else if (fileDate) { + // Add date if it doesn't exist. + $('.kbcb-file-url').after(` +
+ + ${i18n.lastUpdated || 'Last updated:'} + ${fileDate} + ${i18n.utc || 'UTC'} +
+ `); + } + } else { + // Create new file info section before the description. + const fileHtml = ` +
+

${i18n.generatedFile || 'Generated file'}

+
+ + +
+ ${fileDate ? ` +
+ + ${i18n.lastUpdated || 'Last updated:'} + ${fileDate} + ${i18n.utc || 'UTC'} +
+ ` : ''} +
+ `; + $('.kbcb-selected-section h2').after(fileHtml); + } + } + + /** + * Show message + * + * @param {string} message Message text. + * @param {string} type Message type (success, error, warning). + */ + function showMessage(message, type) { + const $message = $('#kbcb-message'); + $message + .removeClass('notice-success notice-error notice-warning') + .addClass('notice notice-' + type + ' is-dismissible') + .html('

' + message + '

') + .show(); + + setTimeout(function () { + $message.fadeOut(); + }, 5000); + } +}); + diff --git a/assets/knowledge-base-chatbot-admin.css b/assets/knowledge-base-chatbot-admin.css new file mode 100644 index 0000000..46cde01 --- /dev/null +++ b/assets/knowledge-base-chatbot-admin.css @@ -0,0 +1,144 @@ +.knowledge-base-chatbot-export-tabs { + margin-top: 30px; +} + +.knowledge-base-chatbot-export-tabs .nav-tab-wrapper { + margin-bottom: 0; + border-bottom: 1px solid #ccd0d4; +} + +.knowledge-base-chatbot-tab-content { + display: none; + padding: 20px; + background: #fff; + border: 1px solid #ccd0d4; + border-top: none; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); +} + +.knowledge-base-chatbot-tab-content.knowledge-base-chatbot-tab-active { + display: block; +} + +.knowledge-base-chatbot-export-section { + margin-top: 0; + padding: 0; + background: transparent; + border: none; + box-shadow: none; +} + +.knowledge-base-chatbot-export-controls { + margin-bottom: 20px; +} + +.knowledge-base-chatbot-export-controls .button { + margin-right: 10px; +} + +.knowledge-base-chatbot-items-list { + max-height: 400px; + overflow-y: auto; + border: 1px solid #ddd; + padding: 10px; + margin-bottom: 20px; + background: #f9f9f9; +} + +.knowledge-base-chatbot-item { + display: block; + padding: 10px; + margin-bottom: 5px; + background: #fff; + border: 1px solid #e5e5e5; + border-radius: 3px; + cursor: pointer; + transition: background-color 0.2s; +} + +.knowledge-base-chatbot-item:hover { + background: #f0f0f0; +} + +.knowledge-base-chatbot-item input[type="checkbox"] { + margin-right: 10px; +} + +.knowledge-base-chatbot-item-title { + font-weight: 600; + color: #23282d; + display: block; + margin-bottom: 5px; +} + +.knowledge-base-chatbot-item-url { + font-size: 12px; + color: #666; + display: block; +} + +.knowledge-base-chatbot-export-buttons { + margin-top: 20px; +} + +.knowledge-base-chatbot-export-buttons .button { + margin-right: 10px; +} + +.knowledge-base-chatbot-export-message { + margin-top: 15px; +} + +.knowledge-base-chatbot-export-message p { + margin: 0.5em 0; +} + +/* Legacy support for old class names */ +.knowledge-base-chatbot-pages-list { + max-height: 400px; + overflow-y: auto; + border: 1px solid #ddd; + padding: 10px; + margin-bottom: 20px; + background: #f9f9f9; +} + +.knowledge-base-chatbot-page-item { + display: block; + padding: 10px; + margin-bottom: 5px; + background: #fff; + border: 1px solid #e5e5e5; + border-radius: 3px; + cursor: pointer; + transition: background-color 0.2s; +} + +.knowledge-base-chatbot-page-item:hover { + background: #f0f0f0; +} + +.knowledge-base-chatbot-page-item input[type="checkbox"] { + margin-right: 10px; +} + +.knowledge-base-chatbot-page-title { + font-weight: 600; + color: #23282d; + display: block; + margin-bottom: 5px; +} + +.knowledge-base-chatbot-page-url { + font-size: 12px; + color: #666; + display: block; +} + +.knowledge-base-chatbot-message { + margin-top: 15px; +} + +.knowledge-base-chatbot-message p { + margin: 0.5em 0; +} diff --git a/assets/knowledge-base-chatbot-admin.js b/assets/knowledge-base-chatbot-admin.js new file mode 100644 index 0000000..d3980d1 --- /dev/null +++ b/assets/knowledge-base-chatbot-admin.js @@ -0,0 +1,194 @@ +jQuery(document).ready(function ($) { + const i18n = (typeof knowledgeBaseChatbotAdmin !== 'undefined' && knowledgeBaseChatbotAdmin.i18n) ? knowledgeBaseChatbotAdmin.i18n : {}; + + // Tab switching + $('.nav-tab-wrapper .nav-tab').on('click', function (e) { + e.preventDefault(); + const tab = $(this).data('tab'); + + // Update tab navigation + $('.nav-tab-wrapper .nav-tab').removeClass('nav-tab-active'); + $(this).addClass('nav-tab-active'); + + // Update tab content + $('.knowledge-base-chatbot-tab-content').removeClass('knowledge-base-chatbot-tab-active'); + $('#knowledge-base-chatbot-tab-' + tab).addClass('knowledge-base-chatbot-tab-active'); + }); + + // Select all items (pages or posts) + $('.knowledge-base-chatbot-select-all').on('click', function () { + const type = $(this).data('type'); + $('.knowledge-base-chatbot-item-checkbox[data-type="' + type + '"]').prop('checked', true); + }); + + // Deselect all items (pages or posts) + $('.knowledge-base-chatbot-deselect-all').on('click', function () { + const type = $(this).data('type'); + $('.knowledge-base-chatbot-item-checkbox[data-type="' + type + '"]').prop('checked', false); + }); + + // Export selected items (any post type) + $('.knowledge-base-chatbot-export-selected').on('click', function () { + const type = $(this).data('type'); + const selectedItems = $('.knowledge-base-chatbot-item-checkbox[data-type="' + type + '"]:checked') + .map(function () { + return $(this).val(); + }) + .get(); + + if (selectedItems.length === 0) { + showMessage( + i18n.selectAtLeastOneToExport || 'Please select at least one item to export.', + 'error', + type + ); + return; + } + + exportSelected(type, selectedItems); + }); + + // Export all items (any post type) + $('.knowledge-base-chatbot-export-all').on('click', function () { + const type = $(this).data('type'); + + if ( + !confirm( + i18n.confirmExportAll || 'Are you sure you want to export all items of this type?' + ) + ) { + return; + } + + exportAll(type); + }); + + /** + * Export selected items (generic for any post type) + * + * @param {string} postType Post type. + * @param {Array} postIds Array of post IDs. + */ + function exportSelected(postType, postIds) { + const $button = $('.knowledge-base-chatbot-export-selected[data-type="' + postType + '"]'); + const originalText = $button.text(); + $button.prop('disabled', true).text(i18n.exporting || 'Exporting...'); + + // Create form and submit + const form = $('
', { + method: 'POST', + action: knowledgeBaseChatbotAdmin.ajaxUrl, + }); + + form.append( + $('', { + type: 'hidden', + name: 'action', + value: 'knowledge-base-chatbot_export_selected', + }) + ); + + form.append( + $('', { + type: 'hidden', + name: 'nonce', + value: knowledgeBaseChatbotAdmin.nonce, + }) + ); + + form.append( + $('', { + type: 'hidden', + name: 'post_type', + value: postType, + }) + ); + + postIds.forEach(function (postId) { + form.append( + $('', { + type: 'hidden', + name: 'post_ids[]', + value: postId, + }) + ); + }); + + $('body').append(form); + form.submit(); + form.remove(); + + setTimeout(function () { + $button.prop('disabled', false).text(originalText); + }, 2000); + } + + /** + * Export all items (generic for any post type) + * + * @param {string} postType Post type. + */ + function exportAll(postType) { + const $button = $('.knowledge-base-chatbot-export-all[data-type="' + postType + '"]'); + const originalText = $button.text(); + $button.prop('disabled', true).text(i18n.exporting || 'Exporting...'); + + // Create form and submit + const form = $('', { + method: 'POST', + action: knowledgeBaseChatbotAdmin.ajaxUrl, + }); + + form.append( + $('', { + type: 'hidden', + name: 'action', + value: 'knowledge-base-chatbot_export_all', + }) + ); + + form.append( + $('', { + type: 'hidden', + name: 'nonce', + value: knowledgeBaseChatbotAdmin.nonce, + }) + ); + + form.append( + $('', { + type: 'hidden', + name: 'post_type', + value: postType, + }) + ); + + $('body').append(form); + form.submit(); + form.remove(); + + setTimeout(function () { + $button.prop('disabled', false).text(originalText); + }, 2000); + } + + /** + * Show message + * + * @param {string} message Message text. + * @param {string} type Message type (success, error, warning). + * @param {string} itemType Item type (pages or posts). + */ + function showMessage(message, type, itemType) { + const $message = $('.knowledge-base-chatbot-export-message[data-type="' + itemType + '"]'); + $message + .removeClass('notice-success notice-error notice-warning') + .addClass('notice notice-' + type + ' is-dismissible') + .html('

' + message + '

') + .show(); + + setTimeout(function () { + $message.fadeOut(); + }, 5000); + } +}); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..723be69 --- /dev/null +++ b/composer.json @@ -0,0 +1,40 @@ +{ + "name": "close/knowledge-base-chatbot", + "description": "Plugin for knowledge base chatbot", + "type": "wordpress-plugin", + "license": "GPL-2.0+", + "authors": [ + { + "name": "Closemarketing", + "email": "info@close.marketing" + } + ], + "require": { + "php": ">=7.4" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "wp-coding-standards/wpcs": "^3.0", + "szepeviktor/phpstan-wordpress": "^1.0", + "phpstan/extension-installer": "^1.0", + "php-stubs/wordpress-stubs": "^6.0", + "phpcompatibility/phpcompatibility-wp": "^2.1" + }, + "autoload": { + "psr-4": { + "CLOSE\\KnowledgeBaseChatbot\\": "includes/" + } + }, + "scripts": { + "format": "phpcbf --standard=.phpcs.xml.dist", + "lint": "phpcs --standard=.phpcs.xml.dist", + "phpstan": "phpstan analyse --memory-limit=2048M" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "phpstan/extension-installer": true + } + } +} + diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..a902dde --- /dev/null +++ b/composer.lock @@ -0,0 +1,945 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "4b71347c78f7ae7edcac86c018675d52", + "packages": [], + "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-11T04:32:07+00:00" + }, + { + "name": "php-stubs/wordpress-stubs", + "version": "v6.9.0", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "5171cb6650e6c583a96943fd6ea0dfa3e1089a8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/5171cb6650e6c583a96943fd6ea0dfa3e1089a8a", + "reference": "5171cb6650e6c583a96943fd6ea0dfa3e1089a8a", + "shasum": "" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^5.5", + "php": "^7.4 || ^8.0", + "php-stubs/generator": "^0.8.3", + "phpdocumentor/reflection-docblock": "^5.4.1", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.5", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/wordpress-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.0" + }, + "time": "2025-12-03T23:06:24+00:00" + }, + { + "name": "phpcompatibility/php-compatibility", + "version": "9.3.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, + "time": "2019-12-27T09:44:58+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-paragonie", + "version": "1.3.4", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "paragonie/random_compat": "dev-master", + "paragonie/sodium_compat": "dev-master" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "paragonie", + "phpcs", + "polyfill", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-09-19T17:43:28+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-wp", + "version": "2.1.8", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/7c8d18b4d90dac9e86b0869a608fa09158e168fa", + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0", + "phpcompatibility/phpcompatibility-paragonie": "^1.0", + "squizlabs/php_codesniffer": "^3.3" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityWP/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-10-18T00:05:59+00:00" + }, + { + "name": "phpcsstandards/phpcsextra", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "b598aa890815b8df16363271b659d73280129101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101", + "reference": "b598aa890815b8df16363271b659d73280129101", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.2.0", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "phpcsstandards/phpcsdevtools": "^1.2.1", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-12T23:06:57+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "d71128c702c180ca3b27c761b6773f883394f162" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/d71128c702c180ca3b27c761b6773f883394f162", + "reference": "d71128c702c180ca3b27c761b6773f883394f162", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPCSUtils/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + } + ], + "description": "A suite of utility functions for use with PHP_CodeSniffer", + "homepage": "https://phpcsutils.com/", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "phpcs3", + "phpcs4", + "standards", + "static analysis", + "tokens", + "utility" + ], + "support": { + "docs": "https://phpcsutils.com/", + "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSUtils" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-17T12:58:33+00:00" + }, + { + "name": "phpstan/extension-installer", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.9.0 || ^2.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2.0", + "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPStan\\ExtensionInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPStan\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer plugin for automatic installation of PHPStan extensions", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/extension-installer/issues", + "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" + }, + "time": "2024-09-04T20:21:43+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.12.32", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", + "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-09-30T10:16:31+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-04T16:30:35+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "szepeviktor/phpstan-wordpress", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/szepeviktor/phpstan-wordpress.git", + "reference": "7f8cfe992faa96b6a33bbd75c7bace98864161e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/7f8cfe992faa96b6a33bbd75c7bace98864161e7", + "reference": "7f8cfe992faa96b6a33bbd75c7bace98864161e7", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "php-stubs/wordpress-stubs": "^4.7 || ^5.0 || ^6.0", + "phpstan/phpstan": "^1.10.31", + "symfony/polyfill-php73": "^1.12.0" + }, + "require-dev": { + "composer/composer": "^2.1.14", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/phpstan-strict-rules": "^1.2", + "phpunit/phpunit": "^8.0 || ^9.0", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "SzepeViktor\\PHPStan\\WordPress\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress extensions for PHPStan", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.5" + }, + "time": "2024-06-28T22:27:19+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", + "php": ">=7.2", + "phpcsstandards/phpcsextra": "^1.5.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.4" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^10.0.0@dev", + "phpcsstandards/phpcsdevtools": "^1.2.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "suggest": { + "ext-iconv": "For improved results", + "ext-mbstring": "For improved results" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/php_codesniffer", + "type": "custom" + } + ], + "time": "2025-11-25T12:08:04+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.4" + }, + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/includes/Admin/Export.php b/includes/Admin/Export.php new file mode 100644 index 0000000..882e5b1 --- /dev/null +++ b/includes/Admin/Export.php @@ -0,0 +1,538 @@ +export_posts_to_markdown( $page_ids, 'page' ); + + if ( empty( $markdown ) ) { + wp_die( esc_html__( 'No content to export.', 'knowledge-base-chatbot' ) ); + } + + // Send file. + $filename = 'knowledge-base-chatbot-export-pages-' . gmdate( 'Y-m-d-H-i-s' ) . '.md'; + header( 'Content-Type: text/markdown; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); + header( 'Content-Length: ' . strlen( $markdown ) ); + echo $markdown; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + exit; + } + + /** + * Handle export of all pages + * + * @return void + */ + public function handle_export_all_pages() { + // Check nonce. + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'knowledge-base-chatbot_export' ) ) { + wp_die( esc_html__( 'Security check failed.', 'knowledge-base-chatbot' ) ); + } + + // Check permissions. + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You do not have permission to export pages.', 'knowledge-base-chatbot' ) ); + } + + // Get all pages. + $pages = get_pages( + array( + 'post_status' => 'publish', + 'number' => -1, + ) + ); + + if ( empty( $pages ) ) { + wp_die( esc_html__( 'No pages found.', 'knowledge-base-chatbot' ) ); + } + + $page_ids = array_map( + function ( $page ) { + return $page->ID; + }, + $pages + ); + + $markdown = $this->export_posts_to_markdown( $page_ids, 'page' ); + + if ( empty( $markdown ) ) { + wp_die( esc_html__( 'No content to export.', 'knowledge-base-chatbot' ) ); + } + + // Send file. + $filename = 'knowledge-base-chatbot-export-all-pages-' . gmdate( 'Y-m-d-H-i-s' ) . '.md'; + header( 'Content-Type: text/markdown; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); + header( 'Content-Length: ' . strlen( $markdown ) ); + echo $markdown; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + exit; + } + + /** + * Handle export of selected posts + * + * @return void + */ + public function handle_export_posts() { + // Check nonce. + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'knowledge-base-chatbot_export' ) ) { + wp_die( esc_html__( 'Security check failed.', 'knowledge-base-chatbot' ) ); + } + + // Check permissions. + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You do not have permission to export posts.', 'knowledge-base-chatbot' ) ); + } + + // Get post IDs. + if ( ! isset( $_POST['post_ids'] ) || ! is_array( $_POST['post_ids'] ) ) { + wp_die( esc_html__( 'No posts selected.', 'knowledge-base-chatbot' ) ); + } + + $post_ids = array_map( 'intval', $_POST['post_ids'] ); + $markdown = $this->export_posts_to_markdown( $post_ids, 'post' ); + + if ( empty( $markdown ) ) { + wp_die( esc_html__( 'No content to export.', 'knowledge-base-chatbot' ) ); + } + + // Send file. + $filename = 'knowledge-base-chatbot-export-posts-' . gmdate( 'Y-m-d-H-i-s' ) . '.md'; + header( 'Content-Type: text/markdown; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); + header( 'Content-Length: ' . strlen( $markdown ) ); + echo $markdown; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + exit; + } + + /** + * Handle export of all posts + * + * @return void + */ + public function handle_export_all_posts() { + // Check nonce. + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'knowledge-base-chatbot_export' ) ) { + wp_die( esc_html__( 'Security check failed.', 'knowledge-base-chatbot' ) ); + } + + // Check permissions. + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You do not have permission to export posts.', 'knowledge-base-chatbot' ) ); + } + + // Get all posts. + $posts = get_posts( + array( + 'post_status' => 'publish', + 'numberposts' => -1, + 'post_type' => 'post', + ) + ); + + if ( empty( $posts ) ) { + wp_die( esc_html__( 'No posts found.', 'knowledge-base-chatbot' ) ); + } + + $post_ids = array_map( + function ( $post ) { + return $post->ID; + }, + $posts + ); + + $markdown = $this->export_posts_to_markdown( $post_ids, 'post' ); + + if ( empty( $markdown ) ) { + wp_die( esc_html__( 'No content to export.', 'knowledge-base-chatbot' ) ); + } + + // Send file. + $filename = 'knowledge-base-chatbot-export-all-posts-' . gmdate( 'Y-m-d-H-i-s' ) . '.md'; + header( 'Content-Type: text/markdown; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); + header( 'Content-Length: ' . strlen( $markdown ) ); + echo $markdown; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + exit; + } + + /** + * Handle export of selected items (generic for any post type) + * + * @return void + */ + public function handle_export_selected() { + // Check nonce. + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'knowledge-base-chatbot_export' ) ) { + wp_die( esc_html__( 'Security check failed.', 'knowledge-base-chatbot' ) ); + } + + // Check permissions. + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You do not have permission to export content.', 'knowledge-base-chatbot' ) ); + } + + // Get post type and IDs. + if ( ! isset( $_POST['post_type'] ) ) { + wp_die( esc_html__( 'Post type not specified.', 'knowledge-base-chatbot' ) ); + } + + if ( ! isset( $_POST['post_ids'] ) || ! is_array( $_POST['post_ids'] ) ) { + wp_die( esc_html__( 'No items selected.', 'knowledge-base-chatbot' ) ); + } + + $post_type = sanitize_text_field( wp_unslash( $_POST['post_type'] ) ); + $post_ids = array_map( 'intval', $_POST['post_ids'] ); + $markdown = $this->export_posts_to_markdown( $post_ids, $post_type ); + + if ( empty( $markdown ) ) { + wp_die( esc_html__( 'No content to export.', 'knowledge-base-chatbot' ) ); + } + + // Get post type label for filename. + $post_type_obj = get_post_type_object( $post_type ); + $type_label = $post_type_obj ? $post_type_obj->labels->name : $post_type; + + // Send file. + $filename = 'knowledge-base-chatbot-export-' . sanitize_file_name( strtolower( $type_label ) ) . '-' . gmdate( 'Y-m-d-H-i-s' ) . '.md'; + header( 'Content-Type: text/markdown; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); + header( 'Content-Length: ' . strlen( $markdown ) ); + echo $markdown; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + exit; + } + + /** + * Handle export of all items (generic for any post type) + * + * @return void + */ + public function handle_export_all() { + // Check nonce. + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'knowledge-base-chatbot_export' ) ) { + wp_die( esc_html__( 'Security check failed.', 'knowledge-base-chatbot' ) ); + } + + // Check permissions. + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You do not have permission to export content.', 'knowledge-base-chatbot' ) ); + } + + // Get post type. + if ( ! isset( $_POST['post_type'] ) ) { + wp_die( esc_html__( 'Post type not specified.', 'knowledge-base-chatbot' ) ); + } + + $post_type = sanitize_text_field( wp_unslash( $_POST['post_type'] ) ); + + // Get all posts of this type. + $posts = get_posts( + array( + 'post_status' => 'publish', + 'numberposts' => -1, + 'post_type' => $post_type, + ) + ); + + if ( empty( $posts ) ) { + $post_type_obj = get_post_type_object( $post_type ); + $type_label = $post_type_obj ? $post_type_obj->labels->name : $post_type; + /* translators: %s: Post type name */ + wp_die( sprintf( esc_html__( 'No %s found.', 'knowledge-base-chatbot' ), esc_html( strtolower( $type_label ) ) ) ); + } + + $post_ids = array_map( + function ( $post ) { + return $post->ID; + }, + $posts + ); + + $markdown = $this->export_posts_to_markdown( $post_ids, $post_type ); + + if ( empty( $markdown ) ) { + wp_die( esc_html__( 'No content to export.', 'knowledge-base-chatbot' ) ); + } + + // Get post type label for filename. + $post_type_obj = get_post_type_object( $post_type ); + $type_label = $post_type_obj ? $post_type_obj->labels->name : $post_type; + + // Send file. + $filename = 'knowledge-base-chatbot-export-all-' . sanitize_file_name( strtolower( $type_label ) ) . '-' . gmdate( 'Y-m-d-H-i-s' ) . '.md'; + header( 'Content-Type: text/markdown; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); + header( 'Content-Length: ' . strlen( $markdown ) ); + echo $markdown; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + exit; + } + + /** + * Export posts/pages to markdown format + * + * @param array $post_ids Array of post/page IDs. + * @param string $post_type Post type ('page' or 'post'). + * @return string Markdown content. + */ + private function export_posts_to_markdown( $post_ids, $post_type = 'page' ) { + $markdown = ''; + $type_label = ( 'page' === $post_type ) ? __( 'Page', 'knowledge-base-chatbot' ) : __( 'Post', 'knowledge-base-chatbot' ); + + foreach ( $post_ids as $post_id ) { + $post = get_post( $post_id ); + + if ( ! $post || $post_type !== $post->post_type ) { + continue; + } + + // Post/Page title. + $markdown .= '# ' . $this->escape_markdown( $post->post_title ) . "\n\n"; + + // Post/Page metadata. + $post_url = get_permalink( $post_id ); + $markdown .= '**' . __( 'Type', 'knowledge-base-chatbot' ) . ':** ' . $type_label . "\n\n"; + $markdown .= '**URL:** ' . $post_url . "\n\n"; + $markdown .= '**' . __( 'Published on', 'knowledge-base-chatbot' ) . ':** ' . get_the_date( 'Y-m-d H:i:s', $post_id ) . "\n\n"; + + // Add author if it's a post. + if ( 'post' === $post_type ) { + $author = get_the_author_meta( 'display_name', $post->post_author ); + if ( $author ) { + $markdown .= '**' . __( 'Author', 'knowledge-base-chatbot' ) . ':** ' . $author . "\n\n"; + } + + // Add categories if it's a post. + $categories = get_the_category( $post_id ); + if ( ! empty( $categories ) ) { + $cat_names = array_map( + function ( $cat ) { + return $cat->name; + }, + $categories + ); + $markdown .= '**' . __( 'Categories', 'knowledge-base-chatbot' ) . ':** ' . implode( ', ', $cat_names ) . "\n\n"; + } + + // Add tags if it's a post. + $tags = get_the_tags( $post_id ); + if ( ! empty( $tags ) ) { + $tag_names = array_map( + function ( $tag ) { + return $tag->name; + }, + $tags + ); + $markdown .= '**' . __( 'Tags', 'knowledge-base-chatbot' ) . ':** ' . implode( ', ', $tag_names ) . "\n\n"; + } + } + + // Post/Page content. + $content = $post->post_content; + + // Convert HTML to markdown-like format. + $content = $this->convert_html_to_markdown( $content ); + + $markdown .= $content . "\n\n"; + + // Separator between posts/pages. + $markdown .= "---\n\n"; + } + + return $markdown; + } + + /** + * Convert HTML content to markdown-like format + * + * @param string $html HTML content. + * @return string Markdown content. + */ + private function convert_html_to_markdown( $html ) { + // Remove WordPress shortcodes. + $html = strip_shortcodes( $html ); + + // Convert blockquotes. + $html = preg_replace( '/]*>(.*?)<\/blockquote>/is', '> $1', $html ); + + // Convert code blocks. + $html = preg_replace( '/]*>]*>(.*?)<\/code><\/pre>/is', '```\n$1\n```', $html ); + $html = preg_replace( '/]*>(.*?)<\/code>/is', '`$1`', $html ); + + // Convert headings. + $html = preg_replace( '/]*>(.*?)<\/h1>/is', "\n# $1\n\n", $html ); + $html = preg_replace( '/]*>(.*?)<\/h2>/is', "\n## $1\n\n", $html ); + $html = preg_replace( '/]*>(.*?)<\/h3>/is', "\n### $1\n\n", $html ); + $html = preg_replace( '/]*>(.*?)<\/h4>/is', "\n#### $1\n\n", $html ); + $html = preg_replace( '/]*>(.*?)<\/h5>/is', "\n##### $1\n\n", $html ); + $html = preg_replace( '/]*>(.*?)<\/h6>/is', "\n###### $1\n\n", $html ); + + // Convert bold. + $html = preg_replace( '/]*>(.*?)<\/strong>/is', '**$1**', $html ); + $html = preg_replace( '/]*>(.*?)<\/b>/is', '**$1**', $html ); + + // Convert italic. + $html = preg_replace( '/]*>(.*?)<\/em>/is', '*$1*', $html ); + $html = preg_replace( '/]*>(.*?)<\/i>/is', '*$1*', $html ); + + // Convert links. + $html = preg_replace( '/]*href=["\']([^"\']*)["\'][^>]*>(.*?)<\/a>/is', '[$2]($1)', $html ); + + // Convert images - handle full URLs. + $html = preg_replace_callback( + '/]*src=["\']([^"\']*)["\'][^>]*alt=["\']([^"\']*)["\'][^>]*>/is', + function ( $matches ) { + $src = $matches[1]; + $alt = $matches[2]; + // Convert relative URLs to absolute. + if ( ! preg_match( '/^https?:\/\//', $src ) ) { + $src = site_url( $src ); + } + return "![{$alt}]({$src})"; + }, + $html + ); + $html = preg_replace_callback( + '/]*src=["\']([^"\']*)["\'][^>]*>/is', + function ( $matches ) { + $src = $matches[1]; + // Convert relative URLs to absolute. + if ( ! preg_match( '/^https?:\/\//', $src ) ) { + $src = site_url( $src ); + } + return "![]({$src})"; + }, + $html + ); + + // Convert ordered lists. + $html = preg_replace_callback( + '/]*>(.*?)<\/ol>/is', + function ( $matches ) { + $content = $matches[1]; + $items = preg_split( '/]*>/', $content ); + $result = "\n"; + $counter = 1; + foreach ( $items as $item ) { + $item = preg_replace( '/<\/li>/', '', $item ); + $item = trim( $item ); + if ( ! empty( $item ) ) { + $result .= $counter . '. ' . $item . "\n"; + $counter++; + } + } + return $result . "\n"; + }, + $html + ); + + // Convert unordered lists. + $html = preg_replace_callback( + '/]*>(.*?)<\/ul>/is', + function ( $matches ) { + $content = $matches[1]; + $items = preg_split( '/]*>/', $content ); + $result = "\n"; + foreach ( $items as $item ) { + $item = preg_replace( '/<\/li>/', '', $item ); + $item = trim( $item ); + if ( ! empty( $item ) ) { + $result .= '- ' . $item . "\n"; + } + } + return $result . "\n"; + }, + $html + ); + + // Convert paragraphs. + $html = preg_replace( '/]*>(.*?)<\/p>/is', '$1' . "\n\n", $html ); + + // Convert line breaks. + $html = preg_replace( '/]*\/?>/is', "\n", $html ); + + // Convert horizontal rules. + $html = preg_replace( '/]*\/?>/is', "\n---\n\n", $html ); + + // Remove remaining HTML tags. + $html = wp_strip_all_tags( $html ); + + // Decode HTML entities. + $html = html_entity_decode( $html, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); + + // Clean up multiple newlines. + $html = preg_replace( '/\n{3,}/', "\n\n", $html ); + + // Trim. + $html = trim( $html ); + + return $html; + } + + /** + * Escape markdown special characters + * + * @param string $text Text to escape. + * @return string Escaped text. + */ + private function escape_markdown( $text ) { + $special_chars = array( '#', '*', '_', '[', ']', '(', ')', '!', '`', '<', '>', '&' ); + foreach ( $special_chars as $char ) { + $text = str_replace( $char, '\\' . $char, $text ); + } + return $text; + } +} diff --git a/includes/Admin/ExportPage.php b/includes/Admin/ExportPage.php new file mode 100644 index 0000000..4bbc746 --- /dev/null +++ b/includes/Admin/ExportPage.php @@ -0,0 +1,221 @@ + admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'knowledge-base-chatbot_export' ), + 'i18n' => array( + 'selectAtLeastOneToExport' => __( 'Please select at least one item to export.', 'knowledge-base-chatbot' ), + 'confirmExportAll' => __( 'Are you sure you want to export all items of this type?', 'knowledge-base-chatbot' ), + 'exporting' => __( 'Exporting...', 'knowledge-base-chatbot' ), + ), + ) + ); + + wp_enqueue_style( + 'knowledge-base-chatbot-admin', + KBCB_PLUGIN_URL . 'assets/knowledge-base-chatbot-admin.css', + array(), + KBCB_VERSION + ); + } + + /** + * Get all public post types (excluding built-in attachments, revisions, etc.) + * + * @return array Array of post type objects. + */ + private function get_exportable_post_types() { + $post_types = get_post_types( + array( + 'public' => true, + 'show_ui' => true, + ), + 'objects' + ); + + // Exclude some built-in types. + $excluded = array( 'attachment', 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset', 'oembed_cache', 'user_request', 'wp_block' ); + + $exportable = array(); + foreach ( $post_types as $post_type ) { + if ( ! in_array( $post_type->name, $excluded, true ) ) { + $exportable[ $post_type->name ] = $post_type; + } + } + + return $exportable; + } + + /** + * Render export page + * + * @return void + */ + public function render_export_page() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $post_types = $this->get_exportable_post_types(); + $first_tab = true; + ?> +
+

+

+ +

+ +
+ + + $post_type_obj ) : + $tab_id = 'knowledge-base-chatbot-tab-' . $post_type_name; + $tab_active = $first_content ? 'knowledge-base-chatbot-tab-active' : ''; + $first_content = false; + + // Get posts for this post type. + $items = get_posts( + array( + 'post_status' => 'publish', + 'numberposts' => -1, + 'post_type' => $post_type_name, + 'orderby' => 'title', + 'order' => 'ASC', + ) + ); + ?> +
+ +
+
+ + +
+ +
+ + + +
+ +
+ + +
+ + +
+ +

+ labels->name ) ) ); + ?> +

+ +
+ +
+
+ admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'kbcb_generate' ), + 'fileUrl' => $this->get_file_url(), + 'fileDate' => $this->get_file_date(), + 'i18n' => array( + 'selectAtLeastOne' => __( 'Please select at least one item.', 'knowledge-base-chatbot' ), + 'urlCopied' => __( 'URL copied to clipboard.', 'knowledge-base-chatbot' ), + 'adding' => __( 'Adding...', 'knowledge-base-chatbot' ), + 'addError' => __( 'Error adding items.', 'knowledge-base-chatbot' ), + 'removeConfirm' => __( 'Are you sure you want to remove this item from the list?', 'knowledge-base-chatbot' ), + 'removeError' => __( 'Error removing item.', 'knowledge-base-chatbot' ), + 'serverError' => __( 'Communication error with the server.', 'knowledge-base-chatbot' ), + 'noItemsToSave' => __( 'There are no items to save.', 'knowledge-base-chatbot' ), + 'saving' => __( 'Saving...', 'knowledge-base-chatbot' ), + 'saveOrderError' => __( 'Error saving order.', 'knowledge-base-chatbot' ), + 'regenerateConfirm' => __( 'Are you sure you want to regenerate the file? This will overwrite the existing file.', 'knowledge-base-chatbot' ), + 'generating' => __( 'Generating...', 'knowledge-base-chatbot' ), + 'generateFileError' => __( 'Error generating file.', 'knowledge-base-chatbot' ), + 'emptySelectedItems' => __( 'No items selected. Add items from the tabs above.', 'knowledge-base-chatbot' ), + 'generatedFile' => __( 'Generated file', 'knowledge-base-chatbot' ), + 'copyUrl' => __( 'Copy URL', 'knowledge-base-chatbot' ), + 'lastUpdated' => __( 'Last updated:', 'knowledge-base-chatbot' ), + 'utc' => __( 'UTC', 'knowledge-base-chatbot' ), + ), + ) + ); + + wp_enqueue_style( + 'knowledge-base-chatbot-generate', + KBCB_PLUGIN_URL . 'assets/generate.css', + array(), + KBCB_VERSION + ); + } + + /** + * Get all public post types + * + * @return array Array of post type objects. + */ + private function get_exportable_post_types() { + $post_types = get_post_types( + array( + 'public' => true, + 'show_ui' => true, + ), + 'objects' + ); + + $excluded = array( 'attachment' ); + $exportable = array(); + foreach ( $post_types as $post_type ) { + if ( ! in_array( $post_type->name, $excluded, true ) ) { + $exportable[ $post_type->name ] = $post_type; + } + } + + return $exportable; + } + + /** + * Get selected pages + * + * @return array Array of selected page IDs with order. + */ + private function get_selected_pages() { + $selected = get_option( self::OPTION_SELECTED_PAGES, array() ); + return is_array( $selected ) ? $selected : array(); + } + + /** + * Save selected pages + * + * @param array $pages Array of page IDs. + * @return bool + */ + private function save_selected_pages( $pages ) { + return update_option( self::OPTION_SELECTED_PAGES, $pages ); + } + + /** + * Get plugin uploads directory path (under wp-content/uploads/knowledge-base-chatbot/) + * + * @return string + */ + private function get_plugin_uploads_path() { + $upload_dir = wp_upload_dir(); + return trailingslashit( $upload_dir['basedir'] ) . self::UPLOADS_SUBFOLDER . '/'; + } + + /** + * Get plugin uploads directory URL + * + * @return string + */ + private function get_plugin_uploads_url() { + $upload_dir = wp_upload_dir(); + return trailingslashit( $upload_dir['baseurl'] ) . self::UPLOADS_SUBFOLDER . '/'; + } + + /** + * Get file URL + * + * @return string + */ + private function get_file_url() { + $file_path = $this->get_plugin_uploads_path() . self::GENERATED_FILE_NAME; + if ( file_exists( $file_path ) ) { + return $this->get_plugin_uploads_url() . self::GENERATED_FILE_NAME; + } + return ''; + } + + /** + * Get file modification date + * + * @return string + */ + private function get_file_date() { + $file_path = $this->get_plugin_uploads_path() . self::GENERATED_FILE_NAME; + if ( file_exists( $file_path ) ) { + return gmdate( 'Y-m-d H:i:s', filemtime( $file_path ) ); + } + return ''; + } + + /** + * Handle add pages AJAX + * + * @return void + */ + public function handle_add_pages() { + check_ajax_referer( 'kbcb_generate', 'nonce' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'knowledge-base-chatbot' ) ) ); + } + + $post_ids = isset( $_POST['post_ids'] ) ? array_map( 'intval', $_POST['post_ids'] ) : array(); + + if ( empty( $post_ids ) ) { + wp_send_json_error( array( 'message' => __( 'No items were selected.', 'knowledge-base-chatbot' ) ) ); + } + + $selected = $this->get_selected_pages(); + + // Add new pages to the end if they don't exist. + foreach ( $post_ids as $post_id ) { + if ( ! in_array( $post_id, $selected, true ) ) { + $selected[] = $post_id; + } + } + + $this->save_selected_pages( $selected ); + + // Auto-regenerate file when pages are added. + $this->generate_markdown_file(); + + // Get page details for response. + $pages = array(); + foreach ( $selected as $post_id ) { + $post = get_post( $post_id ); + if ( $post ) { + $pages[] = array( + 'id' => $post_id, + 'title' => $post->post_title, + 'type' => get_post_type_object( $post->post_type )->labels->singular_name, + ); + } + } + + wp_send_json_success( + array( + 'message' => __( 'Items added successfully. File regenerated.', 'knowledge-base-chatbot' ), + 'pages' => $pages, + 'fileUrl' => $this->get_file_url(), + 'fileDate' => $this->get_file_date(), + ) + ); + } + + /** + * Handle remove page AJAX + * + * @return void + */ + public function handle_remove_page() { + check_ajax_referer( 'kbcb_generate', 'nonce' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'knowledge-base-chatbot' ) ) ); + } + + $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0; + + if ( ! $post_id ) { + wp_send_json_error( array( 'message' => __( 'Invalid item ID.', 'knowledge-base-chatbot' ) ) ); + } + + $selected = $this->get_selected_pages(); + $key = array_search( $post_id, $selected, true ); + + if ( false !== $key ) { + unset( $selected[ $key ] ); + $selected = array_values( $selected ); // Reindex array. + $this->save_selected_pages( $selected ); + + // Auto-regenerate file when page is removed. + $this->generate_markdown_file(); + } + + wp_send_json_success( + array( + 'message' => __( 'Item removed successfully. File regenerated.', 'knowledge-base-chatbot' ), + 'fileUrl' => $this->get_file_url(), + 'fileDate' => $this->get_file_date(), + ) + ); + } + + /** + * Handle save order AJAX + * + * @return void + */ + public function handle_save_order() { + check_ajax_referer( 'kbcb_generate', 'nonce' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'knowledge-base-chatbot' ) ) ); + } + + $order = isset( $_POST['order'] ) ? array_map( 'intval', $_POST['order'] ) : array(); + + if ( empty( $order ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid order.', 'knowledge-base-chatbot' ) ) ); + } + + $this->save_selected_pages( $order ); + + wp_send_json_success( array( 'message' => __( 'Order saved successfully.', 'knowledge-base-chatbot' ) ) ); + } + + /** + * Handle regenerate AJAX + * + * @return void + */ + public function handle_regenerate() { + check_ajax_referer( 'kbcb_generate', 'nonce' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'knowledge-base-chatbot' ) ) ); + } + + $result = $this->generate_markdown_file(); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( array( 'message' => $result->get_error_message() ) ); + } + + wp_send_json_success( + array( + 'message' => __( 'File generated successfully.', 'knowledge-base-chatbot' ), + 'fileUrl' => $this->get_file_url(), + 'fileDate' => $this->get_file_date(), + ) + ); + } + + /** + * Generate markdown file + * + * @return true|\WP_Error + */ + private function generate_markdown_file() { + $selected = $this->get_selected_pages(); + + if ( empty( $selected ) ) { + return new \WP_Error( 'no_pages', __( 'No items selected.', 'knowledge-base-chatbot' ) ); + } + + $markdown = "# Knowledge Base\n\n"; + $markdown .= 'Generated on: ' . gmdate( 'Y-m-d H:i:s' ) . " UTC\n\n"; + $markdown .= "---\n\n"; + + foreach ( $selected as $post_id ) { + $post = get_post( $post_id ); + if ( ! $post ) { + continue; + } + + $markdown .= '## ' . $post->post_title . "\n\n"; + $markdown .= '**URL:** ' . get_permalink( $post_id ) . "\n\n"; + $markdown .= '**Type:** ' . get_post_type( $post_id ) . "\n\n"; + + // Get content and clean it. + $content = $post->post_content; + + // Strip HTML tags. + $content = wp_strip_all_tags( $content ); + + // Decode HTML entities with proper UTF-8 handling. + $content = html_entity_decode( $content, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); + + // Convert to UTF-8 if needed. + if ( ! mb_check_encoding( $content, 'UTF-8' ) ) { + $content = mb_convert_encoding( $content, 'UTF-8', mb_detect_encoding( $content ) ); + } + + // Clean up whitespace. + $content = preg_replace( '/\n\s*\n/', "\n\n", $content ); + $content = trim( $content ); + + $markdown .= $content . "\n\n"; + $markdown .= "---\n\n"; + } + + // Ensure markdown content is UTF-8. + $markdown = mb_convert_encoding( $markdown, 'UTF-8', 'UTF-8' ); + + $upload_dir = wp_upload_dir(); + if ( ! empty( $upload_dir['error'] ) ) { + return new \WP_Error( 'upload_dir', $upload_dir['error'] ); + } + $plugin_uploads_path = $this->get_plugin_uploads_path(); + if ( ! wp_mkdir_p( $plugin_uploads_path ) ) { + return new \WP_Error( 'dir_error', __( 'Could not create uploads directory.', 'knowledge-base-chatbot' ) ); + } + $file_path = $plugin_uploads_path . self::GENERATED_FILE_NAME; + + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + global $wp_filesystem; + + if ( $wp_filesystem ) { + $result = $wp_filesystem->put_contents( $file_path, $markdown, FS_CHMOD_FILE ); + } else { + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Fallback when WP_Filesystem is unavailable. + $result = file_put_contents( $file_path, $markdown, LOCK_EX ); + } + + if ( false === $result ) { + return new \WP_Error( 'file_error', __( 'Error saving the file.', 'knowledge-base-chatbot' ) ); + } + + return true; + } + + /** + * Render generate page + * + * @return void + */ + public function render_generate_page() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $post_types = $this->get_exportable_post_types(); + $selected = $this->get_selected_pages(); + $first_tab = true; + ?> +
+

+

+ +

+ +
+ +
+

+ +
+ + + $post_type_obj ) : + $tab_id = 'kbcb-tab-' . $post_type_name; + $tab_active = $first_content ? 'kbcb-tab-active' : ''; + $first_content = false; + + // Get posts for this post type. + $items = get_posts( + array( + 'post_status' => 'publish', + 'numberposts' => -1, + 'post_type' => $post_type_name, + 'orderby' => 'title', + 'order' => 'ASC', + ) + ); + ?> +
+ +
+ + ID, $selected, true ); ?> + + +
+
+ + + +
+ +

+ labels->name ) ) ); + ?> +

+ +
+ +
+
+ + +
+ get_file_url() ) : ?> +
+

+
+ + +
+ get_file_date() ) : ?> +
+ + + get_file_date() ); ?> + +
+ +
+ + +

+

+ +

+ +
+ post_type ); + ?> +
+ +
+ post_title ); ?> + labels->singular_name ); ?> +
+ +
+ +

+ +
+ +
+ + +
+ + +
+
+
+ 'string', + 'sanitize_callback' => array( $this, 'sanitize_chat_url' ), + 'default' => '', + ) + ); + + register_setting( + self::OPTION_GROUP, + self::OPTION_ICON, + array( + 'type' => 'integer', + 'sanitize_callback' => array( $this, 'sanitize_icon' ), + 'default' => 0, + ) + ); + + register_setting( + self::OPTION_GROUP, + self::OPTION_ICON_SIZE, + array( + 'type' => 'integer', + 'sanitize_callback' => array( $this, 'sanitize_icon_size' ), + 'default' => 56, + ) + ); + + add_settings_section( + 'knowledge-base-chatbot_main_section', + __( 'Chat Configuration', 'knowledge-base-chatbot' ), + array( $this, 'render_section_description' ), + 'knowledge-base-chatbot' + ); + + add_settings_field( + 'knowledge-base-chatbot_chat_url', + __( 'Chat URL', 'knowledge-base-chatbot' ), + array( $this, 'render_chat_url_field' ), + 'knowledge-base-chatbot', + 'knowledge-base-chatbot_main_section' + ); + + add_settings_field( + 'knowledge-base-chatbot_icon', + __( 'Chat Icon', 'knowledge-base-chatbot' ), + array( $this, 'render_icon_field' ), + 'knowledge-base-chatbot', + 'knowledge-base-chatbot_main_section' + ); + + add_settings_field( + 'knowledge-base-chatbot_icon_size', + __( 'Icon Size (px)', 'knowledge-base-chatbot' ), + array( $this, 'render_icon_size_field' ), + 'knowledge-base-chatbot', + 'knowledge-base-chatbot_main_section' + ); + } + + /** + * Sanitize chat URL + * + * @param string $value Chat URL value. + * @return string + */ + public function sanitize_chat_url( $value ) { + return esc_url_raw( $value ); + } + + /** + * Sanitize icon ID and validate it's an SVG or PNG + * + * @param mixed $value Icon ID value. + * @return int + */ + public function sanitize_icon( $value ) { + $icon_id = absint( $value ); + + if ( 0 === $icon_id ) { + return 0; + } + + // Validate that the attachment is an SVG or PNG. + $mime_type = get_post_mime_type( $icon_id ); + $allowed_mimes = array( 'image/svg+xml', 'image/png' ); + if ( ! in_array( $mime_type, $allowed_mimes, true ) ) { + // If it's not SVG or PNG, reset to 0 and show error. + add_settings_error( + 'knowledge-base-chatbot_messages', + 'knowledge-base-chatbot_icon_error', + __( 'Only SVG or PNG files are allowed for the chat icon.', 'knowledge-base-chatbot' ), + 'error' + ); + return 0; + } + + return $icon_id; + } + + /** + * Sanitize icon size + * + * @param mixed $value Icon size value. + * @return int + */ + public function sanitize_icon_size( $value ) { + $size = absint( $value ); + // Ensure minimum size of 16px and maximum of 200px. + return max( 16, min( 200, $size ) ); + } + + /** + * Render section description + * + * @return void + */ + public function render_section_description() { + echo '

' . esc_html__( 'Configure the chat URL that will be displayed on your website.', 'knowledge-base-chatbot' ) . '

'; + } + + /** + * Render chat URL field + * + * @return void + */ + public function render_chat_url_field() { + $value = get_option( self::OPTION_NAME, '' ); + ?> + +

+ +

+ +
+ +
+ + <?php esc_attr_e( 'Chat Icon', 'knowledge-base-chatbot' ); ?> + +

+ +
+ + +

+ +

+
+ + +

+ +

+ +
+

+ + + +
+ array( + 'mediaTitle' => __( 'Select Icon (SVG or PNG)', 'knowledge-base-chatbot' ), + 'mediaButton' => __( 'Use this icon', 'knowledge-base-chatbot' ), + 'svgOrPngOnly' => __( 'Please select only SVG or PNG files.', 'knowledge-base-chatbot' ), + 'noIconSelected' => __( 'No icon selected. Default icon will be used.', 'knowledge-base-chatbot' ), + 'chatIconAlt' => __( 'Chat icon', 'knowledge-base-chatbot' ), + ), + ) + ); + } + } +} diff --git a/includes/Plugin_Main.php b/includes/Plugin_Main.php new file mode 100644 index 0000000..1bfafa2 --- /dev/null +++ b/includes/Plugin_Main.php @@ -0,0 +1,33 @@ +