From 9a6311598a72a58a6ed51faf34a4c91ecf5ff1d3 Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Fri, 5 Dec 2025 14:20:07 +0100 Subject: [PATCH 1/9] init --- .cursor/rules/development.mdc | 61 +++ .distignore | 22 + .github/workflows/php-lint.yml | 65 +++ .gitignore | 12 + .phpcs.xml.dist | 47 ++ .phplint.yml | 4 + assets/admin.js | 85 +++ assets/icon-chat.svg | 75 +++ assets/multichats-admin.css | 144 +++++ assets/multichats-admin.js | 192 +++++++ assets/multichats.css | 86 +++ assets/multichats.js | 8 + composer.json | 40 ++ composer.lock | 945 +++++++++++++++++++++++++++++++++ includes/Admin/Export.php | 538 +++++++++++++++++++ includes/Admin/ExportPage.php | 216 ++++++++ includes/Admin/Settings.php | 402 ++++++++++++++ includes/Plugin_Main.php | 39 ++ knowledge-base-chatbot.php | 31 ++ phpstan.neon.dist | 12 + tests/bootstrap.php | 83 +++ 21 files changed, 3107 insertions(+) create mode 100644 .cursor/rules/development.mdc create mode 100644 .distignore create mode 100644 .github/workflows/php-lint.yml create mode 100644 .gitignore create mode 100644 .phpcs.xml.dist create mode 100644 .phplint.yml create mode 100644 assets/admin.js create mode 100644 assets/icon-chat.svg create mode 100644 assets/multichats-admin.css create mode 100644 assets/multichats-admin.js create mode 100644 assets/multichats.css create mode 100644 assets/multichats.js create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 includes/Admin/Export.php create mode 100644 includes/Admin/ExportPage.php create mode 100644 includes/Admin/Settings.php create mode 100644 includes/Plugin_Main.php create mode 100644 knowledge-base-chatbot.php create mode 100644 phpstan.neon.dist create mode 100644 tests/bootstrap.php 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..4ae22ea --- /dev/null +++ b/.distignore @@ -0,0 +1,22 @@ +.distignore +.editorconfig +.git +.gitignore +.travis.yml +circle.yml +.DS_Store +.github +.cursor +composer.json +composer.lock +.wordpress-org +*.sql +*.tar.gz +*.zip +*.po +*.pot +phpstan.neon.dist +.phpcs.xml.dist +.phplint.yml +tests/ + 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/assets/admin.js b/assets/admin.js new file mode 100644 index 0000000..a011552 --- /dev/null +++ b/assets/admin.js @@ -0,0 +1,85 @@ +/** + * Multichats Admin Script + * + * @package CLOSE\KnowledgeBaseChatbot + * @author Closemarketing + * @copyright 2025 Closemarketing + */ + +(function($) { + 'use strict'; + + $(document).ready(function() { + let fileFrame; + + // 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: 'Select Icon (SVG or PNG)', + button: { + text: '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('Por favor, selecciona solo archivos SVG o PNG.'); + return; + } + + // Check file extension as fallback. + const filename = attachment.filename ? attachment.filename.toLowerCase() : ''; + if (filename && !filename.endsWith('.svg') && !filename.endsWith('.png')) { + alert('Por favor, selecciona solo archivos SVG o PNG.'); + return; + } + + fileInput.val(attachment.id); + preview.html('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('

No icon selected. Default icon will be used.

'); + button.hide(); + }); + }); +})(jQuery); + diff --git a/assets/icon-chat.svg b/assets/icon-chat.svg new file mode 100644 index 0000000..545fe31 --- /dev/null +++ b/assets/icon-chat.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/multichats-admin.css b/assets/multichats-admin.css new file mode 100644 index 0000000..46cde01 --- /dev/null +++ b/assets/multichats-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/multichats-admin.js b/assets/multichats-admin.js new file mode 100644 index 0000000..ddfd5df --- /dev/null +++ b/assets/multichats-admin.js @@ -0,0 +1,192 @@ +jQuery(document).ready(function ($) { + // 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( + 'Por favor, selecciona al menos un elemento para exportar.', + '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( + '¿Estás seguro de que deseas exportar todos los elementos de este tipo?' + ) + ) { + 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('Exportando...'); + + // Create form and submit + const form = $('
', { + method: 'POST', + action: knowledge-base-chatbotAdmin.ajaxUrl, + }); + + form.append( + $('', { + type: 'hidden', + name: 'action', + value: 'knowledge-base-chatbot_export_selected', + }) + ); + + form.append( + $('', { + type: 'hidden', + name: 'nonce', + value: knowledge-base-chatbotAdmin.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('Exportando...'); + + // Create form and submit + const form = $('', { + method: 'POST', + action: knowledge-base-chatbotAdmin.ajaxUrl, + }); + + form.append( + $('', { + type: 'hidden', + name: 'action', + value: 'knowledge-base-chatbot_export_all', + }) + ); + + form.append( + $('', { + type: 'hidden', + name: 'nonce', + value: knowledge-base-chatbotAdmin.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/assets/multichats.css b/assets/multichats.css new file mode 100644 index 0000000..65fc93d --- /dev/null +++ b/assets/multichats.css @@ -0,0 +1,86 @@ +#multichat-button { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 9999; + background-color: var(--primary-light); + border: none; + border-radius: 50%; + width: 70px; + height: 70px; + padding: 10px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + transition: background-color 0.3s ease; +} + +#multichat-button:hover { + background-color: var(--primary); +} + +#multichat-button svg { + width: 56px; + height: 56px; + fill: white; +} + +#multichat-button img { + width: 56px; + height: 56px; + object-fit: contain; + display: block; +} + +/* Allow inline styles to override default sizes */ +#multichat-icon { + max-width: 100%; + height: auto; +} + +#multichat-iframe { + visibility: hidden; + opacity: 0; + transition: opacity 0.3s ease; + position: fixed; + bottom: 100px; + right: 20px; + width: 400px; + height: 600px; + z-index: 9998; + border: none; + border-radius: 12px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); +} + +#multichat-iframe.active { + visibility: visible; + opacity: 1; +} + +@media (max-width: 600px) { + #multichat-iframe { + width: 90vw; + height: 80vh; + right: 5vw; + bottom: 90px; + border-radius: 12px; + } + + #multichat-button { + width: 60px; + height: 60px; + } + + #multichat-button svg { + width: 28px; + height: 28px; + } + + #multichat-button img { + width: 28px; + height: 28px; + } +} \ No newline at end of file diff --git a/assets/multichats.js b/assets/multichats.js new file mode 100644 index 0000000..9bd3938 --- /dev/null +++ b/assets/multichats.js @@ -0,0 +1,8 @@ +document.addEventListener('DOMContentLoaded', function () { + const button = document.getElementById('multichat-button'); + const iframe = document.getElementById('multichat-iframe'); + + button.addEventListener('click', function () { + iframe.classList.toggle('active'); + }); +}); \ No newline at end of file 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..64c95ca --- /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": "be41eb2b6489912cef5abfd420d6a9ad", + "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.8.3", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "abeb5a8b58fda7ac21f15ee596f302f2959a7114" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/abeb5a8b58fda7ac21f15ee596f302f2959a7114", + "reference": "abeb5a8b58fda7ac21f15ee596f302f2959a7114", + "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.8.3" + }, + "time": "2025-09-30T20:58:47+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..55d935f --- /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 ) ? __( 'Página', 'knowledge-base-chatbot' ) : __( 'Entrada', '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 .= '**Tipo:** ' . $type_label . "\n\n"; + $markdown .= '**URL:** ' . $post_url . "\n\n"; + $markdown .= '**Fecha de publicación:** ' . 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 .= '**Autor:** ' . $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 .= '**Categorías:** ' . 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 .= '**Etiquetas:** ' . 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..8710131 --- /dev/null +++ b/includes/Admin/ExportPage.php @@ -0,0 +1,216 @@ + admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'knowledge-base-chatbot_export' ), + ) + ); + + wp_enqueue_style( + 'knowledge-base-chatbot-admin', + MULTICHATS_PLUGIN_URL . 'assets/knowledge-base-chatbot-admin.css', + array(), + MULTICHATS_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 ) ) ); + ?> +

+ +
+ +
+
+ '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', + __( 'Solo se permiten archivos SVG o PNG para el icono del chat.', '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' ); ?> + +

+ +
+ + +

+ +

+
+ + +

+ +

+ +
+

+ + + +
+ Date: Fri, 5 Dec 2025 16:04:12 +0100 Subject: [PATCH 2/9] fix --- ...dmin.css => knowledge-base-chatbot-admin.css} | 0 ...-admin.js => knowledge-base-chatbot-admin.js} | 8 ++++---- includes/Admin/ExportPage.php | 12 ++++++------ includes/Admin/Settings.php | 10 +++++----- includes/Plugin_Main.php | 2 -- tests/bootstrap.php | 16 ++++++++-------- 6 files changed, 23 insertions(+), 25 deletions(-) rename assets/{multichats-admin.css => knowledge-base-chatbot-admin.css} (100%) rename assets/{multichats-admin.js => knowledge-base-chatbot-admin.js} (95%) diff --git a/assets/multichats-admin.css b/assets/knowledge-base-chatbot-admin.css similarity index 100% rename from assets/multichats-admin.css rename to assets/knowledge-base-chatbot-admin.css diff --git a/assets/multichats-admin.js b/assets/knowledge-base-chatbot-admin.js similarity index 95% rename from assets/multichats-admin.js rename to assets/knowledge-base-chatbot-admin.js index ddfd5df..401ebd7 100644 --- a/assets/multichats-admin.js +++ b/assets/knowledge-base-chatbot-admin.js @@ -75,7 +75,7 @@ jQuery(document).ready(function ($) { // Create form and submit const form = $('
', { method: 'POST', - action: knowledge-base-chatbotAdmin.ajaxUrl, + action: knowledgeBaseChatbotAdmin.ajaxUrl, }); form.append( @@ -90,7 +90,7 @@ jQuery(document).ready(function ($) { $('', { type: 'hidden', name: 'nonce', - value: knowledge-base-chatbotAdmin.nonce, + value: knowledgeBaseChatbotAdmin.nonce, }) ); @@ -134,7 +134,7 @@ jQuery(document).ready(function ($) { // Create form and submit const form = $('', { method: 'POST', - action: knowledge-base-chatbotAdmin.ajaxUrl, + action: knowledgeBaseChatbotAdmin.ajaxUrl, }); form.append( @@ -149,7 +149,7 @@ jQuery(document).ready(function ($) { $('', { type: 'hidden', name: 'nonce', - value: knowledge-base-chatbotAdmin.nonce, + value: knowledgeBaseChatbotAdmin.nonce, }) ); diff --git a/includes/Admin/ExportPage.php b/includes/Admin/ExportPage.php index 8710131..197acfd 100644 --- a/includes/Admin/ExportPage.php +++ b/includes/Admin/ExportPage.php @@ -51,21 +51,21 @@ public function add_export_page() { * @return void */ public function enqueue_scripts( $hook ) { - if ( 'knowledge-base-chatbot_page_knowledge-base-chatbot-export' !== $hook ) { + if ( 'kb-chatbot_page_knowledge-base-chatbot-export' !== $hook ) { return; } wp_enqueue_script( 'knowledge-base-chatbot-admin', - MULTICHATS_PLUGIN_URL . 'assets/knowledge-base-chatbot-admin.js', + KBCB_PLUGIN_URL . 'assets/knowledge-base-chatbot-admin.js', array( 'jquery' ), - MULTICHATS_VERSION, + KBCB_VERSION, true ); wp_localize_script( 'knowledge-base-chatbot-admin', - 'knowledge-base-chatbotAdmin', + 'knowledgeBaseChatbotAdmin', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'knowledge-base-chatbot_export' ), @@ -74,9 +74,9 @@ public function enqueue_scripts( $hook ) { wp_enqueue_style( 'knowledge-base-chatbot-admin', - MULTICHATS_PLUGIN_URL . 'assets/knowledge-base-chatbot-admin.css', + KBCB_PLUGIN_URL . 'assets/knowledge-base-chatbot-admin.css', array(), - MULTICHATS_VERSION + KBCB_VERSION ); } diff --git a/includes/Admin/Settings.php b/includes/Admin/Settings.php index 518dd6b..2af5e1c 100644 --- a/includes/Admin/Settings.php +++ b/includes/Admin/Settings.php @@ -65,8 +65,8 @@ public function __construct() { public function add_settings_page() { // Main menu page. add_menu_page( - __( 'Multichats', 'knowledge-base-chatbot' ), - __( 'Multichats', 'knowledge-base-chatbot' ), + __( 'Knowledge Base Chatbot', 'knowledge-base-chatbot' ), + __( 'KB Chatbot', 'knowledge-base-chatbot' ), 'manage_options', 'knowledge-base-chatbot', array( $this, 'render_settings_page' ), @@ -360,7 +360,7 @@ public static function get_chat_icon() { } } // Return default icon. - return MULTICHATS_PLUGIN_URL . 'assets/icon-chat.svg'; + return KBCB_PLUGIN_URL . 'assets/icon-chat.svg'; } /** @@ -392,9 +392,9 @@ public function enqueue_admin_scripts( $hook ) { // Enqueue our script for icon upload. wp_enqueue_script( 'knowledge-base-chatbot-admin', - MULTICHATS_PLUGIN_URL . 'assets/admin.js', + KBCB_PLUGIN_URL . 'assets/admin.js', array( 'jquery' ), - MULTICHATS_VERSION, + KBCB_VERSION, true ); } diff --git a/includes/Plugin_Main.php b/includes/Plugin_Main.php index f5c56f4..ed0a2cb 100644 --- a/includes/Plugin_Main.php +++ b/includes/Plugin_Main.php @@ -32,8 +32,6 @@ public function __construct() { new Settings(); new Export(); new ExportPage(); - } else { - new Chat(); } } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 1d1ea00..d339082 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -12,20 +12,20 @@ defined( 'ABSPATH' ) || exit; // Define plugin constants that are used throughout the codebase. -if ( ! defined( 'MULTICHATS_VERSION' ) ) { - define( 'MULTICHATS_VERSION', '1.0.0' ); +if ( ! defined( 'KBCB_VERSION' ) ) { + define( 'KBCB_VERSION', '1.0.0' ); } -if ( ! defined( 'MULTICHATS_PLUGIN' ) ) { - define( 'MULTICHATS_PLUGIN', __FILE__ ); +if ( ! defined( 'KBCB_PLUGIN' ) ) { + define( 'KBCB_PLUGIN', __FILE__ ); } -if ( ! defined( 'MULTICHATS_PLUGIN_URL' ) ) { - define( 'MULTICHATS_PLUGIN_URL', 'http://localhost/wp-content/plugins/knowledge-base-chatbot/' ); +if ( ! defined( 'KBCB_PLUGIN_URL' ) ) { + define( 'KBCB_PLUGIN_URL', 'http://localhost/wp-content/plugins/knowledge-base-chatbot/' ); } -if ( ! defined( 'MULTICHATS_PLUGIN_PATH' ) ) { - define( 'MULTICHATS_PLUGIN_PATH', '/path/to/wordpress/wp-content/plugins/knowledge-base-chatbot/' ); +if ( ! defined( 'KBCB_PLUGIN_PATH' ) ) { + define( 'KBCB_PLUGIN_PATH', '/path/to/wordpress/wp-content/plugins/knowledge-base-chatbot/' ); } // Define WordPress constants that might be missing. From 34057dd8db4d2d7abfd1a2911b9d8281a6ee9a08 Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Fri, 5 Dec 2025 16:21:51 +0100 Subject: [PATCH 3/9] finished css --- assets/generate.css | 306 ++++++++++++++++++ assets/generate.js | 320 +++++++++++++++++++ includes/Admin/GeneratePage.php | 540 ++++++++++++++++++++++++++++++++ includes/Plugin_Main.php | 8 +- 4 files changed, 1168 insertions(+), 6 deletions(-) create mode 100644 assets/generate.css create mode 100644 assets/generate.js create mode 100644 includes/Admin/GeneratePage.php 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..5617e3a --- /dev/null +++ b/assets/generate.js @@ -0,0 +1,320 @@ +jQuery(document).ready(function ($) { + // 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('Por favor, selecciona al menos un elemento.', '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('URL copiada al portapapeles.', '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('Añadiendo...'); + + $.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 || 'Error al añadir páginas.', 'error'); + } + }, + error: function () { + showMessage('Error de comunicación con el servidor.', 'error'); + }, + complete: function () { + $button.prop('disabled', false).text(originalText); + } + }); + } + + /** + * Remove page from selected list + * + * @param {number} postId Post ID. + */ + function removePage(postId) { + if (!confirm('¿Estás seguro de eliminar esta página de la lista?')) { + 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('

No hay páginas seleccionadas. Añade páginas desde las pestañas de arriba.

'); + } + }); + + // 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 || 'Error al eliminar página.', 'error'); + } + }, + error: function () { + showMessage('Error de comunicación con el servidor.', '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('No hay páginas para guardar.', 'error'); + return; + } + + const $button = $('#kbcb-save-order'); + const originalText = $button.text(); + $button.prop('disabled', true).text('Guardando...'); + + $.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 || 'Error al guardar orden.', 'error'); + } + }, + error: function () { + showMessage('Error de comunicación con el servidor.', 'error'); + }, + complete: function () { + $button.prop('disabled', false).text(originalText); + } + }); + } + + /** + * Regenerate markdown file + */ + function regenerateFile() { + if (!confirm('¿Estás seguro de regenerar el archivo? Esto sobrescribirá el archivo existente.')) { + return; + } + + const $button = $('#kbcb-regenerate'); + const originalText = $button.text(); + $button.prop('disabled', true).text('Generando...'); + + $.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 || 'Error al generar archivo.', 'error'); + } + }, + error: function () { + showMessage('Error de comunicación con el servidor.', '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(` +
+ + Última actualización: + ${fileDate} + UTC +
+ `); + } + } else { + // Create new file info section. + const fileHtml = ` +
+

Archivo Generado

+
+ + +
+ ${fileDate ? ` +
+ + Última actualización: + ${fileDate} + UTC +
+ ` : ''} +
+ `; + $('#kbcb-message').before(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/includes/Admin/GeneratePage.php b/includes/Admin/GeneratePage.php new file mode 100644 index 0000000..7600dd4 --- /dev/null +++ b/includes/Admin/GeneratePage.php @@ -0,0 +1,540 @@ + admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'kbcb_generate' ), + 'fileUrl' => $this->get_file_url(), + 'fileDate' => $this->get_file_date(), + ) + ); + + 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 file URL + * + * @return string + */ + private function get_file_url() { + $file_path = ABSPATH . self::GENERATED_FILE_NAME; + if ( file_exists( $file_path ) ) { + return home_url( '/' . self::GENERATED_FILE_NAME ); + } + return ''; + } + + /** + * Get file modification date + * + * @return string + */ + private function get_file_date() { + $file_path = ABSPATH . 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' => __( 'No tienes permisos para realizar esta acción.', '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 se seleccionaron páginas.', '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' => __( 'Páginas añadidas correctamente. Archivo regenerado.', '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' => __( 'No tienes permisos para realizar esta acción.', 'knowledge-base-chatbot' ) ) ); + } + + $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0; + + if ( ! $post_id ) { + wp_send_json_error( array( 'message' => __( 'ID de página inválido.', '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' => __( 'Página eliminada correctamente. Archivo regenerado.', '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' => __( 'No tienes permisos para realizar esta acción.', 'knowledge-base-chatbot' ) ) ); + } + + $order = isset( $_POST['order'] ) ? array_map( 'intval', $_POST['order'] ) : array(); + + if ( empty( $order ) ) { + wp_send_json_error( array( 'message' => __( 'Orden inválido.', 'knowledge-base-chatbot' ) ) ); + } + + $this->save_selected_pages( $order ); + + wp_send_json_success( array( 'message' => __( 'Orden guardado correctamente.', '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' => __( 'No tienes permisos para realizar esta acción.', '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' => __( 'Archivo generado correctamente.', '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 hay páginas seleccionadas.', '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; + $content = wp_strip_all_tags( $content ); + $content = html_entity_decode( $content ); + $content = preg_replace( '/\n\s*\n/', "\n\n", $content ); + + $markdown .= $content . "\n\n"; + $markdown .= "---\n\n"; + } + + // Save file to WordPress root. + $file_path = ABSPATH . self::GENERATED_FILE_NAME; + $result = file_put_contents( $file_path, $markdown ); + + if ( false === $result ) { + return new \WP_Error( 'file_error', __( 'Error al guardar el archivo.', '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 ) ) ); + ?> +

+ +
+ +
+
+ + +
+

+

+ +

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

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

+
+ + +
+ get_file_date() ) : ?> +
+ + + get_file_date() ); ?> + +
+ +
+ + + +
+
+
+ Date: Thu, 22 Jan 2026 11:45:04 +0100 Subject: [PATCH 4/9] moved to english --- .distignore | 5 +- .wordpress-org/screenshot-1.png | Bin 0 -> 66097 bytes assets/admin.js | 13 +- assets/generate.js | 53 +++--- assets/knowledge-base-chatbot-admin.js | 10 +- composer.lock | 14 +- includes/Admin/Export.php | 12 +- includes/Admin/ExportPage.php | 19 ++- includes/Admin/GeneratePage.php | 142 ++++++++++------ includes/Admin/Settings.php | 16 +- languages/knowledge-base-chatbot-es_ES.po | 197 ++++++++++++++++++++++ readme.txt | 75 +++++++- 12 files changed, 447 insertions(+), 109 deletions(-) create mode 100644 .wordpress-org/screenshot-1.png create mode 100644 languages/knowledge-base-chatbot-es_ES.po diff --git a/.distignore b/.distignore index 4ae22ea..38aa1c8 100644 --- a/.distignore +++ b/.distignore @@ -7,16 +7,13 @@ circle.yml .DS_Store .github .cursor -composer.json composer.lock .wordpress-org *.sql *.tar.gz *.zip -*.po -*.pot phpstan.neon.dist .phpcs.xml.dist .phplint.yml tests/ - +languages/ diff --git a/.wordpress-org/screenshot-1.png b/.wordpress-org/screenshot-1.png new file mode 100644 index 0000000000000000000000000000000000000000..feb0107b8b199a191a6c54ed647f507192562c56 GIT binary patch literal 66097 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 M<<;a$WQ>CT58S'); + preview.html('' + (i18n.chatIconAlt || 'Chat icon') + ''); removeBtn.show(); }); @@ -77,7 +78,7 @@ const preview = button.closest('.knowledge-base-chatbot-icon-field').find('.knowledge-base-chatbot-icon-preview'); fileInput.val(''); - preview.html('

No icon selected. Default icon will be used.

'); + preview.html('

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

'); button.hide(); }); }); diff --git a/assets/generate.js b/assets/generate.js index 5617e3a..6ec908b 100644 --- a/assets/generate.js +++ b/assets/generate.js @@ -1,4 +1,6 @@ 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(); @@ -35,7 +37,7 @@ jQuery(document).ready(function ($) { .get(); if (selectedItems.length === 0) { - showMessage('Por favor, selecciona al menos un elemento.', 'error'); + showMessage(i18n.selectAtLeastOne || 'Please select at least one item.', 'error'); return; } @@ -63,7 +65,7 @@ jQuery(document).ready(function ($) { const $input = $(this).prev('input'); $input.select(); document.execCommand('copy'); - showMessage('URL copiada al portapapeles.', 'success'); + showMessage(i18n.urlCopied || 'URL copied to clipboard.', 'success'); }); // Make selected list sortable. @@ -86,7 +88,7 @@ jQuery(document).ready(function ($) { function addPages(postIds) { const $button = $('.kbcb-add-selected'); const originalText = $button.text(); - $button.prop('disabled', true).text('Añadiendo...'); + $button.prop('disabled', true).text(i18n.adding || 'Adding...'); $.ajax({ url: kbcbGenerate.ajaxUrl, @@ -108,11 +110,11 @@ jQuery(document).ready(function ($) { location.reload(); }, 1000); } else { - showMessage(response.data.message || 'Error al añadir páginas.', 'error'); + showMessage(response.data.message || (i18n.addError || 'Error adding items.'), 'error'); } }, error: function () { - showMessage('Error de comunicación con el servidor.', 'error'); + showMessage(i18n.serverError || 'Communication error with the server.', 'error'); }, complete: function () { $button.prop('disabled', false).text(originalText); @@ -126,7 +128,7 @@ jQuery(document).ready(function ($) { * @param {number} postId Post ID. */ function removePage(postId) { - if (!confirm('¿Estás seguro de eliminar esta página de la lista?')) { + if (!confirm(i18n.removeConfirm || 'Are you sure you want to remove this item from the list?')) { return; } @@ -146,7 +148,7 @@ jQuery(document).ready(function ($) { // Show empty message if no items left. if ($('.kbcb-selected-item').length === 0) { - $('#kbcb-selected-list').html('

No hay páginas seleccionadas. Añade páginas desde las pestañas de arriba.

'); + $('#kbcb-selected-list').html('

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

'); } }); @@ -158,11 +160,11 @@ jQuery(document).ready(function ($) { showMessage(response.data.message, 'success'); } else { - showMessage(response.data.message || 'Error al eliminar página.', 'error'); + showMessage(response.data.message || (i18n.removeError || 'Error removing item.'), 'error'); } }, error: function () { - showMessage('Error de comunicación con el servidor.', 'error'); + showMessage(i18n.serverError || 'Communication error with the server.', 'error'); } }); } @@ -177,13 +179,13 @@ jQuery(document).ready(function ($) { }); if (order.length === 0) { - showMessage('No hay páginas para guardar.', 'error'); + 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('Guardando...'); + $button.prop('disabled', true).text(i18n.saving || 'Saving...'); $.ajax({ url: kbcbGenerate.ajaxUrl, @@ -197,11 +199,11 @@ jQuery(document).ready(function ($) { if (response.success) { showMessage(response.data.message, 'success'); } else { - showMessage(response.data.message || 'Error al guardar orden.', 'error'); + showMessage(response.data.message || (i18n.saveOrderError || 'Error saving order.'), 'error'); } }, error: function () { - showMessage('Error de comunicación con el servidor.', 'error'); + showMessage(i18n.serverError || 'Communication error with the server.', 'error'); }, complete: function () { $button.prop('disabled', false).text(originalText); @@ -213,13 +215,13 @@ jQuery(document).ready(function ($) { * Regenerate markdown file */ function regenerateFile() { - if (!confirm('¿Estás seguro de regenerar el archivo? Esto sobrescribirá el archivo existente.')) { + 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('Generando...'); + $button.prop('disabled', true).text(i18n.generating || 'Generating...'); $.ajax({ url: kbcbGenerate.ajaxUrl, @@ -235,11 +237,11 @@ jQuery(document).ready(function ($) { // Update file info. updateFileInfo(response.data.fileUrl, response.data.fileDate); } else { - showMessage(response.data.message || 'Error al generar archivo.', 'error'); + showMessage(response.data.message || (i18n.generateFileError || 'Error generating file.'), 'error'); } }, error: function () { - showMessage('Error de comunicación con el servidor.', 'error'); + showMessage(i18n.serverError || 'Communication error with the server.', 'error'); }, complete: function () { $button.prop('disabled', false).text(originalText); @@ -268,34 +270,34 @@ jQuery(document).ready(function ($) { $('.kbcb-file-url').after(`
- Última actualización: + ${i18n.lastUpdated || 'Last updated:'} ${fileDate} - UTC + ${i18n.utc || 'UTC'}
`); } } else { - // Create new file info section. + // Create new file info section before the description. const fileHtml = `
-

Archivo Generado

+

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

-
${fileDate ? `
- Última actualización: + ${i18n.lastUpdated || 'Last updated:'} ${fileDate} - UTC + ${i18n.utc || 'UTC'}
` : ''}
`; - $('#kbcb-message').before(fileHtml); + $('.kbcb-selected-section h2').after(fileHtml); } } @@ -318,3 +320,4 @@ jQuery(document).ready(function ($) { }, 5000); } }); + diff --git a/assets/knowledge-base-chatbot-admin.js b/assets/knowledge-base-chatbot-admin.js index 401ebd7..d3980d1 100644 --- a/assets/knowledge-base-chatbot-admin.js +++ b/assets/knowledge-base-chatbot-admin.js @@ -1,4 +1,6 @@ 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(); @@ -36,7 +38,7 @@ jQuery(document).ready(function ($) { if (selectedItems.length === 0) { showMessage( - 'Por favor, selecciona al menos un elemento para exportar.', + i18n.selectAtLeastOneToExport || 'Please select at least one item to export.', 'error', type ); @@ -52,7 +54,7 @@ jQuery(document).ready(function ($) { if ( !confirm( - '¿Estás seguro de que deseas exportar todos los elementos de este tipo?' + i18n.confirmExportAll || 'Are you sure you want to export all items of this type?' ) ) { return; @@ -70,7 +72,7 @@ jQuery(document).ready(function ($) { function exportSelected(postType, postIds) { const $button = $('.knowledge-base-chatbot-export-selected[data-type="' + postType + '"]'); const originalText = $button.text(); - $button.prop('disabled', true).text('Exportando...'); + $button.prop('disabled', true).text(i18n.exporting || 'Exporting...'); // Create form and submit const form = $('', { @@ -129,7 +131,7 @@ jQuery(document).ready(function ($) { function exportAll(postType) { const $button = $('.knowledge-base-chatbot-export-all[data-type="' + postType + '"]'); const originalText = $button.text(); - $button.prop('disabled', true).text('Exportando...'); + $button.prop('disabled', true).text(i18n.exporting || 'Exporting...'); // Create form and submit const form = $('', { diff --git a/composer.lock b/composer.lock index 64c95ca..a902dde 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "be41eb2b6489912cef5abfd420d6a9ad", + "content-hash": "4b71347c78f7ae7edcac86c018675d52", "packages": [], "packages-dev": [ { @@ -105,16 +105,16 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.8.3", + "version": "v6.9.0", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "abeb5a8b58fda7ac21f15ee596f302f2959a7114" + "reference": "5171cb6650e6c583a96943fd6ea0dfa3e1089a8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/abeb5a8b58fda7ac21f15ee596f302f2959a7114", - "reference": "abeb5a8b58fda7ac21f15ee596f302f2959a7114", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/5171cb6650e6c583a96943fd6ea0dfa3e1089a8a", + "reference": "5171cb6650e6c583a96943fd6ea0dfa3e1089a8a", "shasum": "" }, "conflict": { @@ -150,9 +150,9 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.8.3" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.0" }, - "time": "2025-09-30T20:58:47+00:00" + "time": "2025-12-03T23:06:24+00:00" }, { "name": "phpcompatibility/php-compatibility", diff --git a/includes/Admin/Export.php b/includes/Admin/Export.php index 55d935f..882e5b1 100644 --- a/includes/Admin/Export.php +++ b/includes/Admin/Export.php @@ -330,7 +330,7 @@ function ( $post ) { */ private function export_posts_to_markdown( $post_ids, $post_type = 'page' ) { $markdown = ''; - $type_label = ( 'page' === $post_type ) ? __( 'Página', 'knowledge-base-chatbot' ) : __( 'Entrada', 'knowledge-base-chatbot' ); + $type_label = ( 'page' === $post_type ) ? __( 'Page', 'knowledge-base-chatbot' ) : __( 'Post', 'knowledge-base-chatbot' ); foreach ( $post_ids as $post_id ) { $post = get_post( $post_id ); @@ -344,15 +344,15 @@ private function export_posts_to_markdown( $post_ids, $post_type = 'page' ) { // Post/Page metadata. $post_url = get_permalink( $post_id ); - $markdown .= '**Tipo:** ' . $type_label . "\n\n"; + $markdown .= '**' . __( 'Type', 'knowledge-base-chatbot' ) . ':** ' . $type_label . "\n\n"; $markdown .= '**URL:** ' . $post_url . "\n\n"; - $markdown .= '**Fecha de publicación:** ' . get_the_date( 'Y-m-d H:i:s', $post_id ) . "\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 .= '**Autor:** ' . $author . "\n\n"; + $markdown .= '**' . __( 'Author', 'knowledge-base-chatbot' ) . ':** ' . $author . "\n\n"; } // Add categories if it's a post. @@ -364,7 +364,7 @@ function ( $cat ) { }, $categories ); - $markdown .= '**Categorías:** ' . implode( ', ', $cat_names ) . "\n\n"; + $markdown .= '**' . __( 'Categories', 'knowledge-base-chatbot' ) . ':** ' . implode( ', ', $cat_names ) . "\n\n"; } // Add tags if it's a post. @@ -376,7 +376,7 @@ function ( $tag ) { }, $tags ); - $markdown .= '**Etiquetas:** ' . implode( ', ', $tag_names ) . "\n\n"; + $markdown .= '**' . __( 'Tags', 'knowledge-base-chatbot' ) . ':** ' . implode( ', ', $tag_names ) . "\n\n"; } } diff --git a/includes/Admin/ExportPage.php b/includes/Admin/ExportPage.php index 197acfd..e2ce946 100644 --- a/includes/Admin/ExportPage.php +++ b/includes/Admin/ExportPage.php @@ -69,6 +69,11 @@ public function enqueue_scripts( $hook ) { array( 'ajaxUrl' => 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' ), + ), ) ); @@ -121,9 +126,9 @@ public function render_export_page() { $first_tab = true; ?>
-

+

- +

@@ -164,10 +169,10 @@ public function render_export_page() {
@@ -185,13 +190,13 @@ public function render_export_page() {
@@ -202,7 +207,7 @@ public function render_export_page() {

labels->name ) ) ); + printf( esc_html__( 'No published %s found to export.', 'knowledge-base-chatbot' ), esc_html( strtolower( $post_type_obj->labels->name ) ) ); ?>

diff --git a/includes/Admin/GeneratePage.php b/includes/Admin/GeneratePage.php index 7600dd4..d9af97a 100644 --- a/includes/Admin/GeneratePage.php +++ b/includes/Admin/GeneratePage.php @@ -93,6 +93,26 @@ public function enqueue_scripts( $hook ) { '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' ), + ), ) ); @@ -184,13 +204,13 @@ public function handle_add_pages() { check_ajax_referer( 'kbcb_generate', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( array( 'message' => __( 'No tienes permisos para realizar esta acción.', 'knowledge-base-chatbot' ) ) ); + 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 se seleccionaron páginas.', 'knowledge-base-chatbot' ) ) ); + wp_send_json_error( array( 'message' => __( 'No items were selected.', 'knowledge-base-chatbot' ) ) ); } $selected = $this->get_selected_pages(); @@ -222,7 +242,7 @@ public function handle_add_pages() { wp_send_json_success( array( - 'message' => __( 'Páginas añadidas correctamente. Archivo regenerado.', 'knowledge-base-chatbot' ), + 'message' => __( 'Items added successfully. File regenerated.', 'knowledge-base-chatbot' ), 'pages' => $pages, 'fileUrl' => $this->get_file_url(), 'fileDate' => $this->get_file_date(), @@ -239,13 +259,13 @@ public function handle_remove_page() { check_ajax_referer( 'kbcb_generate', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( array( 'message' => __( 'No tienes permisos para realizar esta acción.', 'knowledge-base-chatbot' ) ) ); + 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' => __( 'ID de página inválido.', 'knowledge-base-chatbot' ) ) ); + wp_send_json_error( array( 'message' => __( 'Invalid item ID.', 'knowledge-base-chatbot' ) ) ); } $selected = $this->get_selected_pages(); @@ -262,7 +282,7 @@ public function handle_remove_page() { wp_send_json_success( array( - 'message' => __( 'Página eliminada correctamente. Archivo regenerado.', 'knowledge-base-chatbot' ), + 'message' => __( 'Item removed successfully. File regenerated.', 'knowledge-base-chatbot' ), 'fileUrl' => $this->get_file_url(), 'fileDate' => $this->get_file_date(), ) @@ -278,18 +298,18 @@ public function handle_save_order() { check_ajax_referer( 'kbcb_generate', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( array( 'message' => __( 'No tienes permisos para realizar esta acción.', 'knowledge-base-chatbot' ) ) ); + 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' => __( 'Orden inválido.', 'knowledge-base-chatbot' ) ) ); + wp_send_json_error( array( 'message' => __( 'Invalid order.', 'knowledge-base-chatbot' ) ) ); } $this->save_selected_pages( $order ); - wp_send_json_success( array( 'message' => __( 'Orden guardado correctamente.', 'knowledge-base-chatbot' ) ) ); + wp_send_json_success( array( 'message' => __( 'Order saved successfully.', 'knowledge-base-chatbot' ) ) ); } /** @@ -301,7 +321,7 @@ public function handle_regenerate() { check_ajax_referer( 'kbcb_generate', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( array( 'message' => __( 'No tienes permisos para realizar esta acción.', 'knowledge-base-chatbot' ) ) ); + wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'knowledge-base-chatbot' ) ) ); } $result = $this->generate_markdown_file(); @@ -312,7 +332,7 @@ public function handle_regenerate() { wp_send_json_success( array( - 'message' => __( 'Archivo generado correctamente.', 'knowledge-base-chatbot' ), + 'message' => __( 'File generated successfully.', 'knowledge-base-chatbot' ), 'fileUrl' => $this->get_file_url(), 'fileDate' => $this->get_file_date(), ) @@ -328,7 +348,7 @@ private function generate_markdown_file() { $selected = $this->get_selected_pages(); if ( empty( $selected ) ) { - return new \WP_Error( 'no_pages', __( 'No hay páginas seleccionadas.', 'knowledge-base-chatbot' ) ); + return new \WP_Error( 'no_pages', __( 'No items selected.', 'knowledge-base-chatbot' ) ); } $markdown = "# Knowledge Base\n\n"; @@ -347,20 +367,46 @@ private function generate_markdown_file() { // Get content and clean it. $content = $post->post_content; + + // Strip HTML tags. $content = wp_strip_all_tags( $content ); - $content = html_entity_decode( $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"; } - // Save file to WordPress root. + // Ensure markdown content is UTF-8. + $markdown = mb_convert_encoding( $markdown, 'UTF-8', 'UTF-8' ); + + // Save file to WordPress root with UTF-8 encoding. $file_path = ABSPATH . self::GENERATED_FILE_NAME; - $result = file_put_contents( $file_path, $markdown ); + + // Use WP_Filesystem for better compatibility. + 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 { + // Fallback to file_put_contents. + $result = file_put_contents( $file_path, $markdown, LOCK_EX ); + } if ( false === $result ) { - return new \WP_Error( 'file_error', __( 'Error al guardar el archivo.', 'knowledge-base-chatbot' ) ); + return new \WP_Error( 'file_error', __( 'Error saving the file.', 'knowledge-base-chatbot' ) ); } return true; @@ -381,15 +427,15 @@ public function render_generate_page() { $first_tab = true; ?>
-

+

- +

-

+

labels->name ) ) ); + printf( esc_html__( 'No published %s found.', 'knowledge-base-chatbot' ), esc_html( strtolower( $post_type_obj->labels->name ) ) ); ?>

@@ -468,9 +514,29 @@ class="kbcb-item-checkbox"
-

+ get_file_url() ) : ?> +
+

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

- +

@@ -496,7 +562,7 @@ class="kbcb-item-checkbox" endforeach; else : ?> -

+

@@ -504,33 +570,13 @@ class="kbcb-item-checkbox"
- get_file_url() ) : ?> -
-

-
- - -
- get_file_date() ) : ?> -
- - - get_file_date() ); ?> - -
- -
- -
diff --git a/includes/Admin/Settings.php b/includes/Admin/Settings.php index 2af5e1c..41f8a4f 100644 --- a/includes/Admin/Settings.php +++ b/includes/Admin/Settings.php @@ -184,7 +184,7 @@ public function sanitize_icon( $value ) { add_settings_error( 'knowledge-base-chatbot_messages', 'knowledge-base-chatbot_icon_error', - __( 'Solo se permiten archivos SVG o PNG para el icono del chat.', 'knowledge-base-chatbot' ), + __( 'Only SVG or PNG files are allowed for the chat icon.', 'knowledge-base-chatbot' ), 'error' ); return 0; @@ -397,6 +397,20 @@ public function enqueue_admin_scripts( $hook ) { KBCB_VERSION, true ); + + wp_localize_script( + 'knowledge-base-chatbot-admin', + 'knowledgeBaseChatbotSettings', + array( + 'i18n' => 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/languages/knowledge-base-chatbot-es_ES.po b/languages/knowledge-base-chatbot-es_ES.po new file mode 100644 index 0000000..66dcd50 --- /dev/null +++ b/languages/knowledge-base-chatbot-es_ES.po @@ -0,0 +1,197 @@ +msgid "" +msgstr "" +"Project-Id-Version: Knowledge Base Chatbot 1.0.0\n" +"POT-Creation-Date: 2026-01-22 00:00+0000\n" +"PO-Revision-Date: 2026-01-22 00:00+0000\n" +"Language-Team: Spanish (Spain)\n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Domain: knowledge-base-chatbot\n" + +#. Admin / Generator (PHP). +msgid "Generate Knowledge Base" +msgstr "Generar Knowledge Base" + +msgid "Select the content you want to include in the knowledge file and reorder it as needed." +msgstr "Selecciona las páginas que deseas incluir en el archivo de conocimiento y ordénalas según tus preferencias." + +msgid "Select content" +msgstr "Seleccionar Páginas" + +msgid "Select all" +msgstr "Seleccionar todas" + +msgid "Deselect all" +msgstr "Deseleccionar todas" + +msgid "Add selected" +msgstr "Añadir seleccionadas" + +msgid "No published %s found." +msgstr "No hay %s publicados." + +msgid "Generated file" +msgstr "Archivo Generado" + +msgid "Copy URL" +msgstr "Copiar URL" + +msgid "Last updated:" +msgstr "Última actualización:" + +msgid "Selected items" +msgstr "Páginas Seleccionadas" + +msgid "Drag and drop to reorder items." +msgstr "Arrastra para reordenar las páginas según tus preferencias." + +msgid "No items selected. Add items from the tabs above." +msgstr "No hay páginas seleccionadas. Añade páginas desde las pestañas de arriba." + +msgid "Save order" +msgstr "Guardar Orden" + +msgid "Regenerate file" +msgstr "Regenerar Archivo" + +#. Admin / Generator (AJAX responses). +msgid "You do not have permission to perform this action." +msgstr "No tienes permisos para realizar esta acción." + +msgid "No items were selected." +msgstr "No se seleccionaron páginas." + +msgid "Items added successfully. File regenerated." +msgstr "Páginas añadidas correctamente. Archivo regenerado." + +msgid "Invalid item ID." +msgstr "ID de página inválido." + +msgid "Item removed successfully. File regenerated." +msgstr "Página eliminada correctamente. Archivo regenerado." + +msgid "Invalid order." +msgstr "Orden inválido." + +msgid "Order saved successfully." +msgstr "Orden guardado correctamente." + +msgid "File generated successfully." +msgstr "Archivo generado correctamente." + +msgid "No items selected." +msgstr "No hay páginas seleccionadas." + +msgid "Error saving the file." +msgstr "Error al guardar el archivo." + +#. Admin / Export (PHP). +msgid "Export to Markdown" +msgstr "Exportar a Markdown" + +msgid "Select the content you want to export to Markdown, or export all items for a given content type." +msgstr "Selecciona las páginas, entradas o contenido personalizado que deseas exportar en formato Markdown o exporta todo el contenido de la web." + +msgid "Export selected %s" +msgstr "Exportar %s seleccionados" + +msgid "Export all %s" +msgstr "Exportar todos los %s" + +msgid "No published %s found to export." +msgstr "No hay %s publicados para exportar." + +#. Exported Markdown labels. +msgid "Page" +msgstr "Página" + +msgid "Post" +msgstr "Entrada" + +msgid "Type" +msgstr "Tipo" + +msgid "Published on" +msgstr "Fecha de publicación" + +msgid "Author" +msgstr "Autor" + +msgid "Categories" +msgstr "Categorías" + +msgid "Tags" +msgstr "Etiquetas" + +#. Settings. +msgid "Only SVG or PNG files are allowed for the chat icon." +msgstr "Solo se permiten archivos SVG o PNG para el icono del chat." + +#. Admin / Generator (JS). +msgid "Please select at least one item." +msgstr "Por favor, selecciona al menos un elemento." + +msgid "URL copied to clipboard." +msgstr "URL copiada al portapapeles." + +msgid "Adding..." +msgstr "Añadiendo..." + +msgid "Error adding items." +msgstr "Error al añadir páginas." + +msgid "Are you sure you want to remove this item from the list?" +msgstr "¿Estás seguro de eliminar esta página de la lista?" + +msgid "Error removing item." +msgstr "Error al eliminar página." + +msgid "Communication error with the server." +msgstr "Error de comunicación con el servidor." + +msgid "There are no items to save." +msgstr "No hay páginas para guardar." + +msgid "Saving..." +msgstr "Guardando..." + +msgid "Error saving order." +msgstr "Error al guardar orden." + +msgid "Are you sure you want to regenerate the file? This will overwrite the existing file." +msgstr "¿Estás seguro de regenerar el archivo? Esto sobrescribirá el archivo existente." + +msgid "Generating..." +msgstr "Generando..." + +msgid "Error generating file." +msgstr "Error al generar archivo." + +#. Admin / Export (JS). +msgid "Please select at least one item to export." +msgstr "Por favor, selecciona al menos un elemento para exportar." + +msgid "Are you sure you want to export all items of this type?" +msgstr "¿Estás seguro de que deseas exportar todos los elementos de este tipo?" + +msgid "Exporting..." +msgstr "Exportando..." + +#. Settings (JS). +msgid "Select Icon (SVG or PNG)" +msgstr "Seleccionar icono (SVG o PNG)" + +msgid "Use this icon" +msgstr "Usar este icono" + +msgid "Please select only SVG or PNG files." +msgstr "Por favor, selecciona solo archivos SVG o PNG." + +msgid "No icon selected. Default icon will be used." +msgstr "No se ha seleccionado ningún icono. Se usará el icono por defecto." + +msgid "Chat icon" +msgstr "Icono del chat" diff --git a/readme.txt b/readme.txt index 35746d6..d41716e 100644 --- a/readme.txt +++ b/readme.txt @@ -1 +1,74 @@ -== Knowledge Base Chatbot == \ No newline at end of file +=== Knowledge Base Chatbot === +Contributors: closemarketing +Tags: knowledge base, chatbot, ai, llm, markdown, export, documentation +Requires at least: 5.8 +Tested up to: 6.4 +Requires PHP: 7.4 +Stable tag: 1.0.0 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +Generate a Markdown knowledge base file from selected WordPress content (Pages, Posts, and custom post types) to power an external chatbot / LLM workflow. + +== Description == + +Knowledge Base Chatbot helps you curate the exact content you want to use as a "knowledge base" and generates a single Markdown file (`llm-knowledge-chatbot.md`) that can be consumed by an external chatbot or LLM pipeline. + +From the WordPress admin, you can: + +- Select content from any public post type that is available in the admin (Pages, Posts, and most custom post types). +- Add selected items to a curated list. +- Reorder the list via drag & drop. +- Regenerate the Markdown file at any time (it is also regenerated automatically after adding/removing items). + +The generated file is saved in your WordPress root folder and can be accessed via a public URL such as: +`https://example.com/llm-knowledge-chatbot.md` + +== Installation == + +1. Upload the plugin folder to the `/wp-content/plugins/` directory, or install the plugin through the WordPress Plugins screen. +2. Activate the plugin through the "Plugins" screen in WordPress. +3. In wp-admin, open **KB Chatbot** from the left menu. +4. Select the content you want to include and click **Add selected**. +5. Reorder the selected items and click **Save order** (optional). +6. Click **Regenerate file** to create/update `llm-knowledge-chatbot.md`. + +== Frequently Asked Questions == + += Where is the Markdown file saved? = +The file is saved in the WordPress installation root, using the filename `llm-knowledge-chatbot.md`. + += Does the file become publicly accessible? = +Yes. Since the file is created in the web root, it can be accessible at `https://your-site.com/llm-knowledge-chatbot.md` (depending on your server configuration). + +If you need this file to be private, you should restrict access at the server level (recommended). + += What content types can I include? = +The generator lists public post types that are visible in the admin. This usually includes Pages, Posts, and custom post types created by your theme/plugins (excluding media attachments). + += Does this plugin send my content to any external service? = +No. The plugin generates a local Markdown file on your server. It does not upload content anywhere by itself. + += What permissions are required to use the generator? = +Only administrators (users with the `manage_options` capability) can access the generator and perform actions. + += The file is not being generated. What should I check? = +- Ensure your WordPress root folder is writable by WordPress / PHP. +- Confirm you have selected at least one item. + +== Screenshots == + +1. KB Chatbot admin page (content selection by post type). +2. Selected items list with drag & drop ordering. +3. Generated file URL and last updated time. + +== Changelog == + += 1.0.0 = +* Initial release. +* Admin generator to curate content and create `llm-knowledge-chatbot.md`. + +== Upgrade Notice == + += 1.0.0 = +Initial release. \ No newline at end of file From 472c99625a27e6ced6a852c332217b049210779d Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Thu, 22 Jan 2026 11:58:39 +0100 Subject: [PATCH 5/9] minor fixes --- .wordpress-org/blueprints/blueprint.json | 18 +++++ assets/icon-chat.svg | 75 --------------------- assets/multichats.css | 86 ------------------------ assets/multichats.js | 8 --- knowledge-base-chatbot.php | 4 +- readme.txt | 8 +-- 6 files changed, 24 insertions(+), 175 deletions(-) create mode 100644 .wordpress-org/blueprints/blueprint.json delete mode 100644 assets/icon-chat.svg delete mode 100644 assets/multichats.css delete mode 100644 assets/multichats.js 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/assets/icon-chat.svg b/assets/icon-chat.svg deleted file mode 100644 index 545fe31..0000000 --- a/assets/icon-chat.svg +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/assets/multichats.css b/assets/multichats.css deleted file mode 100644 index 65fc93d..0000000 --- a/assets/multichats.css +++ /dev/null @@ -1,86 +0,0 @@ -#multichat-button { - position: fixed; - bottom: 20px; - right: 20px; - z-index: 9999; - background-color: var(--primary-light); - border: none; - border-radius: 50%; - width: 70px; - height: 70px; - padding: 10px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s ease; -} - -#multichat-button:hover { - background-color: var(--primary); -} - -#multichat-button svg { - width: 56px; - height: 56px; - fill: white; -} - -#multichat-button img { - width: 56px; - height: 56px; - object-fit: contain; - display: block; -} - -/* Allow inline styles to override default sizes */ -#multichat-icon { - max-width: 100%; - height: auto; -} - -#multichat-iframe { - visibility: hidden; - opacity: 0; - transition: opacity 0.3s ease; - position: fixed; - bottom: 100px; - right: 20px; - width: 400px; - height: 600px; - z-index: 9998; - border: none; - border-radius: 12px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); -} - -#multichat-iframe.active { - visibility: visible; - opacity: 1; -} - -@media (max-width: 600px) { - #multichat-iframe { - width: 90vw; - height: 80vh; - right: 5vw; - bottom: 90px; - border-radius: 12px; - } - - #multichat-button { - width: 60px; - height: 60px; - } - - #multichat-button svg { - width: 28px; - height: 28px; - } - - #multichat-button img { - width: 28px; - height: 28px; - } -} \ No newline at end of file diff --git a/assets/multichats.js b/assets/multichats.js deleted file mode 100644 index 9bd3938..0000000 --- a/assets/multichats.js +++ /dev/null @@ -1,8 +0,0 @@ -document.addEventListener('DOMContentLoaded', function () { - const button = document.getElementById('multichat-button'); - const iframe = document.getElementById('multichat-iframe'); - - button.addEventListener('click', function () { - iframe.classList.toggle('active'); - }); -}); \ No newline at end of file diff --git a/knowledge-base-chatbot.php b/knowledge-base-chatbot.php index 07665ef..4d44574 100644 --- a/knowledge-base-chatbot.php +++ b/knowledge-base-chatbot.php @@ -1,8 +1,8 @@ Date: Thu, 22 Jan 2026 12:00:05 +0100 Subject: [PATCH 6/9] author --- knowledge-base-chatbot.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/knowledge-base-chatbot.php b/knowledge-base-chatbot.php index 4d44574..67601bc 100644 --- a/knowledge-base-chatbot.php +++ b/knowledge-base-chatbot.php @@ -4,8 +4,8 @@ * Plugin URI: https://wordpress.org/plugins/knowledge-base-chatbot/ * Description: Generate a Markdown knowledge base file from selected WordPress content (pages, posts, and CPTs) to power an external chatbot/LLM workflow. * Version: 1.0.0 - * Author: Closemarketing - * Author URI: https://close.marketing + * Author: Closetechnology + * Author URI: https://close.technology * Text Domain: knowledge-base-chatbot * Domain Path: /languages * License: GPL-2.0+ From 29385e22e23c3c6b9300d4562d6674277b01c7dc Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Thu, 29 Jan 2026 12:47:04 +0100 Subject: [PATCH 7/9] review fixes --- includes/Admin/GeneratePage.php | 50 +++++++++++++++++++++++++++------ knowledge-base-chatbot.php | 2 +- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/includes/Admin/GeneratePage.php b/includes/Admin/GeneratePage.php index d9af97a..34ceb5d 100644 --- a/includes/Admin/GeneratePage.php +++ b/includes/Admin/GeneratePage.php @@ -32,6 +32,13 @@ class GeneratePage { */ const GENERATED_FILE_NAME = 'llm-knowledge-chatbot.md'; + /** + * Subfolder name under wp-content/uploads for plugin files + * + * @var string + */ + const UPLOADS_SUBFOLDER = 'knowledge-base-chatbot'; + /** * Constructor * @@ -169,15 +176,35 @@ 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 = ABSPATH . self::GENERATED_FILE_NAME; + $file_path = $this->get_plugin_uploads_path() . self::GENERATED_FILE_NAME; if ( file_exists( $file_path ) ) { - return home_url( '/' . self::GENERATED_FILE_NAME ); + return $this->get_plugin_uploads_url() . self::GENERATED_FILE_NAME; } return ''; } @@ -188,7 +215,7 @@ private function get_file_url() { * @return string */ private function get_file_date() { - $file_path = ABSPATH . self::GENERATED_FILE_NAME; + $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 ) ); } @@ -390,18 +417,23 @@ private function generate_markdown_file() { // Ensure markdown content is UTF-8. $markdown = mb_convert_encoding( $markdown, 'UTF-8', 'UTF-8' ); - // Save file to WordPress root with UTF-8 encoding. - $file_path = ABSPATH . self::GENERATED_FILE_NAME; - - // Use WP_Filesystem for better compatibility. + $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 { - // Fallback to file_put_contents. $result = file_put_contents( $file_path, $markdown, LOCK_EX ); } diff --git a/knowledge-base-chatbot.php b/knowledge-base-chatbot.php index 67601bc..0c2be79 100644 --- a/knowledge-base-chatbot.php +++ b/knowledge-base-chatbot.php @@ -7,7 +7,7 @@ * Author: Closetechnology * Author URI: https://close.technology * Text Domain: knowledge-base-chatbot - * Domain Path: /languages + * * License: GPL-2.0+ * License URI: http://www.gnu.org/licenses/gpl-2.0.txt * Prefix: kbcb_ From 00cd114826d36bc5e53201a9aafbabd442e7ebb7 Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Thu, 29 Jan 2026 13:11:51 +0100 Subject: [PATCH 8/9] finish develop --- .github/workflows/deploy.yml | 18 ++ languages/knowledge-base-chatbot-es_ES.po | 197 ---------------------- readme.txt | 2 +- 3 files changed, 19 insertions(+), 198 deletions(-) create mode 100644 .github/workflows/deploy.yml delete mode 100644 languages/knowledge-base-chatbot-es_ES.po 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/languages/knowledge-base-chatbot-es_ES.po b/languages/knowledge-base-chatbot-es_ES.po deleted file mode 100644 index 66dcd50..0000000 --- a/languages/knowledge-base-chatbot-es_ES.po +++ /dev/null @@ -1,197 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: Knowledge Base Chatbot 1.0.0\n" -"POT-Creation-Date: 2026-01-22 00:00+0000\n" -"PO-Revision-Date: 2026-01-22 00:00+0000\n" -"Language-Team: Spanish (Spain)\n" -"Language: es_ES\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Domain: knowledge-base-chatbot\n" - -#. Admin / Generator (PHP). -msgid "Generate Knowledge Base" -msgstr "Generar Knowledge Base" - -msgid "Select the content you want to include in the knowledge file and reorder it as needed." -msgstr "Selecciona las páginas que deseas incluir en el archivo de conocimiento y ordénalas según tus preferencias." - -msgid "Select content" -msgstr "Seleccionar Páginas" - -msgid "Select all" -msgstr "Seleccionar todas" - -msgid "Deselect all" -msgstr "Deseleccionar todas" - -msgid "Add selected" -msgstr "Añadir seleccionadas" - -msgid "No published %s found." -msgstr "No hay %s publicados." - -msgid "Generated file" -msgstr "Archivo Generado" - -msgid "Copy URL" -msgstr "Copiar URL" - -msgid "Last updated:" -msgstr "Última actualización:" - -msgid "Selected items" -msgstr "Páginas Seleccionadas" - -msgid "Drag and drop to reorder items." -msgstr "Arrastra para reordenar las páginas según tus preferencias." - -msgid "No items selected. Add items from the tabs above." -msgstr "No hay páginas seleccionadas. Añade páginas desde las pestañas de arriba." - -msgid "Save order" -msgstr "Guardar Orden" - -msgid "Regenerate file" -msgstr "Regenerar Archivo" - -#. Admin / Generator (AJAX responses). -msgid "You do not have permission to perform this action." -msgstr "No tienes permisos para realizar esta acción." - -msgid "No items were selected." -msgstr "No se seleccionaron páginas." - -msgid "Items added successfully. File regenerated." -msgstr "Páginas añadidas correctamente. Archivo regenerado." - -msgid "Invalid item ID." -msgstr "ID de página inválido." - -msgid "Item removed successfully. File regenerated." -msgstr "Página eliminada correctamente. Archivo regenerado." - -msgid "Invalid order." -msgstr "Orden inválido." - -msgid "Order saved successfully." -msgstr "Orden guardado correctamente." - -msgid "File generated successfully." -msgstr "Archivo generado correctamente." - -msgid "No items selected." -msgstr "No hay páginas seleccionadas." - -msgid "Error saving the file." -msgstr "Error al guardar el archivo." - -#. Admin / Export (PHP). -msgid "Export to Markdown" -msgstr "Exportar a Markdown" - -msgid "Select the content you want to export to Markdown, or export all items for a given content type." -msgstr "Selecciona las páginas, entradas o contenido personalizado que deseas exportar en formato Markdown o exporta todo el contenido de la web." - -msgid "Export selected %s" -msgstr "Exportar %s seleccionados" - -msgid "Export all %s" -msgstr "Exportar todos los %s" - -msgid "No published %s found to export." -msgstr "No hay %s publicados para exportar." - -#. Exported Markdown labels. -msgid "Page" -msgstr "Página" - -msgid "Post" -msgstr "Entrada" - -msgid "Type" -msgstr "Tipo" - -msgid "Published on" -msgstr "Fecha de publicación" - -msgid "Author" -msgstr "Autor" - -msgid "Categories" -msgstr "Categorías" - -msgid "Tags" -msgstr "Etiquetas" - -#. Settings. -msgid "Only SVG or PNG files are allowed for the chat icon." -msgstr "Solo se permiten archivos SVG o PNG para el icono del chat." - -#. Admin / Generator (JS). -msgid "Please select at least one item." -msgstr "Por favor, selecciona al menos un elemento." - -msgid "URL copied to clipboard." -msgstr "URL copiada al portapapeles." - -msgid "Adding..." -msgstr "Añadiendo..." - -msgid "Error adding items." -msgstr "Error al añadir páginas." - -msgid "Are you sure you want to remove this item from the list?" -msgstr "¿Estás seguro de eliminar esta página de la lista?" - -msgid "Error removing item." -msgstr "Error al eliminar página." - -msgid "Communication error with the server." -msgstr "Error de comunicación con el servidor." - -msgid "There are no items to save." -msgstr "No hay páginas para guardar." - -msgid "Saving..." -msgstr "Guardando..." - -msgid "Error saving order." -msgstr "Error al guardar orden." - -msgid "Are you sure you want to regenerate the file? This will overwrite the existing file." -msgstr "¿Estás seguro de regenerar el archivo? Esto sobrescribirá el archivo existente." - -msgid "Generating..." -msgstr "Generando..." - -msgid "Error generating file." -msgstr "Error al generar archivo." - -#. Admin / Export (JS). -msgid "Please select at least one item to export." -msgstr "Por favor, selecciona al menos un elemento para exportar." - -msgid "Are you sure you want to export all items of this type?" -msgstr "¿Estás seguro de que deseas exportar todos los elementos de este tipo?" - -msgid "Exporting..." -msgstr "Exportando..." - -#. Settings (JS). -msgid "Select Icon (SVG or PNG)" -msgstr "Seleccionar icono (SVG o PNG)" - -msgid "Use this icon" -msgstr "Usar este icono" - -msgid "Please select only SVG or PNG files." -msgstr "Por favor, selecciona solo archivos SVG o PNG." - -msgid "No icon selected. Default icon will be used." -msgstr "No se ha seleccionado ningún icono. Se usará el icono por defecto." - -msgid "Chat icon" -msgstr "Icono del chat" diff --git a/readme.txt b/readme.txt index 08afbd3..1131c2e 100644 --- a/readme.txt +++ b/readme.txt @@ -39,7 +39,7 @@ The generated file is saved in your WordPress root folder and can be accessed vi The file is saved in the WordPress installation root, using the filename `llm-knowledge-chatbot.md`. = Does the file become publicly accessible? = -Yes. Since the file is created in the web root, it can be accessible at `https://your-site.com/llm-knowledge-chatbot.md` (depending on your server configuration). +Yes. Since the file is created in the web root, it can be accessible at `https://your-site.com/wp-content/uploads/knowledge-base-chatbot/llm-knowledge-chatbot.md` (depending on your server configuration). If you need this file to be private, you should restrict access at the server level (recommended). From b959c5f829ef873dc6f58e17a12630901a6f6a38 Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Thu, 29 Jan 2026 13:17:53 +0100 Subject: [PATCH 9/9] fixed automated tests --- includes/Admin/ExportPage.php | 2 +- includes/Admin/GeneratePage.php | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/includes/Admin/ExportPage.php b/includes/Admin/ExportPage.php index e2ce946..4bbc746 100644 --- a/includes/Admin/ExportPage.php +++ b/includes/Admin/ExportPage.php @@ -72,7 +72,7 @@ public function enqueue_scripts( $hook ) { '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' ), + 'exporting' => __( 'Exporting...', 'knowledge-base-chatbot' ), ), ) ); diff --git a/includes/Admin/GeneratePage.php b/includes/Admin/GeneratePage.php index 34ceb5d..1cdee70 100644 --- a/includes/Admin/GeneratePage.php +++ b/includes/Admin/GeneratePage.php @@ -270,8 +270,8 @@ public function handle_add_pages() { wp_send_json_success( array( 'message' => __( 'Items added successfully. File regenerated.', 'knowledge-base-chatbot' ), - 'pages' => $pages, - 'fileUrl' => $this->get_file_url(), + 'pages' => $pages, + 'fileUrl' => $this->get_file_url(), 'fileDate' => $this->get_file_date(), ) ); @@ -369,7 +369,7 @@ public function handle_regenerate() { /** * Generate markdown file * - * @return true|WP_Error + * @return true|\WP_Error */ private function generate_markdown_file() { $selected = $this->get_selected_pages(); @@ -378,8 +378,8 @@ private function generate_markdown_file() { 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 = "# 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 ) { @@ -388,24 +388,24 @@ private function generate_markdown_file() { continue; } - $markdown .= "## " . $post->post_title . "\n\n"; - $markdown .= "**URL:** " . get_permalink( $post_id ) . "\n\n"; - $markdown .= "**Type:** " . get_post_type( $post_id ) . "\n\n"; + $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 ); @@ -434,6 +434,7 @@ private function generate_markdown_file() { 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 ); }