From 6cff0b6f585e4ebcb1b1b2572fa217dea06218ae Mon Sep 17 00:00:00 2001 From: Roberto Guagliardo Date: Sat, 31 Jan 2026 17:48:46 +0000 Subject: [PATCH] Add core classes and tests for Groq AI compatibility, logging, and model services - Implement Groq_AI_Compatibility_Service to manage WooCommerce dependency and admin notices. - Create Groq_AI_Log_Scheduler for scheduled log cleanup based on settings. - Develop Groq_AI_Model_Service for model selection and caching. - Add language translations in POT file for Dutch. - Set up PHPUnit configuration and bootstrap for testing. - Implement unit tests for model exclusions, provider request building, settings management, and term saving functionality. --- .phpunit.result.cache | 1 + assets/js/admin.js | 54 +- assets/js/settings.js | 20 +- assets/js/term-admin.js | 20 +- assets/js/term-bulk.js | 27 +- composer.json | 16 + composer.lock | 1815 +++++++++++++++++ groq-ai-product-text.php | 353 +--- includes/Admin/class-groq-ai-logs-table.php | 12 +- includes/Admin/class-groq-ai-product-ui.php | 12 + .../Admin/class-groq-ai-settings-page.php | 166 +- .../Admin/class-groq-ai-term-admin-base.php | 20 +- .../Core/class-groq-ai-ajax-controller.php | 15 +- .../class-groq-ai-compatibility-service.php | 58 + includes/Core/class-groq-ai-log-scheduler.php | 28 + includes/Core/class-groq-ai-model-service.php | 78 + .../class-groq-ai-generation-logger.php | 14 + .../class-groq-ai-settings-manager.php | 19 + .../siti-ai-product-content-generator.pot | 202 ++ phpunit.xml | 13 + tests/ModelExclusionsTest.php | 17 + tests/ProviderRequestBuilderTest.php | 102 + tests/SettingsManagerTest.php | 112 + tests/TermSaveTest.php | 67 + tests/bootstrap.php | 258 +++ 25 files changed, 3131 insertions(+), 368 deletions(-) create mode 100644 .phpunit.result.cache create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 includes/Core/class-groq-ai-compatibility-service.php create mode 100644 includes/Core/class-groq-ai-log-scheduler.php create mode 100644 includes/Core/class-groq-ai-model-service.php create mode 100644 languages/siti-ai-product-content-generator.pot create mode 100644 phpunit.xml create mode 100644 tests/ModelExclusionsTest.php create mode 100644 tests/ProviderRequestBuilderTest.php create mode 100644 tests/SettingsManagerTest.php create mode 100644 tests/TermSaveTest.php create mode 100644 tests/bootstrap.php diff --git a/.phpunit.result.cache b/.phpunit.result.cache new file mode 100644 index 0000000..c365b02 --- /dev/null +++ b/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":1,"defects":{"SettingsManagerTest::test_logs_retention_days_negative_becomes_zero":3,"ProviderRequestBuilderTest::test_openai_request_payload_respects_settings":4,"ProviderRequestBuilderTest::test_groq_request_payload_uses_default_model_when_missing":4,"ProviderRequestBuilderTest::test_google_request_payload_builds_schema_and_images":4,"TermSaveTest::test_save_term_generation_result_saves_descriptions_and_filtered_meta_key":4},"times":{"ModelExclusionsTest::test_ensure_allowed_blocks_excluded_model":0.002,"ModelExclusionsTest::test_filter_models_removes_excluded_entries":0,"SettingsManagerTest::test_logs_retention_days_sanitized_and_capped":0,"SettingsManagerTest::test_logs_retention_days_allows_zero":0,"SettingsManagerTest::test_logs_retention_days_negative_becomes_zero":0,"SettingsManagerTest::test_sanitize_accepts_all_settings_keys":0,"ProviderRequestBuilderTest::test_openai_request_payload_respects_settings":0,"ProviderRequestBuilderTest::test_groq_request_payload_uses_default_model_when_missing":0,"ProviderRequestBuilderTest::test_google_request_payload_builds_schema_and_images":0,"TermSaveTest::test_save_term_generation_result_saves_descriptions_and_filtered_meta_key":0}} \ No newline at end of file diff --git a/assets/js/admin.js b/assets/js/admin.js index 4ca64a8..1b9786f 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -96,8 +96,35 @@ statusField.setAttribute('data-status', type); } - const loadingText = window.wp && wp.i18n ? wp.i18n.__('AI is bezig met schrijven...', 'siti-ai-product-content-generator') : 'AI is bezig met schrijven...'; - const retryText = window.wp && wp.i18n ? wp.i18n.__('Probeer het opnieuw of pas je prompt/context aan.', 'siti-ai-product-content-generator') : 'Probeer het opnieuw of pas je prompt/context aan.'; + const localized = (window.GroqAIGenerator && GroqAIGenerator.strings) || {}; + const loadingText = localized.loading || (window.wp && wp.i18n ? wp.i18n.__('AI is bezig met schrijven...', 'siti-ai-product-content-generator') : 'AI is bezig met schrijven...'); + const retryText = localized.retry || (window.wp && wp.i18n ? wp.i18n.__('Probeer het opnieuw of pas je prompt/context aan.', 'siti-ai-product-content-generator') : 'Probeer het opnieuw of pas je prompt/context aan.'); + const errorDefaultText = localized.errorDefault || (window.wp && wp.i18n ? wp.i18n.__('Er ging iets mis bij het genereren.', 'siti-ai-product-content-generator') : 'Er ging iets mis bij het genereren.'); + const errorUnknownText = localized.errorUnknown || (window.wp && wp.i18n ? wp.i18n.__('Onbekende fout.', 'siti-ai-product-content-generator') : 'Onbekende fout.'); + const successText = localized.success || (window.wp && wp.i18n ? wp.i18n.__('Structuur gegenereerd. Kopieer of vul velden in.', 'siti-ai-product-content-generator') : 'Structuur gegenereerd. Kopieer of vul velden in.'); + const fieldAppliedText = localized.fieldApplied || (window.wp && wp.i18n ? wp.i18n.__('%s ingevuld.', 'siti-ai-product-content-generator') : '%s ingevuld.'); + const fieldApplyErrorText = localized.fieldApplyError || (window.wp && wp.i18n ? wp.i18n.__('Kon het veld niet automatisch invullen.', 'siti-ai-product-content-generator') : 'Kon het veld niet automatisch invullen.'); + const fieldCopiedText = localized.fieldCopied || (window.wp && wp.i18n ? wp.i18n.__('%s gekopieerd naar het klembord.', 'siti-ai-product-content-generator') : '%s gekopieerd naar het klembord.'); + const jsonCopiedText = localized.jsonCopied || (window.wp && wp.i18n ? wp.i18n.__('JSON gekopieerd naar het klembord.', 'siti-ai-product-content-generator') : 'JSON gekopieerd naar het klembord.'); + const copyFailedText = localized.copyFailed || (window.wp && wp.i18n ? wp.i18n.__('Kopiëren mislukt.', 'siti-ai-product-content-generator') : 'Kopiëren mislukt.'); + + function formatString(template, values) { + if (!template) { + return ''; + } + let autoIndex = 0; + return template.replace(/%(\d+\$)?s/g, (match, position) => { + let valueIndex; + if (position) { + valueIndex = parseInt(position, 10) - 1; + } else { + valueIndex = autoIndex; + autoIndex += 1; + } + const replacement = values[valueIndex]; + return typeof replacement === 'undefined' ? '' : String(replacement); + }); + } function toggleLoading(isLoading) { modal.classList.toggle('is-loading', isLoading); @@ -137,7 +164,7 @@ .then((response) => response.json()) .then((json) => { if (!json.success) { - const errorMessage = json.data && json.data.message ? json.data.message : 'Onbekende fout'; + const errorMessage = json.data && json.data.message ? json.data.message : errorUnknownText; throw new Error(errorMessage); } @@ -154,13 +181,12 @@ if (jsonCopyButton) { jsonCopyButton.disabled = false; } - setStatus('Structuur gegenereerd. Kopieer of vul velden in.', 'success'); + setStatus(successText, 'success'); }) .catch((error) => { - const message = error && error.message ? error.message : 'Er ging iets mis bij het genereren.'; - setStatus(loadingText, 'error'); - const fullMessage = `${loadingText} ${message}. ${retryText}`; - statusField.textContent = fullMessage; + const message = error && error.message ? error.message : errorDefaultText; + const fullMessage = `${errorDefaultText} ${message}. ${retryText}`; + setStatus(fullMessage, 'error'); }) .finally(() => { toggleLoading(false); @@ -308,10 +334,10 @@ } if (applied) { - setStatus(entry.label + ' ingevuld.', 'success'); + setStatus(formatString(fieldAppliedText, [entry.label]), 'success'); setFieldStatus(fieldKey, 'success'); } else { - setStatus('Kon het veld niet automatisch invullen.', 'error'); + setStatus(fieldApplyErrorText, 'error'); setFieldStatus(fieldKey, 'error'); } } @@ -340,10 +366,10 @@ } copyToClipboard(entry.textarea.value) .then(() => { - setStatus(entry.label + ' gekopieerd naar het klembord.', 'success'); + setStatus(formatString(fieldCopiedText, [entry.label]), 'success'); }) .catch(() => { - setStatus('Kopiëren mislukt.', 'error'); + setStatus(copyFailedText, 'error'); }); } @@ -358,10 +384,10 @@ const text = resultField ? resultField.textContent.trim() : ''; copyToClipboard(text) .then(() => { - setStatus('JSON gekopieerd naar het klembord.', 'success'); + setStatus(jsonCopiedText, 'success'); }) .catch(() => { - setStatus('Kopiëren mislukt.', 'error'); + setStatus(copyFailedText, 'error'); }); }); } diff --git a/assets/js/settings.js b/assets/js/settings.js index 628b82f..a74ef5f 100644 --- a/assets/js/settings.js +++ b/assets/js/settings.js @@ -14,6 +14,14 @@ return; } + const strings = data.strings || {}; + const providerUnsupportedText = strings.providerUnsupported || 'Deze aanbieder ondersteunt dit niet.'; + const apiKeyRequiredText = strings.apiKeyRequired || 'Vul eerst de API-sleutel in.'; + const loadingModelsText = strings.loadingModels || 'Modellen worden opgehaald…'; + const errorUnknownText = strings.errorUnknown || 'Onbekende fout'; + const successModelsText = strings.successModels || 'Modellen bijgewerkt.'; + const errorFetchText = strings.errorFetch || 'Ophalen mislukt.'; + const providerSelect = document.querySelector('select[name="' + optionKey + '[provider]"]'); const modelSelect = document.getElementById('groq-ai-model-select'); const refreshButton = document.getElementById('groq-ai-refresh-models'); @@ -164,19 +172,19 @@ const provider = providerSelect ? providerSelect.value : data.currentProvider; const providerData = data.providers && data.providers[provider] ? data.providers[provider] : null; if (!providerData || !providerData.supports_live) { - setRefreshStatus('Deze aanbieder ondersteunt dit niet.', 'error'); + setRefreshStatus(providerUnsupportedText, 'error'); return; } const keyField = document.querySelector('[data-provider-row="' + provider + '"] input'); const apiKey = keyField ? keyField.value.trim() : ''; if (!apiKey) { - setRefreshStatus('Vul eerst de API-sleutel in.', 'error'); + setRefreshStatus(apiKeyRequiredText, 'error'); return; } refreshButton.disabled = true; - setRefreshStatus('Modellen worden opgehaald…', 'loading'); + setRefreshStatus(loadingModelsText, 'loading'); const payload = new URLSearchParams(); payload.append('action', 'groq_ai_refresh_models'); @@ -194,14 +202,14 @@ .then((response) => response.json()) .then((json) => { if (!json.success || !json.data || !Array.isArray(json.data.models)) { - throw new Error((json.data && json.data.message) || 'Onbekende fout'); + throw new Error((json.data && json.data.message) || errorUnknownText); } data.providers[provider].models = json.data.models; buildModelOptions(); - setRefreshStatus('Modellen bijgewerkt.', 'success'); + setRefreshStatus(successModelsText, 'success'); }) .catch((error) => { - setRefreshStatus(error.message || 'Ophalen mislukt.', 'error'); + setRefreshStatus(error.message || errorFetchText, 'error'); }) .finally(() => { refreshButton.disabled = false; diff --git a/assets/js/term-admin.js b/assets/js/term-admin.js index ee4bfa8..56c6c67 100644 --- a/assets/js/term-admin.js +++ b/assets/js/term-admin.js @@ -20,6 +20,14 @@ const includeTopProducts = document.getElementById('groq-ai-term-include-top-products'); const topProductsLimit = document.getElementById('groq-ai-term-top-products-limit'); + const strings = (window.GroqAITermGenerator && GroqAITermGenerator.strings) || {}; + const promptRequiredText = strings.promptRequired || 'Vul eerst een prompt in.'; + const loadingText = strings.loading || 'AI is bezig met schrijven...'; + const successText = strings.success || 'Tekst gegenereerd. Je kunt hem toepassen en opslaan.'; + const applySuccessText = strings.applySuccess || 'Tekst ingevuld. Vergeet niet op "Opslaan" te klikken.'; + const errorDefaultText = strings.errorDefault || 'Er ging iets mis bij het genereren.'; + const errorUnknownText = strings.errorUnknown || 'Onbekende fout'; + function setStatus(message, type) { if (!statusField) { return; @@ -75,7 +83,7 @@ rankmathKeywordsField.value = outputFocusKeywordsField.value || ''; } - setStatus('Tekst ingevuld. Vergeet niet op "Opslaan" te klikken.', 'success'); + setStatus(applySuccessText, 'success'); }); } @@ -83,12 +91,12 @@ event.preventDefault(); const prompt = promptField ? (promptField.value || '').trim() : ''; if (!prompt) { - setStatus('Vul eerst een prompt in.', 'error'); + setStatus(promptRequiredText, 'error'); return; } setLoading(true); - setStatus('AI is bezig met schrijven...', 'loading'); + setStatus(loadingText, 'loading'); if (rawField) { rawField.textContent = ''; } @@ -109,7 +117,7 @@ .then((response) => response.json()) .then((json) => { if (!json.success) { - const errorMessage = json.data && json.data.message ? json.data.message : 'Onbekende fout'; + const errorMessage = json.data && json.data.message ? json.data.message : errorUnknownText; throw new Error(errorMessage); } @@ -137,10 +145,10 @@ rawField.textContent = (json.data && json.data.raw ? String(json.data.raw) : '').trim(); } - setStatus('Tekst gegenereerd. Je kunt hem toepassen en opslaan.', 'success'); + setStatus(successText, 'success'); }) .catch((error) => { - setStatus(error && error.message ? error.message : 'Er ging iets mis bij het genereren.', 'error'); + setStatus(error && error.message ? error.message : errorDefaultText, 'error'); }) .finally(() => { setLoading(false); diff --git a/assets/js/term-bulk.js b/assets/js/term-bulk.js index 390d3f5..c2cacfb 100644 --- a/assets/js/term-bulk.js +++ b/assets/js/term-bulk.js @@ -11,6 +11,13 @@ const strings = data.strings || {}; const allowRegenerate = !!data.allowRegenerate; + const unknownErrorText = strings.unknownError || 'Onbekende fout'; + const unknownTermText = strings.unknownTerm || 'Onbekende term.'; + const confirmStopFallbackText = strings.confirmStopFallback || 'Stoppen?'; + const logErrorDefaultText = strings.logErrorDefault || '%1$s: %2$s'; + const logSuccessDefaultText = strings.logSuccessDefault || '%1$s gevuld.'; + const regenerateErrorDefaultText = strings.regenerateErrorDefault || '%1$s mislukt: %2$s'; + const regenerateDoneDefaultText = strings.regenerateDoneDefault || '%s is bijgewerkt.'; const terms = (Array.isArray(data.terms) ? data.terms : []) .map((term) => { const id = parseInt(term.id, 10); @@ -146,19 +153,19 @@ function handleResponse(term, json, context) { if (!json || !json.success) { - const errorMessage = (json && json.data && json.data.message) || 'Onbekende fout'; - appendLog(formatString(strings.logError || '%1$s: %2$s', [term.name || term.id, errorMessage]), 'error'); + const errorMessage = (json && json.data && json.data.message) || unknownErrorText; + appendLog(formatString(strings.logError || logErrorDefaultText, [term.name || term.id, errorMessage]), 'error'); if (context === 'single') { - setStatus(formatString(strings.regenerateError || '%1$s mislukt: %2$s', [term.name || term.id, errorMessage]), 'error'); + setStatus(formatString(strings.regenerateError || regenerateErrorDefaultText, [term.name || term.id, errorMessage]), 'error'); } return false; } const words = json.data && typeof json.data.words !== 'undefined' ? parseInt(json.data.words, 10) : term.words; markTermCompleted(term, Number.isFinite(words) ? words : term.words); - appendLog(formatString(strings.logSuccess || '%1$s gevuld.', [term.name || term.id, term.words]), 'success'); + appendLog(formatString(strings.logSuccess || logSuccessDefaultText, [term.name || term.id, term.words]), 'success'); if (context === 'single') { - setStatus(formatString(strings.regenerateDone || '%s is bijgewerkt.', [term.name || term.id]), 'success'); + setStatus(formatString(strings.regenerateDone || regenerateDoneDefaultText, [term.name || term.id]), 'success'); } return true; } @@ -233,7 +240,7 @@ if (!isRunning) { return; } - const confirmation = strings.confirmStop ? window.confirm(strings.confirmStop) : window.confirm('Stoppen?'); + const confirmation = strings.confirmStop ? window.confirm(strings.confirmStop) : window.confirm(confirmStopFallbackText); if (confirmation) { abortRequested = true; } @@ -251,7 +258,7 @@ const termId = parseInt(button.getAttribute('data-term-id'), 10); const term = termMap.get(termId); if (!term) { - setStatus('Onbekende term.', 'error'); + setStatus(unknownTermText, 'error'); return; } if (strings.confirmRegenerate) { @@ -270,9 +277,9 @@ handleResponse(term, json, 'single'); }) .catch((error) => { - const message = error && error.message ? error.message : 'Onbekende fout'; - appendLog(formatString(strings.logError || '%1$s: %2$s', [term.name || term.id, message]), 'error'); - setStatus(formatString(strings.regenerateError || '%1$s mislukt: %2$s', [term.name || term.id, message]), 'error'); + const message = error && error.message ? error.message : unknownErrorText; + appendLog(formatString(strings.logError || logErrorDefaultText, [term.name || term.id, message]), 'error'); + setStatus(formatString(strings.regenerateError || regenerateErrorDefaultText, [term.name || term.id, message]), 'error'); }) .finally(() => { button.disabled = false; diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c2fb1a9 --- /dev/null +++ b/composer.json @@ -0,0 +1,16 @@ +{ + "name": "sitiweb/siti-ai-product-content-generator", + "description": "SitiAI Product Teksten WordPress plugin", + "type": "wordpress-plugin", + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "autoload-dev": { + "classmap": [ + "includes/" + ] + }, + "scripts": { + "test": "phpunit" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..7f66cee --- /dev/null +++ b/composer.lock @@ -0,0 +1,1815 @@ +{ + "_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": "589964c1d168d0193d3244292a056025", + "packages": [], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", + "shasum": "" + }, + "require": { + "php": "^8.4" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.58" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2026-01-05T06:47:08+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.32", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:23:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.34", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.10", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T05:45:00+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:22:56+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:03:27+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:10:35+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:57:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/groq-ai-product-text.php b/groq-ai-product-text.php index 3f38034..e6d9bae 100644 --- a/groq-ai-product-text.php +++ b/groq-ai-product-text.php @@ -2,8 +2,9 @@ /** * Plugin Name: SitiAI Product Teksten * Description: Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce. - * Version: 1.8.0 - * Author: SitiAI + * Version: 1.9.0 + * Author: Roberto Guagliardo | SitiWeb + * Author URI: https://sitiweb.nl/ * Text Domain: siti-ai-product-content-generator * Domain Path: /languages */ @@ -44,6 +45,9 @@ if ( ! defined( 'GROQ_AI_DEBUG_TRACE_ADDED' ) && defined( 'WP_DEBUG' ) && WP_DEB require_once __DIR__ . '/includes/Core/class-groq-ai-service-container.php'; require_once __DIR__ . '/includes/Core/class-groq-ai-model-exclusions.php'; require_once __DIR__ . '/includes/Core/class-groq-ai-ajax-controller.php'; +require_once __DIR__ . '/includes/Core/class-groq-ai-compatibility-service.php'; +require_once __DIR__ . '/includes/Core/class-groq-ai-model-service.php'; +require_once __DIR__ . '/includes/Core/class-groq-ai-log-scheduler.php'; require_once __DIR__ . '/includes/Contracts/interface-groq-ai-provider.php'; require_once __DIR__ . '/includes/Providers/class-groq-ai-abstract-openai-provider.php'; require_once __DIR__ . '/includes/Providers/class-groq-ai-provider-groq.php'; @@ -106,8 +110,14 @@ final class Groq_AI_Product_Text_Plugin { /** @var Groq_AI_Product_Text_Product_UI */ private $product_ui; - /** @var bool */ - private $missing_wc_notice = false; + /** @var Groq_AI_Compatibility_Service */ + private $compatibility_service; + + /** @var Groq_AI_Model_Service */ + private $model_service; + + /** @var Groq_AI_Log_Scheduler */ + private $log_scheduler; public static function instance() { if ( null === self::$instance ) { @@ -119,6 +129,9 @@ final class Groq_AI_Product_Text_Plugin { private function __construct() { $this->register_services(); + $this->compatibility_service = new Groq_AI_Compatibility_Service(); + $this->model_service = new Groq_AI_Model_Service(); + $this->log_scheduler = new Groq_AI_Log_Scheduler( $this->get_settings_manager(), $this->get_generation_logger() ); $this->settings_page = new Groq_AI_Product_Text_Settings_Page( $this, $this->get_provider_manager() ); $this->categories_admin = new Groq_AI_Categories_Admin( $this ); @@ -127,8 +140,11 @@ final class Groq_AI_Product_Text_Plugin { $this->product_ui = new Groq_AI_Product_Text_Product_UI( $this ); add_action( 'init', [ $this, 'load_textdomain' ] ); - add_action( 'plugins_loaded', [ $this, 'maybe_create_logs_table' ] ); - add_action( 'load-plugins.php', [ $this, 'maybe_deactivate_if_woocommerce_missing' ] ); + $logger = $this->container->get( 'generation_logger' ); + add_action( 'plugins_loaded', [ $logger, 'maybe_create_table' ] ); + add_action( 'load-plugins.php', [ $this->compatibility_service, 'maybe_deactivate_if_woocommerce_missing' ] ); + add_action( 'init', [ $this->log_scheduler, 'ensure_logs_cleanup_schedule' ] ); + add_action( 'groq_ai_cleanup_logs', [ $this->log_scheduler, 'cleanup_logs' ] ); add_filter( 'groq_ai_term_google_context', [ $this, 'inject_google_term_context' ], 10, 3 ); } @@ -238,60 +254,84 @@ final class Groq_AI_Product_Text_Plugin { return self::OPTION_KEY; } - public function get_provider_manager() { - return $this->container->get( 'provider_manager' ); - } - - public function get_settings_manager() { - return $this->container->get( 'settings_manager' ); - } - - public function get_prompt_builder() { - return $this->container->get( 'prompt_builder' ); - } - - public function get_conversation_manager() { - return $this->container->get( 'conversation_manager' ); - } - - public function get_generation_logger() { - return $this->container->get( 'generation_logger' ); - } - - public function get_settings() { - return $this->get_settings_manager()->all(); - } - - public function sanitize_settings( $input ) { - return $this->get_settings_manager()->sanitize( $input ); - } - - public function maybe_deactivate_if_woocommerce_missing() { - if ( $this->is_woocommerce_active() ) { - return; + public function __call( $name, $arguments ) { + switch ( $name ) { + case 'get_provider_manager': + return $this->container->get( 'provider_manager' ); + case 'get_settings_manager': + return $this->container->get( 'settings_manager' ); + case 'get_prompt_builder': + return $this->container->get( 'prompt_builder' ); + case 'get_conversation_manager': + return $this->container->get( 'conversation_manager' ); + case 'get_generation_logger': + return $this->container->get( 'generation_logger' ); + case 'get_settings': + return $this->container->get( 'settings_manager' )->all(); + case 'sanitize_settings': + return $this->container->get( 'settings_manager' )->sanitize( $arguments[0] ?? [] ); + case 'get_context_field_definitions': + return $this->container->get( 'settings_manager' )->get_context_field_definitions(); + case 'get_default_modules_settings': + return $this->container->get( 'settings_manager' )->get_default_modules_settings(); + case 'get_default_context_fields': + return $this->container->get( 'settings_manager' )->get_default_context_fields(); + case 'normalize_context_fields': + return $this->container->get( 'settings_manager' )->normalize_context_fields( $arguments[0] ?? [] ); + case 'get_module_config': + return $this->container->get( 'settings_manager' )->get_module_config( $arguments[0] ?? '', $arguments[1] ?? null ); + case 'is_module_enabled': + return $this->container->get( 'settings_manager' )->is_module_enabled( $arguments[0] ?? '', $arguments[1] ?? null ); + case 'get_rankmath_focus_keyword_limit': + return $this->container->get( 'settings_manager' )->get_rankmath_focus_keyword_limit( $arguments[0] ?? null ); + case 'get_rankmath_meta_title_pixel_limit': + return $this->container->get( 'settings_manager' )->get_rankmath_meta_title_pixel_limit( $arguments[0] ?? null ); + case 'get_rankmath_meta_description_pixel_limit': + return $this->container->get( 'settings_manager' )->get_rankmath_meta_description_pixel_limit( $arguments[0] ?? null ); + case 'is_response_format_compat_enabled': + return $this->container->get( 'settings_manager' )->is_response_format_compat_enabled( $arguments[0] ?? null ); + case 'get_image_context_mode': + return $this->container->get( 'settings_manager' )->get_image_context_mode( $arguments[0] ?? null ); + case 'get_image_context_limit': + return $this->container->get( 'settings_manager' )->get_image_context_limit( $arguments[0] ?? null ); + case 'get_term_top_description_char_limit': + return $this->container->get( 'settings_manager' )->get_term_top_description_char_limit( $arguments[0] ?? null ); + case 'get_term_bottom_description_char_limit': + return $this->container->get( 'settings_manager' )->get_term_bottom_description_char_limit( $arguments[0] ?? null ); + case 'get_google_safety_settings': + return $this->container->get( 'settings_manager' )->get_google_safety_settings( $arguments[0] ?? null ); + case 'get_google_safety_categories': + return $this->container->get( 'settings_manager' )->get_google_safety_categories(); + case 'get_google_safety_thresholds': + return $this->container->get( 'settings_manager' )->get_google_safety_thresholds(); + case 'get_loggable_settings_snapshot': + return $this->container->get( 'settings_manager' )->get_loggable_settings_snapshot( $arguments[0] ?? null ); + case 'create_settings_renderer': + $values = $arguments[0] ?? null; + if ( null === $values ) { + $values = $this->container->get( 'settings_manager' )->all(); + } + return new Groq_AI_Settings_Renderer( self::OPTION_KEY, $values ); + case 'should_use_response_format': + $provider = $arguments[0] ?? null; + $settings = $arguments[1] ?? null; + if ( ! $provider instanceof Groq_AI_Provider_Interface ) { + return false; + } + return ! $this->container->get( 'settings_manager' )->is_response_format_compat_enabled( $settings ) && $provider->supports_response_format(); + case 'is_rankmath_active': + return $this->compatibility_service->is_rankmath_active(); + case 'is_woocommerce_active': + return $this->compatibility_service->is_woocommerce_active(); + case 'get_selected_model': + return $this->model_service->get_selected_model( $arguments[0], $arguments[1] ?? [] ); + case 'get_cached_models_for_provider': + return $this->model_service->get_cached_models_for_provider( $arguments[0] ?? '' ); + case 'update_cached_models_for_provider': + return $this->model_service->update_cached_models_for_provider( $arguments[0] ?? '', $arguments[1] ?? [] ); } - if ( ! function_exists( 'deactivate_plugins' ) ) { - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - } - - deactivate_plugins( plugin_basename( GROQ_AI_PRODUCT_TEXT_FILE ) ); - $this->missing_wc_notice = true; - - add_action( 'admin_notices', [ $this, 'render_missing_wc_notice' ] ); - } - - public function render_missing_wc_notice() { - if ( ! $this->missing_wc_notice ) { - return; - } - ?> -
-

- -

-
- get_settings_manager()->get_context_field_definitions(); - } - - public function get_default_modules_settings() { - return $this->get_settings_manager()->get_default_modules_settings(); - } - - public function get_default_context_fields() { - return $this->get_settings_manager()->get_default_context_fields(); - } - - public function normalize_context_fields( $fields ) { - return $this->get_settings_manager()->normalize_context_fields( $fields ); - } - - public function get_module_config( $module, $settings = null ) { - return $this->get_settings_manager()->get_module_config( $module, $settings ); - } - - public function is_module_enabled( $module, $settings = null ) { - return $this->get_settings_manager()->is_module_enabled( $module, $settings ); - } - - public function get_rankmath_focus_keyword_limit( $settings = null ) { - return $this->get_settings_manager()->get_rankmath_focus_keyword_limit( $settings ); - } - - public function get_rankmath_meta_title_pixel_limit( $settings = null ) { - return $this->get_settings_manager()->get_rankmath_meta_title_pixel_limit( $settings ); - } - - public function get_rankmath_meta_description_pixel_limit( $settings = null ) { - return $this->get_settings_manager()->get_rankmath_meta_description_pixel_limit( $settings ); - } - - public function is_response_format_compat_enabled( $settings = null ) { - return $this->get_settings_manager()->is_response_format_compat_enabled( $settings ); - } - - public function get_image_context_mode( $settings = null ) { - return $this->get_settings_manager()->get_image_context_mode( $settings ); - } - - public function get_image_context_limit( $settings = null ) { - return $this->get_settings_manager()->get_image_context_limit( $settings ); - } - - public function get_term_top_description_char_limit( $settings = null ) { - return $this->get_settings_manager()->get_term_top_description_char_limit( $settings ); - } - - public function get_term_bottom_description_char_limit( $settings = null ) { - return $this->get_settings_manager()->get_term_bottom_description_char_limit( $settings ); - } - - public function get_google_safety_settings( $settings = null ) { - return $this->get_settings_manager()->get_google_safety_settings( $settings ); - } - - public function get_google_safety_categories() { - return $this->get_settings_manager()->get_google_safety_categories(); - } - - public function get_google_safety_thresholds() { - return $this->get_settings_manager()->get_google_safety_thresholds(); - } - - public function get_loggable_settings_snapshot( $settings = null ) { - return $this->get_settings_manager()->get_loggable_settings_snapshot( $settings ); - } - - public function create_settings_renderer( $values = null ) { - if ( null === $values ) { - $values = $this->get_settings(); - } - - return new Groq_AI_Settings_Renderer( self::OPTION_KEY, $values ); - } - - public function should_use_response_format( Groq_AI_Provider_Interface $provider, $settings ) { - return ! $this->is_response_format_compat_enabled( $settings ) && $provider->supports_response_format(); - } - - public function is_rankmath_active() { - if ( class_exists( 'RankMath' ) ) { - return true; - } - - if ( ! function_exists( 'is_plugin_active' ) ) { - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - } - - return function_exists( 'is_plugin_active' ) && is_plugin_active( 'seo-by-rank-math/rank-math.php' ); - } - - public function is_woocommerce_active() { - if ( class_exists( 'WooCommerce' ) ) { - return true; - } - - if ( ! function_exists( 'is_plugin_active' ) ) { - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - } - - return function_exists( 'is_plugin_active' ) && is_plugin_active( 'woocommerce/woocommerce.php' ); - } - - public function get_selected_model( Groq_AI_Provider_Interface $provider, $settings ) { - $provider_key = $provider->get_key(); - $model = ! empty( $settings['model'] ) ? $settings['model'] : ''; - $model = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $model ); - - if ( '' === $model ) { - $default = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $provider->get_default_model() ); - if ( '' !== $default ) { - return $default; - } - - $available = Groq_AI_Model_Exclusions::filter_models( $provider_key, $provider->get_available_models() ); - if ( ! empty( $available ) ) { - return $available[0]; - } - } - - return $model; - } - - public function get_cached_models_for_provider( $provider ) { - $provider = sanitize_key( (string) $provider ); - $cache = $this->get_models_cache(); - - return isset( $cache[ $provider ] ) ? $cache[ $provider ] : []; - } - - public function update_cached_models_for_provider( $provider, $models ) { - $provider = sanitize_key( (string) $provider ); - $models = $this->sanitize_models_list( $models ); - - $cache = $this->get_models_cache(); - $cache[ $provider ] = $models; - - update_option( self::MODELS_CACHE_OPTION_KEY, $cache ); - - return $models; - } - - private function get_models_cache() { - $cache = get_option( self::MODELS_CACHE_OPTION_KEY, [] ); - - if ( ! is_array( $cache ) ) { - $cache = []; - } - - foreach ( $cache as $provider => $models ) { - $cache[ $provider ] = $this->sanitize_models_list( $models ); - } - - return $cache; - } - - private function sanitize_models_list( $models ) { - if ( ! is_array( $models ) ) { - return []; - } - - $models = array_map( 'sanitize_text_field', $models ); - $models = array_filter( - $models, - function ( $model ) { - return '' !== $model; - } - ); - - $models = array_values( array_unique( $models ) ); - - if ( ! empty( $models ) ) { - sort( $models, SORT_NATURAL | SORT_FLAG_CASE ); - } - - return $models; - } - - public function log_debug( $message, $context = [] ) { - $this->get_generation_logger()->log_debug( $message, $context ); - } - private function extract_content_text( $result ) { - if ( is_array( $result ) && isset( $result['content'] ) ) { - return (string) $result['content']; - } - - return (string) $result; - } - - public function maybe_create_logs_table() { - $this->get_generation_logger()->maybe_create_table(); - } public static function activate() { $logger = new Groq_AI_Generation_Logger(); diff --git a/includes/Admin/class-groq-ai-logs-table.php b/includes/Admin/class-groq-ai-logs-table.php index b232027..a4d07e2 100644 --- a/includes/Admin/class-groq-ai-logs-table.php +++ b/includes/Admin/class-groq-ai-logs-table.php @@ -60,10 +60,14 @@ class Groq_AI_Logs_Table extends WP_List_Table { $current_page = $this->get_pagenum(); $offset = ( $current_page - 1 ) * $per_page; - $orderby = isset( $_REQUEST['orderby'] ) ? sanitize_sql_orderby( wp_unslash( $_REQUEST['orderby'] ) ) : 'created_at'; - if ( ! $orderby ) { - $orderby = 'created_at'; - } + $allowed_orderby = [ + 'created_at' => 'created_at', + 'provider' => 'provider', + 'model' => 'model', + 'status' => 'status', + ]; + $orderby = isset( $_REQUEST['orderby'] ) ? sanitize_key( wp_unslash( $_REQUEST['orderby'] ) ) : 'created_at'; + $orderby = isset( $allowed_orderby[ $orderby ] ) ? $allowed_orderby[ $orderby ] : 'created_at'; $order = isset( $_REQUEST['order'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ) : 'DESC'; $order = in_array( $order, [ 'ASC', 'DESC' ], true ) ? $order : 'DESC'; diff --git a/includes/Admin/class-groq-ai-product-ui.php b/includes/Admin/class-groq-ai-product-ui.php index fccf191..1f6e9a2 100644 --- a/includes/Admin/class-groq-ai-product-ui.php +++ b/includes/Admin/class-groq-ai-product-ui.php @@ -73,6 +73,18 @@ class Groq_AI_Product_Text_Product_UI { 'postId' => $post_id, 'contextDefaults' => isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields(), 'attributeIncludesDefaults' => $attribute_defaults, + 'strings' => [ + 'loading' => __( 'AI is bezig met schrijven...', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'retry' => __( 'Probeer het opnieuw of pas je prompt/context aan.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'errorDefault' => __( 'Er ging iets mis bij het genereren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'errorUnknown' => __( 'Onbekende fout.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'success' => __( 'Structuur gegenereerd. Kopieer of vul velden in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'fieldApplied' => __( '%s ingevuld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'fieldApplyError' => __( 'Kon het veld niet automatisch invullen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'fieldCopied' => __( '%s gekopieerd naar het klembord.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'jsonCopied' => __( 'JSON gekopieerd naar het klembord.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'copyFailed' => __( 'Kopiëren mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ], ] ); } diff --git a/includes/Admin/class-groq-ai-settings-page.php b/includes/Admin/class-groq-ai-settings-page.php index 049b280..97c7a6b 100644 --- a/includes/Admin/class-groq-ai-settings-page.php +++ b/includes/Admin/class-groq-ai-settings-page.php @@ -125,17 +125,7 @@ class Groq_AI_Product_Text_Settings_Page extends Groq_AI_Admin_Base { $brands_url = $this->get_page_url( 'groq-ai-product-text-brands' ); $prompt_preview = $this->plugin->build_prompt_template_preview( $settings ); - $google_notice = isset( $_GET['groq_ai_google_notice'] ) ? sanitize_key( wp_unslash( $_GET['groq_ai_google_notice'] ) ) : ''; - $google_status = isset( $_GET['groq_ai_google_notice_status'] ) ? sanitize_key( wp_unslash( $_GET['groq_ai_google_notice_status'] ) ) : ''; - $google_message = ''; - if ( isset( $_GET['groq_ai_google_notice_message'] ) ) { - $google_message = sanitize_text_field( rawurldecode( wp_unslash( $_GET['groq_ai_google_notice_message'] ) ) ); - } - $google_connected = ! empty( $settings['google_oauth_refresh_token'] ); - $google_connected_email = isset( $settings['google_oauth_connected_email'] ) ? (string) $settings['google_oauth_connected_email'] : ''; - $google_connected_at = isset( $settings['google_oauth_connected_at'] ) ? absint( $settings['google_oauth_connected_at'] ) : 0; - $oauth_redirect = add_query_arg( 'action', 'groq_ai_google_oauth_callback', admin_url( 'admin-post.php' ) ); $google_safety_settings = $this->plugin->get_google_safety_settings( $settings ); $google_safety_categories = $this->plugin->get_google_safety_categories(); $google_safety_thresholds = $this->plugin->get_google_safety_thresholds(); @@ -145,7 +135,7 @@ class Groq_AI_Product_Text_Settings_Page extends Groq_AI_Admin_Base {

- +

@@ -155,12 +145,7 @@ class Groq_AI_Product_Text_Settings_Page extends Groq_AI_Admin_Base {

- -

- +
@@ -243,6 +228,18 @@ class Groq_AI_Product_Text_Settings_Page extends Groq_AI_Admin_Base { 'description' => __( 'Limitering van het aantal tokens per output voor compatibiliteit met verschillende modellen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), ] ); + $renderer->field( + [ + 'label' => __( 'Logboek retentie (dagen)', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'key' => 'logs_retention_days', + 'type' => 'number', + 'attributes' => [ + 'min' => 0, + 'max' => 3650, + ], + 'description' => __( 'Hoe lang logboekregels bewaard blijven. Zet op 0 om logs onbeperkt te bewaren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ] + ); $renderer->field( [ 'label' => __( 'Term meta key (onderste tekst)', GROQ_AI_PRODUCT_TEXT_DOMAIN ), @@ -262,8 +259,80 @@ class Groq_AI_Product_Text_Settings_Page extends Groq_AI_Admin_Base { $renderer->close_table(); ?> +

+ +
+ plugin->get_option_key(), + $this->plugin->get_option_key(), + [ $this->plugin, 'sanitize_settings' ] + ); + } + + public function hide_menu_links() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + ?> + + plugin->get_option_key(); + $settings = $this->plugin->get_settings(); + $current_page = $this->get_page_url( 'groq-ai-product-text-modules' ); + $oauth_redirect = add_query_arg( 'action', 'groq_ai_google_oauth_callback', admin_url( 'admin-post.php' ) ); + $google_notice = isset( $_GET['groq_ai_google_notice'] ) ? sanitize_key( wp_unslash( $_GET['groq_ai_google_notice'] ) ) : ''; + $google_status = isset( $_GET['groq_ai_google_notice_status'] ) ? sanitize_key( wp_unslash( $_GET['groq_ai_google_notice_status'] ) ) : ''; + $google_message = ''; + if ( isset( $_GET['groq_ai_google_notice_message'] ) ) { + $google_message = sanitize_text_field( rawurldecode( wp_unslash( $_GET['groq_ai_google_notice_message'] ) ) ); + } + $google_connected = ! empty( $settings['google_oauth_refresh_token'] ); + $google_connected_email = isset( $settings['google_oauth_connected_email'] ) ? (string) $settings['google_oauth_connected_email'] : ''; + $google_connected_at = isset( $settings['google_oauth_connected_at'] ) ? absint( $settings['google_oauth_connected_at'] ) : 0; + ?> +
+

+

+ + +

+ +
+ + + + + + +

plugin->create_settings_renderer( $settings ); $renderer->open_table(); $renderer->field( [ @@ -320,7 +389,7 @@ class Groq_AI_Product_Text_Settings_Page extends Groq_AI_Admin_Base { ); $renderer->close_table(); ?> -

+

@@ -364,59 +433,6 @@ class Groq_AI_Product_Text_Settings_Page extends Groq_AI_Admin_Base {
plugin->get_option_key(), - $this->plugin->get_option_key(), - [ $this->plugin, 'sanitize_settings' ] - ); - } - - public function hide_menu_links() { - if ( ! current_user_can( 'manage_options' ) ) { - return; - } - ?> - - plugin->get_option_key(); - ?> -
-

-

- -
- - - - - - - - -
-
- [ 'selectModel' => __( 'Selecteer een model via "Live modellen ophalen"', GROQ_AI_PRODUCT_TEXT_DOMAIN ), ], + 'strings' => [ + 'providerUnsupported' => __( 'Deze aanbieder ondersteunt dit niet.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'apiKeyRequired' => __( 'Vul eerst de API-sleutel in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'loadingModels' => __( 'Modellen worden opgehaald…', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'errorUnknown' => __( 'Onbekende fout', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'successModels' => __( 'Modellen bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'errorFetch' => __( 'Ophalen mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ], ]; foreach ( $this->provider_manager->get_providers() as $provider ) { diff --git a/includes/Admin/class-groq-ai-term-admin-base.php b/includes/Admin/class-groq-ai-term-admin-base.php index c1554b4..9b54b7f 100644 --- a/includes/Admin/class-groq-ai-term-admin-base.php +++ b/includes/Admin/class-groq-ai-term-admin-base.php @@ -89,10 +89,20 @@ abstract class Groq_AI_Term_Admin_Base extends Groq_AI_Admin_Base { 'taxonomy' => $taxonomy, 'terms' => $terms, 'allowRegenerate' => false, - 'strings' => [], + 'strings' => [ + 'unknownError' => __( 'Onbekende fout', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'unknownTerm' => __( 'Onbekende term.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'confirmStopFallback' => __( 'Stoppen?', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'logErrorDefault' => __( '%1$s: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'logSuccessDefault' => __( '%1$s gevuld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'regenerateErrorDefault' => __( '%1$s mislukt: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'regenerateDoneDefault' => __( '%s is bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ], ]; $config = wp_parse_args( $overrides, $defaults ); + $override_strings = isset( $overrides['strings'] ) && is_array( $overrides['strings'] ) ? $overrides['strings'] : []; + $config['strings'] = array_merge( $defaults['strings'], $override_strings ); wp_localize_script( 'groq-ai-term-bulk', 'GroqAITermBulk', $config ); } @@ -207,6 +217,14 @@ abstract class Groq_AI_Term_Admin_Base extends Groq_AI_Admin_Base { 'nonce' => wp_create_nonce( 'groq_ai_generate_term' ), 'taxonomy' => $taxonomy, 'termId' => $term_id, + 'strings' => [ + 'promptRequired' => __( 'Vul eerst een prompt in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'loading' => __( 'AI is bezig met schrijven...', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'success' => __( 'Tekst gegenereerd. Je kunt hem toepassen en opslaan.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'applySuccess' => __( 'Tekst ingevuld. Vergeet niet op "Opslaan" te klikken.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'errorDefault' => __( 'Er ging iets mis bij het genereren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'errorUnknown' => __( 'Onbekende fout', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ], ] ); } diff --git a/includes/Core/class-groq-ai-ajax-controller.php b/includes/Core/class-groq-ai-ajax-controller.php index 00728fc..a25e1fc 100644 --- a/includes/Core/class-groq-ai-ajax-controller.php +++ b/includes/Core/class-groq-ai-ajax-controller.php @@ -435,9 +435,22 @@ class Groq_AI_Ajax_Controller { check_ajax_referer( 'groq_ai_generate', 'nonce' ); - $prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : ''; + $prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : ''; $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0; + if ( ! $post_id ) { + wp_send_json_error( [ 'message' => __( 'Post-ID ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 400 ); + } + + $post = get_post( $post_id ); + if ( ! $post || is_wp_error( $post ) || 'product' !== $post->post_type ) { + wp_send_json_error( [ 'message' => __( 'Product niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 404 ); + } + + if ( ! current_user_can( 'edit_post', $post_id ) ) { + wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming om dit product te bewerken.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 403 ); + } + $settings = $this->plugin->get_settings(); $provider_manager = $this->plugin->get_provider_manager(); $provider_key = $settings['provider']; diff --git a/includes/Core/class-groq-ai-compatibility-service.php b/includes/Core/class-groq-ai-compatibility-service.php new file mode 100644 index 0000000..c82e554 --- /dev/null +++ b/includes/Core/class-groq-ai-compatibility-service.php @@ -0,0 +1,58 @@ +is_woocommerce_active() ) { + return; + } + + if ( ! function_exists( 'deactivate_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + deactivate_plugins( plugin_basename( GROQ_AI_PRODUCT_TEXT_FILE ) ); + $this->missing_wc_notice = true; + + add_action( 'admin_notices', [ $this, 'render_missing_wc_notice' ] ); + } + + public function render_missing_wc_notice() { + if ( ! $this->missing_wc_notice ) { + return; + } + ?> +
+

+ +

+
+ settings_manager = $settings_manager; + $this->logger = $logger; + } + + public function ensure_logs_cleanup_schedule() { + if ( wp_next_scheduled( 'groq_ai_cleanup_logs' ) ) { + return; + } + + wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'groq_ai_cleanup_logs' ); + } + + public function cleanup_logs() { + $settings = $this->settings_manager->all(); + $retention_days = $this->settings_manager->get_logs_retention_days( $settings ); + $this->logger->cleanup_old_logs( $retention_days ); + } +} diff --git a/includes/Core/class-groq-ai-model-service.php b/includes/Core/class-groq-ai-model-service.php new file mode 100644 index 0000000..c20c7a5 --- /dev/null +++ b/includes/Core/class-groq-ai-model-service.php @@ -0,0 +1,78 @@ +get_key(); + $model = ! empty( $settings['model'] ) ? $settings['model'] : ''; + $model = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $model ); + + if ( '' === $model ) { + $default = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $provider->get_default_model() ); + if ( '' !== $default ) { + return $default; + } + + $available = Groq_AI_Model_Exclusions::filter_models( $provider_key, $provider->get_available_models() ); + if ( ! empty( $available ) ) { + return $available[0]; + } + } + + return $model; + } + + public function get_cached_models_for_provider( $provider ) { + $provider = sanitize_key( (string) $provider ); + $cache = $this->get_models_cache(); + + return isset( $cache[ $provider ] ) ? $cache[ $provider ] : []; + } + + public function update_cached_models_for_provider( $provider, $models ) { + $provider = sanitize_key( (string) $provider ); + $models = $this->sanitize_models_list( $models ); + + $cache = $this->get_models_cache(); + $cache[ $provider ] = $models; + + update_option( Groq_AI_Product_Text_Plugin::MODELS_CACHE_OPTION_KEY, $cache ); + + return $models; + } + + private function get_models_cache() { + $cache = get_option( Groq_AI_Product_Text_Plugin::MODELS_CACHE_OPTION_KEY, [] ); + + if ( ! is_array( $cache ) ) { + $cache = []; + } + + foreach ( $cache as $provider => $models ) { + $cache[ $provider ] = $this->sanitize_models_list( $models ); + } + + return $cache; + } + + private function sanitize_models_list( $models ) { + if ( ! is_array( $models ) ) { + return []; + } + + $models = array_map( 'sanitize_text_field', $models ); + $models = array_filter( + $models, + function ( $model ) { + return '' !== $model; + } + ); + + $models = array_values( array_unique( $models ) ); + + if ( ! empty( $models ) ) { + sort( $models, SORT_NATURAL | SORT_FLAG_CASE ); + } + + return $models; + } +} diff --git a/includes/Services/Logging/class-groq-ai-generation-logger.php b/includes/Services/Logging/class-groq-ai-generation-logger.php index ffb7a1c..8bcc10a 100644 --- a/includes/Services/Logging/class-groq-ai-generation-logger.php +++ b/includes/Services/Logging/class-groq-ai-generation-logger.php @@ -143,6 +143,20 @@ class Groq_AI_Generation_Logger { update_option( self::OPTION_TABLE_CREATED, 1 ); } + public function cleanup_old_logs( $retention_days ) { + $retention_days = absint( $retention_days ); + if ( $retention_days <= 0 || ! $this->logs_table_exists() ) { + return; + } + + $cutoff = time() - ( $retention_days * DAY_IN_SECONDS ); + $cutoff = gmdate( 'Y-m-d H:i:s', $cutoff ); + + global $wpdb; + $table = $this->get_logs_table_name(); + $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE created_at < %s", $cutoff ) ); + } + private function extract_usage_token_value( $usage, $keys ) { foreach ( (array) $keys as $key ) { if ( isset( $usage[ $key ] ) ) { diff --git a/includes/Services/Settings/class-groq-ai-settings-manager.php b/includes/Services/Settings/class-groq-ai-settings-manager.php index 20eed72..918e133 100644 --- a/includes/Services/Settings/class-groq-ai-settings-manager.php +++ b/includes/Services/Settings/class-groq-ai-settings-manager.php @@ -33,6 +33,7 @@ class Groq_AI_Settings_Manager { 'store_context' => '', 'default_prompt' => '', 'max_output_tokens' => 2048, + 'logs_retention_days' => 90, 'product_attribute_includes' => [], 'term_bottom_description_meta_key' => '', 'groq_api_key' => '', @@ -63,6 +64,8 @@ class Groq_AI_Settings_Manager { $settings['modules'] = $this->sanitize_modules_settings( isset( $settings['modules'] ) ? $settings['modules'] : [] ); $settings['google_safety_settings'] = $this->sanitize_google_safety_settings( isset( $settings['google_safety_settings'] ) ? $settings['google_safety_settings'] : [] ); $settings['model'] = Groq_AI_Model_Exclusions::ensure_allowed( $settings['provider'], isset( $settings['model'] ) ? $settings['model'] : '' ); + $logs_retention_days = isset( $settings['logs_retention_days'] ) ? (int) $settings['logs_retention_days'] : 90; + $settings['logs_retention_days'] = max( 0, min( 3650, $logs_retention_days ) ); $image_mode = isset( $settings['image_context_mode'] ) ? sanitize_text_field( $settings['image_context_mode'] ) : 'url'; if ( 'none' === $image_mode ) { @@ -108,6 +111,7 @@ class Groq_AI_Settings_Manager { 'store_context' => '', 'default_prompt' => '', 'max_output_tokens' => 2048, + 'logs_retention_days' => 90, 'product_attribute_includes' => [], 'term_bottom_description_meta_key' => '', 'groq_api_key' => '', @@ -159,6 +163,10 @@ class Groq_AI_Settings_Manager { // Keep within sane bounds across providers. $max_output_tokens = max( 128, min( 8192, $max_output_tokens ) ); + $logs_retention_days = isset( $input['logs_retention_days'] ) ? (int) $input['logs_retention_days'] : (int) $defaults['logs_retention_days']; + // 0 = keep indefinitely, otherwise cap at 10 years. + $logs_retention_days = max( 0, min( 3650, $logs_retention_days ) ); + $context_fields = $this->normalize_context_fields( $context_posted ? $raw_input['context_fields'] : $defaults['context_fields'] ); if ( 'none' === $image_mode ) { @@ -182,6 +190,7 @@ class Groq_AI_Settings_Manager { 'store_context' => sanitize_textarea_field( $input['store_context'] ), 'default_prompt' => sanitize_textarea_field( $input['default_prompt'] ), 'max_output_tokens' => $max_output_tokens, + 'logs_retention_days' => $logs_retention_days, 'product_attribute_includes' => $this->sanitize_product_attribute_includes( isset( $raw_input['product_attribute_includes'] ) ? $raw_input['product_attribute_includes'] : [] ), 'term_bottom_description_meta_key' => sanitize_key( (string) $input['term_bottom_description_meta_key'] ), 'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ), @@ -442,6 +451,15 @@ class Groq_AI_Settings_Manager { return self::get_google_safety_thresholds_list(); } + public function get_logs_retention_days( $settings = null ) { + if ( null === $settings ) { + $settings = $this->all(); + } + + $value = isset( $settings['logs_retention_days'] ) ? (int) $settings['logs_retention_days'] : 90; + return max( 0, min( 3650, $value ) ); + } + public function get_loggable_settings_snapshot( $settings = null ) { if ( null === $settings ) { $settings = $this->all(); @@ -451,6 +469,7 @@ class Groq_AI_Settings_Manager { 'store_context', 'default_prompt', 'max_output_tokens', + 'logs_retention_days', 'product_attribute_includes', 'context_fields', 'modules', diff --git a/languages/siti-ai-product-content-generator.pot b/languages/siti-ai-product-content-generator.pot new file mode 100644 index 0000000..e283629 --- /dev/null +++ b/languages/siti-ai-product-content-generator.pot @@ -0,0 +1,202 @@ +msgid "" +msgstr "" +"Project-Id-Version: SitiAI Product Teksten\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-01-31 00:00+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Domain: siti-ai-product-content-generator\n" + +#: assets/js/settings.js:166 +msgid "Deze aanbieder ondersteunt dit niet." +msgstr "" + +#: assets/js/settings.js:173 +msgid "Vul eerst de API-sleutel in." +msgstr "" + +#: assets/js/settings.js:177 +msgid "Modellen worden opgehaald…" +msgstr "" + +#: assets/js/settings.js:197 +msgid "Onbekende fout" +msgstr "" + +#: assets/js/settings.js:202 +msgid "Modellen bijgewerkt." +msgstr "" + +#: assets/js/settings.js:205 +msgid "Ophalen mislukt." +msgstr "" + +#: includes/Admin/class-groq-ai-settings-page.php:844 +msgid "Deze aanbieder ondersteunt dit niet." +msgstr "" + +#: includes/Admin/class-groq-ai-settings-page.php:845 +msgid "Vul eerst de API-sleutel in." +msgstr "" + +#: includes/Admin/class-groq-ai-settings-page.php:846 +msgid "Modellen worden opgehaald…" +msgstr "" + +#: includes/Admin/class-groq-ai-settings-page.php:847 +msgid "Onbekende fout" +msgstr "" + +#: includes/Admin/class-groq-ai-settings-page.php:848 +msgid "Modellen bijgewerkt." +msgstr "" + +#: includes/Admin/class-groq-ai-settings-page.php:849 +msgid "Ophalen mislukt." +msgstr "" + +#: assets/js/term-admin.js:24 +msgid "Vul eerst een prompt in." +msgstr "" + +#: assets/js/term-admin.js:25 +msgid "AI is bezig met schrijven..." +msgstr "" + +#: assets/js/term-admin.js:26 +msgid "Tekst gegenereerd. Je kunt hem toepassen en opslaan." +msgstr "" + +#: assets/js/term-admin.js:27 +msgid "Tekst ingevuld. Vergeet niet op \"Opslaan\" te klikken." +msgstr "" + +#: assets/js/term-admin.js:28 +msgid "Er ging iets mis bij het genereren." +msgstr "" + +#: assets/js/term-admin.js:119 +msgid "Onbekende fout" +msgstr "" + +#: includes/Admin/class-groq-ai-term-admin-base.php:212 +msgid "Onbekende fout" +msgstr "" + +#: assets/js/admin.js:100 +msgid "AI is bezig met schrijven..." +msgstr "" + +#: assets/js/admin.js:101 +msgid "Probeer het opnieuw of pas je prompt/context aan." +msgstr "" + +#: assets/js/admin.js:102 +msgid "Er ging iets mis bij het genereren." +msgstr "" + +#: assets/js/admin.js:103 +msgid "Onbekende fout." +msgstr "" + +#: assets/js/admin.js:104 +msgid "Structuur gegenereerd. Kopieer of vul velden in." +msgstr "" + +#: assets/js/admin.js:105 +msgid "%s ingevuld." +msgstr "" + +#: assets/js/admin.js:106 +msgid "Kon het veld niet automatisch invullen." +msgstr "" + +#: assets/js/admin.js:107 +msgid "%s gekopieerd naar het klembord." +msgstr "" + +#: assets/js/admin.js:108 +msgid "JSON gekopieerd naar het klembord." +msgstr "" + +#: assets/js/admin.js:109 +msgid "Kopiëren mislukt." +msgstr "" + + +#: includes/Admin/class-groq-ai-product-ui.php:84 +msgid "%s ingevuld." +msgstr "" + +#: includes/Admin/class-groq-ai-product-ui.php:85 +msgid "Kon het veld niet automatisch invullen." +msgstr "" + +#: includes/Admin/class-groq-ai-product-ui.php:86 +msgid "%s gekopieerd naar het klembord." +msgstr "" + +#: includes/Admin/class-groq-ai-product-ui.php:87 +msgid "JSON gekopieerd naar het klembord." +msgstr "" + +#: includes/Admin/class-groq-ai-product-ui.php:88 +msgid "Kopiëren mislukt." +msgstr "" + +#: assets/js/term-bulk.js:149 +msgid "Onbekende fout" +msgstr "" + +#: assets/js/term-bulk.js:150 +msgid "%1$s: %2$s" +msgstr "" + +#: assets/js/term-bulk.js:152 +msgid "%1$s mislukt: %2$s" +msgstr "" + +#: assets/js/term-bulk.js:159 +msgid "%1$s gevuld." +msgstr "" + +#: assets/js/term-bulk.js:161 +msgid "%s is bijgewerkt." +msgstr "" + +#: assets/js/term-bulk.js:236 +msgid "Stoppen?" +msgstr "" + +#: assets/js/term-bulk.js:254 +msgid "Onbekende term." +msgstr "" + +#: includes/Admin/class-groq-ai-term-admin-base.php:101 +msgid "Onbekende fout" +msgstr "" + +#: includes/Admin/class-groq-ai-term-admin-base.php:102 +msgid "Onbekende term." +msgstr "" + +#: includes/Admin/class-groq-ai-term-admin-base.php:103 +msgid "Stoppen?" +msgstr "" + +#: includes/Admin/class-groq-ai-term-admin-base.php:104 +msgid "%1$s: %2$s" +msgstr "" + +#: includes/Admin/class-groq-ai-term-admin-base.php:105 +msgid "%1$s gevuld." +msgstr "" + +#: includes/Admin/class-groq-ai-term-admin-base.php:106 +msgid "%1$s mislukt: %2$s" +msgstr "" + +#: includes/Admin/class-groq-ai-term-admin-base.php:107 +msgid "%s is bijgewerkt." +msgstr "" diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..b77ac16 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,13 @@ + + + + + tests + + + diff --git a/tests/ModelExclusionsTest.php b/tests/ModelExclusionsTest.php new file mode 100644 index 0000000..1ee9397 --- /dev/null +++ b/tests/ModelExclusionsTest.php @@ -0,0 +1,17 @@ +assertSame( '', $blocked ); + } + + public function test_filter_models_removes_excluded_entries() { + $models = [ 'llama3-70b-8192', 'whisper-large-v3', 'mixtral-8x7b-32768' ]; + $filtered = Groq_AI_Model_Exclusions::filter_models( 'groq', $models ); + + $this->assertSame( [ 'llama3-70b-8192', 'mixtral-8x7b-32768' ], $filtered ); + } +} diff --git a/tests/ProviderRequestBuilderTest.php b/tests/ProviderRequestBuilderTest.php new file mode 100644 index 0000000..25d830f --- /dev/null +++ b/tests/ProviderRequestBuilderTest.php @@ -0,0 +1,102 @@ +generate_content( + [ + 'prompt' => 'Hallo', + 'system_prompt' => 'System', + 'model' => 'gpt-4o-mini', + 'settings' => [ + 'openai_api_key' => 'test-key', + 'max_output_tokens' => 512, + ], + 'temperature' => 0.5, + 'response_format' => [ + 'type' => 'json_object', + ], + ] + ); + + $this->assertIsArray( $result ); + $payload = $result['request_payload']['body']; + $this->assertSame( 'gpt-4o-mini', $payload['model'] ); + $this->assertSame( 0.5, $payload['temperature'] ); + $this->assertSame( 512, $payload['max_tokens'] ); + $this->assertSame( 'json_object', $payload['response_format']['type'] ); + $this->assertSame( 'System', $payload['messages'][0]['content'] ); + $this->assertSame( 'Hallo', $payload['messages'][1]['content'] ); + } + + public function test_groq_request_payload_uses_default_model_when_missing() { + $provider = new Groq_AI_Provider_Groq(); + $result = $provider->generate_content( + [ + 'prompt' => 'Hallo', + 'system_prompt' => 'System', + 'settings' => [ + 'groq_api_key' => 'test-key', + ], + ] + ); + + $this->assertIsArray( $result ); + $payload = $result['request_payload']['body']; + $this->assertSame( $provider->get_default_model(), $payload['model'] ); + $this->assertSame( 'System', $payload['messages'][0]['content'] ); + $this->assertSame( 'Hallo', $payload['messages'][1]['content'] ); + } + + public function test_google_request_payload_builds_schema_and_images() { + $provider = new Groq_AI_Provider_Google(); + $result = $provider->generate_content( + [ + 'prompt' => 'Hallo', + 'system_prompt' => 'System', + 'model' => 'gemini-1.5-flash', + 'settings' => [ + 'google_api_key' => 'test-key', + 'max_output_tokens' => 256, + 'google_safety_settings' => [ + 'HARM_CATEGORY_HARASSMENT' => 'BLOCK_LOW_AND_ABOVE', + ], + ], + 'temperature' => 0.2, + 'response_format' => [ + 'type' => 'json_schema', + 'json_schema' => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'name' => [ + 'type' => 'string', + ], + ], + ], + ], + ], + 'image_context' => [ + [ + 'label' => 'Image 1', + 'mime_type' => 'image/png', + 'data' => 'BASE64DATA', + ], + ], + ] + ); + + $this->assertIsArray( $result ); + $payload = $result['request_payload']['body']; + $this->assertSame( 0.2, $payload['generationConfig']['temperature'] ); + $this->assertSame( 256, $payload['generationConfig']['maxOutputTokens'] ); + $this->assertSame( 'application/json', $payload['generationConfig']['responseMimeType'] ); + $this->assertArrayHasKey( 'responseJsonSchema', $payload['generationConfig'] ); + $this->assertSame( 'System', $payload['contents'][0]['parts'][0]['text'] ); + $this->assertSame( 'Hallo', $payload['contents'][0]['parts'][1]['text'] ); + $this->assertSame( 'image/png', $payload['contents'][0]['parts'][3]['inline_data']['mime_type'] ); + $this->assertSame( 'BASE64DATA', $payload['contents'][0]['parts'][3]['inline_data']['data'] ); + } +} diff --git a/tests/SettingsManagerTest.php b/tests/SettingsManagerTest.php new file mode 100644 index 0000000..411bacd --- /dev/null +++ b/tests/SettingsManagerTest.php @@ -0,0 +1,112 @@ +make_manager(); + + $result = $manager->sanitize( [ + 'logs_retention_days' => 5000, + ] ); + + $this->assertSame( 3650, $result['logs_retention_days'] ); + } + + public function test_logs_retention_days_allows_zero() { + $manager = $this->make_manager(); + + $result = $manager->sanitize( [ + 'logs_retention_days' => 0, + ] ); + + $this->assertSame( 0, $result['logs_retention_days'] ); + } + + public function test_logs_retention_days_negative_becomes_zero() { + $manager = $this->make_manager(); + + $result = $manager->sanitize( [ + 'logs_retention_days' => -5, + ] ); + + $this->assertSame( 0, $result['logs_retention_days'] ); + } + + public function test_sanitize_accepts_all_settings_keys() { + $manager = $this->make_manager(); + $context_fields = $manager->get_default_context_fields(); + $modules = $manager->get_default_modules_settings(); + $google_categories = Groq_AI_Settings_Manager::get_google_safety_categories_list(); + $first_category = array_key_first( $google_categories ); + + $input = [ + 'provider' => 'openai', + 'model' => 'gpt-4o-mini', + 'store_context' => 'Test winkelcontext', + 'default_prompt' => 'Schrijf een korte tekst', + 'max_output_tokens' => 2048, + 'logs_retention_days' => 30, + 'product_attribute_includes' => [ '__all__', '__custom__', 'pa_color', 'invalid key' ], + 'term_bottom_description_meta_key' => 'custom_bottom_key', + 'groq_api_key' => 'groq-key', + 'openai_api_key' => 'openai-key', + 'google_api_key' => 'google-key', + 'google_oauth_client_id' => 'client-id', + 'google_oauth_client_secret' => 'client-secret', + 'google_oauth_refresh_token' => 'refresh-token', + 'google_oauth_connected_email' => 'user@example.com', + 'google_oauth_connected_at' => 123456, + 'google_enable_gsc' => true, + 'google_enable_ga' => false, + 'google_gsc_site_url' => 'https://example.com/', + 'google_ga4_property_id' => '123456', + 'google_safety_settings' => $first_category ? [ $first_category => 'BLOCK_LOW_AND_ABOVE' ] : [], + 'context_fields' => $context_fields, + 'modules' => $modules, + 'image_context_mode' => 'base64', + 'image_context_limit' => 5, + 'response_format_compat' => true, + 'term_top_description_char_limit' => 700, + 'term_bottom_description_char_limit' => 1400, + ]; + + $result = $manager->sanitize( $input ); + + $this->assertSame( 'openai', $result['provider'] ); + $this->assertSame( 'gpt-4o-mini', $result['model'] ); + $this->assertSame( 'Test winkelcontext', $result['store_context'] ); + $this->assertSame( 'Schrijf een korte tekst', $result['default_prompt'] ); + $this->assertSame( 2048, $result['max_output_tokens'] ); + $this->assertSame( 30, $result['logs_retention_days'] ); + $this->assertContains( '__all__', $result['product_attribute_includes'] ); + $this->assertContains( '__custom__', $result['product_attribute_includes'] ); + $this->assertContains( 'pa_color', $result['product_attribute_includes'] ); + $this->assertSame( 'custom_bottom_key', $result['term_bottom_description_meta_key'] ); + $this->assertSame( 'groq-key', $result['groq_api_key'] ); + $this->assertSame( 'openai-key', $result['openai_api_key'] ); + $this->assertSame( 'google-key', $result['google_api_key'] ); + $this->assertSame( 'client-id', $result['google_oauth_client_id'] ); + $this->assertSame( 'client-secret', $result['google_oauth_client_secret'] ); + $this->assertSame( 'refresh-token', $result['google_oauth_refresh_token'] ); + $this->assertSame( 'user@example.com', $result['google_oauth_connected_email'] ); + $this->assertSame( 123456, $result['google_oauth_connected_at'] ); + $this->assertTrue( $result['google_enable_gsc'] ); + $this->assertFalse( $result['google_enable_ga'] ); + $this->assertSame( 'https://example.com/', $result['google_gsc_site_url'] ); + $this->assertSame( '123456', $result['google_ga4_property_id'] ); + $this->assertIsArray( $result['google_safety_settings'] ); + $this->assertIsArray( $result['context_fields'] ); + $this->assertIsArray( $result['modules'] ); + $this->assertSame( 'base64', $result['image_context_mode'] ); + $this->assertSame( 5, $result['image_context_limit'] ); + $this->assertTrue( $result['response_format_compat'] ); + $this->assertSame( 700, $result['term_top_description_char_limit'] ); + $this->assertSame( 1400, $result['term_bottom_description_char_limit'] ); + } +} diff --git a/tests/TermSaveTest.php b/tests/TermSaveTest.php new file mode 100644 index 0000000..f3d3659 --- /dev/null +++ b/tests/TermSaveTest.php @@ -0,0 +1,67 @@ + '' ]; + } + public function is_module_enabled( $module, $settings = null ) { + return false; + } + }; + + $controller_ref = new ReflectionClass( Groq_AI_Ajax_Controller::class ); + $controller = $controller_ref->newInstanceWithoutConstructor(); + $plugin_prop = $controller_ref->getProperty( 'plugin' ); + $plugin_prop->setAccessible( true ); + $plugin_prop->setValue( $controller, $plugin ); + + add_filter( + 'groq_ai_term_bottom_description_meta_key', + function ( $default_key ) { + return 'Custom Key'; + }, + 10, + 3 + ); + + $term = (object) [ + 'term_id' => 12, + 'taxonomy' => 'product_cat', + 'name' => 'Test', + 'description' => '', + ]; + + $result = [ + 'top_description' => '

Dit is een test.

', + 'bottom_description' => '

Onderste tekst.

', + ]; + + $settings = $plugin->get_settings(); + + $method = $controller_ref->getMethod( 'save_term_generation_result' ); + $method->setAccessible( true ); + $saved = $method->invoke( $controller, $term, $result, $settings ); + + $this->assertIsArray( $saved ); + $this->assertSame( 4, $saved['words'] ); + + $this->assertCount( 1, $GLOBALS['wp_term_updates'] ); + $this->assertSame( 12, $GLOBALS['wp_term_updates'][0]['term_id'] ); + $this->assertSame( 'product_cat', $GLOBALS['wp_term_updates'][0]['taxonomy'] ); + $this->assertSame( '

Dit is een test.

', $GLOBALS['wp_term_updates'][0]['args']['description'] ); + + $this->assertArrayHasKey( 12, $GLOBALS['wp_term_meta_updates'] ); + $this->assertArrayHasKey( 'customkey', $GLOBALS['wp_term_meta_updates'][12] ); + $this->assertSame( '

Onderste tekst.

', $GLOBALS['wp_term_meta_updates'][12]['customkey'] ); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..b404e14 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,258 @@ +message = (string) $message; + } + + public function get_error_message() { + return $this->message; + } + } +} + +if ( ! function_exists( 'is_wp_error' ) ) { + function is_wp_error( $thing ) { + return $thing instanceof WP_Error; + } +} + +if ( ! function_exists( '__' ) ) { + function __( $text ) { + return $text; + } +} + +if ( ! function_exists( 'wp_parse_args' ) ) { + function wp_parse_args( $args, $defaults = [] ) { + if ( is_object( $args ) ) { + $args = get_object_vars( $args ); + } + if ( ! is_array( $args ) ) { + $args = []; + } + return array_merge( $defaults, $args ); + } +} + +if ( ! function_exists( 'sanitize_text_field' ) ) { + function sanitize_text_field( $text ) { + return trim( (string) $text ); + } +} + +if ( ! function_exists( 'sanitize_textarea_field' ) ) { + function sanitize_textarea_field( $text ) { + return trim( (string) $text ); + } +} + +if ( ! function_exists( 'sanitize_key' ) ) { + function sanitize_key( $key ) { + $key = strtolower( (string) $key ); + return preg_replace( '/[^a-z0-9_\-]/', '', $key ); + } +} + +if ( ! function_exists( 'absint' ) ) { + function absint( $number ) { + return abs( (int) $number ); + } +} + +if ( ! function_exists( 'esc_url_raw' ) ) { + function esc_url_raw( $url ) { + return (string) $url; + } +} + +if ( ! function_exists( 'add_filter' ) ) { + function add_filter( $tag, $callback, $priority = 10, $accepted_args = 1 ) { + $GLOBALS['wp_filters'][ $tag ][ $priority ][] = [ + 'callback' => $callback, + 'accepted_args' => (int) $accepted_args, + ]; + } +} + +if ( ! function_exists( 'apply_filters' ) ) { + function apply_filters( $tag, $value ) { + $args = func_get_args(); + if ( empty( $GLOBALS['wp_filters'][ $tag ] ) ) { + return $value; + } + ksort( $GLOBALS['wp_filters'][ $tag ] ); + foreach ( $GLOBALS['wp_filters'][ $tag ] as $callbacks ) { + foreach ( $callbacks as $filter ) { + $accepted = isset( $filter['accepted_args'] ) ? (int) $filter['accepted_args'] : 1; + $call_args = array_slice( $args, 0, max( 1, $accepted ) ); + $call_args[0] = $value; + $value = call_user_func_array( $filter['callback'], $call_args ); + $args[0] = $value; + } + } + return $value; + } +} + +if ( ! function_exists( 'wp_kses_post' ) ) { + function wp_kses_post( $content ) { + return (string) $content; + } +} + +if ( ! function_exists( 'wp_strip_all_tags' ) ) { + function wp_strip_all_tags( $text ) { + return strip_tags( (string) $text ); + } +} + +if ( ! function_exists( 'wp_update_term' ) ) { + function wp_update_term( $term_id, $taxonomy, $args = [] ) { + $GLOBALS['wp_term_updates'][] = [ + 'term_id' => (int) $term_id, + 'taxonomy' => (string) $taxonomy, + 'args' => $args, + ]; + return [ 'term_id' => (int) $term_id ]; + } +} + +if ( ! function_exists( 'update_term_meta' ) ) { + function update_term_meta( $term_id, $meta_key, $meta_value ) { + $term_id = (int) $term_id; + if ( ! isset( $GLOBALS['wp_term_meta_updates'][ $term_id ] ) ) { + $GLOBALS['wp_term_meta_updates'][ $term_id ] = []; + } + $GLOBALS['wp_term_meta_updates'][ $term_id ][ (string) $meta_key ] = $meta_value; + return true; + } +} + +if ( ! function_exists( 'wp_json_encode' ) ) { + function wp_json_encode( $data, $options = 0, $depth = 512 ) { + return json_encode( $data, $options, $depth ); + } +} + +if ( ! function_exists( 'add_query_arg' ) ) { + function add_query_arg( $args, $url = '' ) { + if ( is_string( $args ) ) { + return $url; + } + $query = http_build_query( (array) $args ); + $separator = strpos( $url, '?' ) === false ? '?' : '&'; + return $url . $separator . $query; + } +} + +if ( ! function_exists( 'wp_remote_post' ) ) { + function wp_remote_post( $url, $args = [] ) { + $GLOBALS['wp_last_http_request'] = [ + 'url' => $url, + 'args' => $args, + ]; + + $body = json_encode( + [ + 'choices' => [ + [ + 'message' => [ + 'content' => 'ok', + ], + 'finish_reason' => 'stop', + ], + ], + 'usage' => [ + 'prompt_tokens' => 10, + 'completion_tokens' => 20, + 'total_tokens' => 30, + ], + 'candidates' => [ + [ + 'content' => [ + 'parts' => [ + [ 'text' => 'ok' ], + ], + ], + 'finishReason' => 'STOP', + ], + ], + 'usageMetadata' => [ + 'promptTokenCount' => 10, + 'candidatesTokenCount' => 20, + 'totalTokenCount' => 30, + ], + ] + ); + + return [ + 'body' => $body, + 'response' => [ 'code' => 200 ], + ]; + } +} + +if ( ! function_exists( 'wp_remote_get' ) ) { + function wp_remote_get( $url, $args = [] ) { + $GLOBALS['wp_last_http_request'] = [ + 'url' => $url, + 'args' => $args, + ]; + + return [ + 'body' => json_encode( [ 'data' => [], 'models' => [] ] ), + 'response' => [ 'code' => 200 ], + ]; + } +} + +if ( ! function_exists( 'wp_remote_retrieve_body' ) ) { + function wp_remote_retrieve_body( $response ) { + return isset( $response['body'] ) ? $response['body'] : ''; + } +} + +if ( ! function_exists( 'wp_remote_retrieve_response_code' ) ) { + function wp_remote_retrieve_response_code( $response ) { + return isset( $response['response']['code'] ) ? (int) $response['response']['code'] : 0; + } +} + +if ( ! function_exists( 'get_option' ) ) { + function get_option( $key, $default = false ) { + return isset( $GLOBALS['wp_options'][ $key ] ) ? $GLOBALS['wp_options'][ $key ] : $default; + } +} + +if ( ! function_exists( 'update_option' ) ) { + function update_option( $key, $value ) { + $GLOBALS['wp_options'][ $key ] = $value; + return true; + } +} + +require_once __DIR__ . '/../includes/Core/class-groq-ai-model-exclusions.php'; +require_once __DIR__ . '/../includes/Contracts/interface-groq-ai-provider.php'; +require_once __DIR__ . '/../includes/Providers/class-groq-ai-abstract-openai-provider.php'; +require_once __DIR__ . '/../includes/Providers/class-groq-ai-provider-groq.php'; +require_once __DIR__ . '/../includes/Providers/class-groq-ai-provider-openai.php'; +require_once __DIR__ . '/../includes/Providers/class-groq-ai-provider-google.php'; +require_once __DIR__ . '/../includes/Providers/class-groq-ai-provider-manager.php'; +require_once __DIR__ . '/../includes/Services/Settings/class-groq-ai-settings-manager.php'; +require_once __DIR__ . '/../includes/Core/class-groq-ai-ajax-controller.php';