diff --git a/composer.json b/composer.json index 7cccc9417..36b21b0cd 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "yahnis-elsts/plugin-update-checker": "5.1", "ua-parser/uap-php": "dev-master", "aws/aws-sdk-php": "^3.300", - "woocommerce/action-scheduler": "3.8.1" + "woocommerce/action-scheduler": "3.8.1", + "wordpress/php-ai-client": "^0.1" }, "autoload": { "psr-4": { @@ -47,7 +48,8 @@ "config": { "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, - "phpstan/extension-installer": true + "phpstan/extension-installer": true, + "php-http/discovery": true } }, "archive": { diff --git a/composer.lock b/composer.lock index 0e8b41fd8..f6880c441 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5f8e7a0c0c12a517240bd7bee9f43f1f", + "content-hash": "c81fe4a9dc7edac22dd13a13a10cf073", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.334.2", + "version": "3.337.3", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "b19afc076bb1cc2617bdef76efd41587596109e7" + "reference": "06dfc8f76423b49aaa181debd25bbdc724c346d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b19afc076bb1cc2617bdef76efd41587596109e7", - "reference": "b19afc076bb1cc2617bdef76efd41587596109e7", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/06dfc8f76423b49aaa181debd25bbdc724c346d6", + "reference": "06dfc8f76423b49aaa181debd25bbdc724c346d6", "shasum": "" }, "require": { @@ -154,9 +154,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.334.2" + "source": "https://github.com/aws/aws-sdk-php/tree/3.337.3" }, - "time": "2024-12-09T19:30:23+00:00" + "time": "2025-01-21T19:10:05+00:00" }, { "name": "composer/ca-bundle", @@ -164,12 +164,12 @@ "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "bc0593537a463e55cadf45fd938d23b75095b7e1" + "reference": "719026bb30813accb68271fee7e39552a58e9f65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/bc0593537a463e55cadf45fd938d23b75095b7e1", - "reference": "bc0593537a463e55cadf45fd938d23b75095b7e1", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/719026bb30813accb68271fee7e39552a58e9f65", + "reference": "719026bb30813accb68271fee7e39552a58e9f65", "shasum": "" }, "require": { @@ -217,7 +217,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.4" + "source": "https://github.com/composer/ca-bundle/tree/1.5.8" }, "funding": [ { @@ -227,32 +227,28 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-11-27T15:35:25+00:00" + "time": "2025-08-20T18:49:47+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.9.x-dev", + "version": "7.10.x-dev", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -344,7 +340,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" }, "funding": [ { @@ -360,20 +356,20 @@ "type": "tidelift" } ], - "time": "2024-07-24T11:22:20+00:00" + "time": "2025-08-23T22:36:01+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.x-dev", + "version": "2.3.x-dev", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + "reference": "481557b130ef3790cf82b713667b43030dc9c957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", "shasum": "" }, "require": { @@ -381,7 +377,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "default-branch": true, "type": "library", @@ -428,7 +424,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.4" + "source": "https://github.com/guzzle/promises/tree/2.3.0" }, "funding": [ { @@ -444,20 +440,20 @@ "type": "tidelift" } ], - "time": "2024-10-17T10:06:22+00:00" + "time": "2025-08-22T14:34:08+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.x-dev", + "version": "2.8.x-dev", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + "reference": "21dc724a0583619cd1652f673303492272778051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", "shasum": "" }, "require": { @@ -473,7 +469,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -545,7 +541,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.0" + "source": "https://github.com/guzzle/psr7/tree/2.8.0" }, "funding": [ { @@ -561,7 +557,7 @@ "type": "tidelift" } ], - "time": "2024-07-18T11:15:46+00:00" + "time": "2025-08-23T21:21:41+00:00" }, { "name": "mtdowling/jmespath.php", @@ -630,6 +626,253 @@ }, "time": "2024-09-04T18:46:31+00:00" }, + { + "name": "php-http/discovery", + "version": "1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "default-branch": true, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.x-dev", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "37819ce3c78ed2e7d001de68b38e697bbd525f89" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/37819ce3c78ed2e7d001de68b38e697bbd525f89", + "reference": "37819ce3c78ed2e7d001de68b38e697bbd525f89", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.x" + }, + "time": "2024-09-23T13:25:15+00:00" + }, + { + "name": "php-http/message-factory", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-http/message-factory.git", + "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message-factory/zipball/4d8778e1c7d405cbb471574821c1ff5b68cc8f57", + "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "psr/http-message": "^1.0 || ^2.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Factory interfaces for PSR-7 HTTP Message", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "stream", + "uri" + ], + "support": { + "issues": "https://github.com/php-http/message-factory/issues", + "source": "https://github.com/php-http/message-factory/tree/1.1.0" + }, + "abandoned": "psr/http-factory", + "time": "2023-04-14T14:16:17+00:00" + }, + { + "name": "php-http/promise", + "version": "1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "12e12043e9ed9ddc6ea8481593fb230150227416" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/12e12043e9ed9ddc6ea8481593fb230150227416", + "reference": "12e12043e9ed9ddc6ea8481593fb230150227416", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.x" + }, + "time": "2024-10-02T11:48:29+00:00" + }, { "name": "psr/http-client", "version": "dev-master", @@ -855,12 +1098,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -909,15 +1152,16 @@ "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "2369cb908b33d7b7518cce042615de430142497f" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2369cb908b33d7b7518cce042615de430142497f", - "reference": "2369cb908b33d7b7518cce042615de430142497f", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -966,7 +1210,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/1.x" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -977,12 +1221,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-10T14:38:51+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "ua-parser/uap-php", @@ -990,12 +1238,12 @@ "source": { "type": "git", "url": "https://github.com/ua-parser/uap-php.git", - "reference": "b796c5ea5df588e65aeb4e2c6cce3811dec4fed6" + "reference": "f44bdd1b38198801cf60b0681d2d842980e47af5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ua-parser/uap-php/zipball/b796c5ea5df588e65aeb4e2c6cce3811dec4fed6", - "reference": "b796c5ea5df588e65aeb4e2c6cce3811dec4fed6", + "url": "https://api.github.com/repos/ua-parser/uap-php/zipball/f44bdd1b38198801cf60b0681d2d842980e47af5", + "reference": "f44bdd1b38198801cf60b0681d2d842980e47af5", "shasum": "" }, "require": { @@ -1044,9 +1292,9 @@ "description": "A multi-language port of Browserscope's user agent parser.", "support": { "issues": "https://github.com/ua-parser/uap-php/issues", - "source": "https://github.com/ua-parser/uap-php/tree/v3.9.14" + "source": "https://github.com/ua-parser/uap-php/tree/v3.10.0" }, - "time": "2020-10-02T23:36:20+00:00" + "time": "2025-07-17T15:43:24+00:00" }, { "name": "woocommerce/action-scheduler", @@ -1091,6 +1339,73 @@ }, "time": "2024-06-20T19:53:06+00:00" }, + { + "name": "wordpress/php-ai-client", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress/php-ai-client.git", + "reference": "9ec56e70e692791493a3eaff1b69f25f4daeded7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/php-ai-client/zipball/9ec56e70e692791493a3eaff1b69f25f4daeded7", + "reference": "9ec56e70e692791493a3eaff1b69f25f4daeded7", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.4", + "php-http/discovery": "^1.0", + "php-http/httplug": "^2.0", + "php-http/message-factory": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "php-http/curl-client": "^2.0", + "php-http/mock-client": "^1.0", + "phpcompatibility/php-compatibility": "dev-develop", + "phpstan/phpstan": "~2.1", + "phpunit/phpunit": "^9.5 || ^10.0", + "slevomat/coding-standard": "^8.20", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/polyfills.php" + ], + "psr-4": { + "WordPress\\AiClient\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "WordPress AI Team", + "homepage": "https://make.wordpress.org/ai/" + } + ], + "description": "A provider agnostic PHP AI client SDK to communicate with any generative AI models of various capabilities using a uniform API.", + "homepage": "https://github.com/WordPress/php-ai-client", + "keywords": [ + "ai", + "api", + "llm" + ], + "support": { + "issues": "https://github.com/WordPress/php-ai-client/issues", + "source": "https://github.com/WordPress/php-ai-client" + }, + "time": "2025-08-29T22:46:54+00:00" + }, { "name": "yahnis-elsts/plugin-update-checker", "version": "v5.1", @@ -1239,28 +1554,28 @@ }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.0.0", + "version": "v1.1.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "4be43904336affa5c2f70744a348312336afd0da" + "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", - "reference": "4be43904336affa5c2f70744a348312336afd0da", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", + "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", + "composer-plugin-api": "^2.2", "php": ">=5.4", "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { - "composer/composer": "*", + "composer/composer": "^2.2", "ext-json": "*", "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.3.1", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcompatibility/php-compatibility": "^9.0", "yoast/phpunit-polyfills": "^1.0" }, @@ -1280,9 +1595,9 @@ "authors": [ { "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl", - "role": "Developer / IT Manager" + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" }, { "name": "Contributors", @@ -1290,7 +1605,6 @@ } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", "keywords": [ "PHPCodeSniffer", "PHP_CodeSniffer", @@ -1311,9 +1625,28 @@ ], "support": { "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2023-01-05T11:28:13+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-07-17T20:45:56+00:00" }, { "name": "doctrine/instantiator", @@ -1387,21 +1720,22 @@ }, { "name": "johnbillion/wp-compat", - "version": "1.1.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/johnbillion/wp-compat.git", - "reference": "c86aa3c71b560817689723f41bd551f5c10e2a22" + "reference": "ea44e487191b39b2beea0e8e4ee4e1f28376e0eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/johnbillion/wp-compat/zipball/c86aa3c71b560817689723f41bd551f5c10e2a22", - "reference": "c86aa3c71b560817689723f41bd551f5c10e2a22", + "url": "https://api.github.com/repos/johnbillion/wp-compat/zipball/ea44e487191b39b2beea0e8e4ee4e1f28376e0eb", + "reference": "ea44e487191b39b2beea0e8e4ee4e1f28376e0eb", "shasum": "" }, "require": { "php": ">= 7.4", - "phpstan/phpstan": "^2.0" + "phpstan/phpstan": "^2.0", + "wp-hooks/wordpress-core": "^1.10" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", @@ -1413,7 +1747,7 @@ "phpstan/phpstan-strict-rules": "2.0.0", "phpunit/phpunit": "^9.0", "roots/wordpress-core-installer": "1.100.0", - "roots/wordpress-full": "^6.7.0", + "roots/wordpress-full": "*", "wp-coding-standards/wpcs": "3.1.0" }, "suggest": { @@ -1460,7 +1794,7 @@ "type": "github" } ], - "time": "2024-11-30T19:24:46+00:00" + "time": "2025-07-03T15:08:02+00:00" }, { "name": "myclabs/deep-copy", @@ -1468,12 +1802,12 @@ "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "4764e040f8743e92b86c36f488f32d0265dd1dae" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/4764e040f8743e92b86c36f488f32d0265dd1dae", - "reference": "4764e040f8743e92b86c36f488f32d0265dd1dae", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -1513,7 +1847,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.x" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -1521,20 +1855,20 @@ "type": "tidelift" } ], - "time": "2024-11-26T13:04:49+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "f7c23a43eee861070ab4e88819a4e76a611c7e4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f7c23a43eee861070ab4e88819a4e76a611c7e4f", + "reference": "f7c23a43eee861070ab4e88819a4e76a611c7e4f", "shasum": "" }, "require": { @@ -1547,13 +1881,14 @@ "ircmaxell/php-yacc": "^0.0.7", "phpunit/phpunit": "^9.0" }, + "default-branch": true, "bin": [ "bin/php-parse" ], "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -1577,9 +1912,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/master" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2025-08-19T18:41:44+00:00" }, { "name": "phar-io/manifest", @@ -1587,12 +1922,12 @@ "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "54750ef60c58e43759730615a392c31c80e23176" + "reference": "65f90285728eae4eae313b8b6ba11b2f5436038e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", - "reference": "54750ef60c58e43759730615a392c31c80e23176", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/65f90285728eae4eae313b8b6ba11b2f5436038e", + "reference": "65f90285728eae4eae313b8b6ba11b2f5436038e", "shasum": "" }, "require": { @@ -1639,7 +1974,7 @@ "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" + "source": "https://github.com/phar-io/manifest/tree/master" }, "funding": [ { @@ -1647,7 +1982,7 @@ "type": "github" } ], - "time": "2024-03-03T12:33:53+00:00" + "time": "2025-07-05T08:48:25+00:00" }, { "name": "phar-io/version", @@ -1702,25 +2037,28 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.7.1", + "version": "v6.8.2", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "83448e918bf06d1ed3d67ceb6a985fc266a02fd1" + "reference": "9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/83448e918bf06d1ed3d67ceb6a985fc266a02fd1", - "reference": "83448e918bf06d1ed3d67ceb6a985fc266a02fd1", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8", + "reference": "9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8", "shasum": "" }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "nikic/php-parser": "^4.13", + "nikic/php-parser": "^5.5", "php": "^7.4 || ^8.0", "php-stubs/generator": "^0.8.3", "phpdocumentor/reflection-docblock": "^5.4.1", - "phpstan/phpstan": "^1.11", + "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^9.5", "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" @@ -1744,9 +2082,9 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.7.1" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.8.2" }, - "time": "2024-11-24T03:57:09+00:00" + "time": "2025-07-16T06:41:00+00:00" }, { "name": "php-stubs/wp-cli-stubs", @@ -1754,12 +2092,12 @@ "source": { "type": "git", "url": "https://github.com/php-stubs/wp-cli-stubs.git", - "reference": "f27ff9e8e29d7962cb070e58de70dfaf63183007" + "reference": "af16401e299a3fd2229bd0fa9a037638a4174a9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wp-cli-stubs/zipball/f27ff9e8e29d7962cb070e58de70dfaf63183007", - "reference": "f27ff9e8e29d7962cb070e58de70dfaf63183007", + "url": "https://api.github.com/repos/php-stubs/wp-cli-stubs/zipball/af16401e299a3fd2229bd0fa9a037638a4174a9d", + "reference": "af16401e299a3fd2229bd0fa9a037638a4174a9d", "shasum": "" }, "require": { @@ -1789,9 +2127,9 @@ ], "support": { "issues": "https://github.com/php-stubs/wp-cli-stubs/issues", - "source": "https://github.com/php-stubs/wp-cli-stubs/tree/v2.11.0" + "source": "https://github.com/php-stubs/wp-cli-stubs/tree/v2.12.0" }, - "time": "2024-11-25T10:09:13+00:00" + "time": "2025-06-10T09:58:05+00:00" }, { "name": "phpcompatibility/php-compatibility", @@ -1799,18 +2137,18 @@ "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "02835e45d27f5d0ed1642d5c8a5e8bfd2c7d7796" + "reference": "6e10469b0f3827862b37df2ac2b7ec4580ce888f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/02835e45d27f5d0ed1642d5c8a5e8bfd2c7d7796", - "reference": "02835e45d27f5d0ed1642d5c8a5e8bfd2c7d7796", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/6e10469b0f3827862b37df2ac2b7ec4580ce888f", + "reference": "6e10469b0f3827862b37df2ac2b7ec4580ce888f", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.0.12", - "squizlabs/php_codesniffer": "^3.10.0" + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.0" }, "replace": { "wimg/php-compatibility": "*" @@ -1820,8 +2158,8 @@ "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcsstandards/phpcsdevcs": "^1.1.3", "phpcsstandards/phpcsdevtools": "^1.2.0", - "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4 || ^10.1.0", - "yoast/phpunit-polyfills": "^1.0.5 || ^2.0.0" + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4 || ^10.5.32 || ^11.3.3", + "yoast/phpunit-polyfills": "^1.0.5 || ^2.0 || ^3.0" }, "suggest": { "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." @@ -1855,7 +2193,7 @@ } ], "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", - "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "homepage": "https://techblog.wimgodden.be/tag/codesniffer/", "keywords": [ "compatibility", "phpcs", @@ -1879,9 +2217,13 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" } ], - "time": "2024-11-30T16:25:00+00:00" + "time": "2025-08-20T03:33:09+00:00" }, { "name": "phpcompatibility/phpcompatibility-paragonie", @@ -1957,21 +2299,22 @@ }, { "name": "phpcompatibility/phpcompatibility-wp", - "version": "2.1.5", + "version": "2.1.7", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082" + "reference": "5bfbbfbabb3df2b9a83e601de9153e4a7111962c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/01c1ff2704a58e46f0cb1ca9d06aee07b3589082", - "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/5bfbbfbabb3df2b9a83e601de9153e4a7111962c", + "reference": "5bfbbfbabb3df2b9a83e601de9153e4a7111962c", "shasum": "" }, "require": { "phpcompatibility/php-compatibility": "^9.0", - "phpcompatibility/phpcompatibility-paragonie": "^1.0" + "phpcompatibility/phpcompatibility-paragonie": "^1.0", + "squizlabs/php_codesniffer": "^3.3" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^1.0" @@ -2021,9 +2364,13 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" } ], - "time": "2024-04-24T21:37:59+00:00" + "time": "2025-05-12T16:38:37+00:00" }, { "name": "phpcsstandards/phpcsextra", @@ -2031,25 +2378,25 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "31ef149a1ee85ec0d355e58418d78d927e89d185" + "reference": "639d1625f6ff27c616052c8478dd06eb41fd43a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/31ef149a1ee85ec0d355e58418d78d927e89d185", - "reference": "31ef149a1ee85ec0d355e58418d78d927e89d185", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/639d1625f6ff27c616052c8478dd06eb41fd43a5", + "reference": "639d1625f6ff27c616052c8478dd06eb41fd43a5", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.0.9", - "squizlabs/php_codesniffer": "^3.8.0" + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcsstandards/phpcsdevcs": "^1.1.6", "phpcsstandards/phpcsdevtools": "^1.2.1", - "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "default-branch": true, "type": "phpcodesniffer-standard", @@ -2100,9 +2447,13 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2024-11-11T18:03:33+00:00" + "time": "2025-08-18T02:06:20+00:00" }, { "name": "phpcsstandards/phpcsutils", @@ -2110,23 +2461,23 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "9b2671b7342d2c8ad8ec78c8bdac45958d9de065" + "reference": "0f6b8020bb218754b11e0e4845667b5176206b18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/9b2671b7342d2c8ad8ec78c8bdac45958d9de065", - "reference": "9b2671b7342d2c8ad8ec78c8bdac45958d9de065", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/0f6b8020bb218754b11e0e4845667b5176206b18", + "reference": "0f6b8020bb218754b11e0e4845667b5176206b18", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.10.1 || 4.0.x-dev@dev" + "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" }, "require-dev": { "ext-filter": "*", "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcsstandards/phpcsdevcs": "^1.1.6", "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" }, @@ -2166,6 +2517,7 @@ "phpcodesniffer-standard", "phpcs", "phpcs3", + "phpcs4", "standards", "static analysis", "tokens", @@ -2189,9 +2541,13 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2024-11-17T04:37:55+00:00" + "time": "2025-08-25T20:42:19+00:00" }, { "name": "phpstan/extension-installer", @@ -2199,12 +2555,12 @@ "source": { "type": "git", "url": "https://github.com/phpstan/extension-installer.git", - "reference": "e7bd2bf9662c96368303a284be3f2079e204b341" + "reference": "a998f10f8e3f5f38739e82cacebdee731a3c04a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/e7bd2bf9662c96368303a284be3f2079e204b341", - "reference": "e7bd2bf9662c96368303a284be3f2079e204b341", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/a998f10f8e3f5f38739e82cacebdee731a3c04a3", + "reference": "a998f10f8e3f5f38739e82cacebdee731a3c04a3", "shasum": "" }, "require": { @@ -2240,20 +2596,20 @@ "issues": "https://github.com/phpstan/extension-installer/issues", "source": "https://github.com/phpstan/extension-installer/tree/1.4.x" }, - "time": "2024-09-06T14:27:20+00:00" + "time": "2025-08-21T08:44:40+00:00" }, { "name": "phpstan/phpstan", - "version": "2.0.x-dev", + "version": "2.1.x-dev", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "65dc199c3c137db0ee1b05cf94c8fe65d53fedbd" + "reference": "5a39902d7871018ebf1b4a47284a984e2d39c012" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/65dc199c3c137db0ee1b05cf94c8fe65d53fedbd", - "reference": "65dc199c3c137db0ee1b05cf94c8fe65d53fedbd", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/5a39902d7871018ebf1b4a47284a984e2d39c012", + "reference": "5a39902d7871018ebf1b4a47284a984e2d39c012", "shasum": "" }, "require": { @@ -2299,7 +2655,7 @@ "type": "github" } ], - "time": "2024-12-10T13:04:19+00:00" + "time": "2025-08-30T16:32:16+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -2307,17 +2663,17 @@ "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "1cc1259cb91ee4cfbb5c39bca9f635f067c910b4" + "reference": "0b87f525aefac2d0b7472440a9075acc78d23168" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/1cc1259cb91ee4cfbb5c39bca9f635f067c910b4", - "reference": "1cc1259cb91ee4cfbb5c39bca9f635f067c910b4", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/0b87f525aefac2d0b7472440a9075acc78d23168", + "reference": "0b87f525aefac2d0b7472440a9075acc78d23168", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.0" + "phpstan/phpstan": "^2.1.15" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", @@ -2345,9 +2701,9 @@ "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", "support": { "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", - "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.1" + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.x" }, - "time": "2024-11-28T21:56:36+00:00" + "time": "2025-08-21T08:43:21+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2674,12 +3030,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" + "reference": "4d338ecd2317f3a2787bd9f22fba49b8aa0b6404" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", - "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4d338ecd2317f3a2787bd9f22fba49b8aa0b6404", + "reference": "4d338ecd2317f3a2787bd9f22fba49b8aa0b6404", "shasum": "" }, "require": { @@ -2690,7 +3046,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.1", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -2701,11 +3057,11 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.8", + "sebastian/comparator": "^4.0.9", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", "sebastian/exporter": "^4.0.6", - "sebastian/global-state": "^5.0.7", + "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", "sebastian/type": "^3.2.1", @@ -2764,12 +3120,20 @@ "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": "2024-12-05T13:48:26+00:00" + "time": "2025-08-28T10:28:26+00:00" }, { "name": "sebastian/cli-parser", @@ -2944,12 +3308,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "b247957a1c8dc81a671770f74b479c0a78a818f1" + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/b247957a1c8dc81a671770f74b479c0a78a818f1", - "reference": "b247957a1c8dc81a671770f74b479c0a78a818f1", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", "shasum": "" }, "require": { @@ -3002,15 +3366,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" }, "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": "2022-09-14T12:46:14+00:00" + "time": "2025-08-10T06:51:50+00:00" }, { "name": "sebastian/complexity", @@ -3281,12 +3657,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -3329,15 +3705,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "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": "2024-03-02T06:35:11+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", @@ -3514,12 +3902,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { @@ -3561,15 +3949,27 @@ "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.5" + "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": "2023-02-03T06:07:39+00:00" + "time": "2025-08-10T06:57:39+00:00" }, { "name": "sebastian/resource-operations", @@ -3737,23 +4137,23 @@ }, { "name": "sirbrillig/phpcs-changed", - "version": "v2.11.5", + "version": "v2.11.8", "source": { "type": "git", "url": "https://github.com/sirbrillig/phpcs-changed.git", - "reference": "aaa144eb4f14697b6b06e3dcf74081b5a02f85f6" + "reference": "1c6deb684d648b4d75c577bead4c6d43dc46e4ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-changed/zipball/aaa144eb4f14697b6b06e3dcf74081b5a02f85f6", - "reference": "aaa144eb4f14697b6b06e3dcf74081b5a02f85f6", + "url": "https://api.github.com/repos/sirbrillig/phpcs-changed/zipball/1c6deb684d648b4d75c577bead4c6d43dc46e4ab", + "reference": "1c6deb684d648b4d75c577bead4c6d43dc46e4ab", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1 || ^1.0.0", "phpunit/phpunit": "^6.4 || ^9.5", "sirbrillig/phpcs-variable-analysis": "^2.1.3", "squizlabs/php_codesniffer": "^3.2.1", @@ -3785,9 +4185,9 @@ "description": "Run phpcs on files, but only report warnings/errors from lines which were changed.", "support": { "issues": "https://github.com/sirbrillig/phpcs-changed/issues", - "source": "https://github.com/sirbrillig/phpcs-changed/tree/v2.11.5" + "source": "https://github.com/sirbrillig/phpcs-changed/tree/v2.11.8" }, - "time": "2024-05-23T20:01:41+00:00" + "time": "2025-03-11T15:23:48+00:00" }, { "name": "sirbrillig/phpcs-variable-analysis", @@ -3795,12 +4195,12 @@ "source": { "type": "git", "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", - "reference": "6a0023c9ad614d7a3b5230825d1c9a6a7e104996" + "reference": "fb1275cb32208d5bed108292c45bbef216818a33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/6a0023c9ad614d7a3b5230825d1c9a6a7e104996", - "reference": "6a0023c9ad614d7a3b5230825d1c9a6a7e104996", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/fb1275cb32208d5bed108292c45bbef216818a33", + "reference": "fb1275cb32208d5bed108292c45bbef216818a33", "shasum": "" }, "require": { @@ -3812,7 +4212,6 @@ "phpcsstandards/phpcsdevcs": "^1.1", "phpstan/phpstan": "^1.7", "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0 || ^10.5.32 || ^11.3.3", - "sirbrillig/phpcs-import-detection": "^1.1", "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0" }, "default-branch": true, @@ -3846,7 +4245,7 @@ "source": "https://github.com/sirbrillig/phpcs-variable-analysis", "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" }, - "time": "2024-12-09T01:45:11+00:00" + "time": "2025-05-19T20:15:54+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -3854,12 +4253,12 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "0d969c927165b61ae7c211a047652aa36a481328" + "reference": "ca606d9f60838b9a96ceb12dec7b935d5d64efce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0d969c927165b61ae7c211a047652aa36a481328", - "reference": "0d969c927165b61ae7c211a047652aa36a481328", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/ca606d9f60838b9a96ceb12dec7b935d5d64efce", + "reference": "ca606d9f60838b9a96ceb12dec7b935d5d64efce", "shasum": "" }, "require": { @@ -3925,9 +4324,13 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2024-12-07T11:28:44+00:00" + "time": "2025-08-21T23:53:01+00:00" }, { "name": "szepeviktor/phpstan-wordpress", @@ -3935,12 +4338,12 @@ "source": { "type": "git", "url": "https://github.com/szepeviktor/phpstan-wordpress.git", - "reference": "6686dc2412cdf544556d428e67aa8f0b645164ca" + "reference": "4d060f77ad3a0d354a825ad2a184ec78f4e19c7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/6686dc2412cdf544556d428e67aa8f0b645164ca", - "reference": "6686dc2412cdf544556d428e67aa8f0b645164ca", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/4d060f77ad3a0d354a825ad2a184ec78f4e19c7f", + "reference": "4d060f77ad3a0d354a825ad2a184ec78f4e19c7f", "shasum": "" }, "require": { @@ -3990,7 +4393,7 @@ "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/2.x" }, - "time": "2024-12-02T21:31:37+00:00" + "time": "2025-08-14T19:00:44+00:00" }, { "name": "theseer/tokenizer", @@ -4044,16 +4447,16 @@ }, { "name": "wp-coding-standards/wpcs", - "version": "3.1.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7" + "reference": "d2421de7cec3274ae622c22c744de9a62c7925af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/9333efcbff231f10dfd9c56bb7b65818b4733ca7", - "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/d2421de7cec3274ae622c22c744de9a62c7925af", + "reference": "d2421de7cec3274ae622c22c744de9a62c7925af", "shasum": "" }, "require": { @@ -4062,13 +4465,13 @@ "ext-tokenizer": "*", "ext-xmlreader": "*", "php": ">=5.4", - "phpcsstandards/phpcsextra": "^1.2.1", - "phpcsstandards/phpcsutils": "^1.0.10", - "squizlabs/php_codesniffer": "^3.9.0" + "phpcsstandards/phpcsextra": "^1.4.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcompatibility/php-compatibility": "^9.0", "phpcsstandards/phpcsdevtools": "^1.2.0", "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" @@ -4106,7 +4509,89 @@ "type": "custom" } ], - "time": "2024-03-25T16:39:00+00:00" + "time": "2025-07-24T20:08:31+00:00" + }, + { + "name": "wp-hooks/wordpress-core", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/wp-hooks/wordpress-core-hooks.git", + "reference": "127af21a918a52bcead7ce9b743b17b5d64eb148" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wp-hooks/wordpress-core-hooks/zipball/127af21a918a52bcead7ce9b743b17b5d64eb148", + "reference": "127af21a918a52bcead7ce9b743b17b5d64eb148", + "shasum": "" + }, + "replace": { + "johnbillion/wp-hooks": "*" + }, + "require-dev": { + "erusev/parsedown": "1.8.0-beta-7", + "oomphinc/composer-installers-extender": "^2", + "roots/wordpress-core-installer": "^1.0.0", + "roots/wordpress-full": "6.8", + "wp-hooks/generator": "1.0.0" + }, + "type": "library", + "extra": { + "wp-hooks": { + "ignore-files": [ + "wp-admin/includes/deprecated.php", + "wp-admin/includes/ms-deprecated.php", + "wp-content/", + "wp-includes/deprecated.php", + "wp-includes/ID3/", + "wp-includes/ms-deprecated.php", + "wp-includes/pomo/", + "wp-includes/random_compat/", + "wp-includes/Requests/", + "wp-includes/SimplePie/", + "wp-includes/sodium_compat/", + "wp-includes/Text/" + ], + "ignore-hooks": [ + "load-categories.php", + "load-edit-link-categories.php", + "load-edit-tags.php", + "load-page-new.php", + "load-page.php", + "option_enable_xmlrpc", + "edit_post_{$field}", + "pre_post_{$field}", + "post_{$field}", + "pre_option_enable_xmlrpc", + "$page_hook", + "$hook", + "$hook_name" + ] + }, + "wordpress-install-dir": "vendor/wordpress/wordpress" + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "authors": [ + { + "name": "John Blackbourn", + "homepage": "https://johnblackbourn.com/" + } + ], + "description": "All the actions and filters from WordPress core in machine-readable JSON format.", + "support": { + "issues": "https://github.com/wp-hooks/wordpress-core-hooks/issues", + "source": "https://github.com/wp-hooks/wordpress-core-hooks/tree/1.10.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/johnbillion", + "type": "github" + } + ], + "time": "2025-04-16T22:20:41+00:00" }, { "name": "yoast/phpunit-polyfills", @@ -4114,12 +4599,12 @@ "source": { "type": "git", "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", - "reference": "91f339102ac0ae159d0f81201414a3e1857df158" + "reference": "6d6e27234473dd6445e01fa49f46fcb69bd11f53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/91f339102ac0ae159d0f81201414a3e1857df158", - "reference": "91f339102ac0ae159d0f81201414a3e1857df158", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/6d6e27234473dd6445e01fa49f46fcb69bd11f53", + "reference": "6d6e27234473dd6445e01fa49f46fcb69bd11f53", "shasum": "" }, "require": { @@ -4129,12 +4614,12 @@ "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0.0", "php-parallel-lint/php-parallel-lint": "^1.4.0", - "yoast/yoastcs": "^3.1.0" + "yoast/yoastcs": "^3.2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.x-dev" + "dev-main": "4.x-dev" } }, "autoload": { @@ -4169,7 +4654,7 @@ "security": "https://github.com/Yoast/PHPUnit-Polyfills/security/policy", "source": "https://github.com/Yoast/PHPUnit-Polyfills" }, - "time": "2024-12-08T06:51:54+00:00" + "time": "2025-08-12T01:38:30+00:00" } ], "aliases": [ diff --git a/includes/Classifai/Providers/APIRequest.php b/includes/Classifai/Providers/APIRequest.php new file mode 100644 index 000000000..eebf140c5 --- /dev/null +++ b/includes/Classifai/Providers/APIRequest.php @@ -0,0 +1,633 @@ +api_key = $api_key; + $this->feature = $feature; + $this->use_client = $use_client; + + if ( $this->use_client ) { + $this->initialize_client(); + } + } + + /** + * Initialize the AI Client. + */ + protected function initialize_client() { + try { + $this->ai_client = new AiClient(); + $registry = AiClient::defaultRegistry(); + + $registry->setProviderRequestAuthentication( + static::PROVIDER_ID, + new ApiKeyRequestAuthentication( $this->api_key ) + ); + } catch ( \Exception $e ) { + // Fallback to traditional HTTP requests if AI Client fails. + $this->use_client = false; + } + } + + /** + * Make a GET request. + * + * @param string $url The API URL. + * @param array $options Request options. + * @return array|WP_Error + */ + public function get( string $url, array $options = [] ) { + $url = $this->filter_url( 'get', $url, $options ); + $options = $this->filter_options( 'get', $options, $url ); + + $this->add_headers( $options ); + + $response = safe_wp_remote_get( $url, $options ); + $result = $this->get_result( $response ); + + return $this->filter_response( 'get', $result, $url, $options ); + } + + /** + * Make a POST request. + * + * @param string $url The API URL. + * @param array $options Request options. + * @return array|WP_Error + */ + public function post( string $url = '', array $options = [] ) { + $options = wp_parse_args( $options, array( 'timeout' => $this->timeout ) ); + + $url = $this->filter_url( 'post', $url, $options ); + $options = $this->filter_options( 'post', $options, $url ); + + $this->add_headers( $options ); + + $response = safe_wp_remote_post( $url, $options ); + $result = $this->get_result( $response ); + + return $this->filter_response( 'post', $result, $url, $options ); + } + + /** + * Makes an authorized POST request with form data. + * + * @param string $url The OpenAI API URL. + * @param array $body The body of the request. + * @return array|WP_Error + */ + public function post_form( string $url = '', array $body = [] ) { + $url = $this->filter_url( 'post_form', $url, [] ); + + $boundary = wp_generate_password( 24, false ); + $payload = $this->build_form_data( $body, $boundary ); + + $options = $this->filter_options( + 'post_form', + [ + 'body' => $payload, + 'headers' => [ + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, + ], + 'timeout' => 60, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + ], + $url + ); + + $this->add_headers( $options ); + + $response = safe_wp_remote_post( $url, $options ); + $result = $this->get_result( $response ); + + return $this->filter_response( 'post_form', $result, $url, $options ); + } + + /** + * Build the form data for the request. + * + * @param array $body The body of the request. + * @param string $boundary The boundary of the request. + * @return string The form data. + */ + protected function build_form_data( array $body, string $boundary ): string { + $payload = ''; + + // Take all our POST fields and transform them to work with form-data. + foreach ( $body as $name => $value ) { + $payload .= '--' . $boundary; + $payload .= "\r\n"; + + if ( 'file' === $name ) { + $payload .= 'Content-Disposition: form-data; name="file"; filename="' . basename( $value ) . '"' . "\r\n"; + $payload .= "\r\n"; + $payload .= file_get_contents( $value ); // phpcs:ignore + } else { + $payload .= 'Content-Disposition: form-data; name="' . esc_attr( $name ) . + '"' . "\r\n\r\n"; + $payload .= esc_attr( $value ); + } + + $payload .= "\r\n"; + } + + $payload .= '--' . $boundary . '--'; + + return $payload; + } + + /** + * Make a request using the AI Client SDK. + * + * @param string|null $user_prompt Prompt to send. + * @param array $options Options to send. + * @return array|WP_Error + */ + public function client( $user_prompt = null, array $options = [] ) { + if ( ! $this->is_ai_client_available() ) { + return new WP_Error( 'ai_client_not_available', __( 'AI Client is not available', 'classifai' ) ); + } + + $output = 'text'; + + // Get the system prompt from messages. + if ( ! isset( $options['system_instruction'] ) && isset( $options['messages'] ) ) { + $options['system_instruction'] = $this->get_prompt_from_messages( $options['messages'], 'system' ); + } + + // Get the user prompt from messages. + if ( empty( $user_prompt ) && isset( $options['messages'] ) ) { + $user_prompt = $this->get_prompt_from_messages( $options['messages'], 'user' ); + + // If the user prompt is an image, save the image URL to attach later. + if ( isset( $user_prompt[0]['type'] ) && 'image_url' === $user_prompt[0]['type'] ) { + $options['image_url'] = $user_prompt[0]['image_url']['url'] ?? null; + + $user_prompt = null; + } + } + + unset( $options['messages'] ); + + try { + $model_config = $this->process_model_config( $options ); + $prompt_builder = AiClient::prompt( $user_prompt ); + $prompt_builder = $prompt_builder->usingModelConfig( $model_config ); + $prompt_builder = $prompt_builder->usingProvider( static::PROVIDER_ID ); + + // Set the provider and model. + if ( ! empty( $options['model'] ) ) { + $registry = AiClient::defaultRegistry(); + $provider_class_name = $registry->getProviderClassName( static::PROVIDER_ID ); + $prompt_builder = $prompt_builder->usingModel( $provider_class_name::model( $options['model'] ) ); + + if ( str_starts_with( $options['model'], 'gpt-image-' ) ) { + $output = 'image'; + } + } + + // If we have an image, attach it to the list of messages. + if ( ! empty( $options['image_url'] ) ) { + $prompt_builder = $prompt_builder->withFile( $options['image_url'] ); + } + + // If we have an output file type set, we are generating an image. + if ( $model_config->getOutputFileType() !== null ) { + $output = 'image'; + } + + if ( 'text' === $output ) { + return $this->get_client_result( $prompt_builder->generateTexts(), 'text' ); + } elseif ( 'image' === $output ) { + return $this->get_client_result( $prompt_builder->generateImages(), 'image' ); + } + + return []; + } catch ( \Exception $e ) { + return new WP_Error( 'ai_client_error', $e->getMessage() ); + } + } + + /** + * Get the prompt from the messages array. + * + * This is mostly here to maintain backwards compatibility + * with how we used to handle messages and how the PHP SDK + * expects them. + * + * @param array $messages Messages to get the prompt from. + * @param string $prompt_type Prompt type to get. + * @return string + */ + protected function get_prompt_from_messages( array $messages, string $prompt_type = 'system' ): string { + $prompt = array_values( + array_filter( + $messages, + function ( $message ) use ( $prompt_type ) { + return $prompt_type === $message['role']; + } + ) + ); + + return ! empty( $prompt[0]['content'] ) ? $prompt[0]['content'] : ''; + } + + /** + * Process the model config. + * + * @param array $options The options to add to the model config. + * @return ModelConfig + */ + protected function process_model_config( array $options ): ModelConfig { + $custom = [ + 'quality', + 'style', + 'size', + ]; + $options = $this->standardize_option_keys( $options ); + + $schema = ModelConfig::getJsonSchema()['properties']; + $model_config = []; + + foreach ( $options as $key => $value ) { + if ( in_array( $key, $custom, true ) ) { + $model_config['customOptions'][ $key ] = $value; + continue; + } + + if ( ! isset( $schema[ $key ] ) ) { + continue; + } + + $property_schema = $schema[ $key ]; + $type = $property_schema['type'] ?? null; + + $processed_value = (string) $value; + + if ( 'array' === $type || 'object' === $type ) { + $processed_value = (array) $value; + } elseif ( 'integer' === $type ) { + $processed_value = (int) $value; + } elseif ( 'number' === $type ) { + $processed_value = (float) $value; + } elseif ( 'boolean' === $type ) { + $processed_value = filter_var( $value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); + + if ( null === $processed_value ) { + continue; + } + } + + $model_config[ $key ] = $processed_value; + } + + return ModelConfig::fromArray( $model_config ); + } + + /** + * Standardize the option keys. + * + * The keys we use match what the various AI APIs expect + * but we want to standardize them to the PHP SDK's expected keys. + * + * @param array $options The options to standardize. + * @return array + */ + protected function standardize_option_keys( array $options ): array { + if ( empty( $options ) ) { + return []; + } + + $key_mappings = [ + 'frequency_penalty' => 'frequencyPenalty', + 'n' => 'candidateCount', + 'max_tokens' => 'maxTokens', + 'presence_penalty' => 'presencePenalty', + 'stop' => 'stopSequences', + 'system_instruction' => 'systemInstruction', + 'top_p' => 'topP', + 'top_logprobs' => 'topLogprobs', + 'web_search_options' => 'webSearch', + ]; + + $standardized = []; + + foreach ( $options as $key => $value ) { + // Handle special case for response_format. + if ( 'response_format' === $key ) { + if ( is_array( $value ) && isset( $value['json_schema'] ) ) { + $standardized['outputSchema'] = $value['json_schema']; + continue; + } + + if ( + is_string( $options['response_format'] ) && + 'b64_json' === $options['response_format'] + ) { + $standardized['outputFileType'] = FileTypeEnum::inline(); + continue; + } + } + + // Use mapped key if available, otherwise keep original key. + $new_key = $key_mappings[ $key ] ?? $key; + $standardized[ $new_key ] = $value; + } + + return $standardized; + } + + /** + * Process the AI Client response. + * + * @param array $response The AI Client response. + * @param string $type The type of response. + * @return array|WP_Error + */ + protected function get_client_result( array $response, string $type = 'text' ) { + if ( empty( $response ) ) { + return new WP_Error( 'no_choices', __( 'No choices were returned from the AI provider', 'classifai' ) ); + } + + $results = []; + foreach ( $response as $choice ) { + if ( 'image' === $type ) { + $results[] = $this->sanitize_choice( $choice->getBase64Data() ); + } else { + $results[] = $this->sanitize_choice( $choice ); + } + } + + return $results; + } + + /** + * Sanitize a choice from AI response. + * + * @param string $choice The choice to sanitize. + * @return string + */ + protected function sanitize_choice( string $choice ): string { + return sanitize_text_field( trim( $choice, ' "\'' ) ); + } + + /** + * Get results from HTTP response. + * + * @param array|WP_Error $response The HTTP response. + * @return array|WP_Error + */ + public function get_result( $response ) { + if ( is_wp_error( $response ) ) { + return $response; + } + + $headers = wp_remote_retrieve_headers( $response ); + $content_type = $headers['content-type'] ?? false; + $body = wp_remote_retrieve_body( $response ); + $code = wp_remote_retrieve_response_code( $response ); + + // Handle different content types. + if ( ! $content_type || false !== strpos( $content_type, 'application/json' ) ) { + return $this->parse_json_response( $body, $code ); + } elseif ( $content_type && false !== strpos( $content_type, 'audio/' ) ) { + return $response; // Return raw response for audio. + } else { + return new WP_Error( 'invalid_content_type', __( 'Invalid content type received', 'classifai' ) ); + } + } + + /** + * Parse JSON response. + * + * @param string $body Response body. + * @param int $code Response code. + * @return array|WP_Error + */ + protected function parse_json_response( string $body, int $code ) { + $json = json_decode( $body, true ); + + if ( JSON_ERROR_NONE !== json_last_error() ) { + return new WP_Error( 'invalid_json', __( 'Invalid JSON: ', 'classifai' ) . json_last_error_msg() ); + } + + if ( ! empty( $json['error'] ) ) { + $message = is_string( $json['error'] ) + ? $json['error'] + : ( $json['error']['message'] ?? __( 'An error occurred', 'classifai' ) ); + return new WP_Error( $code, $message ); + } + + return $json; + } + + /** + * Add authentication and content headers. + * + * @param array $options Request options, passed by reference. + */ + public function add_headers( array &$options = [] ) { + if ( empty( $options['headers'] ) ) { + $options['headers'] = array(); + } + + $this->add_auth_header( $options ); + $this->add_content_type_header( $options ); + } + + /** + * Add authentication header. + * + * @param array $options Request options, passed by reference. + */ + protected function add_auth_header( array &$options ) { + $auth_header = $this->get_auth_header(); + if ( $auth_header ) { + $options['headers'][ $this->get_auth_header_name() ] = $auth_header; + } + } + + /** + * Add content type header. + * + * @param array $options Request options, passed by reference. + */ + protected function add_content_type_header( array &$options ) { + if ( ! isset( $options['headers']['Content-Type'] ) ) { + $options['headers']['Content-Type'] = 'application/json'; + } + } + + /** + * Get the authentication header value. + * Must be implemented by concrete classes. + * + * @return string + */ + abstract protected function get_auth_header(): string; + + /** + * Get the authentication header name. + * Must be implemented by concrete classes. + * + * @return string + */ + abstract protected function get_auth_header_name(): string; + + /** + * Filter URL before making request. + * + * @param string $method HTTP method. + * @param string $url The URL. + * @param array $options Request options. + * @return string + */ + protected function filter_url( string $method, string $url, array $options ): string { + $filter_name = sprintf( 'classifai_%s_api_request_%s_url', static::PROVIDER_ID, $method ); + + /** + * Filter the URL before making request. + * + * @since x.x.x + * @hook classifai_%s_api_request_%s_url + * + * @param string $url The URL. + * @param array $options The options. + * @param string $this->feature The feature name. + * + * @return string The URL. + */ + return apply_filters( $filter_name, $url, $options, $this->feature ); + } + + /** + * Filter options before making request. + * + * @param string $method HTTP method. + * @param array $options Request options. + * @param string $url The URL. + * @return array + */ + protected function filter_options( string $method, array $options, string $url ): array { + $filter_name = sprintf( 'classifai_%s_api_request_%s_options', static::PROVIDER_ID, $method ); + + /** + * Filter the options before making request. + * + * @since x.x.x + * @hook classifai_%s_api_request_%s_options + * + * @param array $options The options. + * @param string $url The URL. + * @param string $this->feature The feature name. + * + * @return array The options. + */ + return apply_filters( $filter_name, $options, $url, $this->feature ); + } + + /** + * Filter response after receiving it. + * + * @param string $method HTTP method. + * @param array|WP_Error $response The response. + * @param string $url The URL. + * @param array $options Request options. + * @return array|WP_Error + */ + protected function filter_response( string $method, $response, string $url, array $options ) { + $filter_name = sprintf( 'classifai_%s_api_response_%s', static::PROVIDER_ID, $method ); + + /** + * Filter the response after receiving it. + * + * @since x.x.x + * @hook classifai_%s_api_response_%s + * + * @param array|WP_Error $response The response. + * @param string $url The URL. + * @param array $options Request options. + * @param string $this->feature The feature name. + * + * @return array|WP_Error The response. + */ + return apply_filters( $filter_name, $response, $url, $options, $this->feature ); + } + + /** + * Check if AI Client is available and initialized. + * + * @return bool + */ + public function is_ai_client_available(): bool { + return $this->use_client && null !== $this->ai_client; + } +} diff --git a/includes/Classifai/Providers/OpenAI/APIRequest.php b/includes/Classifai/Providers/OpenAI/APIRequest.php index 86681f533..306788949 100644 --- a/includes/Classifai/Providers/OpenAI/APIRequest.php +++ b/includes/Classifai/Providers/OpenAI/APIRequest.php @@ -2,13 +2,11 @@ namespace Classifai\Providers\OpenAI; -use WP_Error; +use Classifai\Providers\APIRequest as APIRequestBase; use function Classifai\safe_wp_remote_post; -use function Classifai\safe_wp_remote_get; /** - * The APIRequest class is a low level class to make OpenAI API - * requests. + * OpenAI API Request implementation. * * The returned response is parsed into JSON and returned as an * associative array. @@ -18,330 +16,25 @@ * $request = new Classifai\Providers\OpenAI\APIRequest(); * $request->post( $openai_url, $options ); */ -class APIRequest { +class APIRequest extends APIRequestBase { - /** - * The OpenAI API key. - * - * @var string - */ - public $api_key; - - /** - * The feature name. - * - * @var string - */ - public $feature; - - /** - * OpenAI APIRequest constructor. - * - * @param string $api_key OpenAI API key. - * @param string $feature Feature name. - */ - public function __construct( string $api_key = '', string $feature = '' ) { - $this->api_key = $api_key; - $this->feature = $feature; - } - - /** - * Makes an authorized GET request. - * - * @param string $url The OpenAI API url - * @param array $options Additional query params - * @return array|WP_Error - */ - public function get( string $url, array $options = [] ) { - /** - * Filter the URL for the get request. - * - * @since 2.4.0 - * @hook classifai_openai_api_request_get_url - * - * @param string $url The URL for the request. - * @param array $options The options for the request. - * @param string $this->feature The feature name. - * - * @return string The URL for the request. - */ - $url = apply_filters( 'classifai_openai_api_request_get_url', $url, $options, $this->feature ); - - /** - * Filter the options for the get request. - * - * @since 2.4.0 - * @hook classifai_openai_api_request_get_options - * - * @param array $options The options for the request. - * @param string $url The URL for the request. - * @param string $this->feature The feature name. - * - * @return array The options for the request. - */ - $options = apply_filters( 'classifai_openai_api_request_get_options', $options, $url, $this->feature ); - - $this->add_headers( $options ); - - /** - * Filter the response from OpenAI for a get request. - * - * @since 2.4.0 - * @hook classifai_openai_api_response_get - * - * @param array|\WP_Error $response The API response. - * @param string $url Request URL. - * @param array $options Request body options. - * @param string $this->feature Feature name. - * - * @return array API response. - */ - return apply_filters( - 'classifai_openai_api_response_get', - $this->get_result( safe_wp_remote_get( $url, $options ) ), - $url, - $options, - $this->feature - ); - } - - /** - * Makes an authorized POST request. - * - * @param string $url The OpenAI API URL. - * @param array $options Additional query params. - * @return array|WP_Error - */ - public function post( string $url = '', array $options = [] ) { - $options = wp_parse_args( - $options, - [ - 'timeout' => 90, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - ] - ); - - /** - * Filter the URL for the post request. - * - * @since 2.4.0 - * @hook classifai_openai_api_request_post_url - * - * @param string $url The URL for the request. - * @param array $options The options for the request. - * @param string $this->feature The feature name. - * - * @return string The URL for the request. - */ - $url = apply_filters( 'classifai_openai_api_request_post_url', $url, $options, $this->feature ); - - /** - * Filter the options for the post request. - * - * @since 2.4.0 - * @hook classifai_openai_api_request_post_options - * - * @param array $options The options for the request. - * @param string $url The URL for the request. - * @param string $this->feature The feature name. - * - * @return array The options for the request. - */ - $options = apply_filters( 'classifai_openai_api_request_post_options', $options, $url, $this->feature ); - - $this->add_headers( $options ); - - /** - * Filter the response from OpenAI for a post request. - * - * @since 2.4.0 - * @hook classifai_openai_api_response_post - * - * @param array|\WP_Error $response The API response. - * @param string $url Request URL. - * @param array $options Request body options. - * @param string $this->feature Feature name. - * - * @return array API response. - */ - return apply_filters( - 'classifai_openai_api_response_post', - $this->get_result( safe_wp_remote_post( $url, $options ) ), - $url, - $options, - $this->feature - ); - } - - /** - * Makes an authorized POST request with form data. - * - * @param string $url The OpenAI API URL. - * @param array $body The body of the request. - * @return array|WP_Error - */ - public function post_form( string $url = '', array $body = [] ) { - /** - * Filter the URL for the post form request. - * - * @since 2.4.0 - * @hook classifai_openai_api_request_post_form_url - * - * @param string $url The URL for the request. - * @param string $this->feature The feature name. - * - * @return string The URL for the request. - */ - $url = apply_filters( 'classifai_openai_api_request_post_form_url', $url, $this->feature ); - - $boundary = wp_generate_password( 24, false ); - $payload = ''; - - // Take all our POST fields and transform them to work with form-data. - foreach ( $body as $name => $value ) { - $payload .= '--' . $boundary; - $payload .= "\r\n"; - - if ( 'file' === $name ) { - $payload .= 'Content-Disposition: form-data; name="file"; filename="' . basename( $value ) . '"' . "\r\n"; - $payload .= "\r\n"; - $payload .= file_get_contents( $value ); // phpcs:ignore - } else { - $payload .= 'Content-Disposition: form-data; name="' . esc_attr( $name ) . - '"' . "\r\n\r\n"; - $payload .= esc_attr( $value ); - } - - $payload .= "\r\n"; - } - - $payload .= '--' . $boundary . '--'; - - /** - * Filter the options for the post form request. - * - * @since 2.4.0 - * @hook classifai_openai_api_request_post_form_options - * - * @param array $options The options for the request. - * @param string $url The URL for the request. - * @param array $body The body of the request. - * @param string $this->feature The feature name. - * - * @return array The options for the request. - */ - $options = apply_filters( - 'classifai_openai_api_request_post_form_options', - [ - 'body' => $payload, - 'headers' => [ - 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ], - 'timeout' => 60, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - ], - $url, - $body, - $this->feature - ); - - $this->add_headers( $options ); - - /** - * Filter the response from OpenAI for a post form request. - * - * @since 2.4.0 - * @hook classifai_openai_api_response_post_form - * - * @param array|\WP_Error $response The API response. - * @param string $url Request URL. - * @param array $options Request body options. - * @param string $this->feature Feature name. - * - * @return array API response. - */ - return apply_filters( - 'classifai_openai_api_response_post_form', - $this->get_result( safe_wp_remote_post( $url, $options ) ), - $url, - $options, - $this->feature - ); - } - - /** - * Get results from the response. - * - * @param object $response The API response. - * @return array|WP_Error - */ - public function get_result( $response ) { - if ( ! is_wp_error( $response ) ) { - $headers = wp_remote_retrieve_headers( $response ); - $content_type = false; - - if ( ! empty( $headers ) ) { - $content_type = isset( $headers['content-type'] ) ? $headers['content-type'] : false; - } - - $body = wp_remote_retrieve_body( $response ); - $code = wp_remote_retrieve_response_code( $response ); - - if ( false === $content_type || false !== strpos( $content_type, 'application/json' ) ) { - $json = json_decode( $body, true ); - - if ( json_last_error() === JSON_ERROR_NONE ) { - if ( empty( $json['error'] ) ) { - return $json; - } else { - $message = $json['error']['message'] ?? esc_html__( 'An error occurred', 'classifai' ); - return new WP_Error( $code, $message ); - } - } else { - return new WP_Error( 'Invalid JSON: ' . json_last_error_msg(), $body ); - } - } elseif ( $content_type && false !== strpos( $content_type, 'audio/mpeg' ) ) { - return $response; - } else { - return new WP_Error( 'Invalid content type', $response ); - } - } else { - return $response; - } - } - - /** - * Add the headers. - * - * @param array $options The header options, passed by reference. - */ - public function add_headers( array &$options = [] ) { - if ( empty( $options['headers'] ) ) { - $options['headers'] = []; - } - - if ( ! isset( $options['headers']['Authorization'] ) ) { - $options['headers']['Authorization'] = $this->get_auth_header(); - } - - if ( ! isset( $options['headers']['Content-Type'] ) ) { - $options['headers']['Content-Type'] = 'application/json'; - } - } + protected const PROVIDER_ID = 'openai'; /** - * Get the auth header. + * Get the authentication header value. * * @return string */ - public function get_auth_header() { - return 'Bearer ' . $this->get_api_key(); + protected function get_auth_header(): string { + return 'Bearer ' . $this->api_key; } /** - * Get the OpenAI API key. + * Get the authentication header name. * * @return string */ - public function get_api_key() { - return $this->api_key; + protected function get_auth_header_name(): string { + return 'Authorization'; } } diff --git a/includes/Classifai/Providers/OpenAI/ChatGPT.php b/includes/Classifai/Providers/OpenAI/ChatGPT.php index 41156620a..698e9ca33 100644 --- a/includes/Classifai/Providers/OpenAI/ChatGPT.php +++ b/includes/Classifai/Providers/OpenAI/ChatGPT.php @@ -278,8 +278,6 @@ public function generate_descriptive_text( int $post_id = 0, array $args = [] ) return new WP_Error( 'not_enabled', esc_html__( 'Descriptive text generation is disabled or OpenAI authentication failed. Please check your settings.', 'classifai' ) ); } - $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name() ); - /** * Filter the prompt we will send to ChatGPT. * @@ -294,17 +292,17 @@ public function generate_descriptive_text( int $post_id = 0, array $args = [] ) $prompt = apply_filters( 'classifai_chatgpt_descriptive_text_prompt', get_default_prompt( $settings[ static::ID ]['prompt'] ?? [] ) ?? $feature->prompt, $post_id ); /** - * Filter the request body before sending to ChatGPT. + * Filter the request data before sending to ChatGPT. * * @since 3.2.0 * @hook classifai_chatgpt_descriptive_text_request_body * - * @param array $body Request body that will be sent to ChatGPT. + * @param array $data Request data that will be sent to ChatGPT. * @param int $post_id ID of attachment we are describing. * - * @return array Request body. + * @return array Request data. */ - $body = apply_filters( + $data = apply_filters( 'classifai_chatgpt_descriptive_text_request_body', [ 'model' => $this->vision_model, @@ -332,24 +330,23 @@ public function generate_descriptive_text( int $post_id = 0, array $args = [] ) $post_id ); + $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name(), true ); + // Make our API request. - $response = $request->post( - $this->chatgpt_url, - [ - 'body' => wp_json_encode( $body ), - ] - ); + $response = $request->client( null, $data ); + + if ( is_wp_error( $response ) ) { + return $response; + } // Extract out the text response, if it exists. - if ( ! is_wp_error( $response ) && ! empty( $response['choices'] ) ) { - foreach ( $response['choices'] as $choice ) { - if ( isset( $choice['message'], $choice['message']['content'] ) ) { - // ChatGPT often adds quotes to strings, so remove those as well as extra spaces. - $response = sanitize_text_field( trim( $choice['message']['content'], ' "\'' ) ); + if ( ! empty( $response ) ) { + foreach ( $response as $choice ) { + // ChatGPT often adds quotes to strings, so remove those as well as extra spaces. + $response = sanitize_text_field( trim( $choice, ' "\'' ) ); - // Save full results for later. - update_post_meta( $post_id, 'classifai_computer_vision_captions', $response ); - } + // Save full results for later. + update_post_meta( $post_id, 'classifai_computer_vision_captions', $response ); } } @@ -379,8 +376,6 @@ public function ocr_processing( int $post_id = 0, array $args = [] ) { return new WP_Error( 'not_enabled', esc_html__( 'Image Text Extraction is disabled or OpenAI authentication failed. Please check your settings.', 'classifai' ) ); } - $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name() ); - /** * Filter the prompt we will send to ChatGPT. * @@ -395,17 +390,17 @@ public function ocr_processing( int $post_id = 0, array $args = [] ) { $prompt = apply_filters( 'classifai_chatgpt_ocr_prompt', get_default_prompt( $settings[ static::ID ]['prompt'] ?? [] ) ?? $feature->prompt, $post_id ); /** - * Filter the request body before sending to ChatGPT. + * Filter the request data before sending to ChatGPT. * * @since 3.3.0 * @hook classifai_chatgpt_ocr_request_body * - * @param array $body Request body that will be sent to ChatGPT. + * @param array $data Request data that will be sent to ChatGPT. * @param int $post_id ID of attachment we are describing. * - * @return array Request body. + * @return array Request data. */ - $body = apply_filters( + $data = apply_filters( 'classifai_chatgpt_ocr_request_body', [ 'model' => $this->vision_model, @@ -433,31 +428,26 @@ public function ocr_processing( int $post_id = 0, array $args = [] ) { $post_id ); + $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name(), true ); + // Make our API request. - $response = $request->post( - $this->chatgpt_url, - [ - 'body' => wp_json_encode( $body ), - ] - ); + $response = $request->client( null, $data ); if ( is_wp_error( $response ) ) { return $response; } // Extract out the text response, if it exists. - if ( ! empty( $response['choices'] ) ) { - foreach ( $response['choices'] as $choice ) { - if ( isset( $choice['message'], $choice['message']['content'] ) ) { - // ChatGPT often adds quotes to strings, so remove those as well as extra spaces. - $response = sanitize_text_field( trim( $choice['message']['content'], ' "\'' ) ); - - if ( ! $response || 'none' === $response ) { - $response = new WP_Error( 'no_choices', esc_html__( 'No text found.', 'classifai' ) ); - } else { - // Save all the results for later - update_post_meta( $post_id, 'classifai_computer_vision_ocr', $response ); - } + if ( ! empty( $response ) ) { + foreach ( $response as $choice ) { + // ChatGPT often adds quotes to strings, so remove those as well as extra spaces. + $response = sanitize_text_field( trim( $choice, ' "\'' ) ); + + if ( ! $response || 'none' === $response ) { + $response = new WP_Error( 'no_choices', esc_html__( 'No text found.', 'classifai' ) ); + } else { + // Save all the results for later + update_post_meta( $post_id, 'classifai_computer_vision_ocr', $response ); } } } else { @@ -490,8 +480,6 @@ public function generate_image_tags( int $post_id = 0, array $args = [] ) { return new WP_Error( 'not_enabled', esc_html__( 'Image tag generation is disabled or OpenAI authentication failed. Please check your settings.', 'classifai' ) ); } - $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name() ); - /** * Filter the prompt we will send to ChatGPT. * @@ -506,17 +494,17 @@ public function generate_image_tags( int $post_id = 0, array $args = [] ) { $prompt = apply_filters( 'classifai_chatgpt_image_tag_prompt', get_default_prompt( $settings[ static::ID ]['prompt'] ?? [] ) ?? $feature->prompt, $post_id ); /** - * Filter the request body before sending to ChatGPT. + * Filter the request data before sending to ChatGPT. * * @since 3.3.0 * @hook classifai_chatgpt_image_tag_request_body * - * @param array $body Request body that will be sent to ChatGPT. + * @param array $data Request data that will be sent to ChatGPT. * @param int $post_id ID of attachment we are describing. * - * @return array Request body. + * @return array Request data. */ - $body = apply_filters( + $data = apply_filters( 'classifai_chatgpt_image_tag_request_body', [ 'model' => $this->vision_model, @@ -544,28 +532,23 @@ public function generate_image_tags( int $post_id = 0, array $args = [] ) { $post_id ); + $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name(), true ); + // Make our API request. - $response = $request->post( - $this->chatgpt_url, - [ - 'body' => wp_json_encode( $body ), - ] - ); + $response = $request->client( null, $data ); if ( is_wp_error( $response ) ) { return $response; } // Extract out the text response, if it exists. - if ( ! empty( $response['choices'] ) ) { - foreach ( $response['choices'] as $choice ) { - if ( isset( $choice['message'], $choice['message']['content'] ) ) { - $response = array_filter( explode( '- ', $choice['message']['content'] ) ); - $response = array_map( 'trim', $response ); + if ( ! empty( $response ) ) { + foreach ( $response as $choice ) { + $response = array_filter( explode( '- ', $choice ) ); + $response = array_map( 'trim', $response ); - // Save all the tags for later. - update_post_meta( $post_id, 'classifai_computer_vision_image_tags', $response ); - } + // Save all the tags for later. + update_post_meta( $post_id, 'classifai_computer_vision_image_tags', $response ); } } else { $response = new WP_Error( 'no_choices', esc_html__( 'No choices were returned from OpenAI.', 'classifai' ) ); @@ -606,8 +589,6 @@ public function generate_excerpt( int $post_id = 0, array $args = [] ) { $excerpt_length = absint( $settings['length'] ?? 55 ); - $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name() ); - // Overwrite the prompt if we are generating an excerpt for a product. if ( 'product' === $post_type ) { $excerpt_prompt = $feature->woo_prompt; @@ -643,17 +624,17 @@ public function generate_excerpt( int $post_id = 0, array $args = [] ) { $message_content = $this->get_content( $post_id, $excerpt_length, false, $args['content'] ); /** - * Filter the request body before sending to ChatGPT. + * Filter the request data before sending to ChatGPT. * * @since 2.0.0 * @hook classifai_chatgpt_excerpt_request_body * - * @param array $body Request body that will be sent to ChatGPT. + * @param array $data Request data that will be sent to ChatGPT. * @param int $post_id ID of post we are summarizing. * - * @return array Request body. + * @return array Request data. */ - $body = apply_filters( + $data = apply_filters( 'classifai_chatgpt_excerpt_request_body', [ 'model' => $this->chatgpt_model, @@ -663,27 +644,16 @@ public function generate_excerpt( int $post_id = 0, array $args = [] ) { $post_id ); - // Make our API request. - $response = $request->post( - $this->chatgpt_url, - [ - 'body' => wp_json_encode( $body ), - ] - ); + $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name(), true ); - set_transient( 'classifai_openai_chatgpt_excerpt_generation_latest_response', $response, DAY_IN_SECONDS * 30 ); + // Make our API request. + $response = $request->client( null, $data ); - // Extract out the text response, if it exists. - if ( ! is_wp_error( $response ) && ! empty( $response['choices'] ) ) { - foreach ( $response['choices'] as $choice ) { - if ( isset( $choice['message'], $choice['message']['content'] ) ) { - // ChatGPT often adds quotes to strings, so remove those as well as extra spaces. - $response = sanitize_text_field( trim( $choice['message']['content'], ' "\'' ) ); - } - } + if ( is_wp_error( $response ) ) { + return $response; } - return $response; + return is_array( $response ) ? $response[0] : $response; } /** @@ -715,8 +685,6 @@ public function generate_titles( int $post_id = 0, array $args = [] ) { return new WP_Error( 'not_enabled', esc_html__( 'Title generation is disabled or OpenAI authentication failed. Please check your settings.', 'classifai' ) ); } - $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name() ); - // Overwrite the prompt if we are generating titles for a product. if ( 'product' === $post_type ) { $prompt = $feature->woo_prompt; @@ -747,17 +715,17 @@ public function generate_titles( int $post_id = 0, array $args = [] ) { $message_content = $this->get_content( $post_id, absint( $args['num'] ) * 15, false, $args['content'] ); /** - * Filter the request body before sending to ChatGPT. + * Filter the request data before sending to ChatGPT. * * @since 2.2.0 * @hook classifai_chatgpt_title_request_body * - * @param array $body Request body that will be sent to ChatGPT. + * @param array $data Request data that will be sent to ChatGPT. * @param int $post_id ID of post we are summarizing. * - * @return array Request body. + * @return array Request data. */ - $body = apply_filters( + $data = apply_filters( 'classifai_chatgpt_title_request_body', [ 'model' => $this->chatgpt_model, @@ -768,34 +736,16 @@ public function generate_titles( int $post_id = 0, array $args = [] ) { $post_id ); - // Make our API request. - $response = $request->post( - $this->chatgpt_url, - [ - 'body' => wp_json_encode( $body ), - ] - ); + $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name(), true ); - set_transient( 'classifai_openai_chatgpt_title_generation_latest_response', $response, DAY_IN_SECONDS * 30 ); + // Make our API request. + $response = $request->client( null, $data ); if ( is_wp_error( $response ) ) { return $response; } - if ( empty( $response['choices'] ) ) { - return new WP_Error( 'no_choices', esc_html__( 'No choices were returned from OpenAI.', 'classifai' ) ); - } - - // Extract out the text response. - $return = []; - foreach ( $response['choices'] as $choice ) { - if ( isset( $choice['message'], $choice['message']['content'] ) ) { - // ChatGPT often adds quotes to strings, so remove those as well as extra spaces. - $return[] = sanitize_text_field( trim( $choice['message']['content'], ' "\'' ) ); - } - } - - return $return; + return $response; } /** @@ -820,8 +770,6 @@ public function resize_content( int $post_id, array $args = array() ) { ] ); - $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name() ); - if ( 'shrink' === $args['resize_type'] ) { $prompt = esc_textarea( get_default_prompt( $settings['condense_text_prompt'] ) ?? $feature->condense_prompt ); } else { @@ -843,17 +791,17 @@ public function resize_content( int $post_id, array $args = array() ) { $prompt = apply_filters( 'classifai_chatgpt_' . $args['resize_type'] . '_content_prompt', $prompt, $post_id, $args ); /** - * Filter the resize request body before sending to ChatGPT. + * Filter the resize request data before sending to ChatGPT. * * @since 2.3.0 * @hook classifai_chatgpt_resize_content_request_body * - * @param array $body Request body that will be sent to ChatGPT. + * @param array $data Request data that will be sent to ChatGPT. * @param int $post_id ID of post. * - * @return array Request body. + * @return array Request data. */ - $body = apply_filters( + $data = apply_filters( 'classifai_chatgpt_resize_content_request_body', [ 'model' => $this->chatgpt_model, @@ -873,35 +821,16 @@ public function resize_content( int $post_id, array $args = array() ) { $post_id ); - // Make our API request. - $response = $request->post( - $this->chatgpt_url, - [ - 'body' => wp_json_encode( $body ), - ] - ); + $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name(), true ); - set_transient( 'classifai_openai_chatgpt_content_resizing_latest_response', $response, DAY_IN_SECONDS * 30 ); + // Make our API request. + $response = $request->client( null, $data ); if ( is_wp_error( $response ) ) { return $response; } - if ( empty( $response['choices'] ) ) { - return new WP_Error( 'no_choices', esc_html__( 'No choices were returned from OpenAI.', 'classifai' ) ); - } - - // Extract out the text response. - $return = []; - - foreach ( $response['choices'] as $choice ) { - if ( isset( $choice['message'], $choice['message']['content'] ) ) { - // ChatGPT often adds quotes to strings, so remove those as well as extra spaces. - $return[] = sanitize_text_field( trim( $choice['message']['content'], ' "\'' ) ); - } - } - - return $return; + return $response; } /** @@ -961,8 +890,6 @@ public function generate_key_takeaways( int $post_id = 0, array $args = [] ) { return new WP_Error( 'no_content', esc_html__( 'No content found. Please add content then click the "Generate results" button.', 'classifai' ) ); } - $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name() ); - $prompt = esc_textarea( get_default_prompt( $settings['key_takeaways_prompt'] ) ?? $feature->prompt ); // Replace our variables in the prompt. @@ -984,17 +911,17 @@ public function generate_key_takeaways( int $post_id = 0, array $args = [] ) { $prompt = apply_filters( 'classifai_chatgpt_key_takeaways_prompt', $prompt, $post_id ); /** - * Filter the request body before sending to ChatGPT. + * Filter the request data before sending to ChatGPT. * * @since 3.3.0 * @hook classifai_chatgpt_key_takeaways_request_body * - * @param array $body Request body that will be sent to ChatGPT. + * @param array $data Request data that will be sent to ChatGPT. * @param int $post_id ID of post we are summarizing. * - * @return array Request body. + * @return array Request data. */ - $body = apply_filters( + $data = apply_filters( 'classifai_chatgpt_key_takeaways_request_body', [ 'model' => $this->chatgpt_model, @@ -1033,40 +960,29 @@ public function generate_key_takeaways( int $post_id = 0, array $args = [] ) { $post_id ); + $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name(), true ); + // Make our API request. - $response = $request->post( - $this->chatgpt_url, - [ - 'body' => wp_json_encode( $body ), - ] - ); + $response = $request->client( null, $data ); - // Extract out the response, if it exists. - if ( ! is_wp_error( $response ) && ! empty( $response['choices'] ) ) { - foreach ( $response['choices'] as $choice ) { - if ( isset( $choice['message'], $choice['message']['content'] ) ) { - // We expect the response to be valid json since we requested that schema. - $takeaways = json_decode( $choice['message']['content'], true ); - - if ( isset( $takeaways['takeaways'] ) && is_array( $takeaways['takeaways'] ) ) { - $response = array_map( - function ( $takeaway ) { - return sanitize_text_field( trim( $takeaway, ' "\'' ) ); - }, - $takeaways['takeaways'] - ); - } else { - return new WP_Error( 'refusal', esc_html__( 'OpenAI request failed', 'classifai' ) ); - } - } else { - return new WP_Error( 'refusal', esc_html__( 'OpenAI request failed', 'classifai' ) ); - } + if ( is_wp_error( $response ) ) { + return $response; + } - // If the request was refused, return an error. - if ( isset( $choice['message'], $choice['message']['refusal'] ) ) { - // translators: %s: error message. - return new WP_Error( 'refusal', sprintf( esc_html__( 'OpenAI request failed: %s', 'classifai' ), wp_kses_post( $choice['message']['refusal'] ) ) ); - } + // Extract out the response, if it exists. + foreach ( $response as $choice ) { + // We expect the response to be valid json since we requested that schema. + $takeaways = json_decode( $choice, true ); + + if ( isset( $takeaways['takeaways'] ) && is_array( $takeaways['takeaways'] ) ) { + $response = array_map( + function ( $takeaway ) { + return sanitize_text_field( trim( $takeaway, ' "\'' ) ); + }, + $takeaways['takeaways'] + ); + } else { + return new WP_Error( 'refusal', esc_html__( 'OpenAI request failed', 'classifai' ) ); } } @@ -1359,16 +1275,13 @@ public function get_debug_information(): array { if ( $this->feature_instance instanceof TitleGeneration ) { $debug_info[ __( 'No. of titles', 'classifai' ) ] = $provider_settings['number_of_suggestions'] ?? 1; $debug_info[ __( 'Generate title prompt', 'classifai' ) ] = wp_json_encode( $settings['generate_title_prompt'] ?? [] ); - $debug_info[ __( 'Latest response', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_openai_chatgpt_title_generation_latest_response' ) ); } elseif ( $this->feature_instance instanceof ExcerptGeneration ) { $debug_info[ __( 'Excerpt length', 'classifai' ) ] = $settings['length'] ?? 55; $debug_info[ __( 'Generate excerpt prompt', 'classifai' ) ] = wp_json_encode( $settings['generate_excerpt_prompt'] ?? [] ); - $debug_info[ __( 'Latest response', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_openai_chatgpt_excerpt_generation_latest_response' ) ); } elseif ( $this->feature_instance instanceof ContentResizing ) { $debug_info[ __( 'No. of suggestions', 'classifai' ) ] = $provider_settings['number_of_suggestions'] ?? 1; $debug_info[ __( 'Expand text prompt', 'classifai' ) ] = wp_json_encode( $settings['expand_text_prompt'] ?? [] ); $debug_info[ __( 'Condense text prompt', 'classifai' ) ] = wp_json_encode( $settings['condense_text_prompt'] ?? [] ); - $debug_info[ __( 'Latest response', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_openai_chatgpt_content_resizing_latest_response' ) ); } return apply_filters( diff --git a/includes/Classifai/Providers/OpenAI/Images.php b/includes/Classifai/Providers/OpenAI/Images.php index 55b0ba176..ea36d065c 100644 --- a/includes/Classifai/Providers/OpenAI/Images.php +++ b/includes/Classifai/Providers/OpenAI/Images.php @@ -300,9 +300,9 @@ public function generate_image( string $prompt = '', array $args = [] ) { return new WP_Error( 'prompt_required', esc_html__( 'A prompt is required to generate an image.', 'classifai' ) ); } - $image_generation = new ImageGeneration(); - $settings = $image_generation->get_settings( static::ID ); - $args = wp_parse_args( + $feature = new ImageGeneration(); + $settings = $feature->get_settings( static::ID ); + $args = wp_parse_args( array_filter( $args ), [ 'num' => $settings['number_of_images'] ?? 1, @@ -323,7 +323,7 @@ public function generate_image( string $prompt = '', array $args = [] ) { $args['size'] = '1024x1024'; } - if ( ! $image_generation->is_feature_enabled() ) { + if ( ! $feature->is_feature_enabled() ) { return new WP_Error( 'not_enabled', esc_html__( 'Image generation is disabled or OpenAI authentication failed. Please check your settings.', 'classifai' ) ); } @@ -347,10 +347,8 @@ public function generate_image( string $prompt = '', array $args = [] ) { return new WP_Error( 'invalid_param', sprintf( esc_html__( 'Your image prompt is too long. Please ensure it doesn\'t exceed %d characters.', 'classifai' ), $max_prompt_chars ) ); } - $request = new APIRequest( $settings['api_key'] ?? '', 'generate-image' ); - $model = $this->get_model(); - $body = [ + $data = [ 'prompt' => sanitize_text_field( $prompt ), 'model' => $model, 'n' => absint( $args['num'] ), @@ -362,63 +360,50 @@ public function generate_image( string $prompt = '', array $args = [] ) { if ( 'gpt-image-1' === $model ) { // The gpt-image-1 model doesn't support response_format or style. - unset( $body['response_format'] ); - unset( $body['style'] ); + unset( $data['response_format'] ); + unset( $data['style'] ); } elseif ( 'dall-e-3' === $model ) { // DALL·E 3 doesn't support multiple images per request. - $body['n'] = 1; + $data['n'] = 1; } /** - * Filter the request body before sending to OpenAI. + * Filter the request data before sending to OpenAI. * * @since 2.0.0 * @hook classifai_dalle_request_body * - * @param array $body Request body that will be sent to OpenAI. + * @param array $data Request data that will be sent to OpenAI. * - * @return array Request body. + * @return array Request data. */ - $body = apply_filters( 'classifai_dalle_request_body', $body ); + $data = apply_filters( 'classifai_dalle_request_body', $data ); + + $request = new APIRequest( $settings['api_key'] ?? '', $feature->get_option_name(), true ); $responses = []; // DALL·E 3 doesn't support multiple images in a single request so make one request per image. if ( 'dall-e-3' === $model ) { for ( $i = 0; $i < $args['num']; $i++ ) { - $responses[] = $request->post( - $this->get_api_url(), - [ - 'body' => wp_json_encode( $body ), - ] - ); + $responses[] = $request->client( $data['prompt'], $data ); } } else { - $responses[] = $request->post( - $this->get_api_url(), - [ - 'body' => wp_json_encode( $body ), - ] - ); + $responses[] = $request->client( $data['prompt'], $data ); } $cleaned_responses = []; foreach ( $responses as $response ) { - // Extract out the image response, if it exists. - if ( ! is_wp_error( $response ) && ! empty( $response['data'] ) ) { - foreach ( $response['data'] as $data ) { - if ( ! empty( $data[ $args['format'] ] ) ) { - if ( 'url' === $args['format'] ) { - $cleaned_responses[] = [ 'url' => esc_url_raw( $data[ $args['format'] ] ) ]; - } else { - $cleaned_responses[] = [ 'url' => $data[ $args['format'] ] ]; - } - } - } - } elseif ( is_wp_error( $response ) ) { + if ( is_wp_error( $response ) ) { return $response; } + + if ( 'url' === $args['format'] ) { + $cleaned_responses[] = [ 'url' => esc_url_raw( $response[0] ) ]; + } else { + $cleaned_responses[] = [ 'url' => $response[0] ]; + } } return $cleaned_responses; @@ -438,7 +423,6 @@ public function get_debug_information(): array { $debug_info[ __( 'Number of images', 'classifai' ) ] = $provider_settings['number_of_images'] ?? 1; $debug_info[ __( 'Quality', 'classifai' ) ] = $provider_settings['quality'] ?? 'standard'; $debug_info[ __( 'Size', 'classifai' ) ] = $provider_settings['image_size'] ?? '1024x1024'; - $debug_info[ __( 'Latest response:', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_openai_dalle_latest_response' ) ); } return apply_filters(